use serde::{Deserialize, Serialize};
pub const MANUAL_CANDIDATES_FILE: &str = "manual-candidates.json";
pub const CARDS_FILE: &str = "cards.json";
pub const MANUAL_CANDIDATES_SCHEMA: &str = "manual-candidates/v1";
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PacketSiblingInputs {
pub manual_candidates: Option<ManualCandidatesFile>,
pub cards: Option<CardsFile>,
pub load_notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PacketRenderBundle {
pub manifest: super::TokmdPacketsManifest,
pub siblings: PacketSiblingInputs,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ManualCandidatesFile {
pub schema_version: String,
#[serde(default)]
pub candidates: Vec<ManualCandidateRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ManualCandidateRecord {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub invariant: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub safe_caller: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unsafe_operation: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub operation_family: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub location_text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proof_mode: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fix_boundary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pr_aperture: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub test_targets: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub do_not_touch: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CardsFile {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema_version: Option<String>,
#[serde(default, alias = "review_cards")]
pub cards: Vec<ReviewCardRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ReviewCardRecord {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub location_text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unsafe_operation: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub operation_family: Option<String>,
}
impl ManualCandidatesFile {
pub fn schema_matches(&self) -> bool {
self.schema_version == MANUAL_CANDIDATES_SCHEMA
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn manual_candidates_roundtrip() {
let file = ManualCandidatesFile {
schema_version: MANUAL_CANDIDATES_SCHEMA.into(),
candidates: vec![ManualCandidateRecord {
id: Some("seed-42".into()),
title: Some("UTF-8 boundary".into()),
invariant: Some("buffer bounds".into()),
..ManualCandidateRecord::default()
}],
};
let json = serde_json::to_string(&file).unwrap();
let back: ManualCandidatesFile = serde_json::from_str(&json).unwrap();
assert_eq!(back, file);
assert!(back.schema_matches());
}
#[test]
fn cards_roundtrip_with_review_cards_alias() {
let raw =
r#"{"schema_version":"0.2","review_cards":[{"id":"rc-17","title":"slice read"}]}"#;
let file: CardsFile = serde_json::from_str(raw).unwrap();
assert_eq!(file.cards.len(), 1);
assert_eq!(file.cards[0].id.as_deref(), Some("rc-17"));
}
}