use chrono::{DateTime, Utc};
use crate::record::{Record, RecordId};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tainted<T>(T);
impl<T> Tainted<T> {
#[must_use]
pub fn new(value: T) -> Self {
Self(value)
}
#[must_use]
pub fn into_inner(self) -> T {
self.0
}
#[must_use]
pub fn inner_ref(&self) -> &T {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Untainted<T>(T);
impl<T> Untainted<T> {
pub(crate) fn new(value: T) -> Self {
Self(value)
}
#[must_use]
pub fn into_inner(self) -> T {
self.0
}
#[must_use]
pub fn inner_ref(&self) -> &T {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct ServerMetadata {
pub id: RecordId,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub version: u64,
}
#[must_use]
#[allow(clippy::needless_pass_by_value)]
pub fn sanitize(tainted: Tainted<Record>, server: ServerMetadata) -> Untainted<Record> {
let issue = tainted.into_inner();
let Record {
id: _,
title,
status,
assignee,
labels,
created_at: _,
updated_at: _,
version: _,
body,
parent_id,
extensions,
} = issue;
let ServerMetadata {
id,
created_at,
updated_at,
version,
} = server;
Untainted::new(Record {
id,
title,
status,
assignee,
labels,
created_at,
updated_at,
version,
body,
parent_id,
extensions,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::RecordStatus;
use chrono::TimeZone;
#[test]
fn tainted_inner_ref_works() {
let t = Tainted::new(42_u64);
assert_eq!(t.inner_ref(), &42_u64);
}
#[test]
fn tainted_into_inner_works() {
let t = Tainted::new(42_u64);
assert_eq!(t.into_inner(), 42_u64);
}
#[test]
fn tainted_derives_work() {
let a = Tainted::new(7_u64);
let b = a.clone();
assert_eq!(a, b);
let _ = format!("{a:?}");
}
#[test]
fn untainted_inner_ref_and_into_inner_work() {
let u = Untainted::new(9_u32);
assert_eq!(u.inner_ref(), &9_u32);
assert_eq!(u.into_inner(), 9_u32);
}
fn tainted_issue_version_999999() -> Tainted<Record> {
let t = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
Tainted::new(Record {
id: RecordId(999_999),
title: "agent-authored title".into(),
status: RecordStatus::Open,
assignee: Some("agent-alpha".into()),
labels: vec!["bug".into()],
created_at: t,
updated_at: t,
version: 999_999,
body: "agent-authored body".into(),
parent_id: None,
extensions: std::collections::BTreeMap::new(),
})
}
fn server_meta() -> ServerMetadata {
let t = Utc.with_ymd_and_hms(2026, 4, 13, 0, 0, 0).unwrap();
ServerMetadata {
id: RecordId(42),
created_at: t,
updated_at: t,
version: 5,
}
}
#[test]
fn server_controlled_frontmatter_fields_are_stripped() {
let tainted = tainted_issue_version_999999();
let meta = server_meta();
let untainted = sanitize(tainted, meta.clone());
let inner = untainted.into_inner();
assert_eq!(inner.id, meta.id);
assert_eq!(inner.version, meta.version);
assert_eq!(inner.created_at, meta.created_at);
assert_eq!(inner.updated_at, meta.updated_at);
assert_eq!(inner.title, "agent-authored title");
assert_eq!(inner.status as u8, RecordStatus::Open as u8);
assert_eq!(inner.assignee.as_deref(), Some("agent-alpha"));
assert_eq!(inner.labels, vec!["bug".to_string()]);
assert_eq!(inner.body, "agent-authored body");
}
#[test]
fn sanitize_is_pure_and_cloneable() {
let meta = server_meta();
let u1 = sanitize(tainted_issue_version_999999(), meta.clone()).into_inner();
let u2 = sanitize(tainted_issue_version_999999(), meta).into_inner();
assert_eq!(u1.id, u2.id);
assert_eq!(u1.version, u2.version);
assert_eq!(u1.title, u2.title);
assert_eq!(u1.body, u2.body);
}
}