use serde::{Deserialize, Serialize};
use time::Date;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum Attribution {
Foundation,
Partner,
ThirdParty,
Community,
#[default]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ContentType {
Doc,
Tutorial,
Reference,
Example,
ContractSource,
SdkSource,
Test,
Readme,
#[default]
#[serde(other)]
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct LanguageTarget {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version_constraint: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SdkDependency {
pub kind: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version_constraint: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub struct Deprecation {
pub is_deprecated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub since: Option<Date>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Provenance {
#[serde(default)]
pub attribution: Attribution,
#[serde(default)]
pub verified: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verified_by: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verified_at: Option<Date>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verification_notes: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub language_targets: Vec<LanguageTarget>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sdk_dependencies: Vec<SdkDependency>,
#[serde(default)]
pub deprecation: Deprecation,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default)]
pub content_type: ContentType,
}
impl Provenance {
#[must_use]
pub fn attributed_to(attribution: Attribution) -> Self {
Self { attribution, ..Self::default() }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_safe() {
let p = Provenance::default();
assert_eq!(p.attribution, Attribution::Unknown);
assert!(!p.verified);
assert!(p.tags.is_empty());
}
#[test]
fn round_trips_full_shape() {
let p = Provenance {
attribution: Attribution::Foundation,
verified: true,
verified_by: Some("midnight-foundation".into()),
verified_at: Date::from_calendar_date(2026, time::Month::April, 1).ok(),
verification_notes: None,
language_targets: vec![LanguageTarget {
name: "compact".into(),
version_constraint: Some(">=0.23".into()),
}],
sdk_dependencies: vec![SdkDependency {
kind: "npm".into(),
name: "@midnight-ntwrk/midnight-js".into(),
version_constraint: Some("^1.4.0".into()),
}],
deprecation: Deprecation::default(),
tags: vec!["quickstart".into(), "tutorial".into()],
content_type: ContentType::Tutorial,
};
let v = serde_json::to_value(&p).unwrap();
let back: Provenance = serde_json::from_value(v).unwrap();
assert_eq!(p, back);
}
#[test]
fn empty_collections_elided() {
let v = serde_json::to_value(Provenance::default()).unwrap();
assert!(v.get("language_targets").is_none());
assert!(v.get("sdk_dependencies").is_none());
assert!(v.get("tags").is_none());
}
#[test]
fn tolerates_unknown_attribution_via_default() {
let v = serde_json::json!({});
let p: Provenance = serde_json::from_value(v).unwrap();
assert_eq!(p.attribution, Attribution::Unknown);
}
#[test]
fn unknown_content_type_falls_back_to_other() {
let v = serde_json::json!({ "content_type": "blog_post_2026_meta" });
let p: Provenance = serde_json::from_value(v).unwrap();
assert_eq!(p.content_type, ContentType::Other);
}
#[test]
fn attribution_serializes_snake_case() {
let v = serde_json::to_value(Attribution::ThirdParty).unwrap();
assert_eq!(v, serde_json::Value::String("third_party".into()));
}
}