1use std::time::Duration;
2
3use fallow_types::envelope::{ElapsedMs, Meta, SchemaVersion, ToolVersion};
4use fallow_types::output::NextStep;
5use serde::Serialize;
6
7use fallow_types::workspace::WorkspaceDiagnostic;
8
9use crate::{
10 GroupByMode, RootEnvelopeMode, apply_root_kind, attach_telemetry_meta, strip_root_prefix,
11};
12
13#[derive(Debug, Clone, Serialize)]
25#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
26#[cfg_attr(feature = "schema", schemars(title = "fallow health --format json"))]
27pub struct HealthOutput<Report, Group> {
28 pub schema_version: SchemaVersion,
29 pub version: ToolVersion,
30 pub elapsed_ms: ElapsedMs,
31 #[serde(flatten)]
32 pub report: Report,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub grouped_by: Option<GroupByMode>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub groups: Option<Vec<Group>>,
37 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
38 pub meta: Option<Meta>,
39 #[serde(default, skip_serializing_if = "Vec::is_empty")]
40 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
41 #[serde(default, skip_serializing_if = "Vec::is_empty")]
44 pub next_steps: Vec<NextStep>,
45}
46
47#[derive(Debug, Clone)]
50pub struct HealthOutputInput<Report, Group> {
51 pub schema_version: u32,
52 pub version: String,
53 pub elapsed: Duration,
54 pub report: Report,
55 pub grouped_by: Option<GroupByMode>,
56 pub groups: Option<Vec<Group>>,
57 pub meta: Option<Meta>,
58 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
59 pub next_steps: Vec<NextStep>,
60}
61
62#[derive(Debug, Clone)]
64pub struct HealthJsonOutputInput<'a, Report, Group> {
65 pub output: HealthOutputInput<Report, Group>,
66 pub root_prefix: Option<&'a str>,
67 pub envelope_mode: RootEnvelopeMode,
68 pub analysis_run_id: Option<&'a str>,
69}
70
71#[must_use]
73pub fn build_health_output<Report, Group>(
74 input: HealthOutputInput<Report, Group>,
75) -> HealthOutput<Report, Group> {
76 HealthOutput {
77 schema_version: SchemaVersion(input.schema_version),
78 version: ToolVersion(input.version),
79 elapsed_ms: ElapsedMs(input.elapsed.as_millis() as u64),
80 report: input.report,
81 grouped_by: input.grouped_by,
82 groups: input.groups,
83 meta: input.meta,
84 workspace_diagnostics: input.workspace_diagnostics,
85 next_steps: input.next_steps,
86 }
87}
88
89pub fn serialize_health_json_output<Report, Group>(
100 input: HealthJsonOutputInput<'_, Report, Group>,
101) -> Result<serde_json::Value, serde_json::Error>
102where
103 Report: Serialize,
104 Group: Serialize,
105{
106 let envelope = build_health_output(input.output);
107 let mut output = serde_json::to_value(envelope)?;
108 apply_root_kind(&mut output, "health", input.envelope_mode);
109 if let Some(root_prefix) = input.root_prefix {
110 strip_root_prefix(&mut output, root_prefix);
111 }
112 attach_telemetry_meta(&mut output, input.analysis_run_id);
113 Ok(output)
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn serialize_health_json_output_tags_and_strips_root_paths() {
122 let output = serialize_health_json_output(HealthJsonOutputInput {
123 output: HealthOutputInput {
124 schema_version: 7,
125 version: "test".to_string(),
126 elapsed: Duration::ZERO,
127 report: serde_json::json!({ "findings": [{ "path": "/repo/src/a.ts" }] }),
128 grouped_by: None,
129 groups: None::<Vec<serde_json::Value>>,
130 meta: None,
131 workspace_diagnostics: Vec::new(),
132 next_steps: Vec::new(),
133 },
134 root_prefix: Some("/repo/"),
135 envelope_mode: RootEnvelopeMode::Tagged,
136 analysis_run_id: Some("run-health"),
137 })
138 .expect("health output should serialize");
139
140 assert_eq!(output["kind"], "health");
141 assert_eq!(output["findings"][0]["path"], "src/a.ts");
142 assert_eq!(
143 output["_meta"]["telemetry"]["analysis_run_id"],
144 "run-health"
145 );
146 }
147
148 #[test]
149 fn serialize_health_json_output_can_keep_legacy_root_shape() {
150 let output = serialize_health_json_output(HealthJsonOutputInput {
151 output: HealthOutputInput {
152 schema_version: 7,
153 version: "test".to_string(),
154 elapsed: Duration::ZERO,
155 report: serde_json::json!({ "summary": {} }),
156 grouped_by: None,
157 groups: None::<Vec<serde_json::Value>>,
158 meta: None,
159 workspace_diagnostics: Vec::new(),
160 next_steps: Vec::new(),
161 },
162 root_prefix: None,
163 envelope_mode: RootEnvelopeMode::Legacy,
164 analysis_run_id: None,
165 })
166 .expect("health output should serialize");
167
168 assert!(output.get("kind").is_none());
169 assert_eq!(output["schema_version"], 7);
170 }
171}