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};
mod aggregate;
mod bindings;
mod builder;
mod client;
mod config;
mod connect_client;
mod handler;
pub(crate) mod node;
mod python;
mod resource_client;
mod resources;
mod server;
mod tokens;
mod wasm;
pub use config::{
BindingsConfig, ClientProtocol, ClientProtocols, CodeGenConfig, CodeGenOutput,
DEFAULT_TRANSPORT_TYPE_PATH, ModelsPath, Runtime,
};
pub(crate) use tokens::{doc_tokens, format_tokens, format_tokens_static};
#[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 {
std::iter::repeat_n("{}", self.params.len())
.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);
}
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)?;
}
let common_code = generate_common_code(&plan, metadata, config)?;
output::write_generated_code(&common_code, &config.output.common)?;
if config.output.models.is_some() {
let subdir = config
.output
.models_subdir_path()
.expect("models is Some so subdir is Some");
std::fs::create_dir_all(&subdir).map_err(Error::Io)?;
if config.generate_resource_enum {
let resource_enum = resources::generate_resource_enum(
&plan,
metadata,
config,
config.error_type_path.as_deref(),
)?;
let mut models_files = GeneratedCode {
files: std::collections::HashMap::new(),
};
models_files
.files
.insert("labels.rs".to_string(), resource_enum);
output::write_generated_code(&models_files, &subdir)?;
}
let mut include_pyo3_impls = false;
if config.output.python.is_some() {
let pyo3_impls = python::generate_pyo3_impls(&plan, metadata, config.runtime)?;
if !pyo3_impls.is_empty() {
let mut pyo3_files = GeneratedCode {
files: std::collections::HashMap::new(),
};
pyo3_files
.files
.insert("pyo3_impls.rs".to_string(), pyo3_impls);
output::write_generated_code(&pyo3_files, &subdir)?;
include_pyo3_impls = true;
}
}
let include_labels = config.generate_resource_enum;
let mod_content = generate_models_mod(
&plan.services,
include_labels,
include_pyo3_impls,
metadata,
config.runtime,
);
let mut mod_files = GeneratedCode {
files: std::collections::HashMap::new(),
};
mod_files.files.insert("mod.rs".to_string(), mod_content);
output::write_generated_code(&mod_files, &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)?;
}
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 generate_common_code(
plan: &GenerationPlan,
metadata: &CodeGenMetadata,
config: &CodeGenConfig,
) -> Result<GeneratedCode> {
let mut files = HashMap::new();
for service in &plan.services {
let handler = ServiceHandler {
plan: service,
metadata,
config,
};
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 service in &plan.services {
let handler = ServiceHandler {
plan: service,
metadata,
config,
};
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 = plan
.services
.iter()
.map(|service| ServiceHandler {
plan: service,
metadata,
config,
})
.collect_vec();
for service in handlers.iter().filter(|s| s.is_resource_scoped()) {
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);
let python_typings_code = python::generate_typings(&handlers);
files.insert(
config.output.python_typings_filename.clone(),
python_typings_code,
);
Ok(GeneratedCode { files })
}
fn generate_node_code(
plan: &GenerationPlan,
metadata: &CodeGenMetadata,
config: &CodeGenConfig,
) -> Result<GeneratedCode> {
let mut files = HashMap::new();
let handlers = plan
.services
.iter()
.map(|service| ServiceHandler {
plan: service,
metadata,
config,
})
.collect_vec();
for service in handlers.iter().filter(|s| s.is_resource_scoped()) {
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 = plan
.services
.iter()
.map(|service| ServiceHandler {
plan: service,
metadata,
config,
})
.collect_vec();
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 = plan
.services
.iter()
.map(|service| ServiceHandler {
plan: service,
metadata,
config,
})
.collect_vec();
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();
for service in &plan.services {
let handler = ServiceHandler {
plan: service,
metadata,
config,
};
let mut emitted_resource_client = false;
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 has_resource_client = protocol == ClientProtocol::Rest
&& config.output.generate_resource_clients
&& resource_client::generate(&handler, plan)?
.map(|code| {
files.insert(format!("{prefix}/resource.rs"), code);
})
.is_some();
files.insert(
format!("{prefix}/mod.rs"),
generate_client_module(has_resource_client),
);
emitted_resource_client |= has_resource_client;
}
if multi {
files.insert(
format!("{}/mod.rs", service.base_path),
generate_protocol_split_module(&protocols),
);
}
let _ = emitted_resource_client;
}
let has_aggregate = aggregate::generate(plan, metadata, config)?
.map(|aggregate_code| {
files.insert("client.rs".to_string(), aggregate_code);
})
.is_some();
let module_code = generate_client_main_module(&plan.services, has_aggregate);
files.insert("mod.rs".to_string(), module_code);
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) -> String {
let allow = quote! {
#[allow(dead_code, clippy::too_many_arguments, clippy::doc_lazy_continuation)]
};
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 tokens = quote! {
#reexport_allow
pub use client::*;
#reexport_allow
pub use builders::*;
#resource_module
#allow
pub mod client;
#allow
pub mod builders;
};
format_tokens_static(tokens)
}
fn generate_protocol_split_module(protocols: &[ClientProtocol]) -> String {
let allow = quote! {
#[allow(dead_code, clippy::too_many_arguments, clippy::doc_lazy_continuation)]
};
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(services: &[ServicePlan], has_aggregate: bool) -> 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 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 = if has_list {
quote! {
use futures::Future;
pub(super) fn stream_paginated<F, Fut, S, T>(
state: S,
op: F,
) -> impl futures::Stream<Item = crate::Result<T>>
where
F: Fn(S, Option<String>) -> Fut + Copy,
Fut: Future<Output = crate::Result<(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 tokens = quote! {
#(#service_modules)*
#aggregate_module
#pagination
};
format_tokens_static(tokens)
}
pub fn generate_models_mod(
services: &[ServicePlan],
include_labels: bool,
include_pyo3_impls: 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))
{
let simple = fq_name
.rfind('.')
.map(|i| &fq_name[i + 1..])
.unwrap_or(fq_name.as_str());
type_names.insert(simple.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")]
pub mod pyo3_impls;
#[cfg(feature = "python")]
pub use pyo3_impls::*;
}
} else {
quote! {}
};
let extractors_mod = format_ident!("{EXTRACTORS_SUBDIR}");
let common_mods: TokenStream = quote! {
#[cfg(feature = "axum")]
pub mod #extractors_mod;
};
let lint_allows = match runtime {
Runtime::Buffa => quote! {
#![allow(non_camel_case_types)]
#![allow(clippy::derivable_impls)]
},
Runtime::Prost => quote! {},
};
let tokens = quote! {
#![allow(unexpected_cfgs)]
#![allow(clippy::empty_docs)]
#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 CodeGenMetadata {
fn get_message_meta(&self, message_name: &str) -> Option<MessageMeta<'_>> {
self.messages.get(message_name).map(|info| MessageMeta {
info,
metadata: self,
})
}
}
pub(crate) struct MessageMeta<'a> {
info: &'a MessageInfo,
#[allow(dead_code)]
metadata: &'a CodeGenMetadata,
}
pub(crate) struct ServiceHandler<'a> {
pub(crate) plan: &'a ServicePlan,
pub(crate) metadata: &'a CodeGenMetadata,
pub(crate) config: &'a CodeGenConfig,
}
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.is_resource_scoped() {
BindingMode::Scoped
} else {
BindingMode::Flat
}
}
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())
}
fn models_segment(&self) -> String {
let segs: Vec<&str> = self
.plan
.package
.split('.')
.filter(|s| !s.is_empty())
.collect();
match segs.as_slice() {
[.., name, version] if is_version_segment(version) => name.to_string(),
[.., name] => name.to_string(),
_ => self.plan.base_path.clone(),
}
}
fn models_version(&self) -> String {
let segs: Vec<&str> = self
.plan
.package
.split('.')
.filter(|s| !s.is_empty())
.collect();
match segs.as_slice() {
[.., _name, version] if is_version_segment(version) => version.to_string(),
_ => "v1".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 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 models_root_path(&self) -> Option<syn::Path> {
let template = &self.config.models_path_template;
let prefix = template.split("{service}").next()?;
let trimmed = prefix.trim_end_matches(':');
if trimmed.is_empty() {
return None;
}
syn::parse_str(trimmed).ok()
}
}
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 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<MessageMeta<'_>> {
if self.plan.metadata.output_type.ends_with("Empty") {
return None;
}
self.metadata
.get_message_meta(&self.plan.metadata.output_type)
}
pub(crate) fn output_type(&self) -> Option<Ident> {
self.output_message()
.map(|t| extract_type_ident(&t.info.name))
}
pub(crate) fn list_output_field(&self) -> Option<&MessageField> {
self.output_message()?
.info
.fields
.iter()
.find(|f| !f.name.contains("page_token"))
}
pub(crate) fn input_message(&self) -> Option<MessageMeta<'_>> {
if self.plan.metadata.input_type == "Empty" {
return None;
}
self.metadata
.get_message_meta(&self.plan.metadata.input_type)
}
pub(crate) fn input_type(&self) -> Option<Ident> {
self.input_message()
.map(|t| extract_type_ident(&t.info.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() => op.operation_id.to_case(Case::Snake),
_ => self.plan.handler_function_name.clone(),
}
}
pub(crate) fn field_type(&self, field_type: &UnifiedType, ctx: RenderContext) -> syn::Type {
let rust_type = types::unified_to_rust(field_type, ctx);
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))
}