You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

126 lines
4.7 KiB

/// This module is designed to automatically add the essential types to a LunaNodeRequest struct, without developer intervention.
/// You should apply this to every LunaNodeRequest struct *BEFORE* any other macros, and especially before any derive macros.
/// Usages:
/// ```rust
/// #[lunanode_request(response="ImageListResponse", endpoint="image/list/")]
/// #[derive(Serialize, Deserialize, Debug)]
/// struct MyStruct {
/// ...
/// ```
///
/// TODO: Improve error messages.
use proc_macro::TokenStream;
use quote::quote;
use syn::parse::Parser;
use syn::{parse_macro_input, AttributeArgs, ItemStruct, Lit, NestedMeta, Meta, MetaNameValue, Type};
enum LunanodeRequestParam {
Invalid,
Response(Type),
EndPoint(String),
}
impl LunanodeRequestParam {
fn from(key: String, val: String) -> Self {
match (key.as_str(), val.as_str()) {
("response", tp) => Self::Response(syn::parse_str(tp).expect("The value given to the 'response' parameter must be a valid type.")),
("endpoint", ep) => Self::EndPoint(ep.to_string()),
_ => Self::Invalid
}
}
}
#[proc_macro_attribute]
pub fn lunanode_response(_attr: TokenStream, input: TokenStream) -> TokenStream {
// Get the structs below the macro
let mut item_struct = parse_macro_input!(input as ItemStruct);
// clone the name to keep compiler happy :)
let name = item_struct.ident.clone();
// Add one field to the struct, see quote! macro below
if let syn::Fields::Named(ref mut fields) = item_struct.fields {
fields.named.push(
syn::Field::parse_named
.parse2(quote! {
#[serde(with="success")]
/// Wheather the request was successful or not. This can be either true or false. If it is false, then .make_request() will produce an Err(LNError::LunaNodeError(LNErrorResponse)).
pub success: bool
})
.unwrap(),
);
}
return quote! {
#[serde_as]
#[derive(Serialize, Deserialize, Debug)]
#item_struct
impl LunaNodeResponse for #name {}
}.into();
}
#[proc_macro_attribute]
pub fn lunanode_request(attr: TokenStream, input: TokenStream) -> TokenStream {
// Get the structs below the macro
let mut item_struct = parse_macro_input!(input as ItemStruct);
// parse the arguments using a custom type
let args_parsed: Vec<LunanodeRequestParam> = parse_macro_input!(attr as AttributeArgs)
.into_iter()
.filter_map(|nm| match nm {
// Only select certain tokens
NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, eq_token: _, lit: Lit::Str(lstr) })) => Some(
// Convert the sigment of the path to a string
(path.segments
.into_iter()
.map(|seg| seg.ident.to_string())
.collect::<Vec<String>>()
.swap_remove(0),
// get the raw value of the LitStr
lstr.value())
),
_ => None
})
// convert the (String, LitStr) tuple to a custom type which only accepts certain key/value pairs
.map(|(k,v)| LunanodeRequestParam::from(k, v))
.collect();
// if the first item in the list is not a response type
let response_type = match args_parsed.get(0).expect("There must be two argument for the macro.") {
LunanodeRequestParam::Response(res) => res,
_ => panic!("The response parameter must be a type; it must also be first."),
};
// if the second item in the list is not a url nedpoint
let url_endpoint = match args_parsed.get(1).expect("There must be two arguments for the macro.") {
LunanodeRequestParam::EndPoint(ep) => ep,
_ => panic!("The endpoint parameter must be a string; it must also be last.")
};
// clone the name to keep compiler happy :)
let name = item_struct.ident.clone();
// Add one field to the struct, see quote! macro below
if let syn::Fields::Named(ref mut fields) = item_struct.fields {
fields.named.push(
syn::Field::parse_named
.parse2(quote! {
#[serde(flatten)]
#[clap(flatten)]
/// A set of keys that must be set by the user if ANY request is to be made.
pub keys: ApiKeys
})
.unwrap(),
);
}
// return this entire section as generated code
return quote! {
#[derive(Serialize, Deserialize, Debug, Hash, PartialEq, Eq, clap::Args)]
#item_struct
impl LunaNodeRequest for #name {
type Response = #response_type; // TODO: Last section that needs to be dynamic
fn get_keys(&self) -> ApiKeys {
self.keys.clone()
}
fn url_endpoint(&self) -> String {
#url_endpoint.to_string()
}
}
}
.into();
}