camel_endpoint_macros/lib.rs
1//! Proc-macro derive for `UriConfig` — generates URI parsing implementations from struct field attributes.
2//!
3//! Main macro: `#[derive(UriConfig)]`. Supports `#[uri_scheme]`, `#[uri_param]`, and related attributes.
4
5mod uri_config;
6
7#[cfg(test)]
8mod expansion_baselines;
9
10use proc_macro::TokenStream;
11use syn::{DeriveInput, parse_macro_input};
12
13/// Derive macro for UriConfig trait implementation.
14///
15/// This macro generates the `from_uri()` implementation based on struct field attributes.
16///
17/// # Attributes
18///
19/// ## Struct-level attributes
20///
21/// - `#[uri_scheme = "xxx"]` - Required, defines the URI scheme
22/// - `#[uri_config(skip_impl)]` - Optional, generates only the parsing helper method
23/// instead of the full trait impl. Use this when you need custom `validate()` logic.
24/// - `#[uri_config(crate = "path")]` - Optional, overrides the generated code's crate
25/// path. Defaults to `camel_endpoint`. Component crates using the
26/// `camel-component-api` re-exports set this to `camel_component_api`.
27/// - `#[uri_config(metadata(scheme = "..", description = "..", producer, consumer,
28/// polling_consumer, streaming))]` - Optional, opts in to generating an inherent
29/// `fn metadata() -> ComponentMetadata` on the config struct. The group mixes bare
30/// capability flags (`producer`, `consumer`, `polling_consumer`, `streaming`) with
31/// `key = "value"` pairs (`scheme`, `description`). When `scheme` is omitted it falls
32/// back to the `#[uri_scheme]` value.
33///
34/// ## Field-level attributes
35///
36/// - `#[uri_param]` - Marks a field as a URI query parameter (uses field name as param name)
37/// - `#[uri_param(default = "value")]` - Provides a default value if param not present
38/// - `#[uri_param(name = "paramName")]` - Maps to a different query parameter name
39/// - `#[uri_param(desc = "text")]` - Human-readable description for the generated
40/// `UriOption`.
41/// - `#[uri_param(required)]` - Marks the option required (bare flag; also accepts
42/// `required = true`). Without it, `Option<T>` fields are not required, and
43/// non-`Option` fields without a `default` are required.
44/// - `#[uri_param(secret)]` - Marks the option as secret (bare flag; also accepts
45/// `secret = true`). Combining `secret` with `default` is a compile error.
46/// - `#[uri_param(deprecated = "reason")]` - Deprecation notice.
47/// - `#[uri_param(aliases = ["a", "b"])]` - Alias parameter names.
48/// - `#[uri_param(kind = "duration|bool|int|float|string|enum:A,B")]` - Overrides the
49/// inferred `OptionKind`. Inference never produces `Enum`; the only way to get an
50/// `Enum` option is an explicit `kind = "enum:..."`. An unrecognized kind string is a
51/// spanned compile error.
52///
53/// # Generated helper functions
54///
55/// In addition to the URI parsing impl, the derive always generates:
56///
57/// - `pub fn uri_options() -> Vec<UriOption>` - one entry per `#[uri_param]` field
58/// (the path field is excluded). `OptionKind` is inferred from the Rust type after
59/// unwrapping `Option<T>`.
60///
61/// And, when `#[uri_config(metadata(..))]` is present:
62///
63/// - `pub fn metadata() -> ComponentMetadata` - built from the metadata attribute and
64/// the derived `uri_options()`. Component structs delegate their `Component::metadata`
65/// override to this (e.g. `fn metadata(&self) -> ComponentMetadata { Config::metadata() }`).
66///
67/// # Example
68///
69/// ## Basic usage
70///
71/// ```ignore
72/// use camel_endpoint::UriConfig;
73///
74/// #[derive(Debug, Clone, UriConfig)]
75/// #[uri_scheme = "timer"]
76/// struct TimerConfig {
77/// // First field without #[uri_param] gets the path component
78/// name: String,
79///
80/// // Query parameters
81/// #[uri_param(default = "1000")]
82/// period: u64,
83///
84/// #[uri_param(default = "true")]
85/// repeat: bool,
86///
87/// #[uri_param(name = "cronExpr")]
88/// cron: Option<String>,
89/// }
90///
91/// // Generated impl allows:
92/// let config = TimerConfig::from_uri("timer:tick?period=5000").unwrap();
93/// assert_eq!(config.name, "tick");
94/// assert_eq!(config.period, 5000);
95/// assert!(config.repeat); // uses default
96/// assert!(config.cron.is_none()); // Option defaults to None
97/// ```
98///
99/// ## Custom validation with `skip_impl`
100///
101/// ```ignore
102/// use camel_endpoint::UriConfig;
103///
104/// #[derive(Debug, Clone, UriConfig)]
105/// #[uri_scheme = "file"]
106/// #[uri_config(skip_impl)]
107/// struct FileConfig {
108/// directory: String,
109/// #[uri_param(default = "false")]
110/// delete: bool,
111/// #[uri_param(name = "move")]
112/// move_to: Option<String>,
113/// }
114///
115/// // Implement the trait manually with custom validation
116/// impl UriConfig for FileConfig {
117/// fn scheme() -> &'static str { "file" }
118///
119/// fn from_uri(uri: &str) -> Result<Self, CamelError> {
120/// let parts = parse_uri(uri)?;
121/// Self::from_components(parts)
122/// }
123///
124/// fn from_components(parts: UriComponents) -> Result<Self, CamelError> {
125/// Self::parse_uri_components(parts)?.validate()
126/// }
127///
128/// fn validate(self) -> Result<Self, CamelError> {
129/// // Custom validation: move_to is None if delete is true
130/// let move_to = if self.delete { None } else { self.move_to };
131/// Ok(Self { move_to, ..self })
132/// }
133/// }
134/// ```
135///
136/// # OptionKind type inference
137///
138/// The `OptionKind` for each `#[uri_param]` field is inferred from its Rust
139/// type after unwrapping `Option<T>`:
140///
141/// | Rust type | Inferred `OptionKind` |
142/// |-------------------------------|-----------------------------------------------------|
143/// | `std::time::Duration` | `Duration` |
144/// | `bool` | `Bool` |
145/// | `u8`, `u16`, `u32`, `u64`, `usize`, `i8`, `i16`, `i32`, `i64`, `isize` | `Int` |
146/// | `f32`, `f64` | `Float` |
147/// | `String`, `&str` | `String` |
148/// | `Vec<T>` | `List(Box::new(inner_kind_of_T))` |
149/// | anything else (enums, custom types, …) | `String` |
150///
151/// **Inference never produces `OptionKind::Enum`.** The only way to get an
152/// `Enum` option is an explicit `kind = "enum:A,B,C"` override.
153///
154/// # Guardrail: `secret` + `default` is a compile error
155///
156/// `#[uri_param(secret, default = "x")]` produces a compile-time error:
157/// *\"`#[uri_param]` cannot have both `secret` and `default`; a secret must
158/// never carry a default value.\"* This prevents sensitive values from being embedded
159/// in generated code or discovery output.
160///
161/// # Delegation convention
162///
163/// The macro generates `uri_options()` and (when opted in) `metadata()` as
164/// inherent methods on the **config** struct. The **component** struct
165/// implements `Component`, whose `metadata()` default returns
166/// `ComponentMetadata::minimal(scheme)` with empty `uri_options`. Every
167/// component MUST override `metadata()` to delegate to its config struct:
168///
169/// ```ignore
170/// impl Component for MyComponent {
171/// fn scheme(&self) -> &str { "my-scheme" }
172///
173/// fn metadata(&self) -> ComponentMetadata {
174/// MyConfig::metadata()
175/// // Or, without the metadata(..) opt-in:
176/// // ComponentMetadata::minimal(self.scheme())
177/// // .with_uri_options(MyConfig::uri_options())
178/// }
179/// }
180/// ```
181///
182/// Without this delegation step, the catalog returns empty `uri_options`.
183///
184/// # Worked example: component with metadata
185///
186/// ```ignore
187/// use camel_endpoint::UriConfig;
188///
189/// #[derive(Debug, Clone, UriConfig)]
190/// #[uri_scheme = "sql"]
191/// #[uri_config(
192/// metadata(
193/// scheme = "sql",
194/// description = "Execute SQL against a configured datasource",
195/// producer,
196/// consumer,
197/// )
198/// )]
199/// struct SqlConfig {
200/// query: String,
201///
202/// #[uri_param(secret, desc = "Database connection URL")]
203/// db_url: String,
204///
205/// #[uri_param(
206/// name = "outputType",
207/// desc = "Output type for query results",
208/// kind = "enum:SelectList,SelectOne,StreamList",
209/// default = "SelectList"
210/// )]
211/// output_type: SqlOutputType,
212///
213/// #[uri_param(
214/// name = "maxConnections",
215/// desc = "Maximum connections in the pool",
216/// default = "5"
217/// )]
218/// max_connections: u32,
219/// }
220///
221/// // Generated:
222/// // - SqlConfig::uri_options() returns 3 UriOption entries
223/// // - SqlConfig::metadata() returns ComponentMetadata with scheme "sql",
224/// // producer + consumer capabilities, and the derived uri_options
225///
226/// // Component delegation (hand-written):
227/// impl Component for SqlComponent {
228/// fn scheme(&self) -> &str { "sql" }
229/// fn metadata(&self) -> ComponentMetadata { SqlConfig::metadata() }
230/// }
231/// ```
232#[proc_macro_derive(UriConfig, attributes(uri_scheme, uri_param, uri_config))]
233pub fn derive_uri_config(input: TokenStream) -> TokenStream {
234 let input = parse_macro_input!(input as DeriveInput);
235 match uri_config::impl_uri_config(&input) {
236 Ok(tokens) => tokens.into(),
237 Err(e) => e.to_compile_error().into(),
238 }
239}