Skip to main content

oas_document/
reputation.rs

1// OAS Spec §5.14 — Reputation Summary
2//! Reputation summary section for OAS Identity Documents.
3//!
4//! The reputation section provides a self-reported track record:
5//! total interactions, endorsement counts, trust scores, and uptime.
6//! These are assertions by the entity itself — third-party verification
7//! requires L2 conformance with attestation service backing.
8
9use serde::{Deserialize, Serialize};
10
11/// Reputation summary section of an OAS Identity Document.
12///
13/// # Trust Model
14///
15/// - L0/L1: Self-reported — treat as claims, not facts.
16/// - L2: Backed by third-party attestation service, verifiable
17///   via `OASAttestationService` endpoint.
18///
19/// # Examples
20///
21/// ```
22/// use oas_document::reputation::ReputationSection;
23///
24/// let rep = ReputationSection {
25///     total_interactions: Some(15_420),
26///     endorsement_count: Some(87),
27///     trust_score: Some("0.94".to_string()),
28///     uptime_30d: Some("99.7%".to_string()),
29///     uptime_90d: Some("99.5%".to_string()),
30///     first_active: Some("2026-01-15T00:00:00Z".to_string()),
31///     endorser_dids: vec![
32///         "did:oas:prod:hmr:alice".to_string(),
33///         "did:oas:prod:enr:acme".to_string(),
34///     ],
35/// };
36/// assert_eq!(rep.endorsement_count, Some(87));
37/// ```
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct ReputationSection {
41    /// Total number of interactions/requests served.
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub total_interactions: Option<u64>,
44
45    /// Number of endorsements received from other DIDs.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub endorsement_count: Option<u32>,
48
49    /// Aggregated trust score as a decimal string (0.0–1.0).
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub trust_score: Option<String>,
52
53    /// Uptime percentage over the last 30 days.
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub uptime_30d: Option<String>,
56
57    /// Uptime percentage over the last 90 days.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub uptime_90d: Option<String>,
60
61    /// ISO 8601 timestamp of when this entity first became active.
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub first_active: Option<String>,
64
65    /// DIDs of entities that have endorsed this entity.
66    #[serde(default, skip_serializing_if = "Vec::is_empty")]
67    pub endorser_dids: Vec<String>,
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn serde_round_trip() {
76        let rep = ReputationSection {
77            total_interactions: Some(10_000),
78            endorsement_count: Some(42),
79            trust_score: Some("0.87".to_string()),
80            uptime_30d: Some("99.9%".to_string()),
81            uptime_90d: Some("99.5%".to_string()),
82            first_active: Some("2026-01-01T00:00:00Z".to_string()),
83            endorser_dids: vec!["did:oas:prod:hmr:alice".to_string()],
84        };
85
86        let json = serde_json::to_string(&rep).unwrap_or_default();
87        let parsed: ReputationSection = serde_json::from_str(&json).unwrap_or_else(|_| rep.clone());
88        assert_eq!(rep, parsed);
89    }
90
91    #[test]
92    fn minimal_section_deserializes() {
93        let json = r#"{}"#;
94        let rep: Result<ReputationSection, _> = serde_json::from_str(json);
95        assert!(rep.is_ok());
96    }
97
98    #[test]
99    fn optional_fields_omitted() {
100        let rep = ReputationSection {
101            total_interactions: None,
102            endorsement_count: None,
103            trust_score: None,
104            uptime_30d: None,
105            uptime_90d: None,
106            first_active: None,
107            endorser_dids: vec![],
108        };
109        let json = serde_json::to_string(&rep).unwrap_or_default();
110        assert_eq!(json, "{}");
111    }
112}