Skip to main content

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}