use std::collections::{BTreeMap, BTreeSet, HashMap};
use convert_case::{Case, Casing};
use itertools::Itertools;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::Ident;
use crate::error::{Error, Result};
use crate::analysis::{
BodyField, GenerationPlan, ManagedResource, MethodPlan, RequestParam, RequestType, ServicePlan,
analyze_metadata, split_body_fields,
};
use crate::output;
use crate::parsing::types::{self, RenderContext, UnifiedType};
use crate::parsing::{CodeGenMetadata, MessageField, MessageInfo};
use crate::utils::last_segment;
use crate::utils::strings::operation_to_method_name;
mod aggregate;
mod bindings;
mod builder;
mod client;
mod client_handler;
mod config;
mod connect_client;
mod error_module;
mod handler;
mod json_int64;
pub(crate) mod node;
mod python;
mod resource_client;
mod resources;
mod server;
mod tokens;
mod transport;
mod wasm;
pub use config::{
BindingsConfig, ClientHandlerImpls, ClientProtocol, ClientProtocols, CodeGenConfig,
CodeGenOutput, DEFAULT_TRANSPORT_TYPE_PATH, ErrorModuleConfig, ModelsPath, PyPackageConfig,
PythonOptions, RestClientOptions, Runtime,
};
pub(crate) use tokens::{doc_tokens, format_tokens, format_tokens_static};
pub use transport::REQWEST_TRANSPORT_TYPE_PATH;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BindingMode {
Scoped,
Flat,
}
pub(crate) struct AccessorSpec {
pub(crate) singular: String,
pub(crate) params: Vec<String>,
pub(crate) nested: bool,
}
impl AccessorSpec {
pub(crate) fn join_format(&self) -> String {
dot_format(self.params.len())
}
}
pub(crate) fn dot_format(n: usize) -> String {
std::iter::repeat_n("{}", n).collect::<Vec<_>>().join(".")
}
impl MethodPlan {
pub(crate) fn resource_client_method(&self) -> Ident {
match &self.scoped_verb {
Some(verb) => format_ident!("{}", verb),
None => format_ident!("{}", self.handler_function_name),
}
}
pub(crate) fn base_method_ident(&self) -> Ident {
format_ident!("{}", self.handler_function_name)
}
}
pub fn generate_code(metadata: &CodeGenMetadata, config: &CodeGenConfig) -> Result<()> {
let models_path = ModelsPath::new(&config.models_path_template)?;
let models_path_crate = ModelsPath::new(&config.models_path_crate_template)?;
for (field, path) in [
("context_type", &config.context_type_path),
("result_type", &config.result_type_path),
] {
syn::parse_str::<syn::Path>(path).map_err(|source| Error::InvalidConfigPath {
field,
path: path.clone(),
source,
})?;
}
if (config.output.python.is_some()
|| config.output.node.is_some()
|| config.output.node_ts.is_some())
&& config.bindings.is_none()
{
return Err(Error::MissingBindingsConfig);
}
if config.client_protocols.connect
&& config.output.client.is_some()
&& config.connect_client_path.is_none()
{
return Err(Error::MissingConnectClientPath);
}
config.validate_options()?;
let plan = analyze_metadata(metadata)?;
for service in &plan.services {
models_path.validate_for(&service.base_path)?;
models_path_crate.validate_for(&service.base_path)?;
}
if let Some(ref common_dir) = config.output.common {
let common_code = generate_common_code(&plan, metadata, config)?;
output::write_generated_code(&common_code, common_dir)?;
}
if let Some(subdir) = config.output.models_subdir_path() {
write_models(&plan, metadata, config, &subdir)?;
}
if let Some(ref server_dir) = config.output.server {
let server_code = generate_server_code(&plan, metadata, config)?;
output::write_generated_code(&server_code, server_dir)?;
}
if let Some(ref client_dir) = config.output.client {
let client_code = generate_client_code(&plan, metadata, config)?;
output::write_generated_code(&client_code, client_dir)?;
}
if let Some(ref python_dir) = config.output.python {
let python_code = generate_python_code(&plan, metadata, config)?;
output::write_generated_code(&python_code, python_dir)?;
let package_files = python::package_files(config, &plan, metadata);
if !package_files.is_empty() {
let files = GeneratedCode {
files: package_files.into_iter().collect(),
};
output::write_generated_code(&files, &config.python_options().package.dir)?;
}
}
if let Some(ref node_dir) = config.output.node {
let node_code = generate_node_code(&plan, metadata, config)?;
output::write_generated_code(&node_code, node_dir)?;
}
if let Some(ref node_ts_dir) = config.output.node_ts {
let node_ts_code = generate_node_ts_code(&plan, metadata, config)?;
output::write_generated_code(&node_ts_code, node_ts_dir)?;
}
if let Some(ref wasm_dir) = config.output.wasm {
let wasm_code = generate_wasm_code(&plan, metadata, config)?;
output::write_generated_code(&wasm_code, wasm_dir)?;
}
Ok(())
}
fn write_models(
plan: &GenerationPlan,
metadata: &CodeGenMetadata,
config: &CodeGenConfig,
subdir: &std::path::Path,
) -> Result<()> {
std::fs::create_dir_all(subdir)?;
if config.generate_resource_enum {
let resource_enum = resources::generate_resource_enum(
plan,
metadata,
config,
config.error_type_path.as_deref(),
)?;
output::write_generated_code(&GeneratedCode::single("labels.rs", resource_enum), subdir)?;
}
let mut include_pyo3_impls = false;
if config.output.python.is_some() {
let pyo3_impls = python::generate_pyo3_impls(plan, metadata, config)?;
if !pyo3_impls.is_empty() {
output::write_generated_code(
&GeneratedCode::single("pyo3_impls.rs", pyo3_impls),
subdir,
)?;
include_pyo3_impls = true;
}
}
let mut mod_content = generate_models_mod(
&plan.services,
config.generate_resource_enum,
include_pyo3_impls,
config.output.common.is_some(),
metadata,
config.runtime,
);
if config.rest_client.json_int64_as_number {
mod_content.push_str(&json_int64::module());
if metadata_has_json_fields(metadata) {
mod_content.push_str(&json_int64::value_module());
}
}
output::write_generated_code(&GeneratedCode::single("mod.rs", mod_content), subdir)
}
pub(crate) fn metadata_has_json_fields(metadata: &CodeGenMetadata) -> bool {
metadata
.messages
.iter()
.filter(|(name, _)| !name.starts_with(".google."))
.flat_map(|(_, info)| &info.fields)
.any(|field| {
types::JsonType::of_field(&field.unified_type).is_some()
|| field
.oneof_variants
.iter()
.flatten()
.any(|v| types::JsonType::of_field(&v.field_type).is_some())
})
}
fn generate_common_code(
plan: &GenerationPlan,
metadata: &CodeGenMetadata,
config: &CodeGenConfig,
) -> Result<GeneratedCode> {
let mut files = HashMap::new();
for handler in ServiceHandler::for_plan(plan, metadata, config) {
let service = handler.plan;
let server_code = server::generate_common(&handler)?;
files.insert(
format!("{EXTRACTORS_SUBDIR}/{}/server.rs", service.base_path),
server_code,
);
let module_code = generate_common_module();
files.insert(
format!("{EXTRACTORS_SUBDIR}/{}/mod.rs", service.base_path),
module_code,
);
}
files.insert(
format!("{EXTRACTORS_SUBDIR}/mod.rs"),
generate_extractors_module(&plan.services),
);
let module_code = main_module(&plan.services);
files.insert("mod.rs".to_string(), module_code);
Ok(GeneratedCode { files })
}
fn generate_server_code(
plan: &GenerationPlan,
metadata: &CodeGenMetadata,
config: &CodeGenConfig,
) -> Result<GeneratedCode> {
let mut files = HashMap::new();
for handler in ServiceHandler::for_plan(plan, metadata, config) {
let service = handler.plan;
let trait_code = handler::generate(&handler)?;
files.insert(format!("{}/handler.rs", service.base_path), trait_code);
let server_code = server::generate_server(&handler)?;
files.insert(format!("{}/server.rs", service.base_path), server_code);
let module_code = generate_server_module(service);
files.insert(format!("{}/mod.rs", service.base_path), module_code);
}
let module_code = main_module(&plan.services);
files.insert("mod.rs".to_string(), module_code);
Ok(GeneratedCode { files })
}
fn generate_python_code(
plan: &GenerationPlan,
metadata: &CodeGenMetadata,
config: &CodeGenConfig,
) -> Result<GeneratedCode> {
let mut files = HashMap::new();
let handlers = ServiceHandler::for_plan(plan, metadata, config);
for service in handlers.iter().filter(|s| s.emits_scoped_binding()) {
let python_code = python::generate(service)?;
files.insert(format!("{}.rs", service.plan.base_path), python_code);
}
let module_code = python::main_module(&handlers)?;
files.insert("mod.rs".to_string(), module_code);
files.extend(python::helper_files(config));
Ok(GeneratedCode { files })
}
fn generate_node_code(
plan: &GenerationPlan,
metadata: &CodeGenMetadata,
config: &CodeGenConfig,
) -> Result<GeneratedCode> {
let mut files = HashMap::new();
let handlers = ServiceHandler::for_plan(plan, metadata, config);
for service in handlers.iter().filter(|s| s.emits_scoped_binding()) {
let napi_code = node::generate(service)?;
files.insert(format!("{}.rs", service.plan.base_path), napi_code);
}
let module_code = node::main_module(&handlers)?;
files.insert("mod.rs".to_string(), module_code);
Ok(GeneratedCode { files })
}
fn generate_node_ts_code(
plan: &GenerationPlan,
metadata: &CodeGenMetadata,
config: &CodeGenConfig,
) -> Result<GeneratedCode> {
let handlers = ServiceHandler::for_plan(plan, metadata, config);
let ts_code = node::typescript::generate_client_ts(&handlers);
let mut files = HashMap::new();
files.insert("client.ts".to_string(), ts_code);
let models_barrel = node::typescript::generate_models_barrel(&handlers);
files.insert("models/index.ts".to_string(), models_barrel);
Ok(GeneratedCode { files })
}
fn generate_wasm_code(
plan: &GenerationPlan,
metadata: &CodeGenMetadata,
config: &CodeGenConfig,
) -> Result<GeneratedCode> {
let handlers = ServiceHandler::for_plan(plan, metadata, config);
let mut files = HashMap::new();
files.insert("mod.rs".to_string(), wasm::generate_bindings(&handlers)?);
files.insert("client.d.ts".to_string(), wasm::generate_dts(&handlers));
Ok(GeneratedCode { files })
}
fn generate_client_code(
plan: &GenerationPlan,
metadata: &CodeGenMetadata,
config: &CodeGenConfig,
) -> Result<GeneratedCode> {
let mut files = HashMap::new();
let protocols = config.client_protocols.selected();
let multi = config.client_protocols.is_multi();
let mut resource_clients = Vec::new();
for handler in ServiceHandler::for_plan(plan, metadata, config) {
let service = handler.plan;
for &protocol in &protocols {
let prefix = if multi {
format!("{}/{}", service.base_path, protocol.subdir())
} else {
service.base_path.clone()
};
let client_code = match protocol {
ClientProtocol::Rest => client::generate(&handler)?,
ClientProtocol::Connect => connect_client::generate(&handler)?,
};
files.insert(format!("{prefix}/client.rs"), client_code);
files.insert(
format!("{prefix}/builders.rs"),
builder::generate(&handler, protocol)?,
);
let resource_code =
if protocol == ClientProtocol::Rest && config.output.generate_resource_clients {
resource_client::generate(&handler, plan)?
} else {
None
};
let has_resource_client = resource_code.is_some();
if let Some(code) = resource_code {
files.insert(format!("{prefix}/resource.rs"), code);
}
if has_resource_client && let Some(scoped) = handler.scoped_client_type() {
resource_clients.push((prefix.clone(), scoped, handler.client_feature_cfg()));
}
let handler_impls = config.rest_client.handler_impls.as_ref().filter(|impls| {
protocol == ClientProtocol::Rest && !impls.skip.contains(&service.base_path)
});
let handler_impl = match handler_impls {
Some(impls) => {
let code = client_handler::generate(&handler, impls)?;
files.insert(format!("{prefix}/handler.rs"), code);
Some(impls.feature.as_str())
}
None => None,
};
files.insert(
format!("{prefix}/mod.rs"),
generate_client_module(has_resource_client, handler_impl),
);
}
if multi {
files.insert(
format!("{}/mod.rs", service.base_path),
generate_protocol_split_module(&protocols),
);
}
}
let aggregate_code = aggregate::generate(plan, metadata, config)?;
let has_aggregate = aggregate_code.is_some();
if let Some(code) = aggregate_code {
files.insert("client.rs".to_string(), code);
}
if config.uses_reqwest_transport() {
files.insert(
"transport.rs".to_string(),
transport::generate(config.rest_client.user_agent.as_deref()),
);
}
if let Some(errors) = &config.rest_client.error_module {
files.insert(
"error.rs".to_string(),
error_module::generate(config, errors, metadata)?,
);
}
files.insert(
"mod.rs".to_string(),
generate_client_main_module(plan, metadata, config, has_aggregate, &resource_clients),
);
Ok(GeneratedCode { files })
}
fn generate_common_module() -> String {
let tokens = quote! {
#[cfg(feature = "axum")]
pub mod server;
};
format_tokens_static(tokens)
}
pub(crate) const EXTRACTORS_SUBDIR: &str = "extractors";
fn generate_extractors_module(services: &[ServicePlan]) -> String {
let decls: Vec<TokenStream> = services
.iter()
.map(|s| format_ident!("{}", s.base_path))
.collect::<BTreeSet<_>>()
.into_iter()
.map(|m| quote! { #[cfg(feature = "axum")] pub mod #m; })
.collect();
format_tokens_static(quote! { #(#decls)* })
}
fn generate_server_module(service: &ServicePlan) -> String {
let handler_ident = format_ident!("{}", service.handler_name);
let tokens = quote! {
pub use handler::#handler_ident;
mod handler;
#[cfg(feature = "axum")]
pub mod server;
};
format_tokens_static(tokens)
}
fn generate_client_module(has_resource_client: bool, handler_impl: Option<&str>) -> String {
let allow = client_module_allow();
let reexport_allow = quote! { #[allow(unused_imports)] };
let resource_module = if has_resource_client {
quote! {
#reexport_allow
pub use resource::*;
#allow
pub mod resource;
}
} else {
quote! {}
};
let handler_module = match handler_impl {
Some(feature) => quote! {
#[cfg(all(feature = #feature, not(target_arch = "wasm32")))]
mod handler;
},
None => quote! {},
};
let tokens = quote! {
#reexport_allow
pub use client::*;
#reexport_allow
pub use builders::*;
#resource_module
#handler_module
#allow
pub mod client;
#allow
pub mod builders;
};
format_tokens_static(tokens)
}
fn client_module_allow() -> TokenStream {
quote! {
#[allow(dead_code, clippy::too_many_arguments, clippy::doc_lazy_continuation)]
}
}
fn generate_protocol_split_module(protocols: &[ClientProtocol]) -> String {
let allow = client_module_allow();
let mods: Vec<TokenStream> = protocols
.iter()
.map(|p| {
let m = format_ident!("{}", p.subdir());
quote! { #allow pub mod #m; }
})
.collect();
format_tokens_static(quote! { #(#mods)* })
}
pub fn main_module(services: &[ServicePlan]) -> String {
let service_modules: Vec<TokenStream> = services
.iter()
.map(|s| {
let module_name = format_ident!("{}", s.base_path);
quote! { pub mod #module_name; }
})
.collect();
let tokens = quote! {
#(#service_modules)*
};
format_tokens_static(tokens)
}
fn generate_client_main_module(
plan: &GenerationPlan,
metadata: &CodeGenMetadata,
config: &CodeGenConfig,
has_aggregate: bool,
resource_clients: &[(String, Ident, TokenStream)],
) -> String {
let services = &plan.services;
let result_path = config.client_result_path();
let service_modules: Vec<TokenStream> = services
.iter()
.map(|s| {
let module_name = format_ident!("{}", s.base_path);
let cfg = ServiceHandler::new(s, metadata, config).client_feature_cfg();
quote! { #cfg pub mod #module_name; }
})
.collect();
let resource_exports: Vec<TokenStream> = resource_clients
.iter()
.map(|(path, scoped, cfg)| {
let segments = path.split('/').map(|segment| format_ident!("{}", segment));
quote! { #cfg pub use #(#segments::)*#scoped; }
})
.collect();
let aggregate_module = if has_aggregate {
quote! {
#[allow(clippy::too_many_arguments, clippy::doc_lazy_continuation)]
pub mod client;
#[allow(unused_imports)]
pub use client::*;
}
} else {
quote! {}
};
let has_list = services
.iter()
.flat_map(|s| s.methods.iter())
.any(|m| matches!(m.request_type, RequestType::List));
let pagination_allow = if config.rest_client.resource_features {
quote! { #[allow(dead_code)] }
} else {
quote! {}
};
let pagination = if has_list {
quote! {
use futures::Future;
#pagination_allow
pub(super) fn stream_paginated<F, Fut, S, T>(
state: S,
op: F,
) -> impl futures::Stream<Item = #result_path<T>>
where
F: Fn(S, Option<String>) -> Fut + Copy,
Fut: Future<Output = #result_path<(T, S, Option<String>)>>,
{
enum PaginationState<T> {
Start(T),
HasMore(T, String),
Done,
}
futures::stream::unfold(
PaginationState::Start(state),
move |state| async move {
let (s, page_token) = match state {
PaginationState::Start(s) => (s, None),
PaginationState::HasMore(s, page_token) if !page_token.is_empty() => {
(s, Some(page_token))
}
_ => {
return None;
}
};
let (resp, s, continuation) = match op(s, page_token).await {
Ok(resp) => resp,
Err(e) => return Some((Err(e), PaginationState::Done)),
};
let next_state = match continuation {
Some(token) => PaginationState::HasMore(s, token),
None => PaginationState::Done,
};
Some((Ok(resp), next_state))
},
)
}
}
} else {
quote! {}
};
let transport_module = config
.uses_reqwest_transport()
.then(|| quote! { pub mod transport; });
let error_module = config
.rest_client
.error_module
.is_some()
.then(|| quote! { pub mod error; });
let encode_path_value = services
.iter()
.flat_map(|s| &s.methods)
.any(MethodPlan::formats_path_values)
.then(|| {
let helper = client::encode_path_value_fn();
quote! { #pagination_allow #helper }
});
let tokens = quote! {
#(#service_modules)*
#(#resource_exports)*
#aggregate_module
#transport_module
#error_module
#pagination
#encode_path_value
};
format_tokens_static(tokens)
}
pub fn generate_models_mod(
services: &[ServicePlan],
include_labels: bool,
include_pyo3_impls: bool,
include_extractors: bool,
metadata: &CodeGenMetadata,
runtime: Runtime,
) -> String {
let mod_segments = |svc: &ServicePlan| -> (String, String) {
let parts: Vec<&str> = svc.package.split('.').collect();
if parts.len() >= 2 {
(
parts[parts.len() - 2].to_string(),
parts[parts.len() - 1].to_string(),
)
} else {
(svc.base_path.clone(), "v1".to_string())
}
};
let mut packages: BTreeMap<String, (String, String, BTreeSet<String>)> = BTreeMap::new();
for svc in services {
let (svc_seg, ver_seg) = mod_segments(svc);
let entry = packages
.entry(svc.package.clone())
.or_insert_with(|| (svc_seg, ver_seg, BTreeSet::new()));
let type_names = &mut entry.2;
type_names.extend(svc.managed_resources.iter().map(|r| r.type_name.clone()));
let fq_prefix = format!(".{}.", svc.package);
let fq_pkg = format!(".{}", svc.package);
for (fq_name, msg_info) in &metadata.messages {
if msg_info.resource_descriptor.is_some()
&& (fq_name.starts_with(&fq_prefix) || fq_name.starts_with(&fq_pkg))
{
type_names.insert(last_segment(fq_name).to_string());
}
}
}
let mut service_mods: Vec<TokenStream> = Vec::new();
let mut reexports: Vec<TokenStream> = Vec::new();
for (package, (svc_seg, ver_seg, type_names)) in &packages {
let svc_mod = format_ident!("{}", svc_seg);
let ver_mod = format_ident!("{}", ver_seg);
let main_include = format!("./{package}.rs");
let tonic_include_tokens = match runtime {
Runtime::Prost => {
let tonic_include = format!("./{package}.tonic.rs");
quote! {
#[cfg(feature = "grpc")]
include!(#tonic_include);
}
}
Runtime::Buffa => quote! {},
};
service_mods.push(quote! {
pub mod #svc_mod {
pub mod #ver_mod {
include!(#main_include);
#tonic_include_tokens
}
}
});
for type_name in type_names {
let type_ident = format_ident!("{}", type_name);
reexports.push(quote! {
pub use #svc_mod::#ver_mod::#type_ident;
});
}
}
let labels_decl: TokenStream = if include_labels {
quote! {
pub mod labels;
pub use labels::{ObjectLabel, Resource};
}
} else {
quote! {}
};
let pyo3_impls_decl: TokenStream = if include_pyo3_impls {
quote! {
#[cfg(feature = "python")]
#[allow(clippy::clone_on_copy)]
pub mod pyo3_impls;
#[cfg(feature = "python")]
pub use pyo3_impls::*;
}
} else {
quote! {}
};
let common_mods: TokenStream = if include_extractors {
let extractors_mod = format_ident!("{EXTRACTORS_SUBDIR}");
quote! {
#[cfg(feature = "axum")]
pub mod #extractors_mod;
}
} else {
quote! {}
};
let lint_allows = match runtime {
Runtime::Buffa => quote! {
#![allow(non_camel_case_types)]
#![allow(clippy::derivable_impls)]
#![allow(clippy::match_single_binding)]
#![allow(clippy::uninlined_format_args)]
#![allow(clippy::doc_lazy_continuation)]
},
Runtime::Prost => quote! {},
};
let tokens = quote! {
#![allow(unexpected_cfgs)]
#![allow(clippy::empty_docs)]
#![allow(clippy::useless_borrows_in_formatting)]
#lint_allows
use std::collections::HashMap;
#labels_decl
#pyo3_impls_decl
#(#reexports)*
pub type PropertyMap = HashMap<String, serde_json::Value>;
#(#service_mods)*
#common_mods
};
format_tokens_static(tokens)
}
#[derive(Debug)]
pub struct GeneratedCode {
pub files: HashMap<String, String>,
}
impl GeneratedCode {
fn single(path: &str, content: String) -> Self {
Self {
files: HashMap::from([(path.to_string(), content)]),
}
}
}
pub(crate) struct ServiceHandler<'a> {
pub(crate) plan: &'a ServicePlan,
pub(crate) metadata: &'a CodeGenMetadata,
pub(crate) config: &'a CodeGenConfig,
}
impl<'a> ServiceHandler<'a> {
pub(crate) fn new(
plan: &'a ServicePlan,
metadata: &'a CodeGenMetadata,
config: &'a CodeGenConfig,
) -> Self {
Self {
plan,
metadata,
config,
}
}
pub(crate) fn for_plan(
plan: &'a GenerationPlan,
metadata: &'a CodeGenMetadata,
config: &'a CodeGenConfig,
) -> Vec<Self> {
plan.services
.iter()
.map(|service| Self::new(service, metadata, config))
.collect()
}
pub(crate) fn with_plan(&self, plan: &'a ServicePlan) -> Self {
Self::new(plan, self.metadata, self.config)
}
}
impl ServiceHandler<'_> {
pub(crate) fn resource(&self) -> Option<&ManagedResource> {
self.plan.managed_resources.first()
}
pub(crate) fn is_resource_scoped(&self) -> bool {
self.resource().is_some()
}
pub(crate) fn binding_mode(&self) -> BindingMode {
if self.emits_scoped_binding() {
BindingMode::Scoped
} else {
BindingMode::Flat
}
}
pub(crate) fn emits_scoped_binding(&self) -> bool {
self.is_resource_scoped() && self.supports_scoped_client()
}
pub(crate) fn accessor_spec(&self) -> Option<AccessorSpec> {
let resource = self.resource()?;
let params = self.plan.resource_accessor_params.clone()?;
let nested = params.len() > 1;
Some(AccessorSpec {
singular: resource.descriptor.singular.clone(),
params,
nested,
})
}
pub(crate) fn methods(&self) -> impl Iterator<Item = MethodHandler<'_>> {
self.plan.methods.iter().map(|plan| MethodHandler {
plan,
metadata: self.metadata,
config: self.config,
})
}
pub(crate) fn rest_methods(&self) -> impl Iterator<Item = MethodHandler<'_>> {
self.methods().filter(|m| m.plan.has_http_route)
}
pub(crate) fn client_type(&self) -> Ident {
if let Some(resource) = self.resource() {
format_ident!(
"{}",
format!("{} client", resource.descriptor.singular).to_case(Case::Pascal)
)
} else {
format_ident!(
"{}Client",
self.plan
.service_name
.trim_end_matches("Service")
.trim_end_matches('s')
)
}
}
pub(crate) fn low_level_client_type(&self, protocol: crate::codegen::ClientProtocol) -> Ident {
if protocol == crate::codegen::ClientProtocol::Connect {
return format_ident!(
"{}ConnectClient",
self.plan.service_name.to_case(Case::Pascal)
);
}
if let Some(resource) = self.resource() {
format_ident!(
"{}ServiceClient",
resource.descriptor.singular.to_case(Case::Pascal)
)
} else {
self.client_type()
}
}
pub(crate) fn connect_inner_client_path(&self) -> crate::Result<syn::Path> {
let base = self.config.connect_client_path.as_deref().ok_or_else(|| {
crate::Error::Build(
"client_protocol = Connect requires `connect_client_path` to be set".to_string(),
)
})?;
let full = format!(
"{base}::{}Client",
self.plan.service_name.to_case(Case::Pascal)
);
syn::parse_str(&full).map_err(|source| crate::error::Error::InvalidRustPath {
path: full.clone(),
message: "connect_client_path + service client name".to_string(),
source,
})
}
pub(crate) fn scoped_client_type(&self) -> Option<Ident> {
self.resource().map(|_| self.client_type())
}
pub(crate) fn supports_scoped_client(&self) -> bool {
!self
.methods()
.filter(|m| m.is_scoped_instance_method())
.any(|m| {
m.required_parameters().any(|p| {
p.is_path_param()
&& !matches!(
p.field_type().base_type,
crate::parsing::types::BaseType::String
)
})
})
}
fn models_segment(&self) -> String {
package_models_segment(&self.plan.package)
.map_or_else(|| self.plan.base_path.clone(), str::to_string)
}
fn models_version(&self) -> String {
package_models_version(&self.plan.package).to_string()
}
pub(crate) fn models_path(&self) -> syn::Path {
ModelsPath::from_template(&self.config.models_path_template)
.resolve(&self.models_segment(), &self.models_version())
}
pub(crate) fn foreign_models_imports(&self) -> Vec<TokenStream> {
let template = ModelsPath::from_template(&self.config.models_path_template);
self.methods()
.flat_map(|m| {
[
m.plan.metadata.input_type.as_str(),
m.plan.metadata.output_type.as_str(),
]
})
.filter_map(message_package)
.filter(|package| *package != self.plan.package && !package.starts_with("google."))
.unique()
.filter_map(|package| {
let segment = package_models_segment(&package)?;
let path = template.resolve(segment, package_models_version(&package));
Some(quote! { use #path::*; })
})
.collect()
}
pub(crate) fn models_path_crate(&self) -> syn::Path {
ModelsPath::from_template(&self.config.models_path_crate_template)
.resolve(&self.models_segment(), &self.models_version())
}
pub(crate) fn client_feature_cfg(&self) -> TokenStream {
if self.config.rest_client.resource_features {
let feature = self.models_segment();
quote! { #[cfg(feature = #feature)] }
} else {
quote! {}
}
}
pub(crate) fn models_root_path(&self) -> Option<syn::Path> {
let models_path = ModelsPath::from_template(&self.config.models_path_template);
syn::parse_str(models_path.root()?).ok()
}
}
fn message_package(type_name: &str) -> Option<String> {
let segments: Vec<&str> = type_name
.trim_start_matches('.')
.split('.')
.take_while(|s| s.starts_with(|c: char| c.is_ascii_lowercase()))
.collect();
(!segments.is_empty()).then(|| segments.join("."))
}
pub(crate) fn package_models_segment(package: &str) -> Option<&str> {
let segs: Vec<&str> = package.split('.').filter(|s| !s.is_empty()).collect();
match segs.as_slice() {
[.., name, version] if is_version_segment(version) => Some(name),
[.., name] => Some(name),
[] => None,
}
}
pub(crate) fn package_models_version(package: &str) -> &str {
let segs: Vec<&str> = package.split('.').filter(|s| !s.is_empty()).collect();
match segs.as_slice() {
[.., _name, version] if is_version_segment(version) => version,
_ => "v1",
}
}
fn is_version_segment(seg: &str) -> bool {
seg.strip_prefix('v')
.is_some_and(|rest| rest.starts_with(|c: char| c.is_ascii_digit()))
}
pub(crate) struct MethodHandler<'a> {
plan: &'a MethodPlan,
metadata: &'a CodeGenMetadata,
config: &'a CodeGenConfig,
}
impl MethodHandler<'_> {
pub(crate) fn query_key<'p>(
&self,
param: &'p crate::analysis::QueryParam,
) -> std::borrow::Cow<'p, str> {
let input = self.plan.metadata.input_type.trim_start_matches('.');
let json_names = self
.config
.rest_client
.json_query_packages
.iter()
.any(|package| {
input
.strip_prefix(package.as_str())
.is_some_and(|rest| rest.starts_with('.'))
});
param.key(json_names)
}
pub(crate) fn is_collection_method(&self) -> bool {
crate::analysis::is_collection_method(
&self.plan.request_type,
&self.plan.metadata.method_name,
)
}
pub(crate) fn emit_shape(&self) -> crate::analysis::EmitShape {
crate::analysis::emit_shape(&self.plan.request_type, self.is_collection_method())
}
pub(crate) fn is_scoped_instance_method(&self) -> bool {
if matches!(
self.plan.request_type,
RequestType::Get | RequestType::Update | RequestType::Delete
) {
return true;
}
matches!(self.plan.request_type, RequestType::Custom(_))
&& !self.is_collection_method()
&& self.plan.path_parameters().next().is_some()
}
pub(crate) fn param_plan(&self, mode: BindingMode) -> Vec<&RequestParam> {
let (required, optional) = self.param_plan_split(mode);
required.into_iter().chain(optional).collect()
}
pub(crate) fn param_plan_split(
&self,
mode: BindingMode,
) -> (Vec<&RequestParam>, Vec<&RequestParam>) {
use crate::analysis::EmitShape;
let shape = self.emit_shape();
let is_collection_shape = matches!(shape, EmitShape::List | EmitShape::Create);
let drop_path = !is_collection_shape && mode == BindingMode::Scoped;
let drop_page_token = shape == EmitShape::List;
let keep = |p: &&RequestParam| {
let is_dropped_path = drop_path && p.is_path_param();
let is_dropped_page_token = drop_page_token && p.name() == "page_token";
!is_dropped_path && !is_dropped_page_token
};
let required = self.required_parameters().filter(keep).collect();
let optional = self.optional_parameters().filter(keep).collect();
(required, optional)
}
pub(crate) fn output_message(&self) -> Option<&MessageInfo> {
if self.plan.metadata.output_type.ends_with("Empty") {
return None;
}
self.metadata.messages.get(&self.plan.metadata.output_type)
}
pub(crate) fn output_type(&self) -> Option<Ident> {
self.output_message().map(|t| extract_type_ident(&t.name))
}
pub(crate) fn handler_output_type(&self) -> TokenStream {
if self.plan.has_response {
let output = self.output_type();
quote! { #output }
} else {
quote! { () }
}
}
pub(crate) fn result_type(&self) -> TokenStream {
match self.output_type() {
Some(output) if self.plan.metadata.server_streaming => quote! { Vec<#output> },
Some(output) => quote! { #output },
None if self.is_existence_check() => quote! { bool },
None => quote! { () },
}
}
pub(crate) fn is_existence_check(&self) -> bool {
self.plan.http_method.eq_ignore_ascii_case("head") && self.output_type().is_none()
}
pub(crate) fn response_header_fields(&self) -> Vec<&MessageField> {
self.output_message()
.map(|output| {
output
.fields
.iter()
.filter(|f| f.response_header.is_some())
.collect()
})
.unwrap_or_default()
}
pub(crate) fn list_output_field(&self) -> Option<&MessageField> {
self.output_message()?
.fields
.iter()
.find(|f| !f.name.contains("page_token"))
}
pub(crate) fn input_message(&self) -> Option<&MessageInfo> {
if self.plan.metadata.input_type == "Empty" {
return None;
}
self.metadata.messages.get(&self.plan.metadata.input_type)
}
pub(crate) fn input_type(&self) -> Option<Ident> {
self.input_message().map(|t| extract_type_ident(&t.name))
}
pub(crate) fn builder_type(&self) -> Ident {
format_ident!("{}Builder", self.plan.metadata.method_name)
}
pub(crate) fn binding_method_name(&self) -> Ident {
format_ident!("{}", self.binding_method_name_str())
}
pub(crate) fn binding_method_name_str(&self) -> String {
match self.plan.metadata.operation.as_ref() {
Some(op) if !op.operation_id.is_empty() => operation_to_method_name(&op.operation_id),
_ => self.plan.handler_function_name.clone(),
}
}
pub(crate) fn js_method_name(&self) -> String {
self.binding_method_name_str().to_case(Case::Camel)
}
pub(crate) fn field_type(&self, field_type: &UnifiedType, ctx: RenderContext) -> syn::Type {
let rust_type = types::unified_to_rust(field_type, ctx, self.config.runtime);
syn::parse_str(&rust_type).unwrap_or_else(|e| {
panic!("internal: unified_to_rust produced an invalid type `{rust_type}`: {e}")
})
}
pub(crate) fn field_assignment(
&self,
field_type: &UnifiedType,
field_ident: &proc_macro2::Ident,
ctx: &RenderContext,
) -> TokenStream {
types::field_assignment(field_type, field_ident, ctx, self.config.runtime)
}
pub(crate) fn required_parameters(&self) -> impl Iterator<Item = &RequestParam> {
self.plan
.parameters
.iter()
.filter(|param| !param.is_optional())
}
pub(crate) fn optional_parameters(&self) -> impl Iterator<Item = &RequestParam> {
self.plan
.parameters
.iter()
.filter(|param| param.is_optional())
}
pub(crate) fn split_body_fields(&self) -> (Vec<&BodyField>, Vec<&BodyField>) {
split_body_fields(self.plan)
}
}
pub(crate) fn extract_type_ident(full_type: &str) -> Ident {
format_ident!("{}", crate::utils::extract_simple_type_name(full_type))
}
#[cfg(test)]
mod tests {
use super::message_package;
#[test]
fn message_package_stops_at_the_message_name() {
assert_eq!(
message_package(".unitycatalog.shares.v1.ShareInfo").as_deref(),
Some("unitycatalog.shares.v1")
);
assert_eq!(
message_package("demo.v1.Outer.Inner").as_deref(),
Some("demo.v1")
);
assert_eq!(message_package("Empty"), None);
}
}