use proc_macro2::TokenStream;
use quote::quote;
use syn::{
Data, DeriveInput, Fields, Lit, Meta, Token, Type, TypePath, parse::Parse, parse::ParseStream,
};
#[derive(Clone, Debug, Default)]
struct UriParamAttr {
name: Option<String>,
default: Option<String>,
desc: Option<String>,
required: bool,
secret: bool,
deprecated: Option<String>,
aliases: Vec<String>,
kind_override: Option<syn::LitStr>,
pattern: Option<String>,
}
impl Parse for UriParamAttr {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut attr = UriParamAttr::default();
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
let key_str = ident.to_string();
if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
match key_str.as_str() {
"name" | "default" | "desc" | "deprecated" | "pattern" => {
let lit: Lit = input.parse()?;
if let Lit::Str(lit_str) = lit {
let val = lit_str.value();
match key_str.as_str() {
"name" => attr.name = Some(val),
"default" => attr.default = Some(val),
"desc" => attr.desc = Some(val),
"deprecated" => attr.deprecated = Some(val),
"pattern" => attr.pattern = Some(val),
_ => unreachable!(),
}
} else {
return Err(syn::Error::new_spanned(lit, "expected a string literal"));
}
}
"kind" => {
let lit: Lit = input.parse()?;
if let Lit::Str(lit_str) = lit {
attr.kind_override = Some(lit_str);
} else {
return Err(syn::Error::new_spanned(lit, "expected a string literal"));
}
}
"required" | "secret" => {
let lit: Lit = input.parse()?;
if let Lit::Bool(b) = lit {
let v = b.value;
if key_str == "required" {
attr.required = v;
} else {
attr.secret = v;
}
} else {
return Err(syn::Error::new_spanned(lit, "expected a bool literal"));
}
}
"aliases" => {
let arr: syn::ExprArray = input.parse()?;
let mut items = Vec::new();
for expr in arr.elems {
if let syn::Expr::Lit(syn::ExprLit {
lit: Lit::Str(s), ..
}) = expr
{
items.push(s.value());
} else {
return Err(syn::Error::new_spanned(
expr,
"expected a string literal in aliases array",
));
}
}
attr.aliases = items;
}
_ => {
return Err(syn::Error::new_spanned(
&ident,
format!("unknown attribute key: {}", key_str),
));
}
}
} else {
match key_str.as_str() {
"required" => attr.required = true,
"secret" => attr.secret = true,
_ => {
return Err(syn::Error::new_spanned(
&ident,
format!("unknown attribute key: {}", key_str),
));
}
}
}
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
}
Ok(attr)
}
}
fn extract_scheme(attrs: &[syn::Attribute]) -> syn::Result<String> {
for attr in attrs {
if let Meta::NameValue(nv) = &attr.meta
&& nv.path.is_ident("uri_scheme")
&& let syn::Expr::Lit(expr_lit) = &nv.value
&& let Lit::Str(lit_str) = &expr_lit.lit
{
return Ok(lit_str.value());
}
}
Err(syn::Error::new(
proc_macro2::Span::call_site(),
"missing #[uri_scheme = \"xxx\"] attribute on struct",
))
}
fn parse_uri_param_attr(attrs: &[syn::Attribute]) -> syn::Result<Option<UriParamAttr>> {
for attr in attrs {
if attr.path().is_ident("uri_param") {
match &attr.meta {
Meta::Path(_) => {
return Ok(Some(UriParamAttr::default()));
}
Meta::List(list) => {
let parsed: UriParamAttr = list.parse_args()?;
return Ok(Some(parsed));
}
_ => {
return Err(syn::Error::new_spanned(
attr,
"unexpected attribute format for #[uri_param]",
));
}
}
}
}
Ok(None)
}
struct UriConfigAttr {
skip_impl: bool,
descriptor: bool,
crate_path: syn::Path,
has_metadata: bool,
metadata_scheme: Option<String>,
metadata_description: Option<String>,
supports_producer: bool,
supports_consumer: bool,
supports_polling_consumer: bool,
supports_streaming: bool,
}
fn parse_uri_config_attr(attrs: &[syn::Attribute]) -> syn::Result<UriConfigAttr> {
let mut skip_impl = false;
let mut descriptor = false;
let mut crate_path: Option<syn::Path> = None;
let mut has_metadata = false;
let mut metadata_scheme = None;
let mut metadata_description = None;
let mut supports_producer = false;
let mut supports_consumer = false;
let mut supports_polling_consumer = false;
let mut supports_streaming = false;
for attr in attrs {
if !attr.path().is_ident("uri_config") {
continue;
}
match &attr.meta {
Meta::List(_) => {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("skip_impl") {
skip_impl = true;
return Ok(());
}
if meta.path.is_ident("descriptor") {
descriptor = true;
return Ok(());
}
if meta.path.is_ident("crate") {
let value = meta.value()?;
let lit: syn::LitStr = value.parse()?;
crate_path = Some(lit.parse()?);
return Ok(());
}
if meta.path.is_ident("metadata") {
has_metadata = true;
let content;
syn::parenthesized!(content in meta.input);
while !content.is_empty() {
let key: syn::Ident = content.parse()?;
match key.to_string().as_str() {
"scheme" => {
content.parse::<Token![=]>()?;
let lit: syn::LitStr = content.parse()?;
metadata_scheme = Some(lit.value());
}
"description" => {
content.parse::<Token![=]>()?;
let lit: syn::LitStr = content.parse()?;
metadata_description = Some(lit.value());
}
"producer" => supports_producer = true,
"consumer" => supports_consumer = true,
"polling_consumer" => supports_polling_consumer = true,
"streaming" => supports_streaming = true,
other => {
return Err(syn::Error::new_spanned(
&key,
format!("unknown metadata key: {}", other),
));
}
}
if content.peek(Token![,]) {
content.parse::<Token![,]>()?;
}
}
return Ok(());
}
Err(meta.error("unknown uri_config option"))
})?;
}
_ => {
return Err(syn::Error::new_spanned(
attr,
"unexpected attribute format for #[uri_config]",
));
}
}
}
Ok(UriConfigAttr {
skip_impl,
descriptor,
crate_path: crate_path.unwrap_or_else(|| syn::parse_quote!(camel_endpoint)),
has_metadata,
metadata_scheme,
metadata_description,
supports_producer,
supports_consumer,
supports_polling_consumer,
supports_streaming,
})
}
fn get_type_name(ty: &Type) -> Option<String> {
if let Type::Path(TypePath { path, .. }) = ty {
let segment = path.segments.last()?;
Some(segment.ident.to_string())
} else {
None
}
}
fn is_duration_type(ty: &Type) -> bool {
if let Type::Path(TypePath { path, .. }) = ty {
let segments: Vec<_> = path.segments.iter().map(|s| s.ident.to_string()).collect();
segments.last().is_some_and(|s| s == "Duration")
} else {
false
}
}
fn is_option_type(ty: &Type) -> Option<Type> {
if let Type::Path(TypePath { path, .. }) = ty {
let segment = path.segments.last()?;
if segment.ident == "Option"
&& let syn::PathArguments::AngleBracketed(args) = &segment.arguments
&& let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first()
{
return Some(inner_ty.clone());
}
}
None
}
fn get_vec_inner(ty: &Type) -> Option<Type> {
if let Type::Path(TypePath { path, .. }) = ty {
let segment = path.segments.last()?;
if segment.ident == "Vec"
&& let syn::PathArguments::AngleBracketed(args) = &segment.arguments
&& let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first()
{
return Some(inner_ty.clone());
}
}
None
}
fn is_vec_string_pair(ty: &Type) -> bool {
let Some(inner) = get_vec_inner(ty) else {
return false;
};
let Type::Tuple(tuple) = inner else {
return false;
};
if tuple.elems.len() != 2 {
return false;
}
tuple
.elems
.iter()
.all(|elem| get_type_name(elem).is_some_and(|name| name == "String"))
}
fn parse_kind_override(
kind_str: &str,
span: proc_macro2::Span,
endpoint_crate: &syn::Path,
) -> syn::Result<TokenStream> {
let path = quote! { #endpoint_crate::OptionKind };
match kind_str {
"duration" => Ok(quote! { #path::Duration }),
"bool" => Ok(quote! { #path::Bool }),
"int" => Ok(quote! { #path::Int }),
"float" => Ok(quote! { #path::Float }),
"string" => Ok(quote! { #path::String }),
s if s.starts_with("enum:") => {
let rest = &s[5..];
let variants: Vec<String> = rest
.split(',')
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
.collect();
if variants.is_empty() {
return Err(syn::Error::new(
span,
format!(
"invalid kind override '{}': enum requires at least one variant \
(e.g. kind = \"enum:A,B\")",
kind_str
),
));
}
Ok(quote! { #path::Enum(::std::vec![#(::std::string::String::from(#variants)),*]) })
}
other => Err(syn::Error::new(
span,
format!(
"unknown kind override '{}'. Valid: duration, bool, int, float, string, \
enum:VariantA,VariantB",
other
),
)),
}
}
fn infer_option_kind(ty: &Type, endpoint_crate: &syn::Path) -> TokenStream {
let effective_ty = is_option_type(ty).unwrap_or_else(|| ty.clone());
infer_option_kind_inner(&effective_ty, endpoint_crate)
}
fn infer_option_kind_inner(ty: &Type, endpoint_crate: &syn::Path) -> TokenStream {
let path = quote! { #endpoint_crate::OptionKind };
if is_duration_type(ty) {
return quote! { #path::Duration };
}
let type_name = get_type_name(ty);
match type_name.as_deref() {
Some("bool") => quote! { #path::Bool },
Some("u8") | Some("u16") | Some("u32") | Some("u64") | Some("usize") | Some("i8")
| Some("i16") | Some("i32") | Some("i64") | Some("isize") => quote! { #path::Int },
Some("f32") | Some("f64") => quote! { #path::Float },
Some("String") | Some("str") => quote! { #path::String },
Some("Vec") => {
if let Some(inner) = get_vec_inner(ty) {
let inner_kind = infer_option_kind_inner(&inner, endpoint_crate);
quote! { #path::List(::std::boxed::Box::new(#inner_kind)) }
} else {
quote! { #path::String }
}
}
_ => quote! { #path::String },
}
}
fn generate_param_parsing(
param_name: &str,
field_name: &syn::Ident,
ty: &Type,
default: Option<&str>,
endpoint_crate: &syn::Path,
) -> syn::Result<TokenStream> {
let type_name = get_type_name(ty);
let inner_type = is_option_type(ty);
if let Some(inner_ty) = &inner_type {
let inner_type_name = get_type_name(inner_ty);
return Ok(match inner_type_name.as_deref() {
Some("String") => quote! {
let #field_name = params.get(#param_name).cloned()
},
Some("bool") => quote! {
let #field_name = if let Some(v) = params.get(#param_name) {
Some(#endpoint_crate::uri::parse_bool_param(v).map_err(|e| #endpoint_crate::CamelError::InvalidUri(
format!("invalid value for {}: {}", #param_name, e)
))?)
} else {
None
}
},
Some("u64") | Some("u32") | Some("usize") | Some("i64") | Some("i32")
| Some("isize") => quote! {
let #field_name = if let Some(v) = params.get(#param_name) {
Some(v.parse::<#inner_ty>().map_err(|e| #endpoint_crate::CamelError::InvalidUri(
format!("invalid value for {}: {}", #param_name, e)
))?)
} else {
None
}
},
_ => quote! {
let #field_name = if let Some(v) = params.get(#param_name) {
Some(v.parse::<#inner_ty>().map_err(|e| #endpoint_crate::CamelError::InvalidUri(
format!("invalid value for {}: {}", #param_name, e)
))?)
} else {
None
}
},
});
}
Ok(match type_name.as_deref() {
Some("String") => {
if let Some(default_val) = default {
quote! {
let #field_name = params.get(#param_name).cloned().unwrap_or_else(|| #default_val.to_string())
}
} else {
quote! {
let #field_name = params.get(#param_name).cloned().ok_or_else(|| {
#endpoint_crate::CamelError::InvalidUri(
format!("missing required parameter: {}", #param_name)
)
})?
}
}
}
Some("bool") => {
if let Some(default_val) = default {
let default_bool =
matches!(default_val.to_lowercase().as_str(), "true" | "1" | "yes");
quote! {
let #field_name = match params.get(#param_name) {
Some(v) => #endpoint_crate::uri::parse_bool_param(v).map_err(|e| #endpoint_crate::CamelError::InvalidUri(
format!("invalid value for {}: {}", #param_name, e)
))?,
None => #default_bool,
}
}
} else {
quote! {
let #field_name = #endpoint_crate::uri::parse_bool_param(
¶ms.get(#param_name).ok_or_else(|| #endpoint_crate::CamelError::InvalidUri(
format!("missing required parameter: {}", #param_name)
))?
).map_err(|e| #endpoint_crate::CamelError::InvalidUri(
format!("invalid value for {}: {}", #param_name, e)
))?
}
}
}
Some("u64") => {
if let Some(default_val) = default {
let default_num: u64 = default_val.parse().map_err(|_| {
syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"invalid default value for '{}': '{}' is not a valid u64",
param_name, default_val
),
)
})?;
quote! {
let #field_name = match params.get(#param_name) {
Some(v) => v.parse::<u64>().map_err(|e| #endpoint_crate::CamelError::InvalidUri(
format!("invalid value for {}: {}", #param_name, e)
))?,
None => #default_num,
}
}
} else {
quote! {
let #field_name = params.get(#param_name)
.ok_or_else(|| #endpoint_crate::CamelError::InvalidUri(
format!("missing required parameter: {}", #param_name)
))?
.parse::<u64>()
.map_err(|e| #endpoint_crate::CamelError::InvalidUri(
format!("invalid value for {}: {}", #param_name, e)
))?
}
}
}
Some("u32") => {
if let Some(default_val) = default {
let default_num: u32 = default_val.parse().map_err(|_| {
syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"invalid default value for '{}': '{}' is not a valid u32",
param_name, default_val
),
)
})?;
quote! {
let #field_name = match params.get(#param_name) {
Some(v) => v.parse::<u32>().map_err(|e| #endpoint_crate::CamelError::InvalidUri(
format!("invalid value for {}: {}", #param_name, e)
))?,
None => #default_num,
}
}
} else {
quote! {
let #field_name = params.get(#param_name)
.ok_or_else(|| #endpoint_crate::CamelError::InvalidUri(
format!("missing required parameter: {}", #param_name)
))?
.parse::<u32>()
.map_err(|e| #endpoint_crate::CamelError::InvalidUri(
format!("invalid value for {}: {}", #param_name, e)
))?
}
}
}
Some("usize") => {
if let Some(default_val) = default {
let default_num: usize = default_val.parse().map_err(|_| {
syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"invalid default value for '{}': '{}' is not a valid usize",
param_name, default_val
),
)
})?;
quote! {
let #field_name = match params.get(#param_name) {
Some(v) => v.parse::<usize>().map_err(|e| #endpoint_crate::CamelError::InvalidUri(
format!("invalid value for {}: {}", #param_name, e)
))?,
None => #default_num,
}
}
} else {
quote! {
let #field_name = params.get(#param_name)
.ok_or_else(|| #endpoint_crate::CamelError::InvalidUri(
format!("missing required parameter: {}", #param_name)
))?
.parse::<usize>()
.map_err(|e| #endpoint_crate::CamelError::InvalidUri(
format!("invalid value for {}: {}", #param_name, e)
))?
}
}
}
Some("i64") => {
if let Some(default_val) = default {
let default_num: i64 = default_val.parse().map_err(|_| {
syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"invalid default value for '{}': '{}' is not a valid i64",
param_name, default_val
),
)
})?;
quote! {
let #field_name = match params.get(#param_name) {
Some(v) => v.parse::<i64>().map_err(|e| #endpoint_crate::CamelError::InvalidUri(
format!("invalid value for {}: {}", #param_name, e)
))?,
None => #default_num,
}
}
} else {
quote! {
let #field_name = params.get(#param_name)
.ok_or_else(|| #endpoint_crate::CamelError::InvalidUri(
format!("missing required parameter: {}", #param_name)
))?
.parse::<i64>()
.map_err(|e| #endpoint_crate::CamelError::InvalidUri(
format!("invalid value for {}: {}", #param_name, e)
))?
}
}
}
Some("i32") => {
if let Some(default_val) = default {
let default_num: i32 = default_val.parse().map_err(|_| {
syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"invalid default value for '{}': '{}' is not a valid i32",
param_name, default_val
),
)
})?;
quote! {
let #field_name = match params.get(#param_name) {
Some(v) => v.parse::<i32>().map_err(|e| #endpoint_crate::CamelError::InvalidUri(
format!("invalid value for {}: {}", #param_name, e)
))?,
None => #default_num,
}
}
} else {
quote! {
let #field_name = params.get(#param_name)
.ok_or_else(|| #endpoint_crate::CamelError::InvalidUri(
format!("missing required parameter: {}", #param_name)
))?
.parse::<i32>()
.map_err(|e| #endpoint_crate::CamelError::InvalidUri(
format!("invalid value for {}: {}", #param_name, e)
))?
}
}
}
_ => {
if let Some(default_val) = default {
quote! {
let #field_name = match params.get(#param_name) {
Some(v) => v.parse::<#ty>().map_err(|e| #endpoint_crate::CamelError::InvalidUri(
format!("invalid value for parameter '{}': {}", #param_name, e)
))?,
None => #default_val.parse::<#ty>().map_err(|e| #endpoint_crate::CamelError::InvalidUri(
format!("invalid default value for parameter '{}': {}", #param_name, e)
))?,
};
}
} else {
quote! {
let #field_name = params.get(#param_name)
.ok_or_else(|| #endpoint_crate::CamelError::InvalidUri(
format!("missing required parameter: {}", #param_name)
))?
.parse::<#ty>()
.map_err(|e| #endpoint_crate::CamelError::InvalidUri(
format!("invalid value for parameter '{}': {}", #param_name, e)
))?
}
}
}
})
}
fn build_uri_option_entry(
field_ident: &syn::Ident,
field_type: &Type,
attr: &UriParamAttr,
endpoint_crate: &syn::Path,
is_descriptor: bool,
) -> syn::Result<TokenStream> {
if attr.secret && attr.default.is_some() {
return Err(syn::Error::new_spanned(
field_ident,
"#[uri_param] cannot have both `secret` and `default`; a secret \
must never carry a default value",
));
}
if let Some(sep) = &attr.pattern {
if !is_vec_string_pair(field_type) {
return Err(syn::Error::new_spanned(
field_ident,
"`pattern` is only valid on fields of type `Vec<(String, String)>`",
));
}
if attr.required {
return Err(syn::Error::new_spanned(
field_ident,
"#[uri_param] cannot have both `pattern` and `required`; \
an open namespace cannot require a single key",
));
}
if attr.default.is_some() {
return Err(syn::Error::new_spanned(
field_ident,
"#[uri_param] cannot have both `pattern` and `default`; \
an open namespace has no default value",
));
}
if attr.secret {
return Err(syn::Error::new_spanned(
field_ident,
"#[uri_param] cannot have both `pattern` and `secret`; \
an open namespace has no single secret value",
));
}
if attr.name.is_some() {
return Err(syn::Error::new_spanned(
field_ident,
"#[uri_param] cannot have both `pattern` and `name`; \
the name is derived from the separator",
));
}
if !attr.aliases.is_empty() {
return Err(syn::Error::new_spanned(
field_ident,
"#[uri_param] cannot have both `pattern` and `aliases`; \
a namespace matches by prefix, not by exact alias",
));
}
if let Some(lit) = &attr.kind_override
&& lit.value() != "string"
{
return Err(syn::Error::new_spanned(
field_ident,
"#[uri_param] `kind` on a pattern field must be `string` or omitted",
));
}
if sep.is_empty() {
return Err(syn::Error::new_spanned(
field_ident,
"#[uri_param] `pattern` separator must be non-empty",
));
}
if !sep.ends_with('.') {
return Err(syn::Error::new_spanned(
field_ident,
"#[uri_param] `pattern` separator must end with `.` \
(the only permitted separator shape in this version)",
));
}
}
let param_name = if let Some(sep) = &attr.pattern {
sep[..sep.len() - 1].to_string()
} else {
attr.name.clone().unwrap_or_else(|| field_ident.to_string())
};
let description = attr.desc.clone().unwrap_or_default();
let kind_ts = if attr.pattern.is_some() {
let path = quote! { #endpoint_crate::OptionKind };
quote! { #path::String }
} else if let Some(lit) = &attr.kind_override {
parse_kind_override(&lit.value(), lit.span(), endpoint_crate)?
} else {
infer_option_kind(field_type, endpoint_crate)
};
let is_option = is_option_type(field_type).is_some();
let required = if attr.pattern.is_some() {
false
} else if attr.required {
true
} else if is_descriptor {
false
} else {
!is_option && attr.default.is_none()
};
let mut chain = quote! {
#endpoint_crate::UriOption::new(#param_name, #description, #kind_ts)
};
if required {
chain = quote! { #chain.required() };
}
if let Some(default_val) = &attr.default {
chain = quote! { #chain.with_default(#default_val) };
}
if attr.secret {
chain = quote! { #chain.secret() };
}
if let Some(deprecated_reason) = &attr.deprecated {
chain = quote! { #chain.deprecated(#deprecated_reason) };
}
for alias in &attr.aliases {
chain = quote! { #chain.with_alias(#alias) };
}
if let Some(sep) = &attr.pattern {
chain = quote! { #chain.pattern_prefix(#sep) };
}
Ok(chain)
}
pub fn impl_uri_config(input: &DeriveInput) -> syn::Result<TokenStream> {
let struct_name = &input.ident;
let uri_config_attr = parse_uri_config_attr(&input.attrs)?;
let skip_impl = uri_config_attr.skip_impl;
let endpoint_crate = uri_config_attr.crate_path;
let scheme = extract_scheme(&input.attrs)?;
let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => &fields.named,
_ => {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"UriConfig only supports structs with named fields",
));
}
},
_ => {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"UriConfig can only be derived for structs",
));
}
};
#[derive(Clone)]
enum FieldType {
Path,
Param { attr: UriParamAttr },
DurationFromMs { companion_field: String },
}
let mut field_info: Vec<(syn::Ident, Type, FieldType)> = Vec::new();
let mut path_field_found = false;
let all_field_names: Vec<String> = fields
.iter()
.map(|f| f.ident.as_ref().unwrap().to_string()) .collect();
for field in fields {
let field_name = field.ident.as_ref().unwrap().clone(); let field_type = field.ty.clone();
if is_duration_type(&field.ty) {
let field_name_str = field_name.to_string();
let companion_name = format!("{}_ms", field_name_str);
if all_field_names.contains(&companion_name) {
field_info.push((
field_name,
field_type,
FieldType::DurationFromMs {
companion_field: companion_name,
},
));
continue;
}
}
match parse_uri_param_attr(&field.attrs) {
Ok(Some(attr)) => {
field_info.push((field_name, field_type, FieldType::Param { attr }));
}
Ok(None) => {
if !path_field_found {
path_field_found = true;
field_info.push((field_name, field_type, FieldType::Path));
} else {
return Err(syn::Error::new_spanned(
field,
"only one field can be the path field (first field without #[uri_param])",
));
}
}
Err(e) => {
return Err(e);
}
}
}
let mut bindings = Vec::new();
let field_names: Vec<_> = field_info.iter().map(|(name, _, _)| name.clone()).collect();
for (field_name, field_type, ftype) in &field_info {
match ftype {
FieldType::Path => {
let type_name = get_type_name(field_type);
match type_name.as_deref() {
Some("String") => {
bindings.push(quote! {
let #field_name = parts.path.clone()
});
}
_ => {
let ty = field_type;
bindings.push(quote! {
let #field_name = parts.path.parse::<#ty>()
.map_err(|_| #endpoint_crate::CamelError::InvalidUri(
format!("invalid path value for field: {}", stringify!(#field_name))
))?
});
}
}
}
FieldType::Param { attr } => {
if let Some(sep) = &attr.pattern {
bindings.push(quote! {
let #field_name = params.iter()
.filter_map(|(k, v)| {
if k.starts_with(#sep) && k.len() > #sep.len() {
Some((k[#sep.len()..].to_string(), v.clone()))
} else {
None
}
})
.collect::<::std::vec::Vec<(::std::string::String, ::std::string::String)>>()
});
} else {
let param_name = attr.name.clone().unwrap_or_else(|| field_name.to_string());
let parsing_code = generate_param_parsing(
¶m_name,
field_name,
field_type,
attr.default.as_deref(),
&endpoint_crate,
)?;
bindings.push(parsing_code);
}
}
FieldType::DurationFromMs { .. } => {
}
}
}
for (field_name, _field_type, ftype) in &field_info {
if let FieldType::DurationFromMs { companion_field } = ftype {
let companion_ident: syn::Ident =
syn::Ident::new(companion_field, proc_macro2::Span::call_site());
bindings.push(quote! {
let #field_name = std::time::Duration::from_millis(#companion_ident)
});
}
}
let scheme_lit = scheme;
let parsing_logic = quote! {
if parts.scheme != #scheme_lit {
return Err(#endpoint_crate::CamelError::InvalidUri(
format!("expected scheme '{}' but got '{}'", #scheme_lit, parts.scheme)
));
}
let params = &parts.params;
#(#bindings);*;
Ok(Self {
#(#field_names),*
})
};
let mut uri_option_entries: Vec<TokenStream> = Vec::new();
for (field_name, field_type, ftype) in &field_info {
if let FieldType::Param { attr } = ftype {
uri_option_entries.push(build_uri_option_entry(
field_name,
field_type,
attr,
&endpoint_crate,
uri_config_attr.descriptor,
)?);
}
}
let uri_options_fn = quote! {
pub fn uri_options() -> ::std::vec::Vec<#endpoint_crate::UriOption> {
vec![ #(#uri_option_entries),* ]
}
};
let metadata_fn = if uri_config_attr.has_metadata {
let meta_scheme = uri_config_attr
.metadata_scheme
.unwrap_or_else(|| scheme_lit.clone());
let meta_description = uri_config_attr.metadata_description.unwrap_or_default();
let sp = uri_config_attr.supports_producer;
let sc = uri_config_attr.supports_consumer;
let spc = uri_config_attr.supports_polling_consumer;
let ss = uri_config_attr.supports_streaming;
Some(quote! {
pub fn metadata() -> #endpoint_crate::ComponentMetadata {
#endpoint_crate::ComponentMetadata::minimal(#meta_scheme)
.with_description(#meta_description)
.with_capabilities(#endpoint_crate::ComponentCapabilities {
supports_producer: #sp,
supports_consumer: #sc,
supports_polling_consumer: #spc,
supports_streaming: #ss,
})
.with_uri_options(Self::uri_options())
}
})
} else {
None
};
if skip_impl {
Ok(quote! {
impl #struct_name {
pub fn parse_uri_components(parts: #endpoint_crate::UriComponents) -> Result<Self, #endpoint_crate::CamelError> {
#parsing_logic
}
#uri_options_fn
#metadata_fn
}
})
} else {
Ok(quote! {
impl #endpoint_crate::UriConfig for #struct_name {
fn scheme() -> &'static str {
#scheme_lit
}
fn from_uri(uri: &str) -> Result<Self, #endpoint_crate::CamelError> {
let parts = #endpoint_crate::parse_uri(uri)?;
Self::from_components(parts)
}
fn from_components(parts: #endpoint_crate::UriComponents) -> Result<Self, #endpoint_crate::CamelError> {
let config = Self::parse_uri_components(parts)?;
config.validate()
}
}
impl #struct_name {
pub fn parse_uri_components(parts: #endpoint_crate::UriComponents) -> Result<Self, #endpoint_crate::CamelError> {
#parsing_logic
}
#uri_options_fn
#metadata_fn
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_attr(src: &str) -> UriParamAttr {
syn::parse_str::<UriParamAttr>(src).expect("failed to parse uri_param attr")
}
#[test]
fn parse_secret_flag() {
let attr = parse_attr("secret");
assert!(attr.secret);
assert!(!attr.required);
}
#[test]
fn parse_secret_with_other_keys() {
let attr = parse_attr("secret, default = \"x\"");
assert!(attr.secret);
assert_eq!(attr.default.as_deref(), Some("x"));
}
#[test]
fn parse_deprecated_key() {
let attr = parse_attr("deprecated = \"old\"");
assert_eq!(attr.deprecated.as_deref(), Some("old"));
}
#[test]
fn parse_aliases_array() {
let attr = parse_attr("aliases = [\"a\", \"b\"]");
assert_eq!(attr.aliases, vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn parse_unknown_key_still_errors() {
let res = syn::parse_str::<UriParamAttr>("bogus = 1");
assert!(res.is_err());
let msg = res.unwrap_err().to_string();
assert!(msg.contains("unknown attribute key"), "msg was: {msg}");
}
#[test]
fn parse_required_flag_and_kv() {
let a = parse_attr("required");
assert!(a.required);
let b = parse_attr("required = false");
assert!(!b.required);
}
#[test]
fn parse_kind_override_captured() {
let a = parse_attr("kind = \"enum:A,B\"");
assert_eq!(a.kind_override.as_ref().unwrap().value(), "enum:A,B");
}
#[test]
fn parse_desc_key() {
let a = parse_attr("desc = \"the period\"");
assert_eq!(a.desc.as_deref(), Some("the period"));
}
fn parse_type(src: &str) -> Type {
syn::parse_str::<Type>(src).expect("failed to parse type")
}
fn test_crate() -> syn::Path {
syn::parse_quote!(__test_crate)
}
fn kind_str(ty: &Type) -> String {
infer_option_kind(ty, &test_crate()).to_string()
}
#[test]
fn infer_bool() {
let s = kind_str(&parse_type("bool"));
assert!(s.contains("Bool"), "{s}");
}
#[test]
fn infer_duration() {
let s = kind_str(&parse_type("std::time::Duration"));
assert!(s.contains("Duration"), "{s}");
}
#[test]
fn infer_string() {
let s = kind_str(&parse_type("String"));
assert!(s.contains("String"), "{s}");
}
#[test]
fn infer_option_inner_kind() {
let s = kind_str(&parse_type("Option<u32>"));
assert!(s.contains("Int"), "{s}");
}
#[test]
fn infer_vec_string() {
let s = kind_str(&parse_type("Vec<String>"));
assert!(s.contains("List"), "{s}");
assert!(s.contains("String"), "{s}");
}
#[test]
fn infer_enum_is_string() {
let s = kind_str(&parse_type("MyMode"));
assert!(s.contains("String"), "{s}");
}
#[test]
fn infer_ints_and_floats() {
assert!(kind_str(&parse_type("u64")).contains("Int"));
assert!(kind_str(&parse_type("i32")).contains("Int"));
assert!(kind_str(&parse_type("f64")).contains("Float"));
}
#[test]
fn kind_override_enum() {
let ts = parse_kind_override("enum:A,B", proc_macro2::Span::call_site(), &test_crate())
.expect("valid override");
let s = ts.to_string();
assert!(s.contains("Enum"), "{s}");
assert!(s.contains("A") && s.contains("B"), "{s}");
}
#[test]
fn kind_override_known_strings() {
let c = test_crate();
assert!(
parse_kind_override("duration", proc_macro2::Span::call_site(), &c)
.unwrap()
.to_string()
.contains("Duration")
);
assert!(
parse_kind_override("bool", proc_macro2::Span::call_site(), &c)
.unwrap()
.to_string()
.contains("Bool")
);
}
#[test]
fn kind_typo_errors() {
let res = parse_kind_override("duraton", proc_macro2::Span::call_site(), &test_crate());
assert!(res.is_err());
}
fn parse_uri_config_str(attr_str: &str) -> UriConfigAttr {
let input: syn::DeriveInput =
syn::parse_str(&format!("#[uri_config({attr_str})]\nstruct Dummy;"))
.expect("failed to parse derive input");
parse_uri_config_attr(&input.attrs).expect("failed to parse uri_config attr")
}
#[test]
fn descriptor_flag_parses_as_bare_ident() {
let attr = parse_uri_config_str("skip_impl, descriptor, metadata(scheme = \"x\")");
assert!(attr.descriptor);
}
#[test]
fn absent_descriptor_defaults_to_false() {
let attr = parse_uri_config_str("skip_impl, metadata(scheme = \"x\")");
assert!(!attr.descriptor);
}
}