Skip to main content

wenlan_types/
lint_report.rs

1// SPDX-License-Identifier: Apache-2.0
2use super::*;
3use serde::{de::Error as _, Deserialize, Deserializer, Serialize};
4use std::collections::BTreeSet;
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7pub struct LintTotals {
8    checks: u32,
9    passed: u32,
10    findings: u32,
11    actionable_findings: u32,
12    advisory_findings: u32,
13    incomplete: u32,
14}
15impl LintTotals {
16    fn from_checks(checks: &[LintCheckResult]) -> Result<Self, LintContractError> {
17        let checks_count =
18            u32::try_from(checks.len()).map_err(|_| LintContractError::TooManyChecks)?;
19        let mut totals = Self {
20            checks: checks_count,
21            passed: 0,
22            findings: 0,
23            actionable_findings: 0,
24            advisory_findings: 0,
25            incomplete: 0,
26        };
27        for check in checks {
28            match check.outcome {
29                LintOutcome::Pass => totals.passed += 1,
30                LintOutcome::Finding => {
31                    totals.findings += 1;
32                    match check.gate_effect() {
33                        LintGateEffect::Actionable => totals.actionable_findings += 1,
34                        LintGateEffect::Advisory => totals.advisory_findings += 1,
35                    }
36                }
37                LintOutcome::NotRunPrerequisite
38                | LintOutcome::InconsistentSnapshot
39                | LintOutcome::FailedToRun => totals.incomplete += 1,
40            }
41        }
42        Ok(totals)
43    }
44    pub const fn checks(&self) -> u32 {
45        self.checks
46    }
47    pub const fn findings(&self) -> u32 {
48        self.findings
49    }
50    pub const fn actionable_findings(&self) -> u32 {
51        self.actionable_findings
52    }
53    pub const fn advisory_findings(&self) -> u32 {
54        self.advisory_findings
55    }
56    pub const fn passed(&self) -> u32 {
57        self.passed
58    }
59    pub const fn incomplete(&self) -> u32 {
60        self.incomplete
61    }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
65pub struct LintReport {
66    report_schema_version: u16,
67    check_catalog_version: u16,
68    profile: LintProfile,
69    scope: LintScope,
70    capability_context: LintCapabilityContext,
71    snapshots: LintSnapshotReceipts,
72    config_fingerprint: LintConfigFingerprint,
73    producer_receipt: LintProducerReceipt,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    agent_work: Option<LintAgentWork>,
76    checks: Vec<LintCheckResult>,
77    totals: LintTotals,
78    complete: bool,
79}
80#[derive(Deserialize)]
81struct LintReportWire {
82    report_schema_version: u16,
83    check_catalog_version: u16,
84    profile: LintProfile,
85    scope: LintScope,
86    capability_context: LintCapabilityContext,
87    snapshots: LintSnapshotReceipts,
88    config_fingerprint: LintConfigFingerprint,
89    producer_receipt: LintProducerReceipt,
90    #[serde(default)]
91    agent_work: Option<LintAgentWork>,
92    checks: Vec<LintCheckResult>,
93    totals: LintTotals,
94    complete: bool,
95}
96impl LintReport {
97    pub fn try_new(
98        scope: LintScope,
99        capability_context: LintCapabilityContext,
100        snapshots: LintSnapshotReceipts,
101        config_fingerprint: LintConfigFingerprint,
102        producer_receipt: LintProducerReceipt,
103        checks: Vec<LintCheckResult>,
104    ) -> Result<Self, LintContractError> {
105        Self::try_new_for_profile(
106            LintProfile::General,
107            scope,
108            capability_context,
109            snapshots,
110            config_fingerprint,
111            producer_receipt,
112            checks,
113        )
114    }
115
116    pub fn try_new_for_profile(
117        profile: LintProfile,
118        scope: LintScope,
119        capability_context: LintCapabilityContext,
120        snapshots: LintSnapshotReceipts,
121        config_fingerprint: LintConfigFingerprint,
122        producer_receipt: LintProducerReceipt,
123        checks: Vec<LintCheckResult>,
124    ) -> Result<Self, LintContractError> {
125        Self::try_new_for_profile_with_agent_work(
126            profile,
127            scope,
128            capability_context,
129            snapshots,
130            config_fingerprint,
131            producer_receipt,
132            checks,
133            None,
134        )
135    }
136
137    #[allow(clippy::too_many_arguments)]
138    pub fn try_new_for_profile_with_agent_work(
139        profile: LintProfile,
140        scope: LintScope,
141        capability_context: LintCapabilityContext,
142        snapshots: LintSnapshotReceipts,
143        config_fingerprint: LintConfigFingerprint,
144        producer_receipt: LintProducerReceipt,
145        mut checks: Vec<LintCheckResult>,
146        agent_work: Option<LintAgentWork>,
147    ) -> Result<Self, LintContractError> {
148        if profile == LintProfile::General && agent_work.is_some() {
149            return Err(LintContractError::InvalidAgentWork);
150        }
151        checks.sort_by(|left, right| left.check_id().cmp(right.check_id()));
152        let totals = LintTotals::from_checks(&checks)?;
153        let complete = checks.iter().all(|check| check.outcome.is_complete());
154        Ok(Self {
155            report_schema_version: LINT_REPORT_SCHEMA_VERSION,
156            check_catalog_version: LINT_CHECK_CATALOG_VERSION,
157            profile,
158            scope,
159            capability_context,
160            snapshots,
161            config_fingerprint,
162            producer_receipt,
163            agent_work,
164            checks,
165            totals,
166            complete,
167        })
168    }
169    pub const fn complete(&self) -> bool {
170        self.complete
171    }
172    pub const fn profile(&self) -> LintProfile {
173        self.profile
174    }
175    pub const fn totals(&self) -> &LintTotals {
176        &self.totals
177    }
178    pub fn checks(&self) -> &[LintCheckResult] {
179        &self.checks
180    }
181    pub fn scope(&self) -> &LintScope {
182        &self.scope
183    }
184    pub const fn capability_context(&self) -> LintCapabilityContext {
185        self.capability_context
186    }
187    pub fn snapshots(&self) -> &LintSnapshotReceipts {
188        &self.snapshots
189    }
190    pub fn config_fingerprint(&self) -> &LintConfigFingerprint {
191        &self.config_fingerprint
192    }
193    pub fn producer_receipt(&self) -> &LintProducerReceipt {
194        &self.producer_receipt
195    }
196    pub fn agent_work(&self) -> Option<&LintAgentWork> {
197        self.agent_work.as_ref()
198    }
199}
200impl<'de> Deserialize<'de> for LintReport {
201    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
202    where
203        D: Deserializer<'de>,
204    {
205        let wire = LintReportWire::deserialize(deserializer)?;
206        if wire.report_schema_version != LINT_REPORT_SCHEMA_VERSION {
207            return Err(D::Error::custom(LintContractError::UnsupportedReportSchema));
208        }
209        if wire.check_catalog_version != LINT_CHECK_CATALOG_VERSION {
210            return Err(D::Error::custom(LintContractError::UnsupportedCheckCatalog));
211        }
212        let expected_checks = match wire.profile {
213            LintProfile::General => LINT_GENERAL_CHECK_COUNT,
214            LintProfile::Deep => LINT_DEEP_CHECK_COUNT,
215        };
216        let unique_ids = wire
217            .checks
218            .iter()
219            .map(LintCheckResult::check_id)
220            .collect::<BTreeSet<_>>();
221        if wire.checks.len() != expected_checks || unique_ids.len() != wire.checks.len() {
222            return Err(D::Error::custom(LintContractError::InvalidCatalogShape));
223        }
224        if wire.checks.iter().any(|check| {
225            canonical_gate_effect(wire.profile, check.check_id()) != Some(check.gate_effect())
226        }) {
227            return Err(D::Error::custom(LintContractError::InvalidCatalogShape));
228        }
229        let report = Self::try_new_for_profile_with_agent_work(
230            wire.profile,
231            wire.scope,
232            wire.capability_context,
233            wire.snapshots,
234            wire.config_fingerprint,
235            wire.producer_receipt,
236            wire.checks,
237            wire.agent_work,
238        )
239        .map_err(D::Error::custom)?;
240        if report.totals != wire.totals {
241            return Err(D::Error::custom(LintContractError::InvalidTotals));
242        }
243        if report.complete != wire.complete {
244            return Err(D::Error::custom(LintContractError::InvalidCompleteness));
245        }
246        Ok(report)
247    }
248}
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
250pub struct LintQuery {
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub profile: Option<LintProfile>,
253    #[serde(default, skip_serializing_if = "Option::is_none")]
254    pub space: Option<String>,
255}
256
257impl LintQuery {
258    pub const fn new(profile: Option<LintProfile>, space: Option<String>) -> Self {
259        Self { profile, space }
260    }
261
262    pub fn applied_profile(&self) -> LintProfile {
263        self.profile.unwrap_or_default()
264    }
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268pub struct LintRequestQuery {
269    #[serde(flatten)]
270    lint: LintQuery,
271    #[serde(default, skip_serializing_if = "is_false")]
272    external_egress: bool,
273    #[serde(default, skip_serializing_if = "is_false")]
274    agent_assist: bool,
275}
276
277impl LintRequestQuery {
278    pub const fn new(lint: LintQuery, external_egress: bool, agent_assist: bool) -> Self {
279        Self {
280            lint,
281            external_egress,
282            agent_assist,
283        }
284    }
285
286    pub const fn lint(&self) -> &LintQuery {
287        &self.lint
288    }
289
290    pub const fn external_egress(&self) -> bool {
291        self.external_egress
292    }
293
294    pub const fn agent_assist(&self) -> bool {
295        self.agent_assist
296    }
297}
298
299const fn is_false(value: &bool) -> bool {
300    !*value
301}
302
303#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
304pub struct LintErrorResponse {
305    error: String,
306}
307
308impl LintErrorResponse {
309    pub fn new(error: impl Into<String>) -> Self {
310        Self {
311            error: error.into(),
312        }
313    }
314
315    pub fn error(&self) -> &str {
316        &self.error
317    }
318}