diff --git a/ruma-api-macros/src/api.rs b/ruma-api-macros/src/api.rs index 2034743f..2b5693ae 100644 --- a/ruma-api-macros/src/api.rs +++ b/ruma-api-macros/src/api.rs @@ -19,10 +19,10 @@ pub struct Api { metadata: Metadata, /// The `request` section of the macro. - request: Request, + request: Option, /// The `response` section of the macro. - response: Response, + response: Option, /// The `error` section of the macro. error_ty: Option, @@ -32,12 +32,12 @@ pub fn expand_all(api: Api) -> syn::Result { let ruma_api = util::import_ruma_api(); let http = quote! { #ruma_api::exports::http }; - let description = &api.metadata.description; - let method = &api.metadata.method; - let name = &api.metadata.name; - let path = &api.metadata.path; - let rate_limited: TokenStream = api - .metadata + let metadata = &api.metadata; + let description = &metadata.description; + let method = &metadata.method; + let name = &metadata.name; + let path = &metadata.path; + let rate_limited: TokenStream = metadata .rate_limited .iter() .map(|r| { @@ -66,8 +66,8 @@ pub fn expand_all(api: Api) -> syn::Result { let error_ty = api.error_ty.map_or_else(|| quote! { #ruma_api::error::Void }, |err_ty| quote! { #err_ty }); - let request = api.request.expand(&api.metadata, &error_ty, &ruma_api); - let response = api.response.expand(&api.metadata, &error_ty, &ruma_api); + let request = api.request.map(|req| req.expand(metadata, &error_ty, &ruma_api)); + let response = api.response.map(|res| res.expand(metadata, &error_ty, &ruma_api)); let metadata_doc = format!("Metadata for the `{}` API endpoint.", name.value()); diff --git a/ruma-api-macros/src/api/parse.rs b/ruma-api-macros/src/api/parse.rs index c6865c82..2570a1d0 100644 --- a/ruma-api-macros/src/api/parse.rs +++ b/ruma-api-macros/src/api/parse.rs @@ -26,8 +26,27 @@ mod kw { impl Parse for Api { fn parse(input: ParseStream<'_>) -> syn::Result { let metadata: Metadata = input.parse()?; - let request: Request = input.parse()?; - let response: Response = input.parse()?; + + let attributes = input.call(Attribute::parse_outer)?; + let (request, attributes) = if input.peek(kw::request) { + let request = parse_request(input, attributes)?; + let attributes = input.call(Attribute::parse_outer)?; + + (Some(request), attributes) + } else { + (None, attributes) + }; + + let response = if input.peek(kw::response) { + Some(parse_response(input, attributes)?) + } else if !attributes.is_empty() { + return Err(syn::Error::new_spanned( + &attributes[0], + "attributes are not supported on the error type", + )); + } else { + None + }; // TODO: Use `bool::then` when MSRV >= 1.50 let error_ty = if input.peek(kw::error) { @@ -39,248 +58,239 @@ impl Parse for Api { None }; - let newtype_body_field = request.newtype_body_field(); - if metadata.method == "GET" && (request.has_body_fields() || newtype_body_field.is_some()) { - let mut combined_error: Option = None; - let mut add_error = |field| { - let error = syn::Error::new_spanned(field, "GET endpoints can't have body fields"); - if let Some(combined_error_ref) = &mut combined_error { - combined_error_ref.combine(error); - } else { - combined_error = Some(error); + if let Some(req) = &request { + let newtype_body_field = req.newtype_body_field(); + if metadata.method == "GET" && (req.has_body_fields() || newtype_body_field.is_some()) { + let mut combined_error: Option = None; + let mut add_error = |field| { + let error = + syn::Error::new_spanned(field, "GET endpoints can't have body fields"); + if let Some(combined_error_ref) = &mut combined_error { + combined_error_ref.combine(error); + } else { + combined_error = Some(error); + } + }; + + for field in req.body_fields() { + add_error(field); } - }; - for field in request.body_fields() { - add_error(field); + if let Some(field) = newtype_body_field { + add_error(field); + } + + return Err(combined_error.unwrap()); } - - if let Some(field) = newtype_body_field { - add_error(field); - } - - return Err(combined_error.unwrap()); } Ok(Self { metadata, request, response, error_ty }) } } -impl Parse for Request { - fn parse(input: ParseStream<'_>) -> syn::Result { - let attributes = input.call(Attribute::parse_outer)?; - let request_kw: kw::request = input.parse()?; - let _: Token![:] = input.parse()?; - let fields; - braced!(fields in input); +fn parse_request(input: ParseStream<'_>, attributes: Vec) -> syn::Result { + let request_kw: kw::request = input.parse()?; + let _: Token![:] = input.parse()?; + let fields; + braced!(fields in input); - let mut newtype_body_field = None; - let mut query_map_field = None; - let mut lifetimes = RequestLifetimes::default(); + let mut newtype_body_field = None; + let mut query_map_field = None; + let mut lifetimes = RequestLifetimes::default(); - let fields: Vec<_> = fields - .parse_terminated::(Field::parse_named)? - .into_iter() - .map(|mut field| { - let mut field_kind = None; - let mut header = None; + let fields: Vec<_> = fields + .parse_terminated::(Field::parse_named)? + .into_iter() + .map(|mut field| { + let mut field_kind = None; + let mut header = None; - for attr in mem::take(&mut field.attrs) { - let meta = match Meta::from_attribute(&attr)? { - Some(m) => m, - None => { - field.attrs.push(attr); - continue; - } - }; - - if field_kind.is_some() { - return Err(syn::Error::new_spanned( - attr, - "There can only be one field kind attribute", - )); + for attr in mem::take(&mut field.attrs) { + let meta = match Meta::from_attribute(&attr)? { + Some(m) => m, + None => { + field.attrs.push(attr); + continue; } + }; - field_kind = Some(match meta { - Meta::Word(ident) => match &ident.to_string()[..] { - attr @ "body" | attr @ "raw_body" => req_res_meta_word( - attr, - &field, - &mut newtype_body_field, - RequestFieldKind::NewtypeBody, - RequestFieldKind::NewtypeRawBody, - )?, - "path" => RequestFieldKind::Path, - "query" => RequestFieldKind::Query, - "query_map" => { - if let Some(f) = &query_map_field { - let mut error = syn::Error::new_spanned( - field, - "There can only be one query map field", - ); - error.combine(syn::Error::new_spanned( - f, - "Previous query map field", - )); - return Err(error); - } - - query_map_field = Some(field.clone()); - RequestFieldKind::QueryMap - } - _ => { - return Err(syn::Error::new_spanned( - ident, - "Invalid #[ruma_api] argument, expected one of \ - `body`, `path`, `query`, `query_map`", - )); - } - }, - Meta::NameValue(MetaNameValue { name, value }) => { - req_res_name_value(name, value, &mut header, RequestFieldKind::Header)? - } - }); - } - - match field_kind.unwrap_or(RequestFieldKind::Body) { - RequestFieldKind::Header => { - collect_lifetime_idents(&mut lifetimes.header, &field.ty) - } - RequestFieldKind::Body => { - collect_lifetime_idents(&mut lifetimes.body, &field.ty) - } - RequestFieldKind::NewtypeBody => { - collect_lifetime_idents(&mut lifetimes.body, &field.ty) - } - RequestFieldKind::NewtypeRawBody => { - collect_lifetime_idents(&mut lifetimes.body, &field.ty) - } - RequestFieldKind::Path => { - collect_lifetime_idents(&mut lifetimes.path, &field.ty) - } - RequestFieldKind::Query => { - collect_lifetime_idents(&mut lifetimes.query, &field.ty) - } - RequestFieldKind::QueryMap => { - collect_lifetime_idents(&mut lifetimes.query, &field.ty) - } - } - - Ok(RequestField::new(field_kind.unwrap_or(RequestFieldKind::Body), field, header)) - }) - .collect::>()?; - - if newtype_body_field.is_some() && fields.iter().any(|f| f.is_body()) { - // TODO: highlight conflicting fields, - return Err(syn::Error::new_spanned( - request_kw, - "Can't have both a newtype body field and regular body fields", - )); - } - - if query_map_field.is_some() && fields.iter().any(|f| f.is_query()) { - return Err(syn::Error::new_spanned( - // TODO: raw, - request_kw, - "Can't have both a query map field and regular query fields", - )); - } - - // TODO when/if `&[(&str, &str)]` is supported remove this - if query_map_field.is_some() && !lifetimes.query.is_empty() { - return Err(syn::Error::new_spanned( - request_kw, - "Lifetimes are not allowed for query_map fields", - )); - } - - Ok(Self { attributes, fields, lifetimes }) - } -} - -impl Parse for Response { - fn parse(input: ParseStream<'_>) -> syn::Result { - let attributes = input.call(Attribute::parse_outer)?; - let response_kw: kw::response = input.parse()?; - let _: Token![:] = input.parse()?; - let fields; - braced!(fields in input); - - let mut newtype_body_field = None; - - let fields: Vec<_> = fields - .parse_terminated::(Field::parse_named)? - .into_iter() - .map(|mut field| { - if has_lifetime(&field.ty) { - return Err(syn::Error::new( - field.ident.span(), - "Lifetimes on Response fields cannot be supported until GAT are stable", + if field_kind.is_some() { + return Err(syn::Error::new_spanned( + attr, + "There can only be one field kind attribute", )); } - let mut field_kind = None; - let mut header = None; - - for attr in mem::take(&mut field.attrs) { - let meta = match Meta::from_attribute(&attr)? { - Some(m) => m, - None => { - field.attrs.push(attr); - continue; - } - }; - - if field_kind.is_some() { - return Err(syn::Error::new_spanned( + field_kind = Some(match meta { + Meta::Word(ident) => match &ident.to_string()[..] { + attr @ "body" | attr @ "raw_body" => req_res_meta_word( attr, - "There can only be one field kind attribute", - )); - } - - field_kind = Some(match meta { - Meta::Word(ident) => match &ident.to_string()[..] { - s @ "body" | s @ "raw_body" => req_res_meta_word( - s, - &field, - &mut newtype_body_field, - ResponseFieldKind::NewtypeBody, - ResponseFieldKind::NewtypeRawBody, - )?, - _ => { - return Err(syn::Error::new_spanned( - ident, - "Invalid #[ruma_api] argument with value, expected `body`", + &field, + &mut newtype_body_field, + RequestFieldKind::NewtypeBody, + RequestFieldKind::NewtypeRawBody, + )?, + "path" => RequestFieldKind::Path, + "query" => RequestFieldKind::Query, + "query_map" => { + if let Some(f) = &query_map_field { + let mut error = syn::Error::new_spanned( + field, + "There can only be one query map field", + ); + error.combine(syn::Error::new_spanned( + f, + "Previous query map field", )); + return Err(error); } - }, - Meta::NameValue(MetaNameValue { name, value }) => { - req_res_name_value(name, value, &mut header, ResponseFieldKind::Header)? + + query_map_field = Some(field.clone()); + RequestFieldKind::QueryMap } - }); + _ => { + return Err(syn::Error::new_spanned( + ident, + "Invalid #[ruma_api] argument, expected one of \ + `body`, `path`, `query`, `query_map`", + )); + } + }, + Meta::NameValue(MetaNameValue { name, value }) => { + req_res_name_value(name, value, &mut header, RequestFieldKind::Header)? + } + }); + } + + match field_kind.unwrap_or(RequestFieldKind::Body) { + RequestFieldKind::Header => { + collect_lifetime_idents(&mut lifetimes.header, &field.ty) + } + RequestFieldKind::Body => collect_lifetime_idents(&mut lifetimes.body, &field.ty), + RequestFieldKind::NewtypeBody => { + collect_lifetime_idents(&mut lifetimes.body, &field.ty) + } + RequestFieldKind::NewtypeRawBody => { + collect_lifetime_idents(&mut lifetimes.body, &field.ty) + } + RequestFieldKind::Path => collect_lifetime_idents(&mut lifetimes.path, &field.ty), + RequestFieldKind::Query => collect_lifetime_idents(&mut lifetimes.query, &field.ty), + RequestFieldKind::QueryMap => { + collect_lifetime_idents(&mut lifetimes.query, &field.ty) + } + } + + Ok(RequestField::new(field_kind.unwrap_or(RequestFieldKind::Body), field, header)) + }) + .collect::>()?; + + if newtype_body_field.is_some() && fields.iter().any(|f| f.is_body()) { + // TODO: highlight conflicting fields, + return Err(syn::Error::new_spanned( + request_kw, + "Can't have both a newtype body field and regular body fields", + )); + } + + if query_map_field.is_some() && fields.iter().any(|f| f.is_query()) { + return Err(syn::Error::new_spanned( + // TODO: raw, + request_kw, + "Can't have both a query map field and regular query fields", + )); + } + + // TODO when/if `&[(&str, &str)]` is supported remove this + if query_map_field.is_some() && !lifetimes.query.is_empty() { + return Err(syn::Error::new_spanned( + request_kw, + "Lifetimes are not allowed for query_map fields", + )); + } + + Ok(Request { attributes, fields, lifetimes }) +} + +fn parse_response(input: ParseStream<'_>, attributes: Vec) -> syn::Result { + let response_kw: kw::response = input.parse()?; + let _: Token![:] = input.parse()?; + let fields; + braced!(fields in input); + + let mut newtype_body_field = None; + + let fields: Vec<_> = fields + .parse_terminated::(Field::parse_named)? + .into_iter() + .map(|mut field| { + if has_lifetime(&field.ty) { + return Err(syn::Error::new( + field.ident.span(), + "Lifetimes on Response fields cannot be supported until GAT are stable", + )); + } + + let mut field_kind = None; + let mut header = None; + + for attr in mem::take(&mut field.attrs) { + let meta = match Meta::from_attribute(&attr)? { + Some(m) => m, + None => { + field.attrs.push(attr); + continue; + } + }; + + if field_kind.is_some() { + return Err(syn::Error::new_spanned( + attr, + "There can only be one field kind attribute", + )); } - Ok(match field_kind.unwrap_or(ResponseFieldKind::Body) { - ResponseFieldKind::Body => ResponseField::Body(field), - ResponseFieldKind::Header => { - ResponseField::Header(field, header.expect("missing header name")) + field_kind = Some(match meta { + Meta::Word(ident) => match &ident.to_string()[..] { + s @ "body" | s @ "raw_body" => req_res_meta_word( + s, + &field, + &mut newtype_body_field, + ResponseFieldKind::NewtypeBody, + ResponseFieldKind::NewtypeRawBody, + )?, + _ => { + return Err(syn::Error::new_spanned( + ident, + "Invalid #[ruma_api] argument with value, expected `body`", + )); + } + }, + Meta::NameValue(MetaNameValue { name, value }) => { + req_res_name_value(name, value, &mut header, ResponseFieldKind::Header)? } - ResponseFieldKind::NewtypeBody => ResponseField::NewtypeBody(field), - ResponseFieldKind::NewtypeRawBody => ResponseField::NewtypeRawBody(field), - }) + }); + } + + Ok(match field_kind.unwrap_or(ResponseFieldKind::Body) { + ResponseFieldKind::Body => ResponseField::Body(field), + ResponseFieldKind::Header => { + ResponseField::Header(field, header.expect("missing header name")) + } + ResponseFieldKind::NewtypeBody => ResponseField::NewtypeBody(field), + ResponseFieldKind::NewtypeRawBody => ResponseField::NewtypeRawBody(field), }) - .collect::>()?; + }) + .collect::>()?; - if newtype_body_field.is_some() && fields.iter().any(|f| f.is_body()) { - // TODO: highlight conflicting fields, - return Err(syn::Error::new_spanned( - response_kw, - "Can't have both a newtype body field and regular body fields", - )); - } - - Ok(Self { attributes, fields }) + if newtype_body_field.is_some() && fields.iter().any(|f| f.is_body()) { + // TODO: highlight conflicting fields, + return Err(syn::Error::new_spanned( + response_kw, + "Can't have both a newtype body field and regular body fields", + )); } + + Ok(Response { attributes, fields }) } fn has_lifetime(ty: &Type) -> bool { diff --git a/ruma-api/tests/ruma_api.rs b/ruma-api/tests/ruma_api.rs index fd9605cf..eb62d882 100644 --- a/ruma-api/tests/ruma_api.rs +++ b/ruma-api/tests/ruma_api.rs @@ -5,4 +5,7 @@ fn ui() { t.compile_fail("tests/ui/02-invalid-path.rs"); t.pass("tests/ui/03-move-value.rs"); t.compile_fail("tests/ui/04-attributes.rs"); + t.pass("tests/ui/05-request-only.rs"); + t.pass("tests/ui/06-response-only.rs"); + t.compile_fail("tests/ui/07-error-type-attribute.rs"); } diff --git a/ruma-api/tests/ui/05-request-only.rs b/ruma-api/tests/ui/05-request-only.rs new file mode 100644 index 00000000..083d41b7 --- /dev/null +++ b/ruma-api/tests/ui/05-request-only.rs @@ -0,0 +1,50 @@ +use std::convert::TryFrom; + +use ruma_api::{ + error::{FromHttpResponseError, IntoHttpError, Void}, + ruma_api, +}; +use ruma_serde::Outgoing; + +ruma_api! { + metadata: { + description: "Does something.", + method: POST, // An `http::Method` constant. No imports required. + name: "some_endpoint", + path: "/_matrix/some/endpoint/:baz", + rate_limited: false, + authentication: None, + } + + #[derive(PartialEq)] // Make sure attributes work + request: { + // With no attribute on the field, it will be put into the body of the request. + pub foo: String, + } +} + +#[derive(Outgoing)] +pub struct Response; + +impl TryFrom>> for Response { + type Error = FromHttpResponseError; + + fn try_from(_: http::Response>) -> Result { + todo!() + } +} + +impl TryFrom for http::Response> { + type Error = IntoHttpError; + + fn try_from(_: Response) -> Result { + todo!() + } +} + +fn main() { + let req1 = Request { foo: "foo".into() }; + let req2 = req1.clone(); + + assert_eq!(req1, req2); +} diff --git a/ruma-api/tests/ui/06-response-only.rs b/ruma-api/tests/ui/06-response-only.rs new file mode 100644 index 00000000..9745dac5 --- /dev/null +++ b/ruma-api/tests/ui/06-response-only.rs @@ -0,0 +1,24 @@ +use ruma_api::ruma_api; + +ruma_api! { + metadata: { + description: "Does something.", + method: POST, // An `http::Method` constant. No imports required. + name: "some_endpoint", + path: "/_matrix/some/endpoint/:baz", + rate_limited: false, + authentication: None, + } + + #[derive(PartialEq)] // Make sure attributes work + response: { + pub flag: bool, + } +} + +fn main() { + let res1 = Response { flag: false }; + let res2 = res1.clone(); + + assert_eq!(res1, res2); +} diff --git a/ruma-api/tests/ui/07-error-type-attribute.rs b/ruma-api/tests/ui/07-error-type-attribute.rs new file mode 100644 index 00000000..b60be7ca --- /dev/null +++ b/ruma-api/tests/ui/07-error-type-attribute.rs @@ -0,0 +1,21 @@ +use ruma_api::ruma_api; + +ruma_api! { + metadata: { + description: "Does something.", + method: POST, // An `http::Method` constant. No imports required. + name: "some_endpoint", + path: "/_matrix/some/endpoint/:baz", + rate_limited: false, + authentication: None, + } + + request: {} + + response: {} + + #[derive(Default)] + error: ruma_api::error::Void +} + +fn main() {} diff --git a/ruma-api/tests/ui/07-error-type-attribute.stderr b/ruma-api/tests/ui/07-error-type-attribute.stderr new file mode 100644 index 00000000..74d77e42 --- /dev/null +++ b/ruma-api/tests/ui/07-error-type-attribute.stderr @@ -0,0 +1,5 @@ +error: unexpected token + --> $DIR/07-error-type-attribute.rs:17:5 + | +17 | #[derive(Default)] + | ^