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::input_format::{InputFormat, annotate_format};
23use crate::model::SecurityCompileContext;
24use crate::yaml::{RouteDslRoutes, route_dsl_to_declarative_route};
25
26pub fn parse_json_to_declarative(
28 json: &str,
29) -> Result<Vec<crate::model::DeclarativeRoute>, CamelError> {
30 annotate_format(InputFormat::Json, parse_json_to_declarative_inner(json))
31}
32
33fn parse_json_to_declarative_inner(
34 json: &str,
35) -> Result<Vec<crate::model::DeclarativeRoute>, CamelError> {
36 let mut dsl: RouteDslRoutes = serde_json::from_str(json)
37 .map_err(|e| CamelError::RouteError(format!("JSON parse error: {e}")))?;
38
39 crate::rest::expand_rest_into(&mut dsl.routes, &dsl.rest)?;
44 crate::rest::check_duplicate_route_ids(&dsl.routes)?;
45
46 dsl.routes
47 .into_iter()
48 .map(route_dsl_to_declarative_route)
49 .collect()
50}
51
52pub fn parse_json(json: &str) -> Result<Vec<RouteDefinition>, CamelError> {
54 annotate_format(InputFormat::Json, parse_json_inner(json))
55}
56
57fn parse_json_inner(json: &str) -> Result<Vec<RouteDefinition>, CamelError> {
58 parse_json_to_declarative_inner(json)?
59 .into_iter()
60 .map(compile_declarative_route)
61 .collect()
62}
63
64pub fn parse_json_with_threshold(
66 json: &str,
67 stream_cache_threshold: usize,
68) -> Result<Vec<RouteDefinition>, CamelError> {
69 annotate_format(
70 InputFormat::Json,
71 parse_json_with_threshold_inner(json, stream_cache_threshold),
72 )
73}
74
75fn parse_json_with_threshold_inner(
76 json: &str,
77 stream_cache_threshold: usize,
78) -> Result<Vec<RouteDefinition>, CamelError> {
79 parse_json_with_threshold_and_security_inner(
80 json,
81 stream_cache_threshold,
82 SecurityCompileContext::default(),
83 )
84}
85
86pub fn parse_json_with_threshold_and_security(
87 json: &str,
88 stream_cache_threshold: usize,
89 security_ctx: SecurityCompileContext,
90) -> Result<Vec<RouteDefinition>, CamelError> {
91 annotate_format(
92 InputFormat::Json,
93 parse_json_with_threshold_and_security_inner(json, stream_cache_threshold, security_ctx),
94 )
95}
96
97fn parse_json_with_threshold_and_security_inner(
98 json: &str,
99 stream_cache_threshold: usize,
100 security_ctx: SecurityCompileContext,
101) -> Result<Vec<RouteDefinition>, CamelError> {
102 parse_json_to_declarative_inner(json)?
103 .into_iter()
104 .map(|route| {
105 compile_declarative_route_with_stream_cache_threshold(
106 route,
107 stream_cache_threshold,
108 security_ctx.clone(),
109 )
110 })
111 .collect()
112}
113
114pub fn parse_json_to_canonical(json: &str) -> Result<Vec<CanonicalRouteSpec>, CamelError> {
116 annotate_format(InputFormat::Json, parse_json_to_canonical_inner(json))
117}
118
119fn parse_json_to_canonical_inner(json: &str) -> Result<Vec<CanonicalRouteSpec>, CamelError> {
120 let routes = parse_json_to_declarative_inner(json)?;
121 for route in &routes {
122 if route.security_policy.is_some() {
123 return Err(CamelError::RouteError(
124 "routes with security_policy cannot use the canonical/hot-reload path (not yet supported)".into(),
125 ));
126 }
127 }
128 routes
129 .into_iter()
130 .map(|r| compile_declarative_route_to_canonical(r, false).map(|(spec, _)| spec))
131 .collect()
132}
133
134use crate::util::read_route_file_capped;
135
136pub fn load_json_from_file(path: &Path) -> Result<Vec<RouteDefinition>, CamelError> {
140 let content = read_route_file_capped(path)?;
141 let annotated = annotate_format(InputFormat::Json, parse_json_inner(&content));
142 annotated.map_err(|e| match e {
143 CamelError::RouteError(msg) => {
144 CamelError::RouteError(format!("{msg} (in {})", path.display()))
145 }
146 other => other,
147 })
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::model::DeclarativeStep;
154 use std::fs::File;
155
156 #[test]
158 fn test_basic_route_to_declarative() {
159 let json = r#"
160 {
161 "routes": [
162 {
163 "id": "test-route",
164 "from": "timer:tick?period=1000",
165 "steps": [
166 {"set_header": {"key": "source", "value": "timer"}},
167 {"to": "log:info"}
168 ]
169 }
170 ]
171 }"#;
172 let routes = parse_json_to_declarative(json).unwrap();
173 assert_eq!(routes.len(), 1);
174 assert_eq!(routes[0].route_id, "test-route");
175 assert_eq!(routes[0].from, "timer:tick?period=1000");
176 assert_eq!(routes[0].steps.len(), 2);
177 }
178
179 #[test]
181 fn test_route_metadata_auto_startup_and_startup_order() {
182 let json = r#"
183 {
184 "routes": [
185 {
186 "id": "meta-route",
187 "from": "direct:start",
188 "auto_startup": false,
189 "startup_order": 42
190 }
191 ]
192 }"#;
193 let routes = parse_json_to_declarative(json).unwrap();
194 assert_eq!(routes.len(), 1);
195 assert!(!routes[0].auto_startup);
196 assert_eq!(routes[0].startup_order, 42);
197 }
198
199 #[test]
201 fn test_error_handler_with_handled_by() {
202 let json = r#"
203 {
204 "routes": [
205 {
206 "id": "eh-route",
207 "from": "direct:start",
208 "error_handler": {
209 "dead_letter_channel": "log:dlc",
210 "on_exceptions": [
211 {
212 "kind": "Io",
213 "retry": {
214 "max_attempts": 3,
215 "handled_by": "log:io"
216 }
217 }
218 ]
219 }
220 }
221 ]
222 }"#;
223 let routes = parse_json_to_declarative(json).unwrap();
224 let eh = routes[0]
225 .error_handler
226 .as_ref()
227 .expect("error handler should be present");
228 let clauses = eh
229 .on_exceptions
230 .as_ref()
231 .expect("on_exceptions should be present");
232 assert_eq!(clauses.len(), 1);
233 assert_eq!(clauses[0].kind.as_deref(), Some("Io"));
234 let retry = clauses[0].retry.as_ref().expect("retry should be present");
235 assert_eq!(retry.max_attempts, 3);
236 assert_eq!(retry.handled_by.as_deref(), Some("log:io"));
237 }
238
239 #[test]
241 fn test_empty_id_fails() {
242 let json = r#"
243 {
244 "routes": [
245 {
246 "id": "",
247 "from": "timer:tick"
248 }
249 ]
250 }"#;
251 let result = parse_json_to_declarative(json);
252 assert!(result.is_err());
253 let err = result.unwrap_err().to_string();
254 assert!(
255 err.contains("route 'id' must not be empty"),
256 "unexpected error: {err}"
257 );
258 }
259
260 #[test]
262 fn test_invalid_json_error_says_json() {
263 let json = "{ not valid json }}}";
264 let result = parse_json_to_declarative(json);
265 assert!(result.is_err());
266 let err = result.unwrap_err().to_string();
267 assert!(
268 err.contains("JSON DSL error: JSON parse error:"),
269 "expected 'JSON DSL error: JSON parse error:' in error, got: {err}"
270 );
271 }
272
273 #[test]
275 fn test_compiled_route_via_parse_json() {
276 let json = r#"
277 {
278 "routes": [
279 {
280 "id": "compiled-route",
281 "from": "timer:tick",
282 "steps": [
283 {"to": "log:info"}
284 ]
285 }
286 ]
287 }"#;
288 let defs = parse_json(json).unwrap();
289 assert_eq!(defs.len(), 1);
290 assert_eq!(defs[0].route_id(), "compiled-route");
291 assert_eq!(defs[0].from_uri(), "timer:tick");
292 }
293
294 #[test]
296 fn test_threshold_parse() {
297 let json = r#"
298 {
299 "routes": [
300 {
301 "id": "threshold-route",
302 "from": "timer:tick",
303 "steps": [
304 {"stream_cache": true},
305 {"to": "log:info"}
306 ]
307 }
308 ]
309 }"#;
310 let defs = parse_json_with_threshold(json, 8192).unwrap();
311 assert_eq!(defs.len(), 1);
312 assert_eq!(defs[0].route_id(), "threshold-route");
313 }
314
315 #[test]
317 fn test_canonical_conversion() {
318 let json = r#"
319 {
320 "routes": [
321 {
322 "id": "canonical-v1",
323 "from": "direct:start",
324 "steps": [
325 {"to": "mock:out"},
326 {"log": {"message": "hello"}},
327 {"stop": true}
328 ]
329 }
330 ]
331 }"#;
332 let routes = parse_json_to_canonical(json).unwrap();
333 assert_eq!(routes.len(), 1);
334 assert_eq!(routes[0].route_id, "canonical-v1");
335 assert_eq!(routes[0].from, "direct:start");
336 assert_eq!(routes[0].version, 2);
337 assert_eq!(routes[0].steps.len(), 3);
338 }
339
340 #[test]
342 fn test_loop_step_json_key() {
343 let json = r#"
344 {
345 "routes": [
346 {
347 "id": "loop-route",
348 "from": "direct:start",
349 "steps": [
350 {"loop": 3}
351 ]
352 }
353 ]
354 }"#;
355 let routes = parse_json_to_declarative(json).unwrap();
356 assert_eq!(routes.len(), 1);
357 match &routes[0].steps[0] {
358 DeclarativeStep::Loop(def) => {
359 assert_eq!(def.count, Some(3));
360 }
361 other => panic!("expected Loop step, got {:?}", other),
362 }
363 }
364
365 #[test]
367 fn test_multiple_routes() {
368 let json = r#"
369 {
370 "routes": [
371 {
372 "id": "route-a",
373 "from": "timer:tick",
374 "steps": [{"to": "log:info"}]
375 },
376 {
377 "id": "route-b",
378 "from": "timer:tock",
379 "auto_startup": false,
380 "startup_order": 10
381 }
382 ]
383 }"#;
384 let defs = parse_json(json).unwrap();
385 assert_eq!(defs.len(), 2);
386 assert_eq!(defs[0].route_id(), "route-a");
387 assert_eq!(defs[1].route_id(), "route-b");
388 }
389
390 #[test]
392 fn test_defaults() {
393 let json = r#"
394 {
395 "routes": [
396 {
397 "id": "default-route",
398 "from": "timer:tick"
399 }
400 ]
401 }"#;
402 let defs = parse_json(json).unwrap();
403 assert!(defs[0].auto_startup());
404 assert_eq!(defs[0].startup_order(), 1000);
405 }
406
407 #[test]
409 fn test_file_loading() {
410 use std::io::Write;
411 let mut file = tempfile::NamedTempFile::new().unwrap();
412
413 let json_content = r#"
414 {
415 "routes": [
416 {
417 "id": "file-route",
418 "from": "timer:tick",
419 "steps": [{"to": "log:info"}]
420 }
421 ]
422 }"#;
423
424 file.write_all(json_content.as_bytes()).unwrap();
425
426 let defs = load_json_from_file(file.path()).unwrap();
427 assert_eq!(defs.len(), 1);
428 assert_eq!(defs[0].route_id(), "file-route");
429 }
430
431 #[test]
433 fn test_missing_file_error() {
434 let result = load_json_from_file(Path::new("/nonexistent/path/routes.json"));
435 assert!(result.is_err());
436 let err = result.err().unwrap().to_string();
437 assert!(
438 err.contains("Failed to read"),
439 "expected file read error, got: {err}"
440 );
441 }
442
443 #[test]
444 fn test_function_step_json_parses_and_compiles() {
445 let json = r#"
446 {
447 "routes": [
448 {
449 "id": "fn-json-route",
450 "from": "direct:start",
451 "steps": [
452 {
453 "function": {
454 "runtime": "deno",
455 "source": "return { body: 'ok' };",
456 "timeout_ms": 3000
457 }
458 }
459 ]
460 }
461 ]
462 }"#;
463 let defs = parse_json(json).unwrap();
464 assert_eq!(defs.len(), 1);
465 assert_eq!(defs[0].route_id(), "fn-json-route");
466 }
467
468 #[test]
469 fn test_function_step_json_compiles_to_declarative_function() {
470 let json = r#"
471 {
472 "routes": [
473 {
474 "id": "fn-decl",
475 "from": "direct:start",
476 "steps": [
477 {
478 "function": {
479 "runtime": "deno",
480 "source": "return { body: 1 };"
481 }
482 }
483 ]
484 }
485 ]
486 }"#;
487 let routes = parse_json_to_declarative(json).unwrap();
488 match &routes[0].steps[0] {
489 DeclarativeStep::Function(def) => {
490 assert_eq!(def.runtime, "deno");
491 assert_eq!(def.source, "return { body: 1 };");
492 assert_eq!(def.timeout_ms, None);
493 }
494 other => panic!("expected Function, got {other:?}"),
495 }
496 }
497
498 #[test]
499 fn test_function_step_rejected_by_canonical_json() {
500 let json = r#"
501 {
502 "routes": [
503 {
504 "id": "fn-canonical-reject",
505 "from": "direct:start",
506 "steps": [
507 {
508 "function": {
509 "runtime": "deno",
510 "source": "return {};",
511 "timeout_ms": 1000
512 }
513 }
514 ]
515 }
516 ]
517 }"#;
518 let err = parse_json_to_canonical(json).unwrap_err().to_string();
519 assert!(
520 err.contains("canonical v1 does not support step `function`"),
521 "unexpected error: {err}"
522 );
523 }
524
525 #[test]
526 fn test_set_property_step_json_parses_to_declarative() {
527 let json = r#"
528 {
529 "routes": [
530 {
531 "id": "property-json-route",
532 "from": "direct:start",
533 "steps": [
534 {"set_property": {"name": "traceId", "value": "abc-123"}}
535 ]
536 }
537 ]
538 }"#;
539 let routes = parse_json_to_declarative(json).unwrap();
540 match &routes[0].steps[0] {
541 DeclarativeStep::SetProperty(def) => {
542 assert_eq!(def.key, "traceId");
543 assert_eq!(
544 def.value,
545 crate::model::ValueSourceDef::Literal(serde_json::Value::String(
546 "abc-123".into()
547 ))
548 );
549 }
550 other => panic!("expected SetProperty, got {other:?}"),
551 }
552 }
553
554 #[test]
555 fn dollar_schema_key_is_accepted() {
556 let json = r#"{
557 "$schema": "https://example.com/route-schema.json",
558 "routes": [{"id": "r1", "from": "direct:start", "steps": [{"to": "direct:end"}]}]
559 }"#;
560 let parsed = parse_json_to_declarative(json).expect("$schema key must be accepted");
561 assert_eq!(parsed.len(), 1);
562 }
563
564 #[test]
565 fn parse_json_to_canonical_rejects_security_policy() {
566 let json = r#"{
567 "routes": [{
568 "id": "r-sec",
569 "from": "direct:start",
570 "security_policy": { "roles": ["admin"] },
571 "steps": [{ "to": "log:info" }]
572 }]
573 }"#;
574 let result = parse_json_to_canonical(json);
575 assert!(result.is_err());
576 }
577
578 #[test]
579 fn semantic_error_carries_json_format_prefix() {
580 let json = r#"{"routes": [{"id": "", "from": "timer:tick"}]}"#;
581 let result = parse_json_to_declarative(json);
582 assert!(result.is_err());
583 let err = result.unwrap_err().to_string();
584 assert!(
585 err.contains("JSON DSL error:"),
586 "expected 'JSON DSL error:' prefix, got: {err}"
587 );
588 assert!(
589 err.contains("route 'id' must not be empty"),
590 "expected underlying semantic message, got: {err}"
591 );
592 }
593
594 #[test]
595 fn json_parse_error_carries_format_prefix() {
596 let json = "{ not valid json }}}";
597 let result = parse_json_to_declarative(json);
598 assert!(result.is_err());
599 let err = result.unwrap_err().to_string();
600 assert!(
601 err.contains("JSON DSL error: JSON parse error:"),
602 "expected double prefix for parse errors, got: {err}"
603 );
604 }
605
606 #[test]
610 fn json_rest_block_expands_into_routes() {
611 let json = r#"{
612 "rest": [
613 {
614 "host": "0.0.0.0",
615 "port": 8080,
616 "path": "/users",
617 "operations": [
618 { "method": "GET", "path": "/{id}", "operation_id": "getUser", "to": "bean:svc" },
619 { "method": "POST", "path": "/", "operation_id": "createUser", "to": "bean:create" }
620 ]
621 }
622 ]
623 }"#;
624 let routes = parse_json_to_declarative(json).unwrap();
625 assert_eq!(routes.len(), 2);
627 let ids: Vec<&str> = routes.iter().map(|r| r.route_id.as_str()).collect();
628 assert!(ids.contains(&"getUser"));
629 assert!(ids.contains(&"createUser"));
630 let get_route = routes.iter().find(|r| r.route_id == "getUser").unwrap();
632 assert!(get_route.from.contains("/users/{id}"));
633 assert!(get_route.from.contains("httpMethod=GET"));
634 }
635
636 #[test]
639 fn json_rest_rejects_duplicate_method_path() {
640 let json = r#"{
641 "rest": [
642 {
643 "path": "/users",
644 "operations": [
645 { "method": "GET", "path": "/{id}", "operation_id": "a", "to": "bean:x" }
646 ]
647 },
648 {
649 "path": "/users",
650 "operations": [
651 { "method": "GET", "path": "/{id}", "operation_id": "b", "to": "bean:y" }
652 ]
653 }
654 ]
655 }"#;
656 let result = parse_json_to_declarative(json);
657 assert!(
658 result.is_err(),
659 "JSON rest must reject duplicate (method, path)"
660 );
661 }
662
663 #[test]
664 fn load_json_from_file_rejects_oversized_file() {
665 let dir = tempfile::tempdir().unwrap();
666 let path = dir.path().join("oversized.json");
667 std::fs::write(&path, "{}").unwrap();
668 let file = File::create(&path).unwrap();
670 file.set_len(16 * 1024 * 1024 + 1).unwrap();
671 drop(file);
672
673 let err = match load_json_from_file(&path) {
674 Ok(_) => panic!("expected oversized file error"),
675 Err(e) => e,
676 };
677 assert!(
678 err.to_string().contains("exceeds max"),
679 "expected size cap error, got: {err}"
680 );
681 }
682}