Skip to main content

camel_api/
template.rs

1use std::collections::BTreeMap;
2use thiserror::Error;
3use uuid::Uuid;
4
5/// The declared data type of a template parameter.
6#[cfg_attr(feature = "schema", derive(schemars::JsonSchema, ts_rs::TS))]
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
8#[non_exhaustive]
9#[serde(rename_all = "lowercase")]
10pub enum TemplateParamType {
11    #[default]
12    String,
13    Number,
14    Boolean,
15}
16
17/// A single parameter that a route template accepts.
18#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
19pub struct TemplateParameterSpec {
20    /// The parameter name (used inside `{{name}}` placeholders).
21    pub name: String,
22    /// Optional default value used when the caller does not supply one.
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub default_value: Option<String>,
25    /// Optional human-readable description.
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub description: Option<String>,
28    /// The declared data type of this parameter.
29    #[serde(default, rename = "type")]
30    pub parameter_type: TemplateParamType,
31}
32
33/// A reusable route template with declared parameters and a route body.
34#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
35pub struct RouteTemplateSpec {
36    /// Unique identifier for this template.
37    pub id: String,
38    /// Parameters that callers must (or may) supply.
39    pub parameters: Vec<TemplateParameterSpec>,
40    /// Route definition bodies (YAML/JSON fragments as generic values).
41    pub routes: Vec<serde_json::Value>,
42}
43
44/// A request to instantiate a template with concrete parameter values.
45#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
46pub struct TemplatedRouteSpec {
47    /// Reference to the template to instantiate.
48    pub route_template_ref: String,
49    /// Optional explicit route id for the resulting instance.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub route_id: Option<String>,
52    /// Concrete parameter values keyed by parameter name.
53    pub parameters: BTreeMap<String, String>,
54}
55
56/// A successfully instantiated template record.
57#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
58pub struct TemplateInstanceRecord {
59    /// The template that was instantiated.
60    pub template_id: String,
61    /// Unique identifier for this instance.
62    pub instance_id: Uuid,
63    /// The route id assigned to the resulting route.
64    pub route_id: String,
65    /// The parameter values that were applied.
66    pub parameters: BTreeMap<String, String>,
67}
68
69/// Errors that can occur during template processing.
70#[derive(Debug, Clone, Error)]
71#[non_exhaustive]
72pub enum TemplateError {
73    #[error("missing required parameter: {0}")]
74    MissingParameter(String),
75
76    #[error("unknown parameter: {0}")]
77    UnknownParameter(String),
78
79    /// A typed parameter was supplied a value that does not coerce to its
80    /// declared type (`number`/`boolean`). Carries the parameter name, the
81    /// declared type name, and the offending value.
82    #[error("parameter '{0}' declared type {1} but value '{2}' is not coercible")]
83    InvalidParameter(String, String, String),
84
85    #[error("template already registered: {0}")]
86    AlreadyRegistered(String),
87
88    #[error("invalid template body: {0}")]
89    InvalidBody(String),
90
91    /// The materialized route declares a security policy but no authenticator
92    /// was configured — fail-closed classification distinct from body errors.
93    ///
94    /// NOTE: the message field is named `detail`, not `source`, because thiserror
95    /// auto-detects any field literally named `source` as the error source and
96    /// requires it to implement `std::error::Error` (a `String` does not).
97    #[error("security policy requires an authenticator (template {template_id}): {detail}")]
98    SecurityRequired { template_id: String, detail: String },
99
100    /// The referenced template id was never defined.
101    #[error("template not found: {0}")]
102    NotFound(String),
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn template_error_converts_to_camel_error() {
111        use crate::error::CamelError;
112        let err = TemplateError::MissingParameter("host".into());
113        let camel: CamelError = err.into();
114        assert!(matches!(camel, CamelError::Config(_)));
115    }
116
117    #[test]
118    fn template_parameter_spec_serializes() {
119        let spec = TemplateParameterSpec {
120            name: "host".into(),
121            default_value: Some("localhost".into()),
122            description: Some("The target host".into()),
123            parameter_type: TemplateParamType::String,
124        };
125        let json = serde_json::to_string(&spec).unwrap();
126        assert!(json.contains("host"));
127        assert!(json.contains("localhost"));
128    }
129
130    #[test]
131    fn template_parameter_spec_roundtrip() {
132        let spec = TemplateParameterSpec {
133            name: "port".into(),
134            default_value: None,
135            description: None,
136            parameter_type: TemplateParamType::String,
137        };
138        let json = serde_json::to_string(&spec).unwrap();
139        let round: TemplateParameterSpec = serde_json::from_str(&json).unwrap();
140        assert_eq!(round.name, "port");
141        assert!(round.default_value.is_none());
142        assert!(round.description.is_none());
143    }
144
145    #[test]
146    fn route_template_spec_serializes() {
147        let tpl = RouteTemplateSpec {
148            id: "http-route".into(),
149            parameters: vec![TemplateParameterSpec {
150                name: "path".into(),
151                default_value: None,
152                description: None,
153                parameter_type: TemplateParamType::String,
154            }],
155            routes: vec![serde_json::json!({"from": {"uri": "rest:{{path}}"}})],
156        };
157        let json = serde_json::to_string(&tpl).unwrap();
158        assert!(json.contains("http-route"));
159        assert!(json.contains("path"));
160    }
161
162    #[test]
163    fn templated_route_spec_serializes() {
164        let spec = TemplatedRouteSpec {
165            route_template_ref: "http-route".into(),
166            route_id: Some("my-route".into()),
167            parameters: [("path".into(), "/api".into())].into_iter().collect(),
168        };
169        let json = serde_json::to_string(&spec).unwrap();
170        assert!(json.contains("http-route"));
171        assert!(json.contains("/api"));
172    }
173
174    #[test]
175    fn template_instance_record_serializes() {
176        let instance = TemplateInstanceRecord {
177            template_id: "http-route".into(),
178            instance_id: Uuid::nil(),
179            route_id: "my-route".into(),
180            parameters: [("path".into(), "/api".into())].into_iter().collect(),
181        };
182        let json = serde_json::to_string(&instance).unwrap();
183        assert!(json.contains("http-route"));
184        assert!(json.contains("my-route"));
185    }
186
187    #[test]
188    fn template_error_display_messages() {
189        let cases: Vec<(TemplateError, &str)> = vec![
190            (
191                TemplateError::MissingParameter("x".into()),
192                "missing required parameter: x",
193            ),
194            (
195                TemplateError::UnknownParameter("y".into()),
196                "unknown parameter: y",
197            ),
198            (
199                TemplateError::AlreadyRegistered("z".into()),
200                "template already registered: z",
201            ),
202            (
203                TemplateError::InvalidBody("d".into()),
204                "invalid template body: d",
205            ),
206            (
207                TemplateError::InvalidParameter("delay".into(), "number".into(), "abc".into()),
208                "parameter 'delay' declared type number but value 'abc' is not coercible",
209            ),
210        ];
211        for (err, expected) in cases {
212            assert_eq!(format!("{err}"), expected);
213        }
214    }
215
216    #[test]
217    fn template_error_is_clone() {
218        let err = TemplateError::MissingParameter("host".into());
219        let cloned = err.clone();
220        assert!(matches!(cloned, TemplateError::MissingParameter(_)));
221    }
222}