use crate::analysis::{OperationInfo, ParameterInfo, SchemaAnalysis};
use crate::generator::CodeGenerator;
use heck::ToSnakeCase;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use std::collections::BTreeMap;
impl CodeGenerator {
pub fn generate_http_client_struct(&self) -> TokenStream {
let has_retry = self.config().retry_config.is_some();
let has_tracing = self.config().tracing_enabled;
let retry_config_struct = if has_retry {
quote! {
#[derive(Debug, Clone)]
pub struct RetryConfig {
pub max_retries: u32,
pub initial_delay_ms: u64,
pub max_delay_ms: u64,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_retries: 3,
initial_delay_ms: 500,
max_delay_ms: 16000,
}
}
}
}
} else {
quote! {}
};
let client_struct = quote! {
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use std::collections::BTreeMap;
#[derive(Clone)]
pub struct HttpClient {
base_url: String,
api_key: Option<String>,
http_client: ClientWithMiddleware,
custom_headers: BTreeMap<String, String>,
}
};
let constructor = self.generate_constructor(has_retry, has_tracing);
let builder_methods = self.generate_builder_methods();
let default_impl = quote! {
impl Default for HttpClient {
fn default() -> Self {
Self::new()
}
}
};
let path_encoder = quote! {
fn __pct_encode_path_segment(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for &b in s.as_bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
out.push(b as char);
}
_ => {
out.push('%');
out.push_str(&format!("{:02X}", b));
}
}
}
out
}
};
quote! {
#retry_config_struct
#client_struct
impl HttpClient {
#constructor
#builder_methods
}
#default_impl
#path_encoder
}
}
fn generate_constructor(&self, has_retry: bool, has_tracing: bool) -> TokenStream {
let retry_param = if has_retry {
quote! { retry_config: Option<RetryConfig>, }
} else {
quote! {}
};
let tracing_param = if has_tracing {
quote! { enable_tracing: bool, }
} else {
quote! {}
};
let retry_middleware = if has_retry {
quote! {
if let Some(config) = retry_config {
use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff};
let retry_policy = ExponentialBackoff::builder()
.retry_bounds(
std::time::Duration::from_millis(config.initial_delay_ms),
std::time::Duration::from_millis(config.max_delay_ms),
)
.build_with_max_retries(config.max_retries);
let retry_middleware = RetryTransientMiddleware::new_with_policy(retry_policy);
client_builder = client_builder.with(retry_middleware);
}
}
} else {
quote! {}
};
let tracing_middleware = if has_tracing {
quote! {
if enable_tracing {
use reqwest_tracing::TracingMiddleware;
client_builder = client_builder.with(TracingMiddleware::default());
}
}
} else {
quote! {}
};
let default_constructor = if has_retry && has_tracing {
quote! {
pub fn new() -> Self {
Self::with_config(None, true)
}
}
} else if has_retry {
quote! {
pub fn new() -> Self {
Self::with_config(None)
}
}
} else if has_tracing {
quote! {
pub fn new() -> Self {
Self::with_config(true)
}
}
} else {
quote! {
pub fn new() -> Self {
let reqwest_client = reqwest::Client::new();
let client_builder = ClientBuilder::new(reqwest_client);
let http_client = client_builder.build();
Self {
base_url: String::new(),
api_key: None,
http_client,
custom_headers: BTreeMap::new(),
}
}
}
};
if has_retry || has_tracing {
quote! {
#default_constructor
pub fn with_config(#retry_param #tracing_param) -> Self {
let reqwest_client = reqwest::Client::new();
let mut client_builder = ClientBuilder::new(reqwest_client);
#tracing_middleware
#retry_middleware
let http_client = client_builder.build();
Self {
base_url: String::new(),
api_key: None,
http_client,
custom_headers: BTreeMap::new(),
}
}
}
} else {
default_constructor
}
}
fn generate_builder_methods(&self) -> TokenStream {
quote! {
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = base_url.into();
self
}
pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
self.api_key = Some(api_key.into());
self
}
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.custom_headers.insert(name.into(), value.into());
self
}
pub fn with_headers(mut self, headers: BTreeMap<String, String>) -> Self {
self.custom_headers.extend(headers);
self
}
}
}
pub fn generate_operation_methods(&self, analysis: &SchemaAnalysis) -> TokenStream {
let param_enums = self.generate_param_enum_types(analysis);
let op_error_enums: Vec<TokenStream> = analysis
.operations
.values()
.filter_map(|op| self.generate_op_error_enum(op))
.collect();
let methods: Vec<TokenStream> = analysis
.operations
.values()
.map(|op| self.generate_single_operation_method(op))
.collect();
quote! {
#param_enums
#(#op_error_enums)*
impl HttpClient {
#(#methods)*
}
}
}
fn generate_param_enum_types(&self, analysis: &SchemaAnalysis) -> TokenStream {
let mut by_name: BTreeMap<String, &ParameterInfo> = BTreeMap::new();
for op in analysis.operations.values() {
for param in &op.parameters {
if param.enum_values.is_some() {
by_name.entry(param.rust_type.clone()).or_insert(param);
}
}
}
if by_name.is_empty() {
return quote! {};
}
let defs: Vec<TokenStream> = by_name
.values()
.map(|param| self.generate_single_param_enum(param))
.collect();
quote! { #(#defs)* }
}
fn generate_single_param_enum(&self, param: &ParameterInfo) -> TokenStream {
let Some(values) = param.enum_values.as_deref() else {
return quote! {};
};
let enum_ident = format_ident!("{}", param.rust_type);
let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
let variant_names: Vec<String> = values
.iter()
.map(|value| {
let base = self.to_rust_enum_variant(value);
let mut chosen = base.clone();
let mut suffix = 2;
while !used.insert(chosen.clone()) {
chosen = format!("{base}_{suffix}");
suffix += 1;
}
chosen
})
.collect();
let variants: Vec<TokenStream> = values
.iter()
.zip(&variant_names)
.map(|(value, name)| {
let variant_ident = format_ident!("{}", name);
quote! {
#[serde(rename = #value)]
#variant_ident,
}
})
.collect();
let display_arms: Vec<TokenStream> = values
.iter()
.zip(&variant_names)
.map(|(value, name)| {
let variant_ident = format_ident!("{}", name);
quote! { Self::#variant_ident => #value, }
})
.collect();
let doc = format!(
"Allowed values for the `{}` {} parameter.",
param.name, param.location
);
quote! {
#[doc = #doc]
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub enum #enum_ident {
#(#variants)*
}
impl #enum_ident {
pub fn as_str(&self) -> &'static str {
match self {
#(#display_arms)*
}
}
}
impl std::fmt::Display for #enum_ident {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl AsRef<str> for #enum_ident {
fn as_ref(&self) -> &str {
self.as_str()
}
}
}
}
fn generate_op_error_enum(&self, op: &OperationInfo) -> Option<TokenStream> {
let variants: Vec<(String, String)> = op
.response_schemas
.iter()
.filter(|(code, _)| !code.starts_with('2'))
.map(|(code, schema)| (code.clone(), schema.clone()))
.collect();
if variants.is_empty() {
return None;
}
let enum_ident = self.op_error_enum_ident(op);
let variant_decls: Vec<TokenStream> = variants
.iter()
.map(|(code, schema)| {
let variant_ident = Self::op_error_variant_ident(code);
let payload_ty_name = self.to_rust_type_name(schema);
let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
quote! { #variant_ident(#payload_ty) }
})
.collect();
let doc = format!(
"Typed error responses for `{}`. One variant per declared non-2xx response.",
op.operation_id
);
Some(quote! {
#[doc = #doc]
#[derive(Debug, Clone)]
pub enum #enum_ident {
#(#variant_decls,)*
}
})
}
fn op_error_enum_ident(&self, op: &OperationInfo) -> syn::Ident {
use heck::ToPascalCase;
let name = format!(
"{}ApiError",
op.operation_id.replace('.', "_").to_pascal_case()
);
syn::Ident::new(&name, proc_macro2::Span::call_site())
}
fn op_error_variant_ident(status_code: &str) -> syn::Ident {
let raw = match status_code {
"default" | "Default" => "Default".to_string(),
other if other.chars().all(|c| c.is_ascii_digit()) => format!("Status{other}"),
other => format!("Status{}", other.to_ascii_lowercase()),
};
syn::Ident::new(&raw, proc_macro2::Span::call_site())
}
fn op_error_type_token(&self, op: &OperationInfo) -> TokenStream {
if op
.response_schemas
.iter()
.any(|(code, _)| !code.starts_with('2'))
{
let ident = self.op_error_enum_ident(op);
quote! { #ident }
} else {
quote! { serde_json::Value }
}
}
fn generate_single_operation_method(&self, op: &OperationInfo) -> TokenStream {
let method_name = self.get_method_name(op);
let http_method_call = self.http_method_call(op);
let path = &op.path;
let request_param = self.generate_request_param(op);
let request_body = self.generate_request_body(op);
let query_params = self.generate_query_params(op);
let header_params = self.generate_header_params(op);
let auth_application = self.generate_auth_application();
let response_type = self.get_response_type(op);
let has_response_body = self.get_success_response_schema(op).is_some();
let op_error_type = self.op_error_type_token(op);
let error_handling = self.generate_error_handling(op, has_response_body);
let url_construction = self.generate_url_construction(path, op);
let doc_comment = self.generate_operation_doc_comment(op);
quote! {
#doc_comment
pub async fn #method_name(
&self,
#request_param
) -> Result<#response_type, ApiOpError<#op_error_type>> {
#url_construction
let mut req = #http_method_call;
#request_body
#query_params
#header_params
#auth_application
for (name, value) in &self.custom_headers {
req = req.header(name, value);
}
let response = req.send().await?;
#error_handling
}
}
}
fn generate_auth_application(&self) -> TokenStream {
use crate::http_config::AuthConfig;
match &self.config().auth_config {
Some(AuthConfig::Bearer { header_name }) if header_name == "Authorization" => quote! {
if let Some(api_key) = &self.api_key {
req = req.bearer_auth(api_key);
}
},
Some(AuthConfig::Bearer { header_name }) => {
let h = header_name.clone();
quote! {
if let Some(api_key) = &self.api_key {
req = req.header(#h, format!("Bearer {}", api_key));
}
}
}
Some(AuthConfig::ApiKey { header_name }) => {
let h = header_name.clone();
quote! {
if let Some(api_key) = &self.api_key {
req = req.header(#h, api_key.as_str());
}
}
}
Some(AuthConfig::Custom {
header_name,
header_value_prefix,
}) => {
let h = header_name.clone();
let prefix = header_value_prefix.clone().unwrap_or_default();
if prefix.is_empty() {
quote! {
if let Some(api_key) = &self.api_key {
req = req.header(#h, api_key.as_str());
}
}
} else {
let format_str = format!("{}{{}}", prefix);
quote! {
if let Some(api_key) = &self.api_key {
req = req.header(#h, format!(#format_str, api_key));
}
}
}
}
None => quote! {
if let Some(api_key) = &self.api_key {
req = req.bearer_auth(api_key);
}
},
}
}
fn generate_header_params(&self, op: &OperationInfo) -> TokenStream {
let header_params: Vec<_> = op
.parameters
.iter()
.filter(|p| p.location == "header")
.collect();
if header_params.is_empty() {
return quote! {};
}
let mut emit = Vec::new();
for param in header_params {
let param_name_snake = self.param_ident_str(param);
let param_ident = Self::to_field_ident(¶m_name_snake);
let header_name = ¶m.name;
if param.required {
if Self::param_uses_as_ref_str(param) {
emit.push(quote! {
req = req.header(#header_name, #param_ident.as_ref());
});
} else {
emit.push(quote! {
req = req.header(#header_name, #param_ident.to_string());
});
}
} else if Self::param_uses_as_ref_str(param) {
emit.push(quote! {
if let Some(v) = #param_ident {
req = req.header(#header_name, v.as_ref());
}
});
} else {
emit.push(quote! {
if let Some(v) = #param_ident {
req = req.header(#header_name, v.to_string());
}
});
}
}
quote! {
#(#emit)*
}
}
fn generate_query_params(&self, op: &OperationInfo) -> TokenStream {
let query_params: Vec<_> = op
.parameters
.iter()
.filter(|p| p.location == "query")
.collect();
if query_params.is_empty() {
return quote! {};
}
let mut param_building = Vec::new();
for param in query_params {
let param_name_snake = self.param_ident_str(param);
let param_name = Self::to_field_ident(¶m_name_snake);
let param_key = ¶m.name;
if param.required {
if Self::param_uses_as_ref_str(param) {
param_building.push(quote! {
query_params.push((#param_key, #param_name.as_ref().to_string()));
});
} else {
param_building.push(quote! {
query_params.push((#param_key, #param_name.to_string()));
});
}
} else {
if Self::param_uses_as_ref_str(param) {
param_building.push(quote! {
if let Some(v) = #param_name {
query_params.push((#param_key, v.as_ref().to_string()));
}
});
} else {
param_building.push(quote! {
if let Some(v) = #param_name {
query_params.push((#param_key, v.to_string()));
}
});
}
}
}
quote! {
{
let mut query_params: Vec<(&str, String)> = Vec::new();
#(#param_building)*
if !query_params.is_empty() {
req = req.query(&query_params);
}
}
}
}
fn generate_operation_doc_comment(&self, op: &OperationInfo) -> TokenStream {
let method = op.method.to_uppercase();
let path = &op.path;
let mut docs: Vec<String> = Vec::new();
if let Some(s) = &op.summary {
if !s.is_empty() {
docs.push(s.clone());
docs.push(String::new());
}
}
if let Some(d) = &op.description {
if !d.is_empty() {
for line in d.lines() {
docs.push(line.to_string());
}
docs.push(String::new());
}
}
docs.push(format!("`{} {}`", method, path));
let doc_attrs: Vec<TokenStream> = docs
.iter()
.map(|line| {
let prefixed = if line.is_empty() {
String::new()
} else {
format!(" {line}")
};
quote! { #[doc = #prefixed] }
})
.collect();
quote! { #(#doc_attrs)* }
}
fn get_method_name(&self, op: &OperationInfo) -> syn::Ident {
let name = if !op.operation_id.is_empty() {
op.operation_id.to_snake_case()
} else {
format!(
"{}_{}",
op.method,
op.path.replace('/', "_").replace(['{', '}'], "")
)
.to_snake_case()
};
syn::Ident::new(&name, proc_macro2::Span::call_site())
}
fn http_method_call(&self, op: &OperationInfo) -> TokenStream {
match op.method.to_uppercase().as_str() {
"GET" => quote! { self.http_client.get(request_url) },
"POST" => quote! { self.http_client.post(request_url) },
"PUT" => quote! { self.http_client.put(request_url) },
"DELETE" => quote! { self.http_client.delete(request_url) },
"PATCH" => quote! { self.http_client.patch(request_url) },
"HEAD" => quote! { self.http_client.head(request_url) },
"OPTIONS" => quote! {
self.http_client.request(reqwest::Method::OPTIONS, request_url)
},
"TRACE" => quote! {
self.http_client.request(reqwest::Method::TRACE, request_url)
},
other => {
let upper = other.to_string();
quote! {
self.http_client.request(
reqwest::Method::from_bytes(#upper.as_bytes())
.expect("invalid HTTP method"),
request_url,
)
}
}
}
}
fn generate_request_param(&self, op: &OperationInfo) -> TokenStream {
let mut params = Vec::new();
let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut unique_param_ident = |raw: String| -> syn::Ident {
let mut chosen = raw.clone();
let mut suffix = 2;
while !used.insert(chosen.clone()) {
chosen = format!("{raw}_{suffix}");
suffix += 1;
}
Self::to_field_ident(&chosen)
};
for param in &op.parameters {
if param.location == "path" {
let param_name_snake = self.param_ident_str(param);
let param_name = unique_param_ident(param_name_snake);
let param_type = self.get_param_rust_type(param);
params.push(quote! { #param_name: #param_type });
}
}
for param in &op.parameters {
if param.location == "query" {
let param_name_snake = self.param_ident_str(param);
let param_name = unique_param_ident(param_name_snake);
let param_type = self.get_param_rust_type(param);
if param.required {
params.push(quote! { #param_name: #param_type });
} else {
params.push(quote! { #param_name: Option<#param_type> });
}
}
}
for param in &op.parameters {
if param.location == "header" {
let param_name_snake = self.param_ident_str(param);
let param_name = unique_param_ident(param_name_snake);
let param_type = self.get_param_rust_type(param);
if param.required {
params.push(quote! { #param_name: #param_type });
} else {
params.push(quote! { #param_name: Option<#param_type> });
}
}
}
if let Some(ref rb) = op.request_body {
use crate::analysis::RequestBodyContent;
let required = op.request_body_required;
let body_type = match rb {
RequestBodyContent::Json { schema_name }
| RequestBodyContent::FormUrlEncoded { schema_name } => {
let rust_type_name = self.to_rust_type_name(schema_name);
let request_ident =
syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
quote! { #request_ident }
}
RequestBodyContent::Multipart => quote! { reqwest::multipart::Form },
RequestBodyContent::OctetStream => quote! { Vec<u8> },
RequestBodyContent::TextPlain => quote! { String },
};
let body_ident = match rb {
RequestBodyContent::Multipart => quote! { form },
RequestBodyContent::OctetStream | RequestBodyContent::TextPlain => quote! { body },
_ => quote! { request },
};
if required {
params.push(quote! { #body_ident: #body_type });
} else {
params.push(quote! { #body_ident: Option<#body_type> });
}
}
if params.is_empty() {
quote! {}
} else {
quote! { #(#params),* }
}
}
fn get_param_rust_type(&self, param: &crate::analysis::ParameterInfo) -> TokenStream {
if let Some(ref schema_name) = param.schema_ref {
let rust_name = self.to_rust_type_name(schema_name);
let ident = syn::Ident::new(&rust_name, proc_macro2::Span::call_site());
return quote! { #ident };
}
let type_str = ¶m.rust_type;
match type_str.as_str() {
"String" => quote! { impl AsRef<str> },
"i64" => quote! { i64 },
"i32" => quote! { i32 },
"f64" => quote! { f64 },
"bool" => quote! { bool },
_ => {
let type_ident = syn::Ident::new(type_str, proc_macro2::Span::call_site());
quote! { #type_ident }
}
}
}
fn param_uses_as_ref_str(param: &crate::analysis::ParameterInfo) -> bool {
param.schema_ref.is_none() && param.rust_type == "String"
}
fn generate_request_body(&self, op: &OperationInfo) -> TokenStream {
let Some(rb) = op.request_body.as_ref() else {
return quote! {};
};
use crate::analysis::RequestBodyContent;
let required = op.request_body_required;
let (ident, apply): (TokenStream, TokenStream) = match rb {
RequestBodyContent::Json { .. } => (
quote! { request },
quote! {
req = req
.body(serde_json::to_vec(&request).map_err(HttpError::serialization_error)?)
.header("content-type", "application/json");
},
),
RequestBodyContent::FormUrlEncoded { .. } => (
quote! { request },
quote! {
req = req
.body(serde_urlencoded::to_string(&request).map_err(HttpError::serialization_error)?)
.header("content-type", "application/x-www-form-urlencoded");
},
),
RequestBodyContent::Multipart => (
quote! { form },
quote! {
req = req.multipart(form);
},
),
RequestBodyContent::OctetStream => (
quote! { body },
quote! {
req = req
.body(body)
.header("content-type", "application/octet-stream");
},
),
RequestBodyContent::TextPlain => (
quote! { body },
quote! {
req = req
.body(body)
.header("content-type", "text/plain");
},
),
};
if required {
apply
} else {
quote! {
if let Some(#ident) = #ident {
#apply
}
}
}
}
fn get_success_response_schema<'a>(&self, op: &'a OperationInfo) -> Option<&'a String> {
op.response_schemas
.get("200")
.or_else(|| op.response_schemas.get("201"))
.or_else(|| {
op.response_schemas
.iter()
.find(|(code, _)| code.starts_with('2'))
.map(|(_, v)| v)
})
}
fn get_response_type(&self, op: &OperationInfo) -> TokenStream {
if let Some(response_type) = self.get_success_response_schema(op) {
let rust_type_name = self.to_rust_type_name(response_type);
let response_ident = syn::Ident::new(&rust_type_name, proc_macro2::Span::call_site());
quote! { #response_ident }
} else {
quote! { () }
}
}
fn generate_error_handling(&self, op: &OperationInfo, has_response_body: bool) -> TokenStream {
let op_error_type = self.op_error_type_token(op);
let success_branch = if has_response_body {
quote! {
match serde_json::from_str(&body_text) {
Ok(body) => Ok(body),
Err(e) => Err(ApiOpError::Api(ApiError {
status: status_code,
headers: headers,
body: body_text,
typed: None,
parse_error: Some(format!(
"failed to deserialize 2xx response body: {}",
e
)),
})),
}
}
} else {
quote! {
let _ = body_text;
let _ = headers;
Ok(())
}
};
let error_match_arms = self.generate_error_match_arms(op);
quote! {
let status = response.status();
let status_code = status.as_u16();
let headers = response.headers().clone();
let body_text = response.text().await
.map_err(|e| ApiOpError::Transport(HttpError::Network(e)))?;
if status.is_success() {
#success_branch
} else {
let typed: Option<#op_error_type>;
let parse_error: Option<String>;
#error_match_arms
Err(ApiOpError::Api(ApiError {
status: status_code,
headers,
body: body_text,
typed,
parse_error,
}))
}
}
}
fn generate_error_match_arms(&self, op: &OperationInfo) -> TokenStream {
let arms: Vec<TokenStream> = op
.response_schemas
.iter()
.filter(|(code, _)| !code.starts_with('2'))
.filter_map(|(code, schema)| {
let variant_ident = Self::op_error_variant_ident(code);
let payload_ty_name = self.to_rust_type_name(schema);
let payload_ty = syn::Ident::new(&payload_ty_name, proc_macro2::Span::call_site());
let enum_ident = self.op_error_enum_ident(op);
let pattern = match code.as_str() {
"default" | "Default" => return None, other if other.chars().all(|c| c.is_ascii_digit()) => {
let n: u16 = other.parse().ok()?;
quote! { #n }
}
"1XX" | "1xx" => quote! { code if (100..=199).contains(&code) },
"2XX" | "2xx" => quote! { code if (200..=299).contains(&code) },
"3XX" | "3xx" => quote! { code if (300..=399).contains(&code) },
"4XX" | "4xx" => quote! { code if (400..=499).contains(&code) },
"5XX" | "5xx" => quote! { code if (500..=599).contains(&code) },
_ => return None,
};
Some(quote! {
#pattern => {
match serde_json::from_str::<#payload_ty>(&body_text) {
Ok(v) => {
typed = Some(#enum_ident::#variant_ident(v));
parse_error = None;
}
Err(e) => {
typed = None;
parse_error = Some(e.to_string());
}
}
}
})
})
.collect();
let has_typed_enum = op
.response_schemas
.iter()
.any(|(code, _)| !code.starts_with('2'));
let default_arm = if has_typed_enum {
quote! {
_ => {
typed = None;
parse_error = None;
}
}
} else {
quote! {
_ => {
match serde_json::from_str::<serde_json::Value>(&body_text) {
Ok(v) => {
typed = Some(v);
parse_error = None;
}
Err(e) => {
typed = None;
parse_error = Some(e.to_string());
}
}
}
}
};
if arms.is_empty() {
quote! {
match status_code {
#default_arm
}
}
} else {
quote! {
match status_code {
#(#arms)*
#default_arm
}
}
}
}
fn generate_url_construction(&self, path: &str, op: &OperationInfo) -> TokenStream {
if path.contains('{') {
self.generate_url_with_params(path, op)
} else {
quote! {
let request_url = format!("{}{}", self.base_url, #path);
}
}
}
fn generate_url_with_params(&self, path: &str, op: &OperationInfo) -> TokenStream {
let path_params: Vec<_> = op
.parameters
.iter()
.filter(|p| p.location == "path")
.collect();
let mut format_string = String::with_capacity(path.len());
let mut format_args: Vec<TokenStream> = Vec::new();
let mut chars = path.chars().peekable();
while let Some(c) = chars.next() {
if c != '{' {
format_string.push(c);
continue;
}
let mut name = String::new();
while let Some(&n) = chars.peek() {
chars.next();
if n == '}' {
break;
}
name.push(n);
}
let param = path_params.iter().find(|p| p.name == name);
let Some(param) = param else {
format_string.push('{');
format_string.push_str(&name);
format_string.push('}');
continue;
};
format_string.push_str("{}");
let param_name_snake = self.param_ident_str(param);
let param_ident = Self::to_field_ident(¶m_name_snake);
if Self::param_uses_as_ref_str(param) {
format_args.push(quote! {
__pct_encode_path_segment(#param_ident.as_ref())
});
} else {
format_args.push(quote! {
__pct_encode_path_segment(&#param_ident.to_string())
});
}
}
if format_args.is_empty() {
quote! {
let request_url = format!("{}{}", self.base_url, #path);
}
} else {
quote! {
let request_url = format!("{}{}", self.base_url, format!(#format_string, #(#format_args),*));
}
}
}
fn param_ident_str(&self, param: &crate::analysis::ParameterInfo) -> String {
if let Some(ident) = ¶m.rust_ident {
return self.escape_keyword_ident(ident);
}
self.sanitize_param_name(¶m.name)
}
fn escape_keyword_ident(&self, snake_case: &str) -> String {
if matches!(snake_case, "self" | "super" | "crate" | "Self") {
return format!("{snake_case}_param");
}
if Self::is_rust_keyword(snake_case) {
format!("r#{snake_case}")
} else {
snake_case.to_string()
}
}
fn sanitize_param_name(&self, name: &str) -> String {
let suffix = if name.ends_with("<=") {
"_lte"
} else if name.ends_with(">=") {
"_gte"
} else if name.ends_with('<') {
"_lt"
} else if name.ends_with('>') {
"_gt"
} else {
""
};
let stripped = name.trim_end_matches(['<', '>', '=']);
let mut snake_case = stripped.to_snake_case();
snake_case.push_str(suffix);
if matches!(snake_case.as_str(), "self" | "super" | "crate" | "Self") {
return format!("{snake_case}_param");
}
if Self::is_rust_keyword(&snake_case) {
format!("r#{snake_case}")
} else {
snake_case
}
}
}