greentic_flow/
json_output.rs

1use 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
130/// Produce the same JSON emitted by `greentic-flow doctor --json` for builtin linting.
131pub 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}