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
13pub const HEALTH_SCHEMA_VERSION: u32 = 11;
18
19#[cfg(feature = "schema")]
21#[allow(dead_code, reason = "schema-only type used by the field projection")]
22#[derive(schemars::JsonSchema)]
23#[schemars(extend("const" = HEALTH_SCHEMA_VERSION))]
24struct HealthSchemaVersion(u32);
25
26#[derive(Debug, Clone, Serialize)]
38#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
39#[cfg_attr(feature = "schema", schemars(title = "fallow health --format json"))]
40pub struct HealthOutput<Report, Group> {
41 #[cfg_attr(feature = "schema", schemars(with = "HealthSchemaVersion"))]
43 pub schema_version: SchemaVersion,
44 pub version: ToolVersion,
46 pub elapsed_ms: ElapsedMs,
48 #[serde(flatten)]
50 pub report: Report,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub grouped_by: Option<GroupByMode>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub groups: Option<Vec<Group>>,
57 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
59 pub meta: Option<Meta>,
60 #[serde(default, skip_serializing_if = "Vec::is_empty")]
65 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
66 #[serde(default, skip_serializing_if = "Vec::is_empty")]
69 pub next_steps: Vec<NextStep>,
70}
71
72#[derive(Debug, Clone)]
75pub struct HealthOutputInput<Report, Group> {
76 pub schema_version: u32,
78 pub version: String,
80 pub elapsed: Duration,
82 pub report: Report,
84 pub grouped_by: Option<GroupByMode>,
86 pub groups: Option<Vec<Group>>,
88 pub meta: Option<Meta>,
90 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
93 pub next_steps: Vec<NextStep>,
95}
96
97#[derive(Debug, Clone)]
99pub struct HealthJsonOutputInput<'a, Report, Group> {
100 pub output: HealthOutputInput<Report, Group>,
102 pub root_prefix: Option<&'a str>,
104 pub envelope_mode: RootEnvelopeMode,
106 pub analysis_run_id: Option<&'a str>,
108}
109
110#[must_use]
112pub fn build_health_output<Report, Group>(
113 input: HealthOutputInput<Report, Group>,
114) -> HealthOutput<Report, Group> {
115 HealthOutput {
116 schema_version: SchemaVersion(input.schema_version),
117 version: ToolVersion(input.version),
118 elapsed_ms: ElapsedMs(input.elapsed.as_millis() as u64),
119 report: input.report,
120 grouped_by: input.grouped_by,
121 groups: input.groups,
122 meta: input.meta,
123 workspace_diagnostics: input.workspace_diagnostics,
124 next_steps: input.next_steps,
125 }
126}
127
128pub fn serialize_health_json_output<Report, Group>(
139 input: HealthJsonOutputInput<'_, Report, Group>,
140) -> Result<serde_json::Value, serde_json::Error>
141where
142 Report: Serialize,
143 Group: Serialize,
144{
145 let envelope = build_health_output(input.output);
146 let mut output = serde_json::to_value(envelope)?;
147 apply_root_kind(&mut output, "health", input.envelope_mode);
148 if let Some(root_prefix) = input.root_prefix {
149 strip_root_prefix(&mut output, root_prefix);
150 }
151 attach_telemetry_meta(&mut output, input.analysis_run_id);
152 Ok(output)
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn serialize_health_json_output_tags_and_strips_root_paths() {
161 let output = serialize_health_json_output(HealthJsonOutputInput {
162 output: HealthOutputInput {
163 schema_version: 7,
164 version: "test".to_string(),
165 elapsed: Duration::ZERO,
166 report: serde_json::json!({ "findings": [{ "path": "/repo/src/a.ts" }] }),
167 grouped_by: None,
168 groups: None::<Vec<serde_json::Value>>,
169 meta: None,
170 workspace_diagnostics: Vec::new(),
171 next_steps: Vec::new(),
172 },
173 root_prefix: Some("/repo/"),
174 envelope_mode: RootEnvelopeMode::Tagged,
175 analysis_run_id: Some("run-health"),
176 })
177 .expect("health output should serialize");
178
179 assert_eq!(output["kind"], "health");
180 assert_eq!(output["findings"][0]["path"], "src/a.ts");
181 assert_eq!(
182 output["_meta"]["telemetry"]["analysis_run_id"],
183 "run-health"
184 );
185 }
186}