1use std::collections::BTreeMap;
2
3use chrono::{DateTime, Utc};
4use serde::Serialize;
5
6use crate::command::CommandRecord;
7use crate::envfile::EnvFileAnalysis;
8
9#[derive(Clone, Debug, Serialize)]
10pub struct Receipt {
11 pub schema_version: u8,
12 pub tool: ToolReceipt,
13 pub checked_at: DateTime<Utc>,
14 pub mode: String,
15 pub summary: Summary,
16 pub checks: Vec<Check>,
17 #[serde(skip_serializing_if = "Vec::is_empty")]
18 pub commands: Vec<CommandRecord>,
19 #[serde(skip_serializing_if = "Vec::is_empty")]
20 pub env_files: Vec<EnvFileAnalysis>,
21 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
22 pub metadata: BTreeMap<String, String>,
23}
24
25#[derive(Clone, Debug, Serialize)]
26pub struct ToolReceipt {
27 pub name: &'static str,
28 pub version: &'static str,
29}
30
31#[derive(Clone, Debug, Default, Serialize)]
32pub struct Summary {
33 pub passed: usize,
34 pub warned: usize,
35 pub failed: usize,
36 pub skipped: usize,
37}
38
39impl Summary {
40 pub fn from_checks(checks: &[Check]) -> Self {
41 let mut summary = Self::default();
42 for check in checks {
43 match check.status {
44 CheckStatus::Pass => summary.passed += 1,
45 CheckStatus::Warn => summary.warned += 1,
46 CheckStatus::Fail => summary.failed += 1,
47 CheckStatus::Skip => summary.skipped += 1,
48 }
49 }
50 summary
51 }
52}
53
54#[derive(Clone, Debug, Serialize)]
55pub struct Check {
56 pub id: String,
57 pub status: CheckStatus,
58 pub message: String,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub location: Option<Location>,
61 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
62 pub details: BTreeMap<String, String>,
63}
64
65impl Check {
66 pub fn pass(
67 id: impl Into<String>,
68 message: impl Into<String>,
69 location: Option<Location>,
70 ) -> Self {
71 Self::new(id, CheckStatus::Pass, message, location)
72 }
73
74 pub fn warn(
75 id: impl Into<String>,
76 message: impl Into<String>,
77 location: Option<Location>,
78 ) -> Self {
79 Self::new(id, CheckStatus::Warn, message, location)
80 }
81
82 pub fn fail(
83 id: impl Into<String>,
84 message: impl Into<String>,
85 location: Option<Location>,
86 ) -> Self {
87 Self::new(id, CheckStatus::Fail, message, location)
88 }
89
90 pub fn skip(
91 id: impl Into<String>,
92 message: impl Into<String>,
93 location: Option<Location>,
94 ) -> Self {
95 Self::new(id, CheckStatus::Skip, message, location)
96 }
97
98 pub fn with_detail(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
99 self.details.insert(key.into(), value.into());
100 self
101 }
102
103 fn new(
104 id: impl Into<String>,
105 status: CheckStatus,
106 message: impl Into<String>,
107 location: Option<Location>,
108 ) -> Self {
109 Self {
110 id: id.into(),
111 status,
112 message: message.into(),
113 location,
114 details: BTreeMap::new(),
115 }
116 }
117}
118
119#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
120#[serde(rename_all = "lowercase")]
121pub enum CheckStatus {
122 Pass,
123 Warn,
124 Fail,
125 Skip,
126}
127
128#[derive(Clone, Debug, Serialize)]
129pub struct Location {
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub source: Option<String>,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub line: Option<usize>,
134}
135
136impl Location {
137 pub fn new(source: Option<String>, line: Option<usize>) -> Self {
138 Self { source, line }
139 }
140
141 pub fn line(source: Option<&str>, line: usize) -> Self {
142 Self {
143 source: source.map(ToString::to_string),
144 line: Some(line),
145 }
146 }
147}