camel_cli/commands/
lint.rs1use std::path::Path;
11use std::sync::Arc;
12
13use camel_api::component_metadata::ComponentMetadataCatalog;
14use camel_lint::{Diagnostic, LintEngine, Severity};
15use clap::Args;
16
17#[derive(Args, Debug)]
19pub struct LintArgs {
20 pub file: String,
22}
23
24#[derive(Debug)]
26pub struct LintOutcome {
27 pub diagnostics: Vec<Diagnostic>,
29 pub source: String,
32 pub exit_code: i32,
35 pub cli_error: Option<String>,
37}
38
39pub async fn production_engine() -> Result<LintEngine, String> {
46 let mut ctx = camel_core::CamelContext::builder()
47 .build()
48 .await
49 .map_err(|e| format!("failed to build CamelContext: {e}"))?;
50 crate::register_builtin_components_for_lint(&mut ctx);
51 let catalog: Arc<dyn ComponentMetadataCatalog> = Arc::new(ctx.metadata_catalog());
52 Ok(LintEngine::new(catalog).with_default_rules())
53}
54
55pub async fn run_lint(path: &Path) -> LintOutcome {
61 let source = match std::fs::read_to_string(path) {
62 Ok(s) => s,
63 Err(e) => {
64 return LintOutcome {
65 diagnostics: Vec::new(),
66 source: String::new(),
67 exit_code: 2,
68 cli_error: Some(format!("failed to read '{}': {e}", path.display())),
69 };
70 }
71 };
72
73 let engine = match production_engine().await {
74 Ok(engine) => engine,
75 Err(e) => {
76 return LintOutcome {
77 diagnostics: Vec::new(),
78 source,
79 exit_code: 2,
80 cli_error: Some(e),
81 };
82 }
83 };
84 let diagnostics = engine.lint(&source);
85
86 let has_error = diagnostics.iter().any(|d| d.severity == Severity::Error);
87 let exit_code = if has_error { 1 } else { 0 };
88
89 LintOutcome {
90 diagnostics,
91 source,
92 exit_code,
93 cli_error: None,
94 }
95}
96
97pub async fn run(args: LintArgs) {
99 let outcome = run_lint(Path::new(&args.file)).await;
100 if let Some(err) = &outcome.cli_error {
101 eprintln!("error: {err}");
102 } else {
103 let file_id = args.file.as_str();
104 for diag in &outcome.diagnostics {
105 render_diagnostic(diag, file_id, &outcome.source);
106 }
107 }
108 std::process::exit(outcome.exit_code);
109}
110
111fn render_diagnostic(diag: &Diagnostic, file_id: &str, source: &str) {
113 use ariadne::{Label, Report, ReportKind, Source};
114
115 let kind = match diag.severity {
116 Severity::Error => ReportKind::Error,
117 Severity::Warning => ReportKind::Warning,
118 Severity::Info => ReportKind::Advice,
119 };
120 let range = clamp_range(diag.span.start, diag.span.end, source.len());
121
122 Report::build(kind, file_id, range.start)
123 .with_code(diag.code.to_string())
124 .with_message(diag.message.as_str())
125 .with_label(Label::new((file_id, range)))
126 .finish()
127 .eprint((file_id, Source::from(source)))
128 .ok();
129 }
133
134fn clamp_range(start: usize, end: usize, len: usize) -> std::ops::Range<usize> {
137 let s = start.min(len);
138 let e = end.min(len).max(s);
139 s..e
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use std::io::Write;
146
147 #[tokio::test]
148 async fn lint_clean_route_exits_zero() {
149 let yaml = "id: r1\nfrom: timer:foo?period=1s\nsteps:\n - to: log:bar\n";
150 let mut tmp = tempfile::NamedTempFile::new().unwrap(); write!(tmp, "{yaml}").unwrap(); let outcome = run_lint(tmp.path()).await;
154 assert_eq!(
155 outcome.exit_code, 0,
156 "clean route must exit 0; diags = {:?}",
157 outcome.diagnostics
158 );
159 assert!(
160 outcome.diagnostics.is_empty(),
161 "clean route must emit no diagnostics; got = {:?}",
162 outcome.diagnostics
163 );
164 }
165
166 #[tokio::test]
167 async fn lint_route_with_error_exits_one() {
168 let yaml = "id: r1\nfrom: direct:start\nsteps:\n - to: timer:foo?bogusOption=1\n";
169 let mut tmp = tempfile::NamedTempFile::new().unwrap(); write!(tmp, "{yaml}").unwrap(); let outcome = run_lint(tmp.path()).await;
173 assert_eq!(outcome.exit_code, 1, "route with an error must exit 1");
174 assert!(
175 outcome
176 .diagnostics
177 .iter()
178 .any(|d| d.severity == Severity::Error),
179 "expected at least one Error diagnostic; got = {:?}",
180 outcome.diagnostics
181 );
182 }
183
184 #[tokio::test]
185 async fn lint_missing_file_exits_two() {
186 let outcome = run_lint(std::path::Path::new("/nonexistent/route-lint-missing.yaml")).await;
187 assert_eq!(outcome.exit_code, 2);
188 assert!(outcome.cli_error.is_some());
189 assert!(outcome.diagnostics.is_empty());
190 }
191
192 #[test]
193 fn register_for_lint_does_not_capture_handles() {
194 let _: fn(&mut camel_core::CamelContext) = crate::register_builtin_components_for_lint;
198 }
199}