1use std::collections::BTreeMap;
2use thiserror::Error;
3use uuid::Uuid;
4
5#[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#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
19pub struct TemplateParameterSpec {
20 pub name: String,
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub default_value: Option<String>,
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub description: Option<String>,
28 #[serde(default, rename = "type")]
30 pub parameter_type: TemplateParamType,
31}
32
33#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
35pub struct RouteTemplateSpec {
36 pub id: String,
38 pub parameters: Vec<TemplateParameterSpec>,
40 pub routes: Vec<serde_json::Value>,
42}
43
44#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
46pub struct TemplatedRouteSpec {
47 pub route_template_ref: String,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub route_id: Option<String>,
52 pub parameters: BTreeMap<String, String>,
54}
55
56#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
58pub struct TemplateInstanceRecord {
59 pub template_id: String,
61 pub instance_id: Uuid,
63 pub route_id: String,
65 pub parameters: BTreeMap<String, String>,
67}
68
69#[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 #[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 #[error("security policy requires an authenticator (template {template_id}): {detail}")]
98 SecurityRequired { template_id: String, detail: String },
99
100 #[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}