1use 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
17pub 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}