From 61967aa8a90a80125ee0e4c9c5d3fe29217cd3d1 Mon Sep 17 00:00:00 2001 From: Steven Fackler Date: Wed, 10 May 2023 09:59:23 -0400 Subject: [PATCH 01/10] clean up client response handling --- conjure-http/src/client.rs | 41 +++++++++++++++++++++++++++++++++--- conjure-macros/src/client.rs | 29 ++++++++++++------------- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/conjure-http/src/client.rs b/conjure-http/src/client.rs index 4dae12ba..92f72fcb 100644 --- a/conjure-http/src/client.rs +++ b/conjure-http/src/client.rs @@ -328,7 +328,39 @@ pub trait AsyncDeserializeResponse { fn accept() -> Option; /// Deserializes the response. - async fn deserialize(response: Response) -> Result; + async fn deserialize(response: Response) -> Result + where + R: 'async_trait; +} + +/// A response deserializer which ignores the response and returns `()`. +pub enum UnitResponseDeserializer {} + +impl DeserializeResponse<(), R> for UnitResponseDeserializer { + fn accept() -> Option { + None + } + + fn deserialize(_: Response) -> Result<(), Error> { + Ok(()) + } +} + +#[async_trait] +impl AsyncDeserializeResponse<(), R> for UnitResponseDeserializer +where + R: Send, +{ + fn accept() -> Option { + None + } + + async fn deserialize(_: Response) -> Result<(), Error> + where + R: 'async_trait, + { + Ok(()) + } } /// A response deserializer which acts like a Conjure-generated client would. @@ -356,13 +388,16 @@ where impl AsyncDeserializeResponse for ConjureResponseDeserializer where T: DeserializeOwned, - R: Stream> + 'static + Send, + R: Stream> + Send, { fn accept() -> Option { Some(APPLICATION_JSON) } - async fn deserialize(response: Response) -> Result { + async fn deserialize(response: Response) -> Result + where + R: 'async_trait, + { if response.headers().get(CONTENT_TYPE) != Some(&APPLICATION_JSON) { return Err(Error::internal_safe("invalid response Content-Type")); } diff --git a/conjure-macros/src/client.rs b/conjure-macros/src/client.rs index 444be122..9e67cf48 100644 --- a/conjure-macros/src/client.rs +++ b/conjure-macros/src/client.rs @@ -500,22 +500,21 @@ fn handle_response( endpoint: &EndpointConfig, response: &TokenStream, ) -> TokenStream { - match &endpoint.accept { - Some(accept) => { - let trait_ = match asyncness { - Asyncness::Sync => quote!(DeserializeResponse), - Asyncness::Async => quote!(AsyncDeserializeResponse), - }; - let await_ = match asyncness { - Asyncness::Sync => quote!(), - Asyncness::Async => quote!(.await), - }; + let accept = endpoint.accept.as_ref().map_or_else( + || quote!(conjure_http::client::UnitResponseDeserializer), + |t| quote!(#t), + ); + let trait_ = match asyncness { + Asyncness::Sync => quote!(DeserializeResponse), + Asyncness::Async => quote!(AsyncDeserializeResponse), + }; + let await_ = match asyncness { + Asyncness::Sync => quote!(), + Asyncness::Async => quote!(.await), + }; - quote! { - <#accept as conjure_http::client::#trait_<_, _>>::deserialize(#response) #await_ - } - } - None => quote!(conjure_http::private::Result::Ok(())), + quote! { + <#accept as conjure_http::client::#trait_<_, _>>::deserialize(#response) #await_ } } From 1ddd5004f979ab8521916de0e29cad10d05aa8c9 Mon Sep 17 00:00:00 2001 From: Steven Fackler Date: Wed, 10 May 2023 10:07:51 -0400 Subject: [PATCH 02/10] merge encoders --- conjure-http/src/client.rs | 77 ++++++++++++++++++-------------- conjure-macros/src/client.rs | 6 +-- conjure-test/src/test/clients.rs | 17 +++---- 3 files changed, 55 insertions(+), 45 deletions(-) diff --git a/conjure-http/src/client.rs b/conjure-http/src/client.rs index 92f72fcb..186644b1 100644 --- a/conjure-http/src/client.rs +++ b/conjure-http/src/client.rs @@ -414,10 +414,21 @@ pub trait EncodeHeader { fn encode(value: T) -> Result, Error>; } -/// A header encoder which converts values via their `Display` implementation. -pub enum DisplayHeaderEncoder {} +/// A trait implemented by URL parameter encoders used by custom Conjure client trait +/// implementations. +pub trait EncodeParam { + /// Encodes the value into a sequence of parameters. + /// + /// When used with a path parameter, each returned string will be a separate path component. + /// When used with a query parameter, each returned string will be the value of a separate query + /// entry. + fn encode(value: T) -> Result, Error>; +} -impl EncodeHeader for DisplayHeaderEncoder +/// An encoder which converts values via their `Display` implementation. +pub enum DisplayEncoder {} + +impl EncodeHeader for DisplayEncoder where T: Display, { @@ -428,11 +439,20 @@ where } } -/// A header encoder which converts a sequence of values via their individual `Display` +impl EncodeParam for DisplayEncoder +where + T: Display, +{ + fn encode(value: T) -> Result, Error> { + Ok(vec![value.to_string()]) + } +} + +/// An encoder which converts a sequence of values via their individual `Display` /// implementations. -pub enum DisplaySeqHeaderEncoder {} +pub enum DisplaySeqEncoder {} -impl EncodeHeader for DisplaySeqHeaderEncoder +impl EncodeHeader for DisplaySeqEncoder where T: IntoIterator, U: Display, @@ -445,34 +465,7 @@ where } } -/// A trait implemented by URL parameter encoders used by custom Conjure client trait -/// implementations. -pub trait EncodeParam { - /// Encodes the value into a sequence of parameters. - /// - /// When used with a path parameter, each returned string will be a separate path component. - /// When used with a query parameter, each returned string will be the value of a separate query - /// entry. - fn encode(value: T) -> Result, Error>; -} - -/// A param encoder which converts values via their `Display` implementations. -pub enum DisplayParamEncoder {} - -impl EncodeParam for DisplayParamEncoder -where - T: Display, -{ - fn encode(value: T) -> Result, Error> { - Ok(vec![value.to_string()]) - } -} - -/// A param encoder which converts a sequence of values via their individual `Display` -/// implementations. -pub enum DisplaySeqParamEncoder {} - -impl EncodeParam for DisplaySeqParamEncoder +impl EncodeParam for DisplaySeqEncoder where T: IntoIterator, U: Display, @@ -481,3 +474,19 @@ where Ok(value.into_iter().map(|v| v.to_string()).collect()) } } + +#[allow(missing_docs)] +#[deprecated(note = "use DisplayEncoder instead", since = "3.7.0")] +pub type DisplayHeaderEncoder = DisplayEncoder; + +#[allow(missing_docs)] +#[deprecated(note = "use DisplayEncoder instead", since = "3.7.0")] +pub type DisplayParamEncoder = DisplayEncoder; + +#[allow(missing_docs)] +#[deprecated(note = "use DisplaySeqEncoder instead", since = "3.7.0")] +pub type DisplaySeqHeaderEncoder = DisplaySeqEncoder; + +#[allow(missing_docs)] +#[deprecated(note = "use DisplaySeqEncoder instead", since = "3.7.0")] +pub type DisplaySeqParamEncoder = DisplaySeqEncoder; diff --git a/conjure-macros/src/client.rs b/conjure-macros/src/client.rs index 9e67cf48..022a6c54 100644 --- a/conjure-macros/src/client.rs +++ b/conjure-macros/src/client.rs @@ -335,7 +335,7 @@ fn add_path_components( let ident = ¶m.ident; let encoder = param.attr.encoder.as_ref().map_or_else( - || quote!(conjure_http::client::DisplayParamEncoder), + || quote!(conjure_http::client::DisplayEncoder), |e| quote!(#e), ); @@ -365,7 +365,7 @@ fn add_query_arg(builder: &TokenStream, arg: &Arg) -> TokenStream { let name = percent_encoding::percent_encode(arg.attr.name.value().as_bytes(), COMPONENT).to_string(); let encoder = arg.attr.encoder.as_ref().map_or_else( - || quote!(conjure_http::client::DisplayParamEncoder), + || quote!(conjure_http::client::DisplayEncoder), |e| quote!(#e), ); @@ -460,7 +460,7 @@ fn add_header(request: &TokenStream, arg: &Arg) -> TokenStream { let ident = &arg.ident; let name = header_name.as_str(); let encoder = arg.attr.encoder.as_ref().map_or_else( - || quote!(conjure_http::client::DisplayHeaderEncoder), + || quote!(conjure_http::client::DisplayEncoder), |v| quote!(#v), ); diff --git a/conjure-test/src/test/clients.rs b/conjure-test/src/test/clients.rs index 2033d8e7..804520e5 100644 --- a/conjure-test/src/test/clients.rs +++ b/conjure-test/src/test/clients.rs @@ -18,8 +18,7 @@ use async_trait::async_trait; use conjure_error::Error; use conjure_http::client::{ AsyncClient, AsyncRequestBody, AsyncService, AsyncWriteBody, Client, - ConjureResponseDeserializer, DisplaySeqHeaderEncoder, DisplaySeqParamEncoder, RequestBody, - Service, WriteBody, + ConjureResponseDeserializer, DisplaySeqEncoder, RequestBody, Service, WriteBody, }; use conjure_macros::{conjure_client, endpoint}; use conjure_object::{BearerToken, ResourceIdentifier}; @@ -221,21 +220,22 @@ trait CustomService { fn query_param( &self, #[query(name = "normal")] normal: &str, - #[query(name = "list", encoder = DisplaySeqParamEncoder)] list: &[i32], + #[query(name = "list", encoder = DisplaySeqEncoder)] list: &[i32], ) -> Result<(), Error>; #[endpoint(method = GET, path = "/test/pathParams/{foo}/raw/{multi}")] fn path_param( &self, #[path] foo: &str, - #[path(encoder = DisplaySeqParamEncoder)] multi: &[&str], + #[path(encoder = DisplaySeqEncoder)] multi: &[&str], ) -> Result<(), Error>; #[endpoint(method = GET, path = "/test/headers")] fn headers( &self, #[header(name = "Some-Custom-Header")] custom_header: &str, - #[header(name = "Some-Optional-Header", encoder = DisplaySeqHeaderEncoder)] optional_header: Option, + #[header(name = "Some-Optional-Header", encoder = DisplaySeqEncoder)] + optional_header: Option, ) -> Result<(), Error>; #[endpoint(method = POST, path = "/test/jsonRequest")] @@ -261,21 +261,22 @@ trait CustomServiceAsync { async fn query_param( &self, #[query(name = "normal")] normal: &str, - #[query(name = "list", encoder = DisplaySeqParamEncoder)] list: &[i32], + #[query(name = "list", encoder = DisplaySeqEncoder)] list: &[i32], ) -> Result<(), Error>; #[endpoint(method = GET, path = "/test/pathParams/{foo}/raw/{multi}")] async fn path_param( &self, #[path] foo: &str, - #[path(encoder = DisplaySeqParamEncoder)] multi: &[&str], + #[path(encoder = DisplaySeqEncoder)] multi: &[&str], ) -> Result<(), Error>; #[endpoint(method = GET, path = "/test/headers")] async fn headers( &self, #[header(name = "Some-Custom-Header")] custom_header: &str, - #[header(name = "Some-Optional-Header", encoder = DisplaySeqHeaderEncoder)] optional_header: Option, + #[header(name = "Some-Optional-Header", encoder = DisplaySeqEncoder)] + optional_header: Option, ) -> Result<(), Error>; #[endpoint(method = POST, path = "/test/jsonRequest")] From ebac81af933af72bc3ad34f8a014ef1ee2bf03fd Mon Sep 17 00:00:00 2001 From: Steven Fackler Date: Wed, 10 May 2023 12:26:43 -0400 Subject: [PATCH 03/10] improve client error reporting --- conjure-macros/src/client.rs | 488 ++++++++++++++++++++++------------- 1 file changed, 312 insertions(+), 176 deletions(-) diff --git a/conjure-macros/src/client.rs b/conjure-macros/src/client.rs index 022a6c54..6698e5a2 100644 --- a/conjure-macros/src/client.rs +++ b/conjure-macros/src/client.rs @@ -12,16 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. use crate::path::{self, PathComponent}; -use crate::Asyncness; +use crate::{Asyncness, Errors}; use http::HeaderName; use percent_encoding::AsciiSet; -use proc_macro2::{Ident, TokenStream}; +use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use std::collections::HashMap; use structmeta::StructMeta; +use syn::spanned::Spanned; use syn::{ - parse_macro_input, Error, FnArg, ItemTrait, LitStr, Meta, Pat, ReturnType, TraitItem, - TraitItemFn, Type, + parse_macro_input, Error, FnArg, ItemTrait, LitStr, Meta, Pat, PatType, ReturnType, TraitItem, + TraitItemFn, Type, Visibility, }; // https://url.spec.whatwg.org/#query-percent-encode-set @@ -56,8 +57,11 @@ pub fn generate( item: proc_macro::TokenStream, ) -> proc_macro::TokenStream { let mut item = parse_macro_input!(item as ItemTrait); - - let client = generate_client(&mut item); + let service = match Service::new(&mut item) { + Ok(service) => service, + Err(e) => return e.into_compile_error().into(), + }; + let client = generate_client(&service); quote! { #item @@ -67,27 +71,22 @@ pub fn generate( .into() } -fn generate_client(trait_: &mut ItemTrait) -> TokenStream { - let vis = &trait_.vis; - let trait_name = &trait_.ident; +fn generate_client(service: &Service) -> TokenStream { + let vis = &service.vis; + let trait_name = &service.name; let type_name = Ident::new(&format!("{}Client", trait_name), trait_name.span()); - let asyncness = match Asyncness::resolve(trait_) { - Ok(asyncness) => asyncness, - Err(e) => return e.into_compile_error(), - }; - - let service_trait = match asyncness { + let service_trait = match service.asyncness { Asyncness::Sync => quote!(Service), Asyncness::Async => quote!(AsyncService), }; - let impl_attrs = match asyncness { + let impl_attrs = match service.asyncness { Asyncness::Sync => quote!(), Asyncness::Async => quote!(#[conjure_http::private::async_trait]), }; - let where_ = match asyncness { + let where_ = match service.asyncness { Asyncness::Sync => quote!(C: conjure_http::client::Client), Asyncness::Async => quote! { C: conjure_http::client::AsyncClient + Sync + Send, @@ -95,14 +94,10 @@ fn generate_client(trait_: &mut ItemTrait) -> TokenStream { }, }; - let methods = trait_ - .items - .iter_mut() - .filter_map(|item| match item { - TraitItem::Fn(meth) => Some(meth), - _ => None, - }) - .map(|m| generate_client_method(trait_name, asyncness, m)); + let methods = service + .endpoints + .iter() + .map(|endpoint| generate_client_method(service, endpoint)); quote! { #vis struct #type_name { @@ -124,73 +119,44 @@ fn generate_client(trait_: &mut ItemTrait) -> TokenStream { } } -fn generate_client_method( - trait_name: &Ident, - asyncness: Asyncness, - method: &mut TraitItemFn, -) -> TokenStream { - let mut endpoint_attrs = method - .attrs - .iter() - .filter(|attr| attr.path().is_ident("endpoint")); - let Some(endpoint_attr) = endpoint_attrs.next() else { - return Error::new_spanned(method, "missing #[endpoint] attribute").into_compile_error(); - }; - let endpoint = match endpoint_attr.parse_args::() { - Ok(endpoint) => endpoint, - Err(e) => return e.into_compile_error(), - }; - let duplicates = endpoint_attrs - .map(|a| Error::new_spanned(a, "duplicate #[endpoint] attribute").into_compile_error()) - .collect::>(); - if !duplicates.is_empty() { - return quote!(#(#duplicates)*); - } - - let async_ = match asyncness { +fn generate_client_method(service: &Service, endpoint: &Endpoint) -> TokenStream { + let async_ = match service.asyncness { Asyncness::Sync => quote!(), Asyncness::Async => quote!(async), }; - let client_trait = match asyncness { + let client_trait = match service.asyncness { Asyncness::Sync => quote!(Client), Asyncness::Async => quote!(AsyncClient), }; - let await_ = match asyncness { + let await_ = match service.asyncness { Asyncness::Sync => quote!(), Asyncness::Async => quote!(.await), }; - let request_args = match method - .sig - .inputs - .iter_mut() - .flat_map(|a| ArgType::new(a).transpose()) - .collect::, _>>() - { - Ok(request_args) => request_args, - Err(e) => return e.into_compile_error(), - }; - - let name = &method.sig.ident; - let args = &method.sig.inputs; - let ret = &method.sig.output; + let name = &endpoint.ident; + let args = endpoint.args.iter().map(|a| { + let ident = a.ident(); + let ty = a.ty(); + quote!(#ident: #ty) + }); + let ret_ty = &endpoint.ret_ty; let request = quote!(__request); let response = quote!(__response); - let http_method = &endpoint.method; + let http_method = &endpoint.params.method; - let create_request = create_request(asyncness, &request, &request_args); - let add_path = add_path(&request, &request_args, &endpoint); - let add_accept = add_accept(asyncness, &request, &endpoint, &method.sig.output); - let add_auth = add_auth(&request, &request_args); - let add_headers = add_headers(&request, &request_args); - let add_endpoint = add_endpoint(trait_name, method, &endpoint, &request); - let handle_response = handle_response(asyncness, &endpoint, &response); + let create_request = create_request(&request, service, endpoint); + let add_path = add_path(&request, endpoint); + let add_accept = add_accept(&request, service, endpoint); + let add_auth = add_auth(&request, endpoint); + let add_headers = add_headers(&request, endpoint); + let add_endpoint = add_endpoint(&request, service, endpoint); + let handle_response = handle_response(&response, service, endpoint); quote! { - #async_ fn #name(#args) #ret { + #async_ fn #name(&self #(, #args)*) -> #ret_ty { #create_request *#request.method_mut() = conjure_http::private::Method::#http_method; #add_path @@ -204,13 +170,13 @@ fn generate_client_method( } } -fn create_request(asyncness: Asyncness, request: &TokenStream, args: &[ArgType]) -> TokenStream { - let mut it = args.iter().filter_map(|a| match a { +fn create_request(request: &TokenStream, service: &Service, endpoint: &Endpoint) -> TokenStream { + let arg = endpoint.args.iter().find_map(|a| match a { ArgType::Body(arg) => Some(arg), _ => None, }); - let Some(arg) = it.next() else { - let body = match asyncness { + let Some(arg) = arg else { + let body = match service.asyncness { Asyncness::Sync => quote!(RequestBody), Asyncness::Async => quote!(AsyncRequestBody), }; @@ -221,12 +187,7 @@ fn create_request(asyncness: Asyncness, request: &TokenStream, args: &[ArgType]) }; }; - if let Some(arg) = it.next() { - return Error::new_spanned(&arg.ident, "only one #[body] argument allowed") - .into_compile_error(); - } - - let trait_ = match asyncness { + let trait_ = match service.asyncness { Asyncness::Sync => quote!(SerializeRequest), Asyncness::Async => quote!(AsyncSerializeRequest), }; @@ -262,16 +223,13 @@ fn create_request(asyncness: Asyncness, request: &TokenStream, args: &[ArgType]) } } -fn add_path( - request: &TokenStream, - request_args: &[ArgType], - endpoint: &EndpointConfig, -) -> TokenStream { +fn add_path(request: &TokenStream, endpoint: &Endpoint) -> TokenStream { let builder = quote!(__path); - let path_writes = add_path_components(&endpoint.path, &builder, request_args); + let path_writes = add_path_components(&builder, endpoint); - let query_params = request_args + let query_params = endpoint + .args .iter() .filter_map(|arg| match arg { ArgType::Query(arg) => Some(arg), @@ -287,17 +245,9 @@ fn add_path( } } -fn add_path_components( - path_lit: &LitStr, - builder: &TokenStream, - request_args: &[ArgType], -) -> TokenStream { - let path = match path::parse(path_lit) { - Ok(path) => path, - Err(e) => return e.into_compile_error(), - }; - - let path_params = request_args +fn add_path_components(builder: &TokenStream, endpoint: &Endpoint) -> TokenStream { + let path_params = endpoint + .args .iter() .filter_map(|a| match a { ArgType::Path(param) => Some((param.ident.to_string(), param)), @@ -307,7 +257,7 @@ fn add_path_components( let mut path_writes = vec![]; let mut literal_buf = String::new(); - for component in path { + for component in &endpoint.path { match component { PathComponent::Literal(lit) => { literal_buf.push('/'); @@ -323,15 +273,7 @@ fn add_path_components( literal_buf = String::new(); } - let Some(param) = path_params.get(¶m) else { - path_writes.push( - Error::new_spanned( - path_lit, - format_args!("invalid path parameter `{param}`"), - ).into_compile_error(), - ); - continue; - }; + let param = path_params[param]; let ident = ¶m.ident; let encoder = param.attr.encoder.as_ref().map_or_else( @@ -377,29 +319,21 @@ fn add_query_arg(builder: &TokenStream, arg: &Arg) -> TokenStream { } } -fn add_accept( - asyncness: Asyncness, - request: &TokenStream, - endpoint: &EndpointConfig, - ret_ty: &ReturnType, -) -> TokenStream { - let Some(accept) = &endpoint.accept else { +fn add_accept(request: &TokenStream, service: &Service, endpoint: &Endpoint) -> TokenStream { + let Some(accept) = &endpoint.params.accept else { return quote!(); }; - let trait_ = match asyncness { + let trait_ = match service.asyncness { Asyncness::Sync => quote!(DeserializeResponse), Asyncness::Async => quote!(AsyncDeserializeResponse), }; - let ret = match ret_ty { - ReturnType::Default => quote!(()), - ReturnType::Type(_, ty) => quote!(#ty), - }; + let ret_ty = &endpoint.ret_ty; quote! { let __accept = <#accept as conjure_http::client::#trait_< - <#ret as conjure_http::private::ExtractOk>::Ok, + <#ret_ty as conjure_http::private::ExtractOk>::Ok, C::ResponseBody, >>::accept(); if let Some(__accept) = __accept { @@ -408,8 +342,8 @@ fn add_accept( } } -fn add_auth(request: &TokenStream, args: &[ArgType]) -> TokenStream { - let mut it = args.iter().filter_map(|a| match a { +fn add_auth(request: &TokenStream, endpoint: &Endpoint) -> TokenStream { + let mut it = endpoint.args.iter().filter_map(|a| match a { ArgType::Auth(auth) => Some(auth), _ => None, }); @@ -437,8 +371,9 @@ fn add_auth(request: &TokenStream, args: &[ArgType]) -> TokenStream { } } -fn add_headers(request: &TokenStream, args: &[ArgType]) -> TokenStream { - let add_headers = args +fn add_headers(request: &TokenStream, endpoint: &Endpoint) -> TokenStream { + let add_headers = endpoint + .args .iter() .filter_map(|arg| match arg { ArgType::Header(arg) => Some(arg), @@ -475,15 +410,10 @@ fn add_header(request: &TokenStream, arg: &Arg) -> TokenStream { } } -fn add_endpoint( - trait_name: &Ident, - method: &TraitItemFn, - endpoint: &EndpointConfig, - request: &TokenStream, -) -> TokenStream { - let service = trait_name.to_string(); - let name = method.sig.ident.to_string(); - let path = &endpoint.path; +fn add_endpoint(request: &TokenStream, service: &Service, endpoint: &Endpoint) -> TokenStream { + let service = service.name.to_string(); + let name = endpoint.ident.to_string(); + let path = &endpoint.params.path; quote! { #request.extensions_mut().insert(conjure_http::client::Endpoint::new( @@ -495,20 +425,16 @@ fn add_endpoint( } } -fn handle_response( - asyncness: Asyncness, - endpoint: &EndpointConfig, - response: &TokenStream, -) -> TokenStream { - let accept = endpoint.accept.as_ref().map_or_else( +fn handle_response(response: &TokenStream, service: &Service, endpoint: &Endpoint) -> TokenStream { + let accept = endpoint.params.accept.as_ref().map_or_else( || quote!(conjure_http::client::UnitResponseDeserializer), |t| quote!(#t), ); - let trait_ = match asyncness { + let trait_ = match service.asyncness { Asyncness::Sync => quote!(DeserializeResponse), Asyncness::Async => quote!(AsyncDeserializeResponse), }; - let await_ = match asyncness { + let await_ = match service.asyncness { Asyncness::Sync => quote!(), Asyncness::Async => quote!(.await), }; @@ -518,8 +444,198 @@ fn handle_response( } } +struct Service { + vis: Visibility, + name: Ident, + asyncness: Asyncness, + endpoints: Vec, +} + +impl Service { + fn new(trait_: &mut ItemTrait) -> Result { + let mut errors = Errors::new(); + let mut endpoints = vec![]; + for item in &trait_.items { + match Endpoint::new(item) { + Ok(endpoint) => endpoints.push(endpoint), + Err(e) => errors.push(e), + } + } + + let asyncness = match Asyncness::resolve(trait_) { + Ok(asyncness) => Some(asyncness), + Err(e) => { + errors.push(e); + None + } + }; + + strip_trait(trait_); + errors.build()?; + + Ok(Service { + vis: trait_.vis.clone(), + name: trait_.ident.clone(), + asyncness: asyncness.unwrap(), + endpoints, + }) + } +} + +// Rust doesn't support helper attributes in attribute macros so we need to manually strip them out +fn strip_trait(trait_: &mut ItemTrait) { + for item in &mut trait_.items { + if let TraitItem::Fn(fn_) = item { + strip_fn(fn_); + } + } +} + +fn strip_fn(fn_: &mut TraitItemFn) { + for arg in &mut fn_.sig.inputs { + strip_arg(arg); + } +} + +fn strip_arg(arg: &mut FnArg) { + let FnArg::Typed(arg) = arg else { return }; + + arg.attrs.retain(|attr| { + !["path", "query", "header", "body", "auth"] + .iter() + .any(|v| attr.path().is_ident(v)) + }); +} + +struct Endpoint { + ident: Ident, + args: Vec, + ret_ty: Type, + params: EndpointParams, + path: Vec, +} + +impl Endpoint { + fn new(item: &TraitItem) -> Result { + let TraitItem::Fn(item) = item else { + return Err(Error::new_spanned(item, "Conjure traits may only contain methods")); + }; + + let mut errors = Errors::new(); + + let mut endpoint_attrs = item + .attrs + .iter() + .filter(|attr| attr.path().is_ident("endpoint")); + let params = endpoint_attrs + .next() + .ok_or_else(|| Error::new_spanned(item, "missing #[endpoint] attribute")) + .and_then(|a| a.parse_args::()); + let params = match params { + Ok(params) => Some(params), + Err(e) => { + errors.push(e); + None + } + }; + + let mut args = vec![]; + for arg in &item.sig.inputs { + // Ignore the self arg. + let FnArg::Typed(arg) = arg else { continue }; + + match ArgType::new(arg) { + Ok(arg) => args.push(arg), + Err(e) => errors.push(e), + } + } + + let ret_ty = match &item.sig.output { + ReturnType::Default => { + errors.push(Error::new_spanned( + &item.sig.output, + "expected a return type", + )); + None + } + ReturnType::Type(_, ty) => Some((**ty).clone()), + }; + + let path = match params.as_ref().map(|p| path::parse(&p.path)).transpose() { + Ok(path) => path, + Err(e) => { + errors.push(e); + None + } + }; + + if let Err(e) = validate_args(&args, params.as_ref().map(|p| &p.path), path.as_deref()) { + errors.push(e); + } + + errors.build()?; + + Ok(Endpoint { + ident: item.sig.ident.clone(), + args, + ret_ty: ret_ty.unwrap(), + params: params.unwrap(), + path: path.unwrap(), + }) + } +} + +fn validate_args( + args: &[ArgType], + path: Option<&LitStr>, + path_components: Option<&[PathComponent]>, +) -> Result<(), Error> { + let mut errors = Errors::new(); + + let mut body_args = args.iter().filter(|a| matches!(a, ArgType::Body(_))); + if body_args.next().is_some() { + for arg in body_args { + errors.push(Error::new(arg.span(), "duplicate `#[body]` arg")); + } + } + + let mut auth_args = args.iter().filter(|a| matches!(a, ArgType::Auth(_))); + if auth_args.next().is_some() { + for arg in auth_args { + errors.push(Error::new(arg.span(), "duplicate `#[auth]` arg")); + } + } + + if let (Some(path), Some(path_components)) = (path, path_components) { + let mut path_params = args + .iter() + .filter_map(|a| match a { + ArgType::Path(arg) => Some((arg.ident.to_string(), arg.span)), + _ => None, + }) + .collect::>(); + + for component in path_components { + let PathComponent::Parameter(param) = component else { continue }; + + if path_params.remove(param).is_none() { + errors.push(Error::new_spanned( + path, + format!("invalid path parameter `{param}`"), + )); + } + } + + for span in path_params.values() { + errors.push(Error::new(*span, "unused path parameter")); + } + } + + errors.build() +} + #[derive(StructMeta)] -struct EndpointConfig { +struct EndpointParams { method: Ident, path: LitStr, accept: Option, @@ -533,8 +649,42 @@ enum ArgType { Body(Arg), } +impl ArgType { + fn ident(&self) -> &Ident { + match self { + ArgType::Path(arg) => &arg.ident, + ArgType::Query(arg) => &arg.ident, + ArgType::Header(arg) => &arg.ident, + ArgType::Auth(arg) => &arg.ident, + ArgType::Body(arg) => &arg.ident, + } + } + + fn ty(&self) -> &Type { + match self { + ArgType::Path(arg) => &arg.ty, + ArgType::Query(arg) => &arg.ty, + ArgType::Header(arg) => &arg.ty, + ArgType::Auth(arg) => &arg.ty, + ArgType::Body(arg) => &arg.ty, + } + } + + fn span(&self) -> Span { + match self { + ArgType::Path(arg) => arg.span, + ArgType::Query(arg) => arg.span, + ArgType::Header(arg) => arg.span, + ArgType::Auth(arg) => arg.span, + ArgType::Body(arg) => arg.span, + } + } +} + struct Arg { ident: Ident, + ty: Type, + span: Span, attr: T, } @@ -560,25 +710,17 @@ struct BodyAttr { } impl ArgType { - fn new(arg: &mut FnArg) -> syn::Result> { - // Ignore the self arg. - let FnArg::Typed(pat_type) = arg else { return Ok(None); }; - + fn new(arg: &PatType) -> syn::Result { // FIXME we should probably just rename the arguments in our impl? - let ident = match &*pat_type.pat { + let ident = match &*arg.pat { Pat::Ident(pat_ident) => &pat_ident.ident, - _ => { - return Err(Error::new_spanned( - &pat_type.pat, - "expected an ident pattern", - )) - } + _ => return Err(Error::new_spanned(&arg.pat, "expected an ident pattern")), }; let mut type_ = None; // FIXME detect multiple attrs - for attr in &pat_type.attrs { + for attr in &arg.attrs { if attr.path().is_ident("path") { let attr = match attr.meta { Meta::Path(_) => PathAttr { encoder: None }, @@ -586,16 +728,22 @@ impl ArgType { }; type_ = Some(ArgType::Path(Arg { ident: ident.clone(), + ty: (*arg.ty).clone(), + span: arg.span(), attr, })); } else if attr.path().is_ident("query") { type_ = Some(ArgType::Query(Arg { ident: ident.clone(), + ty: (*arg.ty).clone(), + span: arg.span(), attr: attr.parse_args()?, })); } else if attr.path().is_ident("header") { type_ = Some(ArgType::Header(Arg { ident: ident.clone(), + ty: (*arg.ty).clone(), + span: arg.span(), attr: attr.parse_args()?, })); } else if attr.path().is_ident("auth") { @@ -605,6 +753,8 @@ impl ArgType { }; type_ = Some(ArgType::Auth(Arg { ident: ident.clone(), + ty: (*arg.ty).clone(), + span: arg.span(), attr, })); } else if attr.path().is_ident("body") { @@ -614,27 +764,13 @@ impl ArgType { }; type_ = Some(ArgType::Body(Arg { ident: ident.clone(), + ty: (*arg.ty).clone(), + span: arg.span(), attr, })); } } - // Rust doesn't support "helper" attributes in attribute macros, so we need to strip out our - // helper attributes on arguments. - strip_arg_attrs(arg); - - type_ - .ok_or_else(|| Error::new_spanned(arg, "missing argument type annotation")) - .map(Some) + type_.ok_or_else(|| Error::new_spanned(arg, "missing parameter type attribute")) } } - -fn strip_arg_attrs(arg: &mut FnArg) { - let FnArg::Typed(arg) = arg else { return }; - - arg.attrs.retain(|attr| { - !["path", "query", "header", "body", "auth"] - .iter() - .any(|v| attr.path().is_ident(v)) - }); -} From 0eaefccf6e088438bdb8dd920af5bfbc5ca14337 Mon Sep 17 00:00:00 2001 From: Steven Fackler Date: Wed, 10 May 2023 15:05:32 -0400 Subject: [PATCH 04/10] support client parameters --- conjure-macros/src/client.rs | 120 +++++++++++++++++++++++++------ conjure-test/src/test/clients.rs | 83 ++++++++++++++++++++- 2 files changed, 181 insertions(+), 22 deletions(-) diff --git a/conjure-macros/src/client.rs b/conjure-macros/src/client.rs index 6698e5a2..1e2574c4 100644 --- a/conjure-macros/src/client.rs +++ b/conjure-macros/src/client.rs @@ -21,8 +21,8 @@ use std::collections::HashMap; use structmeta::StructMeta; use syn::spanned::Spanned; use syn::{ - parse_macro_input, Error, FnArg, ItemTrait, LitStr, Meta, Pat, PatType, ReturnType, TraitItem, - TraitItemFn, Type, Visibility, + parse_macro_input, Error, FnArg, GenericParam, Generics, ItemTrait, LitStr, Meta, Pat, PatType, + ReturnType, TraitItem, TraitItemFn, Type, Visibility, }; // https://url.spec.whatwg.org/#query-percent-encode-set @@ -86,18 +86,47 @@ fn generate_client(service: &Service) -> TokenStream { Asyncness::Async => quote!(#[conjure_http::private::async_trait]), }; - let where_ = match service.asyncness { - Asyncness::Sync => quote!(C: conjure_http::client::Client), - Asyncness::Async => quote! { - C: conjure_http::client::AsyncClient + Sync + Send, - C::ResponseBody: 'static + Send, - }, + let (_, type_generics, _) = service.generics.split_for_impl(); + + let mut impl_generics = service.generics.clone(); + + let client_param = quote!(__C); + impl_generics.params.push(syn::parse2(quote!(__C)).unwrap()); + + let where_clause = impl_generics.make_where_clause(); + let client_trait = match service.asyncness { + Asyncness::Sync => quote!(Client), + Asyncness::Async => quote!(AsyncClient), }; + let mut client_bindings = vec![]; + if let Some(param) = &service.request_writer_param { + client_bindings.push(quote!(BodyWriter = #param)); + } + if let Some(param) = &service.response_body_param { + client_bindings.push(quote!(ResponseBody = #param)); + } + let extra_client_predicates = match service.asyncness { + Asyncness::Sync => quote!(), + Asyncness::Async => quote!(+ Sync + Send), + }; + where_clause.predicates.push( + syn::parse2(quote! { + #client_param: conjure_http::client::#client_trait<#(#client_bindings),*> #extra_client_predicates + }) + .unwrap(), + ); + if let Asyncness::Async = service.asyncness { + where_clause + .predicates + .push(syn::parse2(quote!(#client_param::ResponseBody: 'static + Send)).unwrap()); + } + + let (impl_generics, _, where_clause) = impl_generics.split_for_impl(); let methods = service .endpoints .iter() - .map(|endpoint| generate_client_method(service, endpoint)); + .map(|endpoint| generate_client_method(&client_param, service, endpoint)); quote! { #vis struct #type_name { @@ -111,15 +140,19 @@ fn generate_client(service: &Service) -> TokenStream { } #impl_attrs - impl #trait_name for #type_name - where #where_ + impl #impl_generics #trait_name #type_generics for #type_name<#client_param> + #where_clause { #(#methods)* } } } -fn generate_client_method(service: &Service, endpoint: &Endpoint) -> TokenStream { +fn generate_client_method( + client_param: &TokenStream, + service: &Service, + endpoint: &Endpoint, +) -> TokenStream { let async_ = match service.asyncness { Asyncness::Sync => quote!(), Asyncness::Async => quote!(async), @@ -147,9 +180,9 @@ fn generate_client_method(service: &Service, endpoint: &Endpoint) -> TokenStream let response = quote!(__response); let http_method = &endpoint.params.method; - let create_request = create_request(&request, service, endpoint); + let create_request = create_request(client_param, &request, service, endpoint); let add_path = add_path(&request, endpoint); - let add_accept = add_accept(&request, service, endpoint); + let add_accept = add_accept(client_param, &request, service, endpoint); let add_auth = add_auth(&request, endpoint); let add_headers = add_headers(&request, endpoint); let add_endpoint = add_endpoint(&request, service, endpoint); @@ -170,7 +203,12 @@ fn generate_client_method(service: &Service, endpoint: &Endpoint) -> TokenStream } } -fn create_request(request: &TokenStream, service: &Service, endpoint: &Endpoint) -> TokenStream { +fn create_request( + client_param: &TokenStream, + request: &TokenStream, + service: &Service, + endpoint: &Endpoint, +) -> TokenStream { let arg = endpoint.args.iter().find_map(|a| match a { ArgType::Body(arg) => Some(arg), _ => None, @@ -200,13 +238,13 @@ fn create_request(request: &TokenStream, service: &Service, endpoint: &Endpoint) quote! { let __content_type = < - #serializer as conjure_http::client::#trait_<_, C::BodyWriter> + #serializer as conjure_http::client::#trait_<_, #client_param::BodyWriter> >::content_type(&#ident); let __content_length = < - #serializer as conjure_http::client::#trait_<_, C::BodyWriter> + #serializer as conjure_http::client::#trait_<_, #client_param::BodyWriter> >::content_length(&#ident); let __body = < - #serializer as conjure_http::client::#trait_<_, C::BodyWriter> + #serializer as conjure_http::client::#trait_<_, #client_param::BodyWriter> >::serialize(#ident)?; let mut #request = conjure_http::private::Request::new(__body); @@ -319,7 +357,12 @@ fn add_query_arg(builder: &TokenStream, arg: &Arg) -> TokenStream { } } -fn add_accept(request: &TokenStream, service: &Service, endpoint: &Endpoint) -> TokenStream { +fn add_accept( + client_param: &TokenStream, + request: &TokenStream, + service: &Service, + endpoint: &Endpoint, +) -> TokenStream { let Some(accept) = &endpoint.params.accept else { return quote!(); }; @@ -334,7 +377,7 @@ fn add_accept(request: &TokenStream, service: &Service, endpoint: &Endpoint) -> quote! { let __accept = <#accept as conjure_http::client::#trait_< <#ret_ty as conjure_http::private::ExtractOk>::Ok, - C::ResponseBody, + #client_param::ResponseBody, >>::accept(); if let Some(__accept) = __accept { #request.headers_mut().insert(conjure_http::private::header::ACCEPT, __accept); @@ -447,6 +490,9 @@ fn handle_response(response: &TokenStream, service: &Service, endpoint: &Endpoin struct Service { vis: Visibility, name: Ident, + generics: Generics, + request_writer_param: Option, + response_body_param: Option, asyncness: Asyncness, endpoints: Vec, } @@ -470,12 +516,32 @@ impl Service { } }; + let mut request_writer_param = None; + let mut response_body_param = None; + for param in &trait_.generics.params { + let GenericParam::Type(param) = param else { + errors.push(Error::new_spanned(param, "unexpected parameter")); + continue; + }; + + for attr in ¶m.attrs { + if attr.path().is_ident("request_writer") { + request_writer_param = Some(param.ident.clone()); + } else if attr.path().is_ident("response_body") { + response_body_param = Some(param.ident.clone()); + } + } + } + strip_trait(trait_); errors.build()?; Ok(Service { vis: trait_.vis.clone(), name: trait_.ident.clone(), + generics: trait_.generics.clone(), + request_writer_param, + response_body_param, asyncness: asyncness.unwrap(), endpoints, }) @@ -484,6 +550,10 @@ impl Service { // Rust doesn't support helper attributes in attribute macros so we need to manually strip them out fn strip_trait(trait_: &mut ItemTrait) { + for param in &mut trait_.generics.params { + strip_param(param); + } + for item in &mut trait_.items { if let TraitItem::Fn(fn_) = item { strip_fn(fn_); @@ -491,6 +561,16 @@ fn strip_trait(trait_: &mut ItemTrait) { } } +fn strip_param(param: &mut GenericParam) { + let GenericParam::Type(param) = param else { return }; + + param.attrs.retain(|attr| { + !["request_writer", "response_body"] + .iter() + .any(|v| attr.path().is_ident(v)) + }); +} + fn strip_fn(fn_: &mut TraitItemFn) { for arg in &mut fn_.sig.inputs { strip_arg(arg); diff --git a/conjure-test/src/test/clients.rs b/conjure-test/src/test/clients.rs index 804520e5..48315ee9 100644 --- a/conjure-test/src/test/clients.rs +++ b/conjure-test/src/test/clients.rs @@ -18,14 +18,16 @@ use async_trait::async_trait; use conjure_error::Error; use conjure_http::client::{ AsyncClient, AsyncRequestBody, AsyncService, AsyncWriteBody, Client, - ConjureResponseDeserializer, DisplaySeqEncoder, RequestBody, Service, WriteBody, + ConjureResponseDeserializer, DeserializeResponse, DisplaySeqEncoder, RequestBody, + SerializeRequest, Service, WriteBody, }; use conjure_macros::{conjure_client, endpoint}; use conjure_object::{BearerToken, ResourceIdentifier}; use futures::executor; use http::header::CONTENT_TYPE; -use http::{HeaderMap, Method, Request, Response, StatusCode}; +use http::{HeaderMap, HeaderValue, Method, Request, Response, StatusCode}; use std::collections::{BTreeMap, BTreeSet}; +use std::io::Write; use std::pin::Pin; struct StreamingBody<'a>(&'a [u8]); @@ -630,3 +632,80 @@ fn cookie_auth() { client.cookie_auth(&BearerToken::new("fizzbuzz").unwrap()) ); } + +#[conjure_client] +trait CustomStreamingService<#[response_body] I, #[request_writer] O> +where + O: Write, +{ + #[endpoint(method = POST, path = "/test/streamingRequest")] + fn streaming_request( + &self, + #[body(serializer = RawRequestSerializer)] body: &mut RawRequest, + ) -> Result<(), Error>; + + #[endpoint(method = GET, path = "/test/streamingResponse", accept = RawResponseDeserializer)] + fn streaming_response(&self) -> Result; +} + +struct RawRequest; + +impl WriteBody for RawRequest +where + W: Write, +{ + fn write_body(&mut self, w: &mut W) -> Result<(), Error> { + w.write_all(b"hello world").map_err(Error::internal_safe) + } + + fn reset(&mut self) -> bool { + true + } +} + +enum RawRequestSerializer {} + +impl<'a, W> SerializeRequest<'a, &'a mut RawRequest, W> for RawRequestSerializer +where + W: Write, +{ + fn content_type(_: &&mut RawRequest) -> HeaderValue { + HeaderValue::from_static("text/plain") + } + + fn serialize(value: &'a mut RawRequest) -> Result, Error> { + Ok(RequestBody::Streaming(value)) + } +} + +enum RawResponseDeserializer {} + +impl DeserializeResponse for RawResponseDeserializer { + fn accept() -> Option { + None + } + + fn deserialize(response: Response) -> Result { + Ok(response.into_body()) + } +} + +#[test] +fn custom_streaming_request() { + let client = TestClient::new(Method::POST, "/test/streamingRequest") + .header("Content-Type", "text/plain") + .body(TestBody::Streaming(b"hello world".to_vec())); + + CustomStreamingServiceClient::new(&client) + .streaming_request(&mut RawRequest) + .unwrap(); +} + +#[test] +fn custom_streaming_response() { + let client = TestClient::new(Method::GET, "/test/streamingResponse"); + + CustomStreamingServiceClient::new(&client) + .streaming_response() + .unwrap(); +} From c9be95d6f959437ea5be21686d0c5528f4172688 Mon Sep 17 00:00:00 2001 From: Steven Fackler Date: Wed, 10 May 2023 15:36:05 -0400 Subject: [PATCH 05/10] docs --- conjure-macros/src/lib.rs | 74 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/conjure-macros/src/lib.rs b/conjure-macros/src/lib.rs index 4920e4a3..bc0434b3 100644 --- a/conjure-macros/src/lib.rs +++ b/conjure-macros/src/lib.rs @@ -44,6 +44,11 @@ mod path; /// For a trait named `MyService`, the macro will create a type named `MyServiceClient` which /// implements the Conjure `Client` and `MyService` traits. /// +/// # Parameters +/// +/// The trait can optionally be declared generic over the request body and response writer types by +/// using the `#[request_writer]` and `#[response_body]` annotations on the type parameters. +/// /// # Endpoints /// /// Each method corresponds to a separate HTTP endpoint, and is expected to take `&self` and return @@ -101,10 +106,13 @@ mod path; /// use conjure_error::Error; /// use conjure_http::{conjure_client, endpoint}; /// use conjure_http::client::{ -/// AsyncClient, AsyncService, Client, ConjureResponseDeserializer, DisplaySeqParamEncoder, -/// Service, +/// AsyncClient, AsyncService, Client, ConjureResponseDeserializer, DeserializeResponse, +/// DisplaySeqEncoder, RequestBody, SerializeRequest, Service, WriteBody, /// }; /// use conjure_object::BearerToken; +/// use http::Response; +/// use http::header::HeaderValue; +/// use std::io::Write; /// /// #[conjure_client] /// trait MyService { @@ -115,7 +123,7 @@ mod path; /// fn create_yak( /// &self, /// #[auth] auth_token: &BearerToken, -/// #[query(name = "parentName", encoder = DisplaySeqParamEncoder)] parent_id: Option<&str>, +/// #[query(name = "parentName", encoder = DisplaySeqEncoder)] parent_id: Option<&str>, /// #[body] yak: &str, /// ) -> Result<(), Error>; /// } @@ -141,7 +149,7 @@ mod path; /// async fn create_yak( /// &self, /// #[auth] auth_token: &BearerToken, -/// #[query(name = "parentName", encoder = DisplaySeqParamEncoder)] parent_id: Option<&str>, +/// #[query(name = "parentName", encoder = DisplaySeqEncoder)] parent_id: Option<&str>, /// #[body] yak: &str, /// ) -> Result<(), Error>; /// } @@ -156,6 +164,64 @@ mod path; /// /// Ok(()) /// } +/// +/// #[conjure_client] +/// trait MyStreamingService<#[response_body] I, #[request_writer] O> +/// where +/// O: Write, +/// { +/// #[endpoint(method = POST, path = "/streamData")] +/// fn upload_stream( +/// &self, +/// #[body(serializer = StreamingRequestSerializer)] body: &mut StreamingRequest, +/// ) -> Result<(), Error>; +/// +/// #[endpoint(method = GET, path = "/streamData", accept = StreamingResponseDeserializer)] +/// fn download_stream(&self) -> Result; +/// } +/// +/// struct StreamingRequest; +/// +/// impl WriteBody for StreamingRequest +/// where +/// W: Write, +/// { +/// fn write_body(&mut self, w: &mut W) -> Result<(), Error> { +/// // ... +/// Ok(()) +/// } +/// +/// fn reset(&mut self) -> bool { +/// true +/// } +/// } +/// +/// enum StreamingRequestSerializer {} +/// +/// impl<'a, W> SerializeRequest<'a, &'a mut StreamingRequest, W> for StreamingRequestSerializer +/// where +/// W: Write, +/// { +/// fn content_type(_: &&mut StreamingRequest) -> HeaderValue { +/// HeaderValue::from_static("text/plain") +/// } +/// +/// fn serialize(value: &'a mut StreamingRequest) -> Result, Error> { +/// Ok(RequestBody::Streaming(value)) +/// } +/// } +/// +/// enum StreamingResponseDeserializer {} +/// +/// impl DeserializeResponse for StreamingResponseDeserializer { +/// fn accept() -> Option { +/// None +/// } +/// +/// fn deserialize(response: Response) -> Result { +/// Ok(response.into_body()) +/// } +/// } /// ``` #[proc_macro_attribute] pub fn conjure_client(attr: TokenStream, item: TokenStream) -> TokenStream { From 4c05ba894b13b541233ad7721254fa0c9f12679b Mon Sep 17 00:00:00 2001 From: Steven Fackler Date: Wed, 10 May 2023 15:59:00 -0400 Subject: [PATCH 06/10] validate header names --- conjure-macros/src/client.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/conjure-macros/src/client.rs b/conjure-macros/src/client.rs index 1e2574c4..2b19bfdb 100644 --- a/conjure-macros/src/client.rs +++ b/conjure-macros/src/client.rs @@ -430,13 +430,8 @@ fn add_headers(request: &TokenStream, endpoint: &Endpoint) -> TokenStream { } fn add_header(request: &TokenStream, arg: &Arg) -> TokenStream { - let header_name = match arg.attr.name.value().parse::() { - Ok(header_name) => header_name, - Err(e) => return Error::new_spanned(&arg.attr.name, e).into_compile_error(), - }; - let ident = &arg.ident; - let name = header_name.as_str(); + let name = arg.attr.name.value().to_ascii_lowercase(); let encoder = arg.attr.encoder.as_ref().map_or_else( || quote!(conjure_http::client::DisplayEncoder), |v| quote!(#v), @@ -686,6 +681,13 @@ fn validate_args( } } + for arg in args { + let ArgType::Header(arg) = arg else { continue }; + if let Err(e) = arg.attr.name.value().parse::() { + errors.push(Error::new(arg.span, e)); + } + } + if let (Some(path), Some(path_components)) = (path, path_components) { let mut path_params = args .iter() From 65f7ae6dc17110742d56a555952345d79dc7ea1f Mon Sep 17 00:00:00 2001 From: Steven Fackler Date: Thu, 11 May 2023 11:20:41 -0400 Subject: [PATCH 07/10] don't rely on the prelude --- conjure-http/src/private/mod.rs | 2 ++ conjure-macros/src/client.rs | 11 ++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/conjure-http/src/private/mod.rs b/conjure-http/src/private/mod.rs index 470898c5..af7c325f 100644 --- a/conjure-http/src/private/mod.rs +++ b/conjure-http/src/private/mod.rs @@ -23,8 +23,10 @@ pub use http::{self, header, Extensions, Method, Request, Response}; pub use pin_utils::pin_mut; pub use std::borrow::Cow; pub use std::boxed::Box; +pub use std::env; pub use std::future::Future; pub use std::iter::Iterator; +pub use std::marker::{Send, Sync}; pub use std::option::Option; pub use std::pin::Pin; pub use std::result::Result; diff --git a/conjure-macros/src/client.rs b/conjure-macros/src/client.rs index 2b19bfdb..1a36d3fb 100644 --- a/conjure-macros/src/client.rs +++ b/conjure-macros/src/client.rs @@ -107,7 +107,7 @@ fn generate_client(service: &Service) -> TokenStream { } let extra_client_predicates = match service.asyncness { Asyncness::Sync => quote!(), - Asyncness::Async => quote!(+ Sync + Send), + Asyncness::Async => quote!(+ conjure_http::private::Sync + conjure_http::private::Send), }; where_clause.predicates.push( syn::parse2(quote! { @@ -116,9 +116,10 @@ fn generate_client(service: &Service) -> TokenStream { .unwrap(), ); if let Asyncness::Async = service.asyncness { - where_clause - .predicates - .push(syn::parse2(quote!(#client_param::ResponseBody: 'static + Send)).unwrap()); + where_clause.predicates.push( + syn::parse2(quote!(#client_param::ResponseBody: 'static + conjure_http::private::Send)) + .unwrap(), + ); } let (impl_generics, _, where_clause) = impl_generics.split_for_impl(); @@ -456,7 +457,7 @@ fn add_endpoint(request: &TokenStream, service: &Service, endpoint: &Endpoint) - quote! { #request.extensions_mut().insert(conjure_http::client::Endpoint::new( #service, - std::option::Option::Some(std::env!("CARGO_PKG_VERSION")), + conjure_http::private::Option::Some(conjure_http::private::env!("CARGO_PKG_VERSION")), #name, #path, )); From 0566e35669376bb2a493d5d5321f47f24d650ca0 Mon Sep 17 00:00:00 2001 From: Steven Fackler Date: Thu, 11 May 2023 11:29:31 -0400 Subject: [PATCH 08/10] more type qualifications --- conjure-macros/src/endpoints.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/conjure-macros/src/endpoints.rs b/conjure-macros/src/endpoints.rs index 72e1e19e..4cf98e65 100644 --- a/conjure-macros/src/endpoints.rs +++ b/conjure-macros/src/endpoints.rs @@ -115,7 +115,9 @@ fn generate_endpoints(service: &Service) -> TokenStream { fn endpoints( &self, ) -> conjure_http::private::Vec + Sync + Send, + dyn conjure_http::server::#endpoint_trait<#request_body, #response_writer> + + conjure_http::private::Sync + + conjure_http::private::Send, >> { #(#endpoints)* @@ -163,8 +165,13 @@ fn impl_params(service: &Service) -> ImplParams { let where_clause = impl_generics.make_where_clause(); where_clause.predicates.push( - syn::parse2(quote!(#trait_impl: #trait_name #type_generics + 'static + Sync + Send)) - .unwrap(), + syn::parse2(quote! { + #trait_impl: #trait_name #type_generics + + 'static + + conjure_http::private::Sync + + conjure_http::private::Send + }) + .unwrap(), ); let input_bounds = input_bounds(service); where_clause @@ -189,7 +196,11 @@ fn input_bounds(service: &Service) -> TokenStream { match service.asyncness { Asyncness::Sync => quote!(conjure_http::private::Iterator), - Asyncness::Async => quote!(conjure_http::private::Stream + Sync + Send), + Asyncness::Async => quote! { + conjure_http::private::Stream + + conjure_http::private::Sync + + conjure_http::private::Send + }, } } From 4bd5cf5308e9c0951207a37fbacbc0012ad3262f Mon Sep 17 00:00:00 2001 From: Steven Fackler Date: Fri, 7 Jul 2023 15:09:57 -0400 Subject: [PATCH 09/10] remove deprecated types --- conjure-http/src/client.rs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/conjure-http/src/client.rs b/conjure-http/src/client.rs index 186644b1..3c425c87 100644 --- a/conjure-http/src/client.rs +++ b/conjure-http/src/client.rs @@ -474,19 +474,3 @@ where Ok(value.into_iter().map(|v| v.to_string()).collect()) } } - -#[allow(missing_docs)] -#[deprecated(note = "use DisplayEncoder instead", since = "3.7.0")] -pub type DisplayHeaderEncoder = DisplayEncoder; - -#[allow(missing_docs)] -#[deprecated(note = "use DisplayEncoder instead", since = "3.7.0")] -pub type DisplayParamEncoder = DisplayEncoder; - -#[allow(missing_docs)] -#[deprecated(note = "use DisplaySeqEncoder instead", since = "3.7.0")] -pub type DisplaySeqHeaderEncoder = DisplaySeqEncoder; - -#[allow(missing_docs)] -#[deprecated(note = "use DisplaySeqEncoder instead", since = "3.7.0")] -pub type DisplaySeqParamEncoder = DisplaySeqEncoder; From 1235f824a4fe30b78f31abc463627e549d233f73 Mon Sep 17 00:00:00 2001 From: Steven Fackler Date: Fri, 7 Jul 2023 16:18:08 -0400 Subject: [PATCH 10/10] fix docs --- conjure-macros/src/lib.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/conjure-macros/src/lib.rs b/conjure-macros/src/lib.rs index bc0434b3..b43fdd95 100644 --- a/conjure-macros/src/lib.rs +++ b/conjure-macros/src/lib.rs @@ -95,9 +95,8 @@ mod path; /// /// # Async /// -/// Both blocking and async clients are supported. For technical reasons, async trait -/// implementations must put the `#[conjure_client]` annotation *above* the `#[async_trait]` -/// annotation. +/// Both blocking and async clients are supported. For technical reasons, async trait definitions +/// must put the `#[conjure_client]` annotation *above* the `#[async_trait]` annotation. /// /// # Examples /// @@ -287,9 +286,8 @@ pub fn conjure_client(attr: TokenStream, item: TokenStream) -> TokenStream { /// /// # Async /// -/// Both blocking and async services are supported. For technical reasons, async trait -/// implementations must put the `#[conjure_endpoints]` annotation *above* the `#[async_trait]` -/// annotation. +/// Both blocking and async services are supported. For technical reasons, async trait definitions +/// must put the `#[conjure_endpoints]` annotation *above* the `#[async_trait]` annotation. /// /// # Examples ///