Skip to main content

backhopper_core/jigsaw/
check_wire.rs

1// Copyright (C) 2026 Michael S. Klishin and Contributors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// See LICENSE-APACHE and LICENSE-MIT for details.
4
5use std::collections::BTreeMap;
6
7use serde::{Deserialize, Serialize};
8
9use crate::jigsaw::findings::{JigsawFindingKind, JigsawWarningKind, WithheldReason};
10use crate::jigsaw::plan::TagSourceKind;
11use crate::jigsaw::plugin_set::AppRole;
12use crate::jigsaw::references::{NotChecked, ReferenceSite};
13use crate::jigsaw::report::{FindingSpan, JigsawCell, JigsawReport, TargetAxes};
14use crate::model::names::{ApplicationName, RelativePath};
15use crate::model::non_empty::NonEmpty;
16
17/// What `jigsaw check` does not compare, in every report.
18pub const LIMITS: [&str; 3] = [
19    "OTP header records and macros are not compared",
20    "calls inside OTP header macros are not checked",
21    "dependencies are checked at the exact versions each host release pins, not over a range",
22];
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
26pub struct JigsawCheckPayload {
27    pub host: String,
28    pub series: String,
29    pub tag_source: TagSourceKind,
30    pub build_release: String,
31    pub host_releases: Vec<String>,
32    pub otp_tags: Vec<String>,
33    pub apps: Vec<JigsawAppRowPayload>,
34    pub findings: Vec<JigsawFindingPayload>,
35    pub warnings: Vec<JigsawWarningPayload>,
36    pub host_provided: Vec<ApplicationName>,
37    pub tallies: JigsawTalliesPayload,
38    pub unchecked: Vec<JigsawUncheckedPayload>,
39    pub withheld: Vec<JigsawWithheldPayload>,
40    pub limits: Vec<String>,
41    pub store_digest: String,
42    pub clean: bool,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
47pub struct JigsawAppRowPayload {
48    pub app: ApplicationName,
49    pub role: AppRole,
50    pub cells: Vec<JigsawCell>,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
55pub struct JigsawFindingPayload {
56    pub kind: JigsawFindingKind,
57    pub symbol: String,
58    pub detail: Option<String>,
59    pub apps: Vec<ApplicationName>,
60    pub sites: Vec<JigsawSitePayload>,
61    pub host_release_ranges: Vec<JigsawRangePayload>,
62    pub otp_tag_ranges: Vec<JigsawRangePayload>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
67pub struct JigsawWarningPayload {
68    pub kind: JigsawWarningKind,
69    pub symbol: String,
70    pub detail: Option<String>,
71    pub host_release_ranges: Vec<JigsawRangePayload>,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
76pub struct JigsawSitePayload {
77    pub app: ApplicationName,
78    pub path: RelativePath,
79    pub line: u32,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
84pub struct JigsawUncheckedPayload {
85    pub reason: NotChecked,
86    pub symbol: String,
87    pub app: ApplicationName,
88    pub path: RelativePath,
89    pub line: u32,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
94pub struct JigsawWithheldPayload {
95    pub reason: WithheldReason,
96    pub symbol: String,
97    pub sites: Vec<JigsawSitePayload>,
98    pub host_release_ranges: Vec<JigsawRangePayload>,
99    pub otp_tag_ranges: Vec<JigsawRangePayload>,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
104pub struct JigsawRangePayload {
105    pub first: String,
106    pub last: String,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
111pub struct JigsawTalliesPayload {
112    pub checked: u64,
113    pub not_checked: BTreeMap<NotChecked, u64>,
114    pub withheld: BTreeMap<WithheldReason, u64>,
115}
116
117fn ranges(
118    report: &JigsawReport,
119    span: &FindingSpan,
120) -> (Vec<JigsawRangePayload>, Vec<JigsawRangePayload>) {
121    let targets = report.targets();
122    match span {
123        FindingSpan::Host(members) => (
124            TargetAxes::runs(members)
125                .into_iter()
126                .map(|run| JigsawRangePayload {
127                    first: targets.host_tag(run.first).tag().to_string(),
128                    last: targets.host_tag(run.last).tag().to_string(),
129                })
130                .collect(),
131            Vec::new(),
132        ),
133        FindingSpan::Otp(members) => (
134            Vec::new(),
135            TargetAxes::runs(members)
136                .into_iter()
137                .map(|run| JigsawRangePayload {
138                    first: targets.otp_tag(run.first).tag().to_string(),
139                    last: targets.otp_tag(run.last).tag().to_string(),
140                })
141                .collect(),
142        ),
143        FindingSpan::Everywhere => (Vec::new(), Vec::new()),
144    }
145}
146
147fn sites(sites: &NonEmpty<ReferenceSite>) -> Vec<JigsawSitePayload> {
148    sites
149        .iter()
150        .map(|site| JigsawSitePayload {
151            app: site.file.app.clone(),
152            path: site.file.path.clone(),
153            line: site.line,
154        })
155        .collect()
156}
157
158fn counts<K: Ord + Copy>(tally: &BTreeMap<K, usize>) -> BTreeMap<K, u64> {
159    tally.iter().map(|(&key, &n)| (key, n as u64)).collect()
160}
161
162impl From<&JigsawReport> for JigsawCheckPayload {
163    fn from(report: &JigsawReport) -> Self {
164        let targets = report.targets();
165        let tallies = report.tallies();
166        Self {
167            host: report.host().to_string(),
168            series: report.series().to_string(),
169            tag_source: report.tag_source(),
170            build_release: report.build_release().tag().to_string(),
171            host_releases: targets
172                .hosts()
173                .iter()
174                .map(|tag| tag.tag().to_string())
175                .collect(),
176            otp_tags: targets
177                .otp_tags()
178                .iter()
179                .map(|tag| tag.tag().to_string())
180                .collect(),
181            apps: report
182                .apps()
183                .iter()
184                .map(|(app, &role)| JigsawAppRowPayload {
185                    app: app.clone(),
186                    role,
187                    cells: targets
188                        .host_indices()
189                        .flat_map(|host| {
190                            targets
191                                .otp_indices()
192                                .map(move |otp| report.cell(app, host, otp))
193                        })
194                        .collect(),
195                })
196                .collect(),
197            findings: report
198                .findings()
199                .iter()
200                .map(|ranged| {
201                    let (host_release_ranges, otp_tag_ranges) = ranges(report, &ranged.span);
202                    JigsawFindingPayload {
203                        kind: ranged.finding.kind(),
204                        symbol: ranged.finding.symbol(),
205                        detail: ranged.finding.detail(),
206                        apps: ranged.apps.iter().cloned().collect(),
207                        sites: sites(&ranged.sites),
208                        host_release_ranges,
209                        otp_tag_ranges,
210                    }
211                })
212                .collect(),
213            warnings: report
214                .warnings()
215                .iter()
216                .map(|ranged| JigsawWarningPayload {
217                    kind: ranged.warning.kind(),
218                    symbol: ranged.warning.symbol(),
219                    detail: ranged.warning.detail(),
220                    host_release_ranges: ranges(report, &ranged.span).0,
221                })
222                .collect(),
223            host_provided: report.host_provided().iter().cloned().collect(),
224            tallies: JigsawTalliesPayload {
225                checked: tallies.checked() as u64,
226                not_checked: counts(tallies.not_checked()),
227                withheld: counts(tallies.withheld()),
228            },
229            unchecked: report
230                .unchecked()
231                .iter()
232                .map(|site| JigsawUncheckedPayload {
233                    reason: site.reason(),
234                    symbol: site.symbol.to_string(),
235                    app: site.file.app.clone(),
236                    path: site.file.path.clone(),
237                    line: site.line,
238                })
239                .collect(),
240            withheld: report
241                .withheld()
242                .iter()
243                .map(|ranged| {
244                    let (host_release_ranges, otp_tag_ranges) = ranges(report, &ranged.span);
245                    JigsawWithheldPayload {
246                        reason: ranged.symbol.reason(),
247                        symbol: ranged.symbol.to_string(),
248                        sites: sites(&ranged.sites),
249                        host_release_ranges,
250                        otp_tag_ranges,
251                    }
252                })
253                .collect(),
254            limits: LIMITS.iter().map(|&limit| limit.to_owned()).collect(),
255            store_digest: report.store_digest().to_string(),
256            clean: report.is_clean(),
257        }
258    }
259}