use std::collections::BTreeMap;
use chrono::{DateTime, FixedOffset};
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(
clippy::struct_field_names,
reason = "the `proof_purpose`, `proof_value` and `previous_proof` field names are mandated verbatim by the W3C Data Integrity / FEP-8b32 vocabulary and cannot be renamed without breaking interoperability"
)]
pub struct Proof {
#[serde(rename = "type")]
pub type_: String,
pub cryptosuite: String,
pub created: DateTime<FixedOffset>,
pub verification_method: Url,
pub proof_purpose: String,
pub proof_value: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub challenge: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_proof: Option<Url>,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
impl Proof {
pub const DATA_INTEGRITY_PROOF: &'static str = "DataIntegrityProof";
pub const CRYPTOSUITE_EDDSA_JCS_2022: &'static str = "eddsa-jcs-2022";
pub const PURPOSE_ASSERTION_METHOD: &'static str = "assertionMethod";
pub const PURPOSE_AUTHENTICATION: &'static str = "authentication";
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use serde_json::json;
use super::*;
#[test]
fn fep_8b32_minimal_proof_roundtrips() {
let raw = json!({
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-jcs-2022",
"created": "2026-04-20T10:00:00Z",
"verificationMethod": "https://example.com/users/alice#ed25519-key",
"proofPurpose": "assertionMethod",
"proofValue": "z3F4nT9mC8rE7QXJyV9hP2wKzN8sA5bL"
});
let proof: Proof = serde_json::from_value(raw.clone()).unwrap();
assert_eq!(proof.type_, Proof::DATA_INTEGRITY_PROOF);
assert_eq!(proof.cryptosuite, Proof::CRYPTOSUITE_EDDSA_JCS_2022);
assert_eq!(proof.proof_purpose, Proof::PURPOSE_ASSERTION_METHOD);
let back = serde_json::to_value(&proof).unwrap();
assert_eq!(back, raw);
}
#[test]
fn proof_with_challenge_and_previous_roundtrips() {
let raw = json!({
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-jcs-2022",
"created": "2026-04-20T10:00:00Z",
"verificationMethod": "https://example.com/users/alice#ed25519-key",
"proofPurpose": "authentication",
"proofValue": "z3F4nT9mC8rE7QXJyV9hP2wKzN8sA5bL",
"domain": "example.com",
"challenge": "8b9c0d1e",
"previousProof": "https://example.com/proofs/123"
});
let proof: Proof = serde_json::from_value(raw.clone()).unwrap();
assert_eq!(proof.domain.as_deref(), Some("example.com"));
assert_eq!(proof.challenge.as_deref(), Some("8b9c0d1e"));
assert!(proof.previous_proof.is_some());
let back = serde_json::to_value(&proof).unwrap();
assert_eq!(back, raw);
}
#[test]
fn extra_proof_parameters_roundtrip() {
let raw = json!({
"type": "DataIntegrityProof",
"cryptosuite": "future-suite-2030",
"created": "2026-04-20T10:00:00Z",
"verificationMethod": "https://example.com/key",
"proofPurpose": "assertionMethod",
"proofValue": "zABC",
"futureParam": "futureValue"
});
let proof: Proof = serde_json::from_value(raw.clone()).unwrap();
assert_eq!(proof.extra.len(), 1);
let back = serde_json::to_value(&proof).unwrap();
assert_eq!(back, raw);
}
}