greentic_flow/
json_output.rs1use crate::{
2 error::{FlowError, FlowErrorLocation},
3 flow_bundle::{FlowBundle, load_and_validate_bundle_with_flow},
4 lint::lint_builtin_rules,
5};
6use serde::Serialize;
7
8#[derive(Serialize, Clone, Debug)]
9pub struct JsonDiagnostic {
10 pub message: String,
11 #[serde(skip_serializing_if = "Option::is_none")]
12 pub source_path: Option<String>,
13 #[serde(skip_serializing_if = "Option::is_none")]
14 pub line: Option<usize>,
15 #[serde(skip_serializing_if = "Option::is_none")]
16 pub col: Option<usize>,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 pub json_pointer: Option<String>,
19}
20
21impl JsonDiagnostic {
22 pub fn from_location(message: String, location: FlowErrorLocation) -> Self {
23 let FlowErrorLocation {
24 path,
25 source_path,
26 line,
27 col,
28 json_pointer,
29 } = location;
30 JsonDiagnostic {
31 message,
32 source_path: source_path
33 .as_ref()
34 .map(|p| p.display().to_string())
35 .or(path),
36 line,
37 col,
38 json_pointer,
39 }
40 }
41
42 pub fn from_message(message: String, source_path: Option<String>) -> Self {
43 JsonDiagnostic {
44 message,
45 source_path,
46 line: None,
47 col: None,
48 json_pointer: None,
49 }
50 }
51}
52
53#[derive(Serialize, Clone, Debug)]
54pub struct LintJsonOutput {
55 pub ok: bool,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub bundle: Option<FlowBundle>,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub hash_blake3: Option<String>,
60 #[serde(skip_serializing_if = "Vec::is_empty")]
61 pub errors: Vec<JsonDiagnostic>,
62}
63
64impl LintJsonOutput {
65 pub fn success(bundle: FlowBundle) -> Self {
66 let hash = bundle.hash_blake3.clone();
67 LintJsonOutput {
68 ok: true,
69 hash_blake3: Some(hash),
70 bundle: Some(bundle),
71 errors: Vec::new(),
72 }
73 }
74
75 pub fn lint_failure(messages: Vec<String>, source_path: Option<String>) -> Self {
76 let errors = messages
77 .into_iter()
78 .map(|message| JsonDiagnostic::from_message(message, source_path.clone()))
79 .collect();
80 LintJsonOutput {
81 ok: false,
82 bundle: None,
83 hash_blake3: None,
84 errors,
85 }
86 }
87
88 pub fn error(err: FlowError) -> Self {
89 LintJsonOutput {
90 ok: false,
91 bundle: None,
92 hash_blake3: None,
93 errors: flow_error_to_reports(err),
94 }
95 }
96
97 pub fn into_string(self) -> String {
98 serde_json::to_string(&self).expect("lint output serialization")
99 }
100}
101
102pub fn flow_error_to_reports(err: FlowError) -> Vec<JsonDiagnostic> {
103 let display_message = err.to_string();
104 match err {
105 FlowError::Schema {
106 details, location, ..
107 } => {
108 if details.is_empty() {
109 vec![JsonDiagnostic::from_location(display_message, location)]
110 } else {
111 details
112 .into_iter()
113 .map(|detail| JsonDiagnostic::from_location(detail.message, detail.location))
114 .collect()
115 }
116 }
117 FlowError::Yaml { location, .. }
118 | FlowError::UnknownFlowType { location, .. }
119 | FlowError::InvalidIdentifier { location, .. }
120 | FlowError::NodeComponentShape { location, .. }
121 | FlowError::BadComponentKey { location, .. }
122 | FlowError::Routing { location, .. }
123 | FlowError::MissingNode { location, .. }
124 | FlowError::Internal { location, .. } => {
125 vec![JsonDiagnostic::from_location(display_message, location)]
126 }
127 }
128}
129
130pub fn lint_to_stdout_json(ygtc: &str) -> String {
132 match load_and_validate_bundle_with_flow(ygtc, None) {
133 Ok((bundle, flow)) => {
134 let lint_errors = lint_builtin_rules(&flow);
135 if lint_errors.is_empty() {
136 LintJsonOutput::success(bundle).into_string()
137 } else {
138 LintJsonOutput::lint_failure(lint_errors, None).into_string()
139 }
140 }
141 Err(err) => LintJsonOutput::error(err).into_string(),
142 }
143}