use itertools::Itertools;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use super::config::CodeGenConfig;
use super::{BindingMode, MethodHandler, ServiceHandler, doc_tokens, format_tokens};
use crate::analysis::{GenerationPlan, RequestParam};
use crate::parsing::CodeGenMetadata;
use crate::parsing::types::RenderContext;
pub(crate) fn generate(
plan: &GenerationPlan,
metadata: &CodeGenMetadata,
config: &CodeGenConfig,
) -> crate::error::Result<Option<String>> {
let Some(bindings) = config.bindings.as_ref() else {
return Ok(None);
};
if !config.client_protocols.rest {
return Ok(None);
}
let aggregate_ident = format_ident!("{}", bindings.aggregate_client_name);
let mut services = plan
.services
.iter()
.map(|service| ServiceHandler {
plan: service,
metadata,
config,
})
.collect_vec();
services.sort_by(|a, b| a.plan.service_name.cmp(&b.plan.service_name));
let methods = services
.iter()
.flat_map(|s| service_methods(s))
.collect_vec();
let passthrough_accessors = services.iter().map(passthrough_accessor).collect_vec();
let (passthrough_accessors, methods, gated_blocks) = if config.rest_client.resource_features {
let blocks = services
.iter()
.map(|s| {
let cfg = s.client_feature_cfg();
let accessor = passthrough_accessor(s);
let methods = service_methods(s);
quote! {
#cfg
impl #aggregate_ident {
#accessor
#(#methods)*
}
}
})
.collect_vec();
(Vec::new(), Vec::new(), blocks)
} else {
(passthrough_accessors, methods, Vec::new())
};
let (transport_import, transport_ident) = super::client::transport_tokens(config);
let dual = config.dual_transport();
let native_cfg = if dual {
quote! { #[cfg(not(target_arch = "wasm32"))] }
} else {
quote! {}
};
let token_ctors = if dual || config.uses_default_transport() {
Some((
quote! { #transport_ident::new_unauthenticated() },
quote! { #transport_ident::new_with_token(token) },
))
} else if config.uses_reqwest_transport() {
Some((
quote! { #transport_ident::default() },
quote! { #transport_ident::default().with_token(token.to_string()) },
))
} else {
None
};
let token_ctors = token_ctors.map(|(unauthenticated, with_token)| {
quote! {
#native_cfg
pub fn new_unauthenticated(base_url: Url) -> Self {
Self::new(#unauthenticated, base_url)
}
#native_cfg
pub fn new_with_token(base_url: Url, token: impl ToString) -> Self {
Self::new(#with_token, base_url)
}
}
});
let wasm_ctor = dual.then(|| {
quote! {
#[cfg(target_arch = "wasm32")]
pub fn new_in_browser(base_url: Url) -> Self {
Self::new(#transport_ident::new(), base_url)
}
}
});
let user_agent = config.uses_reqwest_transport().then(|| {
quote! {
pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
self.client = self.client.with_user_agent(user_agent);
self
}
}
});
let convenience_ctors = quote! {
#token_ctors
#wasm_ctor
#user_agent
};
let imports = generate_imports(&services, &transport_import);
let tokens = quote! {
#![allow(unused_imports)]
#imports
#[derive(Clone)]
pub struct #aggregate_ident {
pub(crate) client: #transport_ident,
pub(crate) base_url: Url,
}
impl #aggregate_ident {
pub fn new(client: #transport_ident, mut base_url: Url) -> Self {
if !base_url.path().ends_with('/') {
base_url.set_path(&format!("{}/", base_url.path()));
}
Self { client, base_url }
}
#convenience_ctors
#(#passthrough_accessors)*
#(#methods)*
}
#(#gated_blocks)*
};
Ok(Some(format_tokens(tokens)?))
}
fn passthrough_accessor(service: &ServiceHandler<'_>) -> TokenStream {
let accessor = format_ident!("{}_client", service.plan.base_path);
let client_ty = low_level_client_path(service);
let ctor = low_level_client_ctor(service);
let doc = format!(
"Low-level `{}` client exposing request/response passthrough methods.",
service.plan.base_path
);
quote! {
#[doc = #doc]
pub fn #accessor(&self) -> #client_ty {
#ctor
}
}
}
fn low_level_client_path(service: &ServiceHandler<'_>) -> TokenStream {
let module = format_ident!("{}", service.plan.base_path);
let client = service.low_level_client_type(crate::codegen::ClientProtocol::Rest);
quote! { crate::codegen::#module::#client }
}
fn low_level_client_ctor(service: &ServiceHandler<'_>) -> TokenStream {
let path = low_level_client_path(service);
quote! { #path::new(self.client.clone(), self.base_url.clone()) }
}
fn generate_imports(
services: &[ServiceHandler<'_>],
transport_import: &TokenStream,
) -> TokenStream {
let codegen_imports = services.iter().map(|s| {
let module = format_ident!("{}", s.plan.base_path);
let cfg = s.client_feature_cfg();
quote! {
#cfg
use crate::codegen::#module::*;
}
});
let model_imports = services
.iter()
.map(|s| (s.models_path(), s.client_feature_cfg()))
.unique_by(|(p, _)| quote! { #p }.to_string())
.map(|(mod_path, cfg)| quote! { #cfg use #mod_path::*; });
quote! {
#transport_import
use url::Url;
#(#codegen_imports)*
#(#model_imports)*
}
}
fn service_methods(service: &ServiceHandler<'_>) -> Vec<TokenStream> {
let mode = service.binding_mode();
let mut methods = Vec::new();
for method in service.rest_methods() {
match mode {
BindingMode::Scoped => {
if method.is_collection_method() {
methods.push(collection_method(service, &method));
}
}
BindingMode::Flat => {
methods.push(flat_method(service, &method));
}
}
}
if let Some(accessor) = resource_accessor_method(service) {
methods.push(accessor);
}
methods
}
fn doc_with_arguments(method: &MethodHandler<'_>) -> TokenStream {
let mut doc = method
.plan
.metadata
.documentation
.as_deref()
.map(|d| d.trim_end().to_string())
.unwrap_or_default();
let arg_lines: Vec<String> = method
.required_parameters()
.filter_map(|p| {
p.documentation()
.map(|d| format!("* `{}` - {}", p.name(), d.trim()))
})
.collect();
if !arg_lines.is_empty() {
if !doc.is_empty() {
doc.push('\n');
}
doc.push_str("\n# Arguments\n\n");
doc.push_str(&arg_lines.join("\n"));
}
if doc.is_empty() {
quote! {}
} else {
doc_tokens(Some(&doc))
}
}
fn builder_params(
method: &MethodHandler<'_>,
params: &[&RequestParam],
) -> (Vec<TokenStream>, Vec<TokenStream>) {
let param_defs = params
.iter()
.map(|p| {
let ident = p.field_ident();
let ty = method.field_type(p.field_type(), RenderContext::Constructor);
quote! { #ident: #ty }
})
.collect();
let args = params
.iter()
.map(|p| {
let ident = p.field_ident();
quote! { #ident }
})
.collect();
(param_defs, args)
}
fn collection_method(service: &ServiceHandler<'_>, method: &MethodHandler<'_>) -> TokenStream {
builder_constructor_method(service, method)
}
fn flat_method(service: &ServiceHandler<'_>, method: &MethodHandler<'_>) -> TokenStream {
builder_constructor_method(service, method)
}
fn builder_constructor_method(
service: &ServiceHandler<'_>,
method: &MethodHandler<'_>,
) -> TokenStream {
let doc = doc_with_arguments(method);
let method_name = method.binding_method_name();
let builder_ty = method.builder_type();
let params: Vec<&RequestParam> = method.required_parameters().collect();
let (param_defs, args) = builder_params(method, ¶ms);
let ctor = low_level_client_ctor(service);
quote! {
#doc
pub fn #method_name(&self, #(#param_defs),*) -> #builder_ty {
#builder_ty::new(#ctor, #(#args),*)
}
}
}
fn resource_accessor_method(service: &ServiceHandler<'_>) -> Option<TokenStream> {
if !service.config.output.generate_resource_clients {
return None;
}
if !service.supports_scoped_client() {
return None;
}
let spec = service.accessor_spec()?;
let client_ty = service.scoped_client_type()?;
let method_name = format_ident!("{}", spec.singular);
let ctor = low_level_client_ctor(service);
let param_idents: Vec<_> = spec.params.iter().map(|p| format_ident!("{}", p)).collect();
let param_defs: Vec<_> = param_idents
.iter()
.map(|id| quote! { #id: impl Into<String> })
.collect();
let args = param_idents.iter().map(|id| quote! { #id });
let doc = format!(
" Access the `{}` resource scoped to the given name.",
spec.singular
);
let from_full_name = if spec.nested {
let ffn_method = format_ident!("{}_from_full_name", spec.singular);
let ffn_doc = format!(
" Access the `{}` resource from its dot-joined full name.",
spec.singular
);
quote! {
#[doc = #ffn_doc]
pub fn #ffn_method(&self, full_name: impl Into<String>) -> #client_ty {
#client_ty::from_full_name(full_name, #ctor)
}
}
} else {
quote! {}
};
Some(quote! {
#[doc = #doc]
pub fn #method_name(&self, #(#param_defs),*) -> #client_ty {
#client_ty::new(#(#args,)* #ctor)
}
#from_full_name
})
}