Skip to main content

gha_command_proof/
receipt.rs

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}