diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 00000000..c812f941 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,154 @@ +use quote::{ToTokens, Tokens}; + +use metadata::Metadata; +use parse::Entry; +use request::{Request, RequestField}; +use response::{Response, ResponseField}; + +#[derive(Debug)] +pub struct Api { + metadata: Metadata, + request: Request, + response: Response, +} + +impl Api { + pub fn output(&self) -> Tokens { + let description = &self.metadata.description; + let method = &self.metadata.method; + let name = &self.metadata.name; + let path = &self.metadata.path; + let rate_limited = &self.metadata.rate_limited; + let requires_authentication = &self.metadata.requires_authentication; + + let request_types = self.generate_request_types(); + let response_types = self.generate_response_types(); + + quote! { + use std::convert::TryFrom; + + /// The API endpoint. + #[derive(Debug)] + pub struct Endpoint; + + #request_types + + impl TryFrom for ::hyper::Request { + type Error = (); + + fn try_from(request: Request) -> Result { + Ok( + ::hyper::Request::new( + ::hyper::#method, + "/".parse().expect("failed to parse request URI"), + ) + ) + } + } + + #response_types + + impl TryFrom<::hyper::Response> for Response { + type Error = (); + + fn try_from(hyper_response: ::hyper::Response) -> Result { + Ok(Response) + } + } + + impl ::ruma_api::Endpoint for Endpoint { + type Request = Request; + type Response = Response; + + const METADATA: ::ruma_api::Metadata = ::ruma_api::Metadata { + description: #description, + method: ::hyper::#method, + name: #name, + path: #path, + rate_limited: #rate_limited, + requires_authentication: #requires_authentication, + }; + } + } + } + + fn generate_request_types(&self) -> Tokens { + let mut tokens = quote! { + /// Data for a request to this API endpoint. + #[derive(Debug)] + pub struct Request + }; + + if self.request.fields.len() == 0 { + tokens.append(";"); + } else { + tokens.append("{"); + + for request_field in self.request.fields.iter() { + match *request_field { + RequestField::Body(ref field) => field.to_tokens(&mut tokens), + RequestField::Header(_, ref field) => field.to_tokens(&mut tokens), + RequestField::Path(_, ref field) => field.to_tokens(&mut tokens), + RequestField::Query(ref field) => field.to_tokens(&mut tokens), + } + } + + tokens.append("}"); + } + + tokens + } + + fn generate_response_types(&self) -> Tokens { + let mut tokens = quote! { + /// Data in the response from this API endpoint. + #[derive(Debug)] + pub struct Response + }; + + if self.response.fields.len() == 0 { + tokens.append(";"); + } else { + tokens.append("{"); + + for response in self.response.fields.iter() { + match *response { + ResponseField::Body(ref field) => field.to_tokens(&mut tokens), + ResponseField::Header(_, ref field) => field.to_tokens(&mut tokens), + } + } + + tokens.append("}"); + } + + tokens + } +} + +impl From> for Api { + fn from(entries: Vec) -> Api { + if entries.len() != 3 { + panic!("ruma_api! expects 3 blocks: metadata, request, and response"); + } + + let mut metadata = None; + let mut request = None; + let mut response = None; + + for entry in entries { + match entry { + Entry::Metadata(fields) => metadata = Some(Metadata::from(fields)), + Entry::Request(fields) => request = Some(Request::from(fields)), + Entry::Response(fields) => response = Some(Response::from(fields)), + } + } + + Api { + metadata: metadata.expect("ruma_api! is missing metadata"), + request: request.expect("ruma_api! is missing request"), + response: response.expect("ruma_api! is missing response"), + } + } +} + + diff --git a/src/lib.rs b/src/lib.rs index a0b53232..0041bae1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,12 +12,14 @@ extern crate syn; use proc_macro::TokenStream; -use quote::{ToTokens, Tokens}; -use syn::{Expr, Field, Ident, Lit, MetaItem}; - -use parse::{Entry, parse_entries}; +use api::Api; +use parse::parse_entries; +mod api; +mod metadata; mod parse; +mod request; +mod response; /// Generates a `ruma-api` endpoint. #[proc_macro] @@ -28,266 +30,3 @@ pub fn ruma_api(input: TokenStream) -> TokenStream { api.output().parse().expect("ruma_api! failed to parse output as a TokenStream") } - -#[derive(Debug)] -struct Api { - metadata: Metadata, - request: Request, - response: Response, -} - -impl Api { - fn output(&self) -> Tokens { - let description = &self.metadata.description; - let method = &self.metadata.method; - let name = &self.metadata.name; - let path = &self.metadata.path; - let rate_limited = &self.metadata.rate_limited; - let requires_authentication = &self.metadata.requires_authentication; - - let request_types = self.generate_request_types(); - let response_types = self.generate_response_types(); - - quote! { - use std::convert::TryFrom; - - /// The API endpoint. - #[derive(Debug)] - pub struct Endpoint; - - #request_types - - impl TryFrom for ::hyper::Request { - type Error = (); - - fn try_from(request: Request) -> Result { - Ok( - ::hyper::Request::new( - ::hyper::#method, - "/".parse().expect("failed to parse request URI"), - ) - ) - } - } - - #response_types - - impl TryFrom<::hyper::Response> for Response { - type Error = (); - - fn try_from(hyper_response: ::hyper::Response) -> Result { - Ok(Response) - } - } - - impl ::ruma_api::Endpoint for Endpoint { - type Request = Request; - type Response = Response; - - const METADATA: ::ruma_api::Metadata = ::ruma_api::Metadata { - description: #description, - method: ::hyper::#method, - name: #name, - path: #path, - rate_limited: #rate_limited, - requires_authentication: #requires_authentication, - }; - } - } - } - - fn generate_request_types(&self) -> Tokens { - let mut tokens = quote! { - /// Data for a request to this API endpoint. - #[derive(Debug)] - pub struct Request; - }; - - tokens - } - - fn generate_response_types(&self) -> Tokens { - let mut tokens = quote! { - /// Data in the response from this API endpoint. - #[derive(Debug)] - pub struct Response; - }; - - tokens - } -} - -impl From> for Api { - fn from(entries: Vec) -> Api { - if entries.len() != 3 { - panic!("ruma_api! expects 3 blocks: metadata, request, and response"); - } - - let mut metadata = None; - let mut request = None; - let mut response = None; - - for entry in entries { - match entry { - Entry::Metadata(fields) => metadata = Some(Metadata::from(fields)), - Entry::Request(fields) => request = Some(Request::from(fields)), - Entry::Response(fields) => response = Some(Response::from(fields)), - } - } - - Api { - metadata: metadata.expect("ruma_api! is missing metadata"), - request: request.expect("ruma_api! is missing request"), - response: response.expect("ruma_api! is missing response"), - } - } -} - -#[derive(Debug)] -struct Metadata { - description: Tokens, - method: Tokens, - name: Tokens, - path: Tokens, - rate_limited: Tokens, - requires_authentication: Tokens, -} - -impl From> for Metadata { - fn from(fields: Vec<(Ident, Expr)>) -> Self { - let mut description = None; - let mut method = None; - let mut name = None; - let mut path = None; - let mut rate_limited = None; - let mut requires_authentication = None; - - for field in fields { - let (identifier, expression) = field; - - if identifier == Ident::new("description") { - description = Some(tokens_for(expression)); - } else if identifier == Ident::new("method") { - method = Some(tokens_for(expression)); - } else if identifier == Ident::new("name") { - name = Some(tokens_for(expression)); - } else if identifier == Ident::new("path") { - path = Some(tokens_for(expression)); - } else if identifier == Ident::new("rate_limited") { - rate_limited = Some(tokens_for(expression)); - } else if identifier == Ident::new("requires_authentication") { - requires_authentication = Some(tokens_for(expression)); - } else { - panic!("ruma_api! metadata included unexpected field: {}", identifier); - } - } - - Metadata { - description: description.expect("ruma_api! metadata is missing description"), - method: method.expect("ruma_api! metadata is missing method"), - name: name.expect("ruma_api! metadata is missing name"), - path: path.expect("ruma_api! metadata is missing path"), - rate_limited: rate_limited.expect("ruma_api! metadata is missing rate_limited"), - requires_authentication: requires_authentication - .expect("ruma_api! metadata is missing requires_authentication"), - } - } -} - -#[derive(Debug)] -struct Request { - fields: Vec, -} - -impl From> for Request { - fn from(fields: Vec) -> Self { - let request_fields = fields.into_iter().map(|field| { - for attr in field.attrs.clone().iter() { - match attr.value { - MetaItem::Word(ref ident) => { - if ident == "query" { - return RequestField::Query(field); - } - } - MetaItem::List(_, _) => {} - MetaItem::NameValue(ref ident, ref lit) => { - if ident == "header" { - if let Lit::Str(ref name, _) = *lit { - return RequestField::Header(name.clone(), field); - } else { - panic!("ruma_api! header attribute expects a string value"); - } - } else if ident == "path" { - if let Lit::Str(ref name, _) = *lit { - return RequestField::Path(name.clone(), field); - } else { - panic!("ruma_api! path attribute expects a string value"); - } - } - } - } - } - - return RequestField::Body(field); - }).collect(); - - Request { - fields: request_fields, - } - } -} - -#[derive(Debug)] -enum RequestField { - Body(Field), - Header(String, Field), - Path(String, Field), - Query(Field), -} - -#[derive(Debug)] -struct Response { - fields: Vec, -} - -impl From> for Response { - fn from(fields: Vec) -> Self { - let response_fields = fields.into_iter().map(|field| { - for attr in field.attrs.clone().iter() { - match attr.value { - MetaItem::Word(_) | MetaItem::List(_, _) => {} - MetaItem::NameValue(ref ident, ref lit) => { - if ident == "header" { - if let Lit::Str(ref name, _) = *lit { - return ResponseField::Header(name.clone(), field); - } else { - panic!("ruma_api! header attribute expects a string value"); - } - } - } - } - } - - return ResponseField::Body(field); - }).collect(); - - Response { - fields: response_fields, - } - } -} - -#[derive(Debug)] -enum ResponseField { - Body(Field), - Header(String, Field), -} - -/// Helper method for turning a value into tokens. -fn tokens_for(value: T) -> Tokens where T: ToTokens { - let mut tokens = Tokens::new(); - - value.to_tokens(&mut tokens); - - tokens -} diff --git a/src/metadata.rs b/src/metadata.rs new file mode 100644 index 00000000..e0de6841 --- /dev/null +++ b/src/metadata.rs @@ -0,0 +1,62 @@ +use quote::{ToTokens, Tokens}; +use syn::{Expr, Ident}; + +#[derive(Debug)] +pub struct Metadata { + pub description: Tokens, + pub method: Tokens, + pub name: Tokens, + pub path: Tokens, + pub rate_limited: Tokens, + pub requires_authentication: Tokens, +} + +impl From> for Metadata { + fn from(fields: Vec<(Ident, Expr)>) -> Self { + let mut description = None; + let mut method = None; + let mut name = None; + let mut path = None; + let mut rate_limited = None; + let mut requires_authentication = None; + + for field in fields { + let (identifier, expression) = field; + + if identifier == Ident::new("description") { + description = Some(tokens_for(expression)); + } else if identifier == Ident::new("method") { + method = Some(tokens_for(expression)); + } else if identifier == Ident::new("name") { + name = Some(tokens_for(expression)); + } else if identifier == Ident::new("path") { + path = Some(tokens_for(expression)); + } else if identifier == Ident::new("rate_limited") { + rate_limited = Some(tokens_for(expression)); + } else if identifier == Ident::new("requires_authentication") { + requires_authentication = Some(tokens_for(expression)); + } else { + panic!("ruma_api! metadata included unexpected field: {}", identifier); + } + } + + Metadata { + description: description.expect("ruma_api! metadata is missing description"), + method: method.expect("ruma_api! metadata is missing method"), + name: name.expect("ruma_api! metadata is missing name"), + path: path.expect("ruma_api! metadata is missing path"), + rate_limited: rate_limited.expect("ruma_api! metadata is missing rate_limited"), + requires_authentication: requires_authentication + .expect("ruma_api! metadata is missing requires_authentication"), + } + } +} + +/// Helper method for turning a value into tokens. +fn tokens_for(value: T) -> Tokens where T: ToTokens { + let mut tokens = Tokens::new(); + + value.to_tokens(&mut tokens); + + tokens +} diff --git a/src/request.rs b/src/request.rs new file mode 100644 index 00000000..bfd0f6ee --- /dev/null +++ b/src/request.rs @@ -0,0 +1,52 @@ +use syn::{Field, Lit, MetaItem}; + +#[derive(Debug)] +pub struct Request { + pub fields: Vec, +} + +impl From> for Request { + fn from(fields: Vec) -> Self { + let request_fields = fields.into_iter().map(|field| { + for attr in field.attrs.clone().iter() { + match attr.value { + MetaItem::Word(ref ident) => { + if ident == "query" { + return RequestField::Query(field); + } + } + MetaItem::List(_, _) => {} + MetaItem::NameValue(ref ident, ref lit) => { + if ident == "header" { + if let Lit::Str(ref name, _) = *lit { + return RequestField::Header(name.clone(), field); + } else { + panic!("ruma_api! header attribute expects a string value"); + } + } else if ident == "path" { + if let Lit::Str(ref name, _) = *lit { + return RequestField::Path(name.clone(), field); + } else { + panic!("ruma_api! path attribute expects a string value"); + } + } + } + } + } + + return RequestField::Body(field); + }).collect(); + + Request { + fields: request_fields, + } + } +} + +#[derive(Debug)] +pub enum RequestField { + Body(Field), + Header(String, Field), + Path(String, Field), + Query(Field), +} diff --git a/src/response.rs b/src/response.rs new file mode 100644 index 00000000..16a45e0d --- /dev/null +++ b/src/response.rs @@ -0,0 +1,39 @@ +use syn::{Field, Lit, MetaItem}; + +#[derive(Debug)] +pub struct Response { + pub fields: Vec, +} + +impl From> for Response { + fn from(fields: Vec) -> Self { + let response_fields = fields.into_iter().map(|field| { + for attr in field.attrs.clone().iter() { + match attr.value { + MetaItem::Word(_) | MetaItem::List(_, _) => {} + MetaItem::NameValue(ref ident, ref lit) => { + if ident == "header" { + if let Lit::Str(ref name, _) = *lit { + return ResponseField::Header(name.clone(), field); + } else { + panic!("ruma_api! header attribute expects a string value"); + } + } + } + } + } + + return ResponseField::Body(field); + }).collect(); + + Response { + fields: response_fields, + } + } +} + +#[derive(Debug)] +pub enum ResponseField { + Body(Field), + Header(String, Field), +}