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}