use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ReviewScope {
Full,
Diff,
BuildScriptOnly,
ProcMacroOnly,
MetadataOnly,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum SandboxKind {
Build,
ProcMacro,
}
impl SandboxKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Build => "build",
Self::ProcMacro => "proc-macro",
}
}
#[must_use]
pub fn parse_kind(s: &str) -> Option<Self> {
match s {
"build" => Some(Self::Build),
"proc-macro" | "proc_macro" => Some(Self::ProcMacro),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DepAttestation {
pub crate_name: String,
pub version: String,
#[serde(default = "default_true")]
pub exact_version: bool,
pub reviewable_artifact: PathBuf,
pub review_scope: ReviewScope,
pub signed_by: String,
pub date: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rationale: Option<String>,
}
const fn default_true() -> bool {
true
}
impl DepAttestation {
#[must_use]
pub fn has_reviewable_artifact(&self) -> bool {
let s = self.reviewable_artifact.to_string_lossy();
!s.trim().is_empty()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentHashRecord {
pub crate_name: String,
pub version: String,
pub content_hash: String,
pub hash_source: String,
pub signed_by: String,
pub date: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MaintainerSnapshot {
pub crate_name: String,
pub since_version: String,
pub owners: Vec<String>,
pub signed_by: String,
pub date: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dep_attestation_roundtrip() {
let a = DepAttestation {
crate_name: "serde".to_string(),
version: "1.0.197".to_string(),
exact_version: true,
reviewable_artifact: PathBuf::from("docs/dep-attest/serde-1.0.197.md"),
review_scope: ReviewScope::Full,
signed_by: "alice".to_string(),
date: "2026-05-22".to_string(),
rationale: Some("Workspace baseline; reviewed at major version bump.".to_string()),
};
let json = serde_json::to_string(&a).unwrap();
let back: DepAttestation = serde_json::from_str(&json).unwrap();
assert_eq!(back.crate_name, "serde");
assert!(back.has_reviewable_artifact());
assert_eq!(back.review_scope, ReviewScope::Full);
}
#[test]
fn dep_attestation_empty_artifact_flags() {
let a = DepAttestation {
crate_name: "serde".to_string(),
version: "1.0.197".to_string(),
exact_version: true,
reviewable_artifact: PathBuf::new(),
review_scope: ReviewScope::MetadataOnly,
signed_by: "alice".to_string(),
date: "2026-05-22".to_string(),
rationale: None,
};
assert!(!a.has_reviewable_artifact());
}
#[test]
fn sandbox_kind_str_roundtrip() {
assert_eq!(SandboxKind::Build.as_str(), "build");
assert_eq!(SandboxKind::ProcMacro.as_str(), "proc-macro");
assert_eq!(SandboxKind::parse_kind("build"), Some(SandboxKind::Build));
assert_eq!(
SandboxKind::parse_kind("proc-macro"),
Some(SandboxKind::ProcMacro)
);
assert_eq!(
SandboxKind::parse_kind("proc_macro"),
Some(SandboxKind::ProcMacro)
);
assert_eq!(SandboxKind::parse_kind("unknown"), None);
}
}