use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use super::{MethodHandler, ServiceHandler, doc_tokens, format_tokens};
use crate::analysis::{GenerationPlan, RequestParam, RequestType};
use crate::error::Result;
pub(crate) fn generate(
service: &ServiceHandler<'_>,
plan: &GenerationPlan,
) -> Result<Option<String>> {
let Some(scoped_ident) = service.scoped_client_type() else {
return Ok(None);
};
let Some(spec) = service.accessor_spec() else {
return Ok(None);
};
let low_level_ident = service.low_level_client_type();
let components: Vec<_> = spec.params.iter().map(|p| format_ident!("{}", p)).collect();
let join_format = spec.join_format();
let struct_def = scoped_struct(&scoped_ident, &components, &low_level_ident);
let constructor = scoped_constructor(&components, &low_level_ident);
let from_full_name = if spec.nested {
from_full_name_constructor(&components, &low_level_ident, &spec.singular)
} else {
quote! {}
};
let name_accessors = name_accessors(&components, &join_format);
let methods = instance_methods(service, &components, &join_format);
let child_methods = child_methods(service, plan, &components);
let child_model_imports = child_model_import_paths(service, plan);
let singular_doc = format!(" A client scoped to a single `{}`.", spec.singular);
let mod_path = service.models_path();
let tokens = quote! {
#![allow(unused_imports)]
use #mod_path::*;
#(#child_model_imports)*
use super::builders::*;
use super::client::#low_level_ident;
#[doc = #singular_doc]
#[derive(Clone)]
pub struct #scoped_ident {
#struct_def
pub(crate) client: #low_level_ident,
}
impl #scoped_ident {
#constructor
#from_full_name
#name_accessors
#(#methods)*
#(#child_methods)*
}
};
Ok(Some(format_tokens(tokens)?))
}
fn child_model_import_paths(
service: &ServiceHandler<'_>,
plan: &GenerationPlan,
) -> Vec<TokenStream> {
let own = service.models_path();
let own_str = quote! { #own }.to_string();
let mut seen = std::collections::BTreeSet::new();
let mut imports = Vec::new();
for link in &service.plan.direct_children {
let Some(child_plan) = plan
.services
.iter()
.find(|s| s.base_path == link.child_base_path)
else {
continue;
};
let child = ServiceHandler {
plan: child_plan,
metadata: service.metadata,
config: service.config,
};
let path = child.models_path();
let key = quote! { #path }.to_string();
if key == own_str || !seen.insert(key) {
continue;
}
imports.push(quote! { use #path::*; });
}
imports
}
fn child_methods(
service: &ServiceHandler<'_>,
plan: &GenerationPlan,
parent_components: &[proc_macro2::Ident],
) -> Vec<TokenStream> {
let mut out = Vec::new();
for link in &service.plan.direct_children {
let Some(child_plan) = plan
.services
.iter()
.find(|s| s.base_path == link.child_base_path)
else {
continue;
};
let child = ServiceHandler {
plan: child_plan,
metadata: service.metadata,
config: service.config,
};
if let Some(nav) = child_nav_method(&child, link, parent_components) {
out.push(nav);
}
if let Some(create) = child_create_method(&child, link, parent_components) {
out.push(create);
}
if let Some(list) = child_list_method(&child, link, parent_components) {
out.push(list);
}
}
out
}
fn child_nav_method(
child: &ServiceHandler<'_>,
link: &crate::analysis::ChildLink,
parent_components: &[proc_macro2::Ident],
) -> Option<TokenStream> {
let child_scoped = child.scoped_client_type()?;
let child_low_level = child.low_level_client_type();
let module = format_ident!("{}", link.child_base_path);
let method_name = format_ident!("{}", link.child_singular);
let leaf = link.child_accessor_params.last()?;
let leaf_ident = format_ident!("{}", leaf);
let parent_args = parent_components.iter().map(|c| quote! { &self.#c });
let low_level = child_low_level_ctor(&module, &child_low_level);
let doc = format!(" Access a `{}` within this resource.", link.child_singular);
Some(quote! {
#[doc = #doc]
pub fn #method_name(&self, #leaf_ident: impl Into<String>) -> crate::codegen::#module::#child_scoped {
crate::codegen::#module::#child_scoped::new(
#(#parent_args,)*
#leaf_ident,
#low_level,
)
}
})
}
fn child_create_method(
child: &ServiceHandler<'_>,
link: &crate::analysis::ChildLink,
parent_components: &[proc_macro2::Ident],
) -> Option<TokenStream> {
let create = child
.methods()
.find(|m| m.plan.request_type == RequestType::Create)?;
let child_low_level = child.low_level_client_type();
let module = format_ident!("{}", link.child_base_path);
let builder_ty = create.builder_type();
let method_name = format_ident!("create_{}", link.child_singular);
let parent_names: Vec<String> = parent_components.iter().map(|c| c.to_string()).collect();
let required: Vec<&RequestParam> = create.required_parameters().collect();
let (new_args, method_param_defs) =
render_child_builder_args(&create, &required, &parent_names);
let low_level = child_low_level_ctor(&module, &child_low_level);
let doc = format!(" Create a `{}` within this resource.", link.child_singular);
Some(quote! {
#[doc = #doc]
pub fn #method_name(&self, #(#method_param_defs),*) -> crate::codegen::#module::#builder_ty {
crate::codegen::#module::#builder_ty::new(
#low_level,
#(#new_args),*
)
}
})
}
fn child_list_method(
child: &ServiceHandler<'_>,
link: &crate::analysis::ChildLink,
parent_components: &[proc_macro2::Ident],
) -> Option<TokenStream> {
let list = child
.methods()
.find(|m| m.plan.request_type == RequestType::List)?;
let child_low_level = child.low_level_client_type();
let module = format_ident!("{}", link.child_base_path);
let builder_ty = list.builder_type();
let method_name = list.binding_method_name();
let parent_names: Vec<String> = parent_components.iter().map(|c| c.to_string()).collect();
let required: Vec<&RequestParam> = list.required_parameters().collect();
let (new_args, method_param_defs) = render_child_builder_args(&list, &required, &parent_names);
let low_level = child_low_level_ctor(&module, &child_low_level);
let doc = format!(
" List `{}` resources within this resource.",
link.child_singular
);
Some(quote! {
#[doc = #doc]
pub fn #method_name(&self, #(#method_param_defs),*) -> crate::codegen::#module::#builder_ty {
crate::codegen::#module::#builder_ty::new(
#low_level,
#(#new_args),*
)
}
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BuilderArg {
ParentComponent,
MethodArg,
}
fn classify_builder_args(required: &[&RequestParam], parent_names: &[String]) -> Vec<BuilderArg> {
required
.iter()
.map(|p| {
if parent_names.iter().any(|n| n == p.name()) {
BuilderArg::ParentComponent
} else {
BuilderArg::MethodArg
}
})
.collect()
}
fn child_low_level_ctor(
module: &proc_macro2::Ident,
child_low_level: &proc_macro2::Ident,
) -> TokenStream {
quote! {
crate::codegen::#module::#child_low_level::new(
self.client.client.clone(),
self.client.base_url.clone(),
)
}
}
fn render_child_builder_args(
method: &MethodHandler<'_>,
required: &[&RequestParam],
parent_names: &[String],
) -> (Vec<TokenStream>, Vec<TokenStream>) {
let mut new_args: Vec<TokenStream> = Vec::new();
let mut method_param_defs: Vec<TokenStream> = Vec::new();
for (param, source) in required
.iter()
.zip(classify_builder_args(required, parent_names))
{
match source {
BuilderArg::ParentComponent => {
let field = format_ident!("{}", param.name());
new_args.push(quote! { &self.#field });
}
BuilderArg::MethodArg => {
let ident = param.field_ident();
let ty = method.field_type(
param.field_type(),
crate::parsing::types::RenderContext::Constructor,
);
method_param_defs.push(quote! { #ident: #ty });
new_args.push(quote! { #ident });
}
}
}
(new_args, method_param_defs)
}
fn scoped_struct(
_scoped_ident: &proc_macro2::Ident,
components: &[proc_macro2::Ident],
_low_level_ident: &proc_macro2::Ident,
) -> TokenStream {
let fields = components.iter().map(|c| quote! { pub(crate) #c: String, });
quote! { #(#fields)* }
}
fn scoped_constructor(
components: &[proc_macro2::Ident],
low_level_ident: &proc_macro2::Ident,
) -> TokenStream {
let params = components.iter().map(|c| quote! { #c: impl Into<String> });
let assigns = components.iter().map(|c| quote! { #c: #c.into() });
quote! {
pub fn new(#(#params,)* client: #low_level_ident) -> Self {
Self {
#(#assigns,)*
client,
}
}
}
}
fn from_full_name_constructor(
components: &[proc_macro2::Ident],
low_level_ident: &proc_macro2::Ident,
singular: &str,
) -> TokenStream {
let n = components.len();
let bindings = components.iter().map(|c| {
quote! { let #c = parts.next().unwrap_or_default(); }
});
let args = components.iter().map(|c| quote! { #c });
let doc = format!(
" Create a `{singular}` client from its dot-joined full name (e.g. `\"{}\"`).",
components
.iter()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join(".")
);
quote! {
#[doc = #doc]
pub fn from_full_name(full_name: impl Into<String>, client: #low_level_ident) -> Self {
let full_name = full_name.into();
let mut parts = full_name.splitn(#n, '.');
#(#bindings)*
Self::new(#(#args,)* client)
}
}
}
fn name_accessors(components: &[proc_macro2::Ident], join_format: &str) -> TokenStream {
let Some((leaf, ancestors)) = components.split_last() else {
return quote! {};
};
let ancestor_accessors = ancestors.iter().map(|c| {
let doc = format!(" The `{c}` component of this resource's name.");
quote! {
#[doc = #doc]
pub fn #c(&self) -> &str {
&self.#c
}
}
});
let name_ident = format_ident!("name");
let leaf_accessor = quote! {
pub fn #name_ident(&self) -> &str {
&self.#leaf
}
};
let full_name = if components.iter().any(|c| *c == "full_name") {
quote! {}
} else if components.len() == 1 {
quote! {
pub fn full_name(&self) -> String {
self.#leaf.clone()
}
}
} else {
let field_refs = components.iter().map(|c| quote! { self.#c });
quote! {
pub fn full_name(&self) -> String {
format!(#join_format, #(#field_refs),*)
}
}
};
quote! {
#(#ancestor_accessors)*
#leaf_accessor
#full_name
}
}
fn instance_methods(
service: &ServiceHandler<'_>,
components: &[proc_macro2::Ident],
join_format: &str,
) -> Vec<TokenStream> {
service
.methods()
.filter(|m| m.is_scoped_instance_method())
.map(|m| instance_method(&m, components, join_format))
.collect()
}
fn instance_method(
method: &MethodHandler<'_>,
components: &[proc_macro2::Ident],
join_format: &str,
) -> TokenStream {
let doc = doc_tokens(method.plan.metadata.documentation.as_deref());
let method_name = method.plan.resource_client_method();
let builder_ty = method.builder_type();
let required: Vec<&RequestParam> = method.required_parameters().collect();
let path_param_count = required.iter().filter(|p| p.is_path_param()).count();
let mut new_args: Vec<TokenStream> = Vec::new();
let mut method_param_defs: Vec<TokenStream> = Vec::new();
for param in &required {
if param.is_path_param() {
new_args.push(path_arg_expr(
param,
components,
join_format,
path_param_count,
));
} else {
let ident = param.field_ident();
let ty = method.field_type(
param.field_type(),
crate::parsing::types::RenderContext::Constructor,
);
method_param_defs.push(quote! { #ident: #ty });
new_args.push(quote! { #ident });
}
}
quote! {
#doc
pub fn #method_name(&self, #(#method_param_defs),*) -> #builder_ty {
#builder_ty::new(self.client.clone(), #(#new_args),*)
}
}
}
fn path_arg_expr(
param: &RequestParam,
components: &[proc_macro2::Ident],
join_format: &str,
path_param_count: usize,
) -> TokenStream {
if path_param_count == 1 && components.len() > 1 {
let field_refs = components.iter().map(|c| quote! { self.#c });
quote! { format!(#join_format, #(#field_refs),*) }
} else {
let name = param.name();
let field = components
.iter()
.find(|c| *c == name)
.cloned()
.unwrap_or_else(|| components[0].clone());
quote! { &self.#field }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::{PathParam, QueryParam};
use crate::parsing::types::{BaseType, UnifiedType};
fn idents(names: &[&str]) -> Vec<proc_macro2::Ident> {
names.iter().map(|n| format_ident!("{}", n)).collect()
}
fn string_type() -> UnifiedType {
UnifiedType {
base_type: BaseType::String,
is_optional: false,
is_repeated: false,
}
}
fn path(name: &str) -> RequestParam {
RequestParam::Path(PathParam {
name: name.to_string(),
field_type: string_type(),
documentation: None,
})
}
fn join_format(n: usize) -> String {
std::iter::repeat_n("{}", n).collect::<Vec<_>>().join(".")
}
#[test]
fn flat_single_component_direct_ref() {
let components = idents(&["catalog_name"]);
let expr = path_arg_expr(&path("name"), &components, &join_format(1), 1);
assert_eq!(expr.to_string(), quote! { &self.catalog_name }.to_string());
}
#[test]
fn flat_matching_name_direct_ref() {
let components = idents(&["catalog_name"]);
let expr = path_arg_expr(&path("catalog_name"), &components, &join_format(1), 1);
assert_eq!(expr.to_string(), quote! { &self.catalog_name }.to_string());
}
#[test]
fn nested_single_path_param_joins_components() {
let components = idents(&["catalog_name", "schema_name"]);
let expr = path_arg_expr(&path("full_name"), &components, &join_format(2), 1);
assert_eq!(
expr.to_string(),
quote! { format!("{}.{}", self.catalog_name, self.schema_name) }.to_string()
);
}
#[test]
fn nested_separate_path_params_map_by_name() {
let components = idents(&["catalog_name", "schema_name"]);
let catalog = path_arg_expr(&path("catalog_name"), &components, &join_format(2), 2);
let schema = path_arg_expr(&path("schema_name"), &components, &join_format(2), 2);
assert_eq!(
catalog.to_string(),
quote! { &self.catalog_name }.to_string()
);
assert_eq!(schema.to_string(), quote! { &self.schema_name }.to_string());
}
#[test]
fn query_param_is_not_a_path_param() {
let q = RequestParam::Query(QueryParam {
name: "page_token".to_string(),
field_type: string_type(),
documentation: None,
resource_reference: None,
});
assert!(!q.is_path_param());
assert!(path("name").is_path_param());
}
#[test]
fn create_schema_args_split_by_parent_components() {
let required = [path("name"), path("catalog_name")];
let refs: Vec<&RequestParam> = required.iter().collect();
let parents = vec!["catalog_name".to_string()];
assert_eq!(
classify_builder_args(&refs, &parents),
vec![BuilderArg::MethodArg, BuilderArg::ParentComponent]
);
}
#[test]
fn create_table_args_split_preserves_builder_order() {
let required = [path("name"), path("schema_name"), path("catalog_name")];
let refs: Vec<&RequestParam> = required.iter().collect();
let parents = vec!["catalog_name".to_string(), "schema_name".to_string()];
assert_eq!(
classify_builder_args(&refs, &parents),
vec![
BuilderArg::MethodArg,
BuilderArg::ParentComponent,
BuilderArg::ParentComponent,
]
);
}
#[test]
fn create_args_extra_required_fields_are_method_args() {
let required = [path("name"), path("catalog_name"), path("table_type")];
let refs: Vec<&RequestParam> = required.iter().collect();
let parents = vec!["catalog_name".to_string()];
assert_eq!(
classify_builder_args(&refs, &parents),
vec![
BuilderArg::MethodArg,
BuilderArg::ParentComponent,
BuilderArg::MethodArg,
]
);
}
#[test]
fn list_args_parent_filter_then_pagination() {
let required = [
path("catalog_name"),
path("max_results"),
path("page_token"),
];
let refs: Vec<&RequestParam> = required.iter().collect();
let parents = vec!["catalog_name".to_string()];
assert_eq!(
classify_builder_args(&refs, &parents),
vec![
BuilderArg::ParentComponent,
BuilderArg::MethodArg,
BuilderArg::MethodArg,
]
);
}
#[test]
fn list_args_multi_parent_filters() {
let required = [
path("catalog_name"),
path("schema_name"),
path("max_results"),
];
let refs: Vec<&RequestParam> = required.iter().collect();
let parents = vec!["catalog_name".to_string(), "schema_name".to_string()];
assert_eq!(
classify_builder_args(&refs, &parents),
vec![
BuilderArg::ParentComponent,
BuilderArg::ParentComponent,
BuilderArg::MethodArg,
]
);
}
#[test]
fn from_full_name_splits_into_components() {
let components = idents(&["catalog_name", "schema_name"]);
let low = format_ident!("SchemaServiceClient");
let out = from_full_name_constructor(&components, &low, "schema").to_string();
assert!(out.contains("splitn (2usize"), "expected splitn(2): {out}");
assert!(out.contains("let catalog_name = parts . next ()"), "{out}");
assert!(out.contains("let schema_name = parts . next ()"), "{out}");
assert!(
out.contains("Self :: new (catalog_name , schema_name , client)"),
"forwards parts in order to new: {out}"
);
}
#[test]
fn from_full_name_three_components() {
let components = idents(&["catalog_name", "schema_name", "table_name"]);
let low = format_ident!("TableServiceClient");
let out = from_full_name_constructor(&components, &low, "table").to_string();
assert!(out.contains("splitn (3usize"), "expected splitn(3): {out}");
assert!(
out.contains("Self :: new (catalog_name , schema_name , table_name , client)"),
"{out}"
);
}
use crate::analysis::{ChildLink, GenerationPlan, ServicePlan};
use crate::codegen::{CodeGenConfig, CodeGenOutput, ServiceHandler};
use crate::parsing::CodeGenMetadata;
fn test_config() -> CodeGenConfig {
CodeGenConfig {
context_type_path: "crate::Context".into(),
result_type_path: "crate::Result".into(),
models_path_template: "common::models::{service}::v1".into(),
models_path_crate_template: "crate::models::{service}::v1".into(),
resource_store_crate_name: "olai_store".into(),
runtime: crate::Runtime::Prost,
transport_type_path: crate::DEFAULT_TRANSPORT_TYPE_PATH.into(),
output: CodeGenOutput {
common: "/tmp/c".into(),
models: None,
models_subdir: "_gen".into(),
server: None,
client: None,
python: None,
node: None,
node_ts: None,
wasm: None,
python_typings_filename: "client.pyi".into(),
generate_resource_clients: true,
},
generate_resource_enum: false,
generate_store_integration: false,
error_type_path: None,
generate_object_conversions: false,
bindings: None,
}
}
fn svc(service_name: &str, package: &str, children: Vec<ChildLink>) -> ServicePlan {
use crate::analysis::ManagedResource;
use crate::google::api::ResourceDescriptor;
let base_path = crate::utils::strings::service_to_base_path(service_name);
ServicePlan {
service_name: service_name.to_string(),
handler_name: crate::utils::strings::service_to_handler_name(service_name),
base_path,
package: package.to_string(),
methods: vec![],
managed_resources: vec![ManagedResource {
type_name: "X".to_string(),
descriptor: ResourceDescriptor::default(),
}],
documentation: None,
hierarchy: vec![],
resource_accessor_params: Some(vec!["name".to_string()]),
direct_children: children,
}
}
#[test]
fn child_model_imports_pull_in_child_models() {
let child_link = ChildLink {
child_singular: "schema".to_string(),
child_base_path: "schema".to_string(),
child_accessor_params: vec!["name".to_string(), "schema_name".to_string()],
};
let plan = GenerationPlan {
services: vec![
svc("CatalogService", "example.catalog.v1", vec![child_link]),
svc("SchemaService", "example.schema.v1", vec![]),
],
skipped_methods: vec![],
};
let metadata = CodeGenMetadata::default();
let config = test_config();
let parent = ServiceHandler {
plan: &plan.services[0],
metadata: &metadata,
config: &config,
};
let imports: Vec<String> = child_model_import_paths(&parent, &plan)
.iter()
.map(|t| t.to_string())
.collect();
assert_eq!(
imports.len(),
1,
"expected one child import, got {imports:?}"
);
assert!(
imports[0].contains("schema"),
"child schema models import missing: {imports:?}"
);
assert!(
!imports[0].contains("catalog"),
"parent's own models path should not be re-imported: {imports:?}"
);
}
#[test]
fn name_accessors_flat_uses_generic_name() {
let out = name_accessors(&idents(&["catalog_name"]), &join_format(1)).to_string();
assert!(
out.contains("pub fn name (& self) -> & str { & self . catalog_name }"),
"{out}"
);
assert!(!out.contains("pub fn catalog_name"), "{out}");
assert!(
out.contains("pub fn full_name (& self) -> String { self . catalog_name . clone () }"),
"{out}"
);
}
#[test]
fn name_accessors_nested_ancestors_qualified_leaf_generic() {
let out =
name_accessors(&idents(&["catalog_name", "schema_name"]), &join_format(2)).to_string();
assert!(
out.contains("pub fn catalog_name (& self) -> & str"),
"{out}"
);
assert!(
out.contains("pub fn name (& self) -> & str { & self . schema_name }"),
"{out}"
);
assert!(!out.contains("pub fn schema_name"), "{out}");
assert!(
out.contains("format ! (\"{}.{}\" , self . catalog_name , self . schema_name)"),
"{out}"
);
}
#[test]
fn name_accessors_skip_synth_full_name_on_collision() {
let out = name_accessors(&idents(&["full_name"]), &join_format(1)).to_string();
assert!(
out.contains("pub fn name (& self) -> & str { & self . full_name }"),
"{out}"
);
assert_eq!(out.matches("fn full_name").count(), 0, "{out}");
}
}