diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index 6bef17e..a97a4d7 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -299,6 +299,7 @@ impl<'a> OperationParams<'a> } fn new_operation( + operation_id : Option, default_status : hyper::StatusCode, accepted_types : Option>, schema : ReferenceOr, @@ -334,7 +335,7 @@ fn new_operation( Operation { tags: Vec::new(), - operation_id: None, // TODO + operation_id, parameters: params.into_params(), request_body, responses: Responses { @@ -383,7 +384,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.get = Some(new_operation(Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), None, None, Handler::Res::requires_auth())); + item.get = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), None, None, Handler::Res::requires_auth())); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).read_all::() @@ -395,7 +396,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.get = Some(new_operation(Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None, Handler::Res::requires_auth())); + item.get = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None, Handler::Res::requires_auth())); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).read::() @@ -407,7 +408,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}/search", &self.1); let mut item = (self.0).1.remove_path(&self.1); - item.get = Some(new_operation(Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_query_params(Handler::Query::schema()), None, None, Handler::Res::requires_auth())); + item.get = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_query_params(Handler::Query::schema()), None, None, Handler::Res::requires_auth())); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).search::() @@ -420,7 +421,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.post = Some(new_operation(Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Handler::Body::supported_types(), Handler::Res::requires_auth())); + item.post = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Handler::Body::supported_types(), Handler::Res::requires_auth())); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).create::() @@ -433,7 +434,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.put = Some(new_operation(Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Handler::Body::supported_types(), Handler::Res::requires_auth())); + item.put = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Handler::Body::supported_types(), Handler::Res::requires_auth())); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).update_all::() @@ -446,7 +447,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.put = Some(new_operation(Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), Some(body_schema), Handler::Body::supported_types(), Handler::Res::requires_auth())); + item.put = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), Some(body_schema), Handler::Body::supported_types(), Handler::Res::requires_auth())); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).update::() @@ -458,7 +459,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.delete = Some(new_operation(Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), None, None, Handler::Res::requires_auth())); + item.delete = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), None, None, Handler::Res::requires_auth())); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).delete_all::() @@ -470,7 +471,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.delete = Some(new_operation(Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None, Handler::Res::requires_auth())); + item.delete = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None, Handler::Res::requires_auth())); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).delete::() diff --git a/gotham_restful/src/resource.rs b/gotham_restful/src/resource.rs index 3e417f9..a825cac 100644 --- a/gotham_restful/src/resource.rs +++ b/gotham_restful/src/resource.rs @@ -22,6 +22,12 @@ pub trait Resource pub trait ResourceMethod { type Res : ResourceResult; + + #[cfg(feature = "openapi")] + fn operation_id() -> Option + { + None + } } /// Handle a GET request on the Resource root. diff --git a/gotham_restful_derive/src/lib.rs b/gotham_restful_derive/src/lib.rs index 627dfe4..fbe5544 100644 --- a/gotham_restful_derive/src/lib.rs +++ b/gotham_restful_derive/src/lib.rs @@ -27,7 +27,7 @@ pub fn derive_from_body(tokens : TokenStream) -> TokenStream } #[cfg(feature = "openapi")] -#[proc_macro_derive(OpenapiType)] +#[proc_macro_derive(OpenapiType, attributes(openapi))] pub fn derive_openapi_type(tokens : TokenStream) -> TokenStream { openapi_type::expand(tokens) diff --git a/gotham_restful_derive/src/method.rs b/gotham_restful_derive/src/method.rs index 2fc1e60..6727171 100644 --- a/gotham_restful_derive/src/method.rs +++ b/gotham_restful_derive/src/method.rs @@ -4,8 +4,12 @@ use proc_macro2::{Ident, TokenStream as TokenStream2}; use quote::{format_ident, quote}; use syn::{ Attribute, + AttributeArgs, FnArg, ItemFn, + Lit, + Meta, + NestedMeta, PatType, ReturnType, Type, @@ -96,17 +100,17 @@ impl Method format_ident!("{}", name) } - pub fn mod_ident(&self, resource : String) -> Ident + pub fn mod_ident(&self, resource : &str) -> Ident { format_ident!("_gotham_restful_resource_{}_method_{}", resource.to_snake_case(), self.fn_ident()) } - pub fn handler_struct_ident(&self, resource : String) -> Ident + pub fn handler_struct_ident(&self, resource : &str) -> Ident { format_ident!("{}{}Handler", resource.to_camel_case(), self.trait_ident()) } - pub fn setup_ident(&self, resource : String) -> Ident + pub fn setup_ident(&self, resource : &str) -> Ident { format_ident!("{}_{}_setup_impl", resource.to_snake_case(), self.fn_ident()) } @@ -210,19 +214,61 @@ fn interpret_arg(index : usize, arg : &PatType) -> MethodArgument MethodArgument { ident, ty } } +#[cfg(feature = "openapi")] +fn expand_operation_id(attrs : &AttributeArgs) -> TokenStream2 +{ + let mut operation_id : Option<&Lit> = None; + for meta in attrs + { + match meta { + NestedMeta::Meta(Meta::NameValue(kv)) => { + if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("operation_id".to_owned()) + { + operation_id = Some(&kv.lit) + } + }, + _ => {} + } + } + + match operation_id { + Some(operation_id) => quote! { + fn operation_id() -> Option + { + Some(#operation_id.to_string()) + } + }, + None => quote!() + } +} + +#[cfg(not(feature = "openapi"))] +fn expand_operation_id(_ : &AttributeArgs) -> TokenStream2 +{ + quote!() +} + pub fn expand_method(method : Method, attrs : TokenStream, item : TokenStream) -> TokenStream { let krate = super::krate(); - let resource_ident = parse_macro_input!(attrs as Ident); + + // parse attributes + let mut method_attrs = parse_macro_input!(attrs as AttributeArgs); + let resource_path = match method_attrs.remove(0) { + NestedMeta::Meta(Meta::Path(path)) => path, + _ => panic!("Expected resource name for rest macro") + }; + let resource_name = resource_path.segments.last().expect("Resource name must not be empty").ident.to_string(); + let fun = parse_macro_input!(item as ItemFn); let fun_ident = &fun.sig.ident; let fun_vis = &fun.vis; let trait_ident = method.trait_ident(); let method_ident = method.fn_ident(); - let mod_ident = method.mod_ident(resource_ident.to_string()); - let handler_ident = method.handler_struct_ident(resource_ident.to_string()); - let setup_ident = method.setup_ident(resource_ident.to_string()); + let mod_ident = method.mod_ident(&resource_name); + let handler_ident = method.handler_struct_ident(&resource_name); + let setup_ident = method.setup_ident(&resource_name); let (ret, is_no_content) = match &fun.sig.output { ReturnType::Default => (quote!(#krate::NoContent), true), @@ -298,13 +344,16 @@ pub fn expand_method(method : Method, attrs : TokenStream, item : TokenStream) - } // prepare the where clause - let mut where_clause = quote!(#resource_ident : #krate::Resource,); + let mut where_clause = quote!(#resource_path : #krate::Resource,); for arg in args.iter().filter(|arg| (*arg).ty.is_auth_status()) { let auth_ty = arg.ty.quote_ty(); where_clause = quote!(#where_clause #auth_ty : Clone,); } + // operation id code + let operation_id = expand_operation_id(&method_attrs); + // put everything together let output = quote! { #fun @@ -318,6 +367,8 @@ pub fn expand_method(method : Method, attrs : TokenStream, item : TokenStream) - impl #krate::ResourceMethod for #handler_ident { type Res = #ret; + + #operation_id } impl #krate::#trait_ident for #handler_ident diff --git a/gotham_restful_derive/src/openapi_type.rs b/gotham_restful_derive/src/openapi_type.rs index 4179f1b..90fcf2c 100644 --- a/gotham_restful_derive/src/openapi_type.rs +++ b/gotham_restful_derive/src/openapi_type.rs @@ -1,7 +1,14 @@ use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; +use proc_macro2::{ + Delimiter, + TokenStream as TokenStream2, + TokenTree +}; use quote::quote; +use std::{iter, iter::FromIterator}; use syn::{ + Attribute, + AttributeArgs, Field, Fields, Generics, @@ -9,6 +16,9 @@ use syn::{ Item, ItemEnum, ItemStruct, + Lit, + Meta, + NestedMeta, Variant, parse_macro_input }; @@ -17,11 +27,12 @@ pub fn expand(tokens : TokenStream) -> TokenStream { let input = parse_macro_input!(tokens as Item); - match input { + let output = match input { Item::Enum(item) => expand_enum(item), Item::Struct(item) => expand_struct(item), _ => panic!("derive(OpenapiType) not supported for this context") - }.into() + }; + output.into() } fn expand_where(generics : &Generics) -> TokenStream2 @@ -47,6 +58,73 @@ fn expand_where(generics : &Generics) -> TokenStream2 } } +#[derive(Debug, Default)] +struct Attrs +{ + nullable : bool, + rename : Option +} + +fn to_string(lit : &Lit) -> String +{ + match lit { + Lit::Str(str) => str.value(), + _ => panic!("Expected str, found {}", quote!(#lit)) + } +} + +fn to_bool(lit : &Lit) -> bool +{ + match lit { + Lit::Bool(bool) => bool.value, + _ => panic!("Expected bool, found {}", quote!(#lit)) + } +} + +fn remove_parens(input : TokenStream2) -> TokenStream2 +{ + let iter = input.into_iter().flat_map(|tt| { + if let TokenTree::Group(group) = &tt + { + if group.delimiter() == Delimiter::Parenthesis + { + return Box::new(group.stream().into_iter()) as Box>; + } + } + Box::new(iter::once(tt)) + }); + let output = TokenStream2::from_iter(iter); + output +} + +fn parse_attributes(input : &[Attribute]) -> Result +{ + let mut parsed = Attrs::default(); + for attr in input + { + if attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("openapi".to_owned()) + { + let tokens = remove_parens(attr.tokens.clone()); + let nested = parse_macro_input::parse::(tokens.into())?; + for meta in nested + { + match &meta { + NestedMeta::Meta(Meta::NameValue(kv)) => match kv.path.segments.last().map(|s| s.ident.to_string()) { + Some(key) => match key.as_ref() { + "nullable" => parsed.nullable = to_bool(&kv.lit), + "rename" => parsed.rename = Some(to_string(&kv.lit)), + _ => panic!("Unexpected key: {}", key), + }, + _ => panic!("Unexpected token: {}", quote!(#meta)) + }, + _ => panic!("Unexpected token: {}", quote!(#meta)) + } + } + } + } + Ok(parsed) +} + fn expand_variant(variant : &Variant) -> TokenStream2 { if variant.fields != Fields::Unit @@ -56,8 +134,14 @@ fn expand_variant(variant : &Variant) -> TokenStream2 let ident = &variant.ident; + let attrs = parse_attributes(&variant.attrs).expect("Unable to parse attributes"); + let name = match attrs.rename { + Some(rename) => rename, + None => ident.to_string() + }; + quote! { - enumeration.push(stringify!(#ident).to_string()); + enumeration.push(#name.to_string()); } } @@ -68,6 +152,13 @@ fn expand_enum(input : ItemEnum) -> TokenStream2 let generics = input.generics; let where_clause = expand_where(&generics); + let attrs = parse_attributes(&input.attrs).expect("Unable to parse attributes"); + let nullable = attrs.nullable; + let name = match attrs.rename { + Some(rename) => rename, + None => ident.to_string() + }; + let variants : Vec = input.variants.iter().map(expand_variant).collect(); quote! { @@ -89,8 +180,8 @@ fn expand_enum(input : ItemEnum) -> TokenStream2 })); OpenapiSchema { - name: Some(stringify!(#ident).to_string()), - nullable: false, + name: Some(#name.to_string()), + nullable: #nullable, schema, dependencies: Default::default() } @@ -107,6 +198,13 @@ fn expand_field(field : &Field) -> TokenStream2 }; let ty = &field.ty; + let attrs = parse_attributes(&field.attrs).expect("Unable to parse attributes"); + let nullable = attrs.nullable; + let name = match attrs.rename { + Some(rename) => rename, + None => ident.to_string() + }; + quote! {{ let mut schema = <#ty>::schema(); @@ -114,7 +212,7 @@ fn expand_field(field : &Field) -> TokenStream2 { schema.nullable = false; } - else + else if !#nullable { required.push(stringify!(#ident).to_string()); } @@ -130,16 +228,16 @@ fn expand_field(field : &Field) -> TokenStream2 } match schema.name.clone() { - Some(name) => { + Some(schema_name) => { properties.insert( - stringify!(#ident).to_string(), - ReferenceOr::Reference { reference: format!("#/components/schemas/{}", name) } + #name.to_string(), + ReferenceOr::Reference { reference: format!("#/components/schemas/{}", schema_name) } ); - dependencies.insert(name, schema); + dependencies.insert(schema_name, schema); }, None => { properties.insert( - stringify!(#ident).to_string(), + #name.to_string(), ReferenceOr::Item(Box::new(schema.into_schema())) ); } @@ -154,6 +252,13 @@ pub fn expand_struct(input : ItemStruct) -> TokenStream2 let generics = input.generics; let where_clause = expand_where(&generics); + let attrs = parse_attributes(&input.attrs).expect("Unable to parse attributes"); + let nullable = attrs.nullable; + let name = match attrs.rename { + Some(rename) => rename, + None => ident.to_string() + }; + let fields : Vec = match input.fields { Fields::Named(fields) => { fields.named.iter().map(|field| expand_field(field)).collect() @@ -185,8 +290,8 @@ pub fn expand_struct(input : ItemStruct) -> TokenStream2 })); OpenapiSchema { - name: Some(stringify!(#ident).to_string()), - nullable: false, + name: Some(#name.to_string()), + nullable: #nullable, schema, dependencies } diff --git a/gotham_restful_derive/src/resource.rs b/gotham_restful_derive/src/resource.rs index a4d8eef..e7d0537 100644 --- a/gotham_restful_derive/src/resource.rs +++ b/gotham_restful_derive/src/resource.rs @@ -31,6 +31,7 @@ pub fn expand_resource(tokens : TokenStream) -> TokenStream let krate = super::krate(); let input = parse_macro_input!(tokens as ItemStruct); let ident = input.ident; + let name = ident.to_string(); let methods : Vec = input.attrs.into_iter().filter(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("rest_resource".to_string()) // TODO wtf @@ -39,8 +40,8 @@ pub fn expand_resource(tokens : TokenStream) -> TokenStream m.0.into_iter() }).map(|method| { let method = Method::from_str(&method.to_string()).expect("unknown method"); - let mod_ident = method.mod_ident(ident.to_string()); - let ident = method.setup_ident(ident.to_string()); + let mod_ident = method.mod_ident(&name); + let ident = method.setup_ident(&name); quote!(#mod_ident::#ident(&mut route);) }).collect();