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}