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