Skip to main content

camel_api/
template.rs

1use std::collections::BTreeMap;
2use thiserror::Error;
3use uuid::Uuid;
4
5/// A single parameter that a route template accepts.
6#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
7pub struct TemplateParameterSpec {
8    /// The parameter name (used inside `{{name}}` placeholders).
9    pub name: String,
10    /// Optional default value used when the caller does not supply one.
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub default_value: Option<String>,
13    /// Optional human-readable description.
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub description: Option<String>,
16}
17
18/// A reusable route template with declared parameters and a route body.
19#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
20pub struct RouteTemplateSpec {
21    /// Unique identifier for this template.
22    pub id: String,
23    /// Parameters that callers must (or may) supply.
24    pub parameters: Vec<TemplateParameterSpec>,
25    /// The route definition body (YAML/JSON fragment as a generic value).
26    pub route: serde_json::Value,
27}
28
29/// A request to instantiate a template with concrete parameter values.
30#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
31pub struct TemplatedRouteSpec {
32    /// Reference to the template to instantiate.
33    pub route_template_ref: String,
34    /// Optional explicit route id for the resulting instance.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub route_id: Option<String>,
37    /// Concrete parameter values keyed by parameter name.
38    pub parameters: BTreeMap<String, String>,
39}
40
41/// A successfully instantiated template record.
42#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
43pub struct TemplateInstanceRecord {
44    /// The template that was instantiated.
45    pub template_id: String,
46    /// Unique identifier for this instance.
47    pub instance_id: Uuid,
48    /// The route id assigned to the resulting route.
49    pub route_id: String,
50    /// The parameter values that were applied.
51    pub parameters: BTreeMap<String, String>,
52}
53
54/// Errors that can occur during template processing.
55#[derive(Debug, Clone, Error)]
56#[non_exhaustive]
57pub enum TemplateError {
58    #[error("missing required parameter: {0}")]
59    MissingParameter(String),
60
61    #[error("unknown parameter: {0}")]
62    UnknownParameter(String),
63
64    #[error("template already registered: {0}")]
65    AlreadyRegistered(String),
66
67    #[error("invalid template body: {0}")]
68    InvalidBody(String),
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn template_error_converts_to_camel_error() {
77        use crate::error::CamelError;
78        let err = TemplateError::MissingParameter("host".into());
79        let camel: CamelError = err.into();
80        assert!(matches!(camel, CamelError::Config(_)));
81    }
82
83    #[test]
84    fn template_parameter_spec_serializes() {
85        let spec = TemplateParameterSpec {
86            name: "host".into(),
87            default_value: Some("localhost".into()),
88            description: Some("The target host".into()),
89        };
90        let json = serde_json::to_string(&spec).unwrap();
91        assert!(json.contains("host"));
92        assert!(json.contains("localhost"));
93    }
94
95    #[test]
96    fn template_parameter_spec_roundtrip() {
97        let spec = TemplateParameterSpec {
98            name: "port".into(),
99            default_value: None,
100            description: None,
101        };
102        let json = serde_json::to_string(&spec).unwrap();
103        let round: TemplateParameterSpec = serde_json::from_str(&json).unwrap();
104        assert_eq!(round.name, "port");
105        assert!(round.default_value.is_none());
106        assert!(round.description.is_none());
107    }
108
109    #[test]
110    fn route_template_spec_serializes() {
111        let tpl = RouteTemplateSpec {
112            id: "http-route".into(),
113            parameters: vec![TemplateParameterSpec {
114                name: "path".into(),
115                default_value: None,
116                description: None,
117            }],
118            route: serde_json::json!({"from": {"uri": "rest:{{path}}"}}),
119        };
120        let json = serde_json::to_string(&tpl).unwrap();
121        assert!(json.contains("http-route"));
122        assert!(json.contains("path"));
123    }
124
125    #[test]
126    fn templated_route_spec_serializes() {
127        let spec = TemplatedRouteSpec {
128            route_template_ref: "http-route".into(),
129            route_id: Some("my-route".into()),
130            parameters: [("path".into(), "/api".into())].into_iter().collect(),
131        };
132        let json = serde_json::to_string(&spec).unwrap();
133        assert!(json.contains("http-route"));
134        assert!(json.contains("/api"));
135    }
136
137    #[test]
138    fn template_instance_record_serializes() {
139        let instance = TemplateInstanceRecord {
140            template_id: "http-route".into(),
141            instance_id: Uuid::nil(),
142            route_id: "my-route".into(),
143            parameters: [("path".into(), "/api".into())].into_iter().collect(),
144        };
145        let json = serde_json::to_string(&instance).unwrap();
146        assert!(json.contains("http-route"));
147        assert!(json.contains("my-route"));
148    }
149
150    #[test]
151    fn template_error_display_messages() {
152        let cases: Vec<(TemplateError, &str)> = vec![
153            (
154                TemplateError::MissingParameter("x".into()),
155                "missing required parameter: x",
156            ),
157            (
158                TemplateError::UnknownParameter("y".into()),
159                "unknown parameter: y",
160            ),
161            (
162                TemplateError::AlreadyRegistered("z".into()),
163                "template already registered: z",
164            ),
165            (
166                TemplateError::InvalidBody("d".into()),
167                "invalid template body: d",
168            ),
169        ];
170        for (err, expected) in cases {
171            assert_eq!(format!("{err}"), expected);
172        }
173    }
174
175    #[test]
176    fn template_error_is_clone() {
177        let err = TemplateError::MissingParameter("host".into());
178        let cloned = err.clone();
179        assert!(matches!(cloned, TemplateError::MissingParameter(_)));
180    }
181}