1use std::path::Path;
14
15use camel_api::{CamelError, CanonicalRouteSpec};
16use camel_core::route::RouteDefinition;
17
18use crate::compile::{
19 compile_declarative_route, compile_declarative_route_to_canonical,
20 compile_declarative_route_with_stream_cache_threshold,
21};
22use crate::yaml::{YamlRoutes, yaml_route_to_declarative_route};
23
24pub type JsonRoutes = crate::yaml::YamlRoutes;
26
27pub type JsonRoute = crate::yaml::YamlRoute;
29
30pub type JsonStep = crate::yaml::YamlStep;
32
33pub fn parse_json_to_declarative(
35 json: &str,
36) -> Result<Vec<crate::model::DeclarativeRoute>, CamelError> {
37 let routes: YamlRoutes = serde_json::from_str(json)
38 .map_err(|e| CamelError::RouteError(format!("JSON parse error: {e}")))?;
39
40 routes
41 .routes
42 .into_iter()
43 .map(yaml_route_to_declarative_route)
44 .collect()
45}
46
47pub fn parse_json(json: &str) -> Result<Vec<RouteDefinition>, CamelError> {
49 parse_json_to_declarative(json)?
50 .into_iter()
51 .map(compile_declarative_route)
52 .collect()
53}
54
55pub fn parse_json_with_threshold(
57 json: &str,
58 stream_cache_threshold: usize,
59) -> Result<Vec<RouteDefinition>, CamelError> {
60 parse_json_to_declarative(json)?
61 .into_iter()
62 .map(|route| {
63 compile_declarative_route_with_stream_cache_threshold(route, stream_cache_threshold)
64 })
65 .collect()
66}
67
68pub fn parse_json_to_canonical(json: &str) -> Result<Vec<CanonicalRouteSpec>, CamelError> {
70 parse_json_to_declarative(json)?
71 .into_iter()
72 .map(compile_declarative_route_to_canonical)
73 .collect()
74}
75
76pub fn load_json_from_file(path: &Path) -> Result<Vec<RouteDefinition>, CamelError> {
80 let content = std::fs::read_to_string(path)
81 .map_err(|e| CamelError::Io(format!("Failed to read {}: {e}", path.display())))?;
82 parse_json(&content)
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use crate::model::DeclarativeStep;
89
90 #[test]
92 fn test_basic_route_to_declarative() {
93 let json = r#"
94 {
95 "routes": [
96 {
97 "id": "test-route",
98 "from": "timer:tick?period=1000",
99 "steps": [
100 {"set_header": {"key": "source", "value": "timer"}},
101 {"to": "log:info"}
102 ]
103 }
104 ]
105 }"#;
106 let routes = parse_json_to_declarative(json).unwrap();
107 assert_eq!(routes.len(), 1);
108 assert_eq!(routes[0].route_id, "test-route");
109 assert_eq!(routes[0].from, "timer:tick?period=1000");
110 assert_eq!(routes[0].steps.len(), 2);
111 }
112
113 #[test]
115 fn test_route_metadata_auto_startup_and_startup_order() {
116 let json = r#"
117 {
118 "routes": [
119 {
120 "id": "meta-route",
121 "from": "direct:start",
122 "auto_startup": false,
123 "startup_order": 42
124 }
125 ]
126 }"#;
127 let routes = parse_json_to_declarative(json).unwrap();
128 assert_eq!(routes.len(), 1);
129 assert!(!routes[0].auto_startup);
130 assert_eq!(routes[0].startup_order, 42);
131 }
132
133 #[test]
135 fn test_error_handler_with_handled_by() {
136 let json = r#"
137 {
138 "routes": [
139 {
140 "id": "eh-route",
141 "from": "direct:start",
142 "error_handler": {
143 "dead_letter_channel": "log:dlc",
144 "on_exceptions": [
145 {
146 "kind": "Io",
147 "retry": {
148 "max_attempts": 3,
149 "handled_by": "log:io"
150 }
151 }
152 ]
153 }
154 }
155 ]
156 }"#;
157 let routes = parse_json_to_declarative(json).unwrap();
158 let eh = routes[0]
159 .error_handler
160 .as_ref()
161 .expect("error handler should be present");
162 let clauses = eh
163 .on_exceptions
164 .as_ref()
165 .expect("on_exceptions should be present");
166 assert_eq!(clauses.len(), 1);
167 assert_eq!(clauses[0].kind.as_deref(), Some("Io"));
168 let retry = clauses[0].retry.as_ref().expect("retry should be present");
169 assert_eq!(retry.max_attempts, 3);
170 assert_eq!(retry.handled_by.as_deref(), Some("log:io"));
171 }
172
173 #[test]
175 fn test_empty_id_fails() {
176 let json = r#"
177 {
178 "routes": [
179 {
180 "id": "",
181 "from": "timer:tick"
182 }
183 ]
184 }"#;
185 let result = parse_json_to_declarative(json);
186 assert!(result.is_err());
187 let err = result.unwrap_err().to_string();
188 assert!(
189 err.contains("route 'id' must not be empty"),
190 "unexpected error: {err}"
191 );
192 }
193
194 #[test]
196 fn test_invalid_json_error_says_json() {
197 let json = "{ not valid json }}}";
198 let result = parse_json_to_declarative(json);
199 assert!(result.is_err());
200 let err = result.unwrap_err().to_string();
201 assert!(
202 err.contains("JSON parse error:"),
203 "expected 'JSON parse error:' in error, got: {err}"
204 );
205 }
206
207 #[test]
209 fn test_compiled_route_via_parse_json() {
210 let json = r#"
211 {
212 "routes": [
213 {
214 "id": "compiled-route",
215 "from": "timer:tick",
216 "steps": [
217 {"to": "log:info"}
218 ]
219 }
220 ]
221 }"#;
222 let defs = parse_json(json).unwrap();
223 assert_eq!(defs.len(), 1);
224 assert_eq!(defs[0].route_id(), "compiled-route");
225 assert_eq!(defs[0].from_uri(), "timer:tick");
226 }
227
228 #[test]
230 fn test_threshold_parse() {
231 let json = r#"
232 {
233 "routes": [
234 {
235 "id": "threshold-route",
236 "from": "timer:tick",
237 "steps": [
238 {"stream_cache": true},
239 {"to": "log:info"}
240 ]
241 }
242 ]
243 }"#;
244 let defs = parse_json_with_threshold(json, 8192).unwrap();
245 assert_eq!(defs.len(), 1);
246 assert_eq!(defs[0].route_id(), "threshold-route");
247 }
248
249 #[test]
251 fn test_canonical_conversion() {
252 let json = r#"
253 {
254 "routes": [
255 {
256 "id": "canonical-v1",
257 "from": "direct:start",
258 "steps": [
259 {"to": "mock:out"},
260 {"log": {"message": "hello"}},
261 {"stop": true}
262 ]
263 }
264 ]
265 }"#;
266 let routes = parse_json_to_canonical(json).unwrap();
267 assert_eq!(routes.len(), 1);
268 assert_eq!(routes[0].route_id, "canonical-v1");
269 assert_eq!(routes[0].from, "direct:start");
270 assert_eq!(routes[0].version, 1);
271 assert_eq!(routes[0].steps.len(), 3);
272 }
273
274 #[test]
276 fn test_loop_step_json_key() {
277 let json = r#"
278 {
279 "routes": [
280 {
281 "id": "loop-route",
282 "from": "direct:start",
283 "steps": [
284 {"loop": 3}
285 ]
286 }
287 ]
288 }"#;
289 let routes = parse_json_to_declarative(json).unwrap();
290 assert_eq!(routes.len(), 1);
291 match &routes[0].steps[0] {
292 DeclarativeStep::Loop(def) => {
293 assert_eq!(def.count, Some(3));
294 }
295 other => panic!("expected Loop step, got {:?}", other),
296 }
297 }
298
299 #[test]
301 fn test_multiple_routes() {
302 let json = r#"
303 {
304 "routes": [
305 {
306 "id": "route-a",
307 "from": "timer:tick",
308 "steps": [{"to": "log:info"}]
309 },
310 {
311 "id": "route-b",
312 "from": "timer:tock",
313 "auto_startup": false,
314 "startup_order": 10
315 }
316 ]
317 }"#;
318 let defs = parse_json(json).unwrap();
319 assert_eq!(defs.len(), 2);
320 assert_eq!(defs[0].route_id(), "route-a");
321 assert_eq!(defs[1].route_id(), "route-b");
322 }
323
324 #[test]
326 fn test_defaults() {
327 let json = r#"
328 {
329 "routes": [
330 {
331 "id": "default-route",
332 "from": "timer:tick"
333 }
334 ]
335 }"#;
336 let defs = parse_json(json).unwrap();
337 assert!(defs[0].auto_startup());
338 assert_eq!(defs[0].startup_order(), 1000);
339 }
340
341 #[test]
343 fn test_file_loading() {
344 use std::io::Write;
345 let mut file = tempfile::NamedTempFile::new().unwrap();
346
347 let json_content = r#"
348 {
349 "routes": [
350 {
351 "id": "file-route",
352 "from": "timer:tick",
353 "steps": [{"to": "log:info"}]
354 }
355 ]
356 }"#;
357
358 file.write_all(json_content.as_bytes()).unwrap();
359
360 let defs = load_json_from_file(file.path()).unwrap();
361 assert_eq!(defs.len(), 1);
362 assert_eq!(defs[0].route_id(), "file-route");
363 }
364
365 #[test]
367 fn test_missing_file_error() {
368 let result = load_json_from_file(Path::new("/nonexistent/path/routes.json"));
369 assert!(result.is_err());
370 let err = result.err().unwrap().to_string();
371 assert!(
372 err.contains("Failed to read"),
373 "expected file read error, got: {err}"
374 );
375 }
376
377 #[test]
378 fn test_function_step_json_parses_and_compiles() {
379 let json = r#"
380 {
381 "routes": [
382 {
383 "id": "fn-json-route",
384 "from": "direct:start",
385 "steps": [
386 {
387 "function": {
388 "runtime": "deno",
389 "source": "return { body: 'ok' };",
390 "timeout_ms": 3000
391 }
392 }
393 ]
394 }
395 ]
396 }"#;
397 let defs = parse_json(json).unwrap();
398 assert_eq!(defs.len(), 1);
399 assert_eq!(defs[0].route_id(), "fn-json-route");
400 }
401
402 #[test]
403 fn test_function_step_json_compiles_to_declarative_function() {
404 let json = r#"
405 {
406 "routes": [
407 {
408 "id": "fn-decl",
409 "from": "direct:start",
410 "steps": [
411 {
412 "function": {
413 "runtime": "deno",
414 "source": "return { body: 1 };"
415 }
416 }
417 ]
418 }
419 ]
420 }"#;
421 let routes = parse_json_to_declarative(json).unwrap();
422 match &routes[0].steps[0] {
423 DeclarativeStep::Function(def) => {
424 assert_eq!(def.runtime, "deno");
425 assert_eq!(def.source, "return { body: 1 };");
426 assert_eq!(def.timeout_ms, None);
427 }
428 other => panic!("expected Function, got {other:?}"),
429 }
430 }
431
432 #[test]
433 fn test_function_step_rejected_by_canonical_json() {
434 let json = r#"
435 {
436 "routes": [
437 {
438 "id": "fn-canonical-reject",
439 "from": "direct:start",
440 "steps": [
441 {
442 "function": {
443 "runtime": "deno",
444 "source": "return {};",
445 "timeout_ms": 1000
446 }
447 }
448 ]
449 }
450 ]
451 }"#;
452 let err = parse_json_to_canonical(json).unwrap_err().to_string();
453 assert!(
454 err.contains("canonical v1 does not support step `function`"),
455 "unexpected error: {err}"
456 );
457 }
458}