Skip to main content

fallow_output/
coverage_envelopes.rs

1//! Coverage command output envelopes.
2
3use crate::RuntimeCoverageReport;
4use crate::root_envelopes::{RootEnvelopeMode, attach_telemetry_meta, serialize_named_json_output};
5use fallow_types::envelope::{ElapsedMs, Meta, ToolVersion};
6use serde::Serialize;
7use std::time::Duration;
8
9/// `fallow coverage setup --json` envelope.
10#[derive(Debug, Clone, Serialize)]
11#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12#[cfg_attr(feature = "schema", schemars(title = "fallow coverage setup --json"))]
13pub struct CoverageSetupOutput {
14    pub schema_version: CoverageSetupSchemaVersion,
15    pub framework_detected: CoverageSetupFramework,
16    pub package_manager: Option<CoverageSetupPackageManager>,
17    pub runtime_targets: Vec<CoverageSetupRuntimeTarget>,
18    pub members: Vec<CoverageSetupMember>,
19    pub config_written: Option<serde_json::Value>,
20    pub commands: Vec<String>,
21    pub files_to_edit: Vec<CoverageSetupFileToEdit>,
22    pub snippets: Vec<CoverageSetupSnippet>,
23    pub dockerfile_snippet: Option<String>,
24    pub next_steps: Vec<String>,
25    pub warnings: Vec<String>,
26    #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
27    pub meta: Option<serde_json::Value>,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
31#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
32pub enum CoverageSetupSchemaVersion {
33    #[serde(rename = "1")]
34    V1,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
38#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
39#[serde(rename_all = "snake_case")]
40pub enum CoverageSetupFramework {
41    #[serde(rename = "nextjs")]
42    NextJs,
43    #[serde(rename = "nestjs")]
44    NestJs,
45    Nuxt,
46    #[serde(rename = "sveltekit")]
47    SvelteKit,
48    Astro,
49    Remix,
50    Vite,
51    PlainNode,
52    Unknown,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
56#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
57#[serde(rename_all = "lowercase")]
58pub enum CoverageSetupPackageManager {
59    Npm,
60    Pnpm,
61    Yarn,
62    Bun,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
66#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
67#[serde(rename_all = "lowercase")]
68pub enum CoverageSetupRuntimeTarget {
69    Node,
70    Browser,
71}
72
73#[derive(Debug, Clone, Serialize)]
74#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
75pub struct CoverageSetupMember {
76    pub name: String,
77    pub path: String,
78    pub framework_detected: CoverageSetupFramework,
79    pub package_manager: Option<CoverageSetupPackageManager>,
80    pub runtime_targets: Vec<CoverageSetupRuntimeTarget>,
81    pub files_to_edit: Vec<CoverageSetupFileToEdit>,
82    pub snippets: Vec<CoverageSetupSnippet>,
83    pub dockerfile_snippet: Option<String>,
84    pub warnings: Vec<String>,
85}
86
87#[derive(Debug, Clone, Serialize)]
88#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
89pub struct CoverageSetupFileToEdit {
90    pub path: String,
91    pub reason: String,
92}
93
94#[derive(Debug, Clone, Serialize)]
95#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
96pub struct CoverageSetupSnippet {
97    pub label: String,
98    pub path: String,
99    pub content: String,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
103#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
104pub enum CoverageAnalyzeSchemaVersion {
105    #[serde(rename = "1")]
106    V1,
107}
108
109#[derive(Debug, Clone, Serialize)]
110#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
111#[cfg_attr(
112    feature = "schema",
113    schemars(title = "fallow coverage analyze --format json")
114)]
115pub struct CoverageAnalyzeOutput {
116    pub schema_version: CoverageAnalyzeSchemaVersion,
117    pub version: ToolVersion,
118    pub elapsed_ms: ElapsedMs,
119    pub runtime_coverage: RuntimeCoverageReport,
120    #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
121    pub meta: Option<Meta>,
122}
123
124/// Serialize the `fallow coverage setup --json` envelope.
125///
126/// # Errors
127///
128/// Returns a serde error when the envelope cannot be converted to JSON.
129pub fn serialize_coverage_setup_json_output(
130    output: CoverageSetupOutput,
131    mode: RootEnvelopeMode,
132    analysis_run_id: Option<&str>,
133) -> Result<serde_json::Value, serde_json::Error> {
134    let mut value = serialize_named_json_output(output, "coverage-setup", mode)?;
135    attach_telemetry_meta(&mut value, analysis_run_id);
136    Ok(value)
137}
138
139/// Build the `fallow coverage analyze --format json` envelope.
140#[must_use]
141pub fn build_coverage_analyze_output(
142    report: &RuntimeCoverageReport,
143    elapsed: Duration,
144    version: impl Into<String>,
145) -> CoverageAnalyzeOutput {
146    CoverageAnalyzeOutput {
147        schema_version: CoverageAnalyzeSchemaVersion::V1,
148        version: ToolVersion(version.into()),
149        elapsed_ms: ElapsedMs(u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX)),
150        runtime_coverage: report.clone(),
151        meta: None,
152    }
153}
154
155/// Serialize the `fallow coverage analyze --format json` envelope.
156///
157/// `explain_meta` is inserted after typed-envelope serialization because the
158/// existing command metadata is a JSON object shared with docs/schema helpers.
159///
160/// # Errors
161///
162/// Returns a serde error when the envelope cannot be converted to JSON.
163pub fn serialize_coverage_analyze_json_output(
164    output: CoverageAnalyzeOutput,
165    mode: RootEnvelopeMode,
166    explain_meta: Option<serde_json::Value>,
167    analysis_run_id: Option<&str>,
168) -> Result<serde_json::Value, serde_json::Error> {
169    let mut value = serialize_named_json_output(output, "coverage-analyze", mode)?;
170    if let Some(meta) = explain_meta
171        && let Some(map) = value.as_object_mut()
172    {
173        map.insert("_meta".to_owned(), meta);
174    }
175    attach_telemetry_meta(&mut value, analysis_run_id);
176    Ok(value)
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use serde_json::json;
183
184    #[test]
185    fn coverage_setup_json_output_uses_named_root_contract() {
186        let output = CoverageSetupOutput {
187            schema_version: CoverageSetupSchemaVersion::V1,
188            framework_detected: CoverageSetupFramework::Unknown,
189            package_manager: None,
190            runtime_targets: Vec::new(),
191            members: Vec::new(),
192            config_written: None,
193            commands: Vec::new(),
194            files_to_edit: Vec::new(),
195            snippets: Vec::new(),
196            dockerfile_snippet: None,
197            next_steps: Vec::new(),
198            warnings: Vec::new(),
199            meta: None,
200        };
201
202        let value =
203            serialize_coverage_setup_json_output(output, RootEnvelopeMode::Tagged, Some("run-1"))
204                .expect("coverage setup should serialize");
205
206        assert_eq!(value["kind"], "coverage-setup");
207        assert_eq!(value["schema_version"], "1");
208        assert_eq!(value["_meta"]["telemetry"]["analysis_run_id"], "run-1");
209    }
210
211    #[test]
212    fn coverage_analyze_json_output_inserts_explain_meta_and_telemetry() {
213        let report = RuntimeCoverageReport::default();
214        let output = build_coverage_analyze_output(&report, Duration::from_millis(7), "test");
215
216        let value = serialize_coverage_analyze_json_output(
217            output,
218            RootEnvelopeMode::Tagged,
219            Some(json!({"docs": "coverage"})),
220            Some("run-2"),
221        )
222        .expect("coverage analyze should serialize");
223
224        assert_eq!(value["kind"], "coverage-analyze");
225        assert_eq!(value["schema_version"], "1");
226        assert_eq!(value["elapsed_ms"], 7);
227        assert_eq!(value["_meta"]["docs"], "coverage");
228        assert_eq!(value["_meta"]["telemetry"]["analysis_run_id"], "run-2");
229    }
230}