oas-document 1.1.1

OAS Identity Document types and validation for the Open Agent Specification
Documentation
// OAS Spec §5.14 — Reputation Summary
//! Reputation summary section for OAS Identity Documents.
//!
//! The reputation section provides a self-reported track record:
//! total interactions, endorsement counts, trust scores, and uptime.
//! These are assertions by the entity itself — third-party verification
//! requires L2 conformance with attestation service backing.

use serde::{Deserialize, Serialize};

/// Reputation summary section of an OAS Identity Document.
///
/// # Trust Model
///
/// - L0/L1: Self-reported — treat as claims, not facts.
/// - L2: Backed by third-party attestation service, verifiable
///   via `OASAttestationService` endpoint.
///
/// # Examples
///
/// ```
/// use oas_document::reputation::ReputationSection;
///
/// let rep = ReputationSection {
///     total_interactions: Some(15_420),
///     endorsement_count: Some(87),
///     trust_score: Some("0.94".to_string()),
///     uptime_30d: Some("99.7%".to_string()),
///     uptime_90d: Some("99.5%".to_string()),
///     first_active: Some("2026-01-15T00:00:00Z".to_string()),
///     endorser_dids: vec![
///         "did:oas:prod:hmr:alice".to_string(),
///         "did:oas:prod:enr:acme".to_string(),
///     ],
/// };
/// assert_eq!(rep.endorsement_count, Some(87));
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReputationSection {
    /// Total number of interactions/requests served.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub total_interactions: Option<u64>,

    /// Number of endorsements received from other DIDs.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub endorsement_count: Option<u32>,

    /// Aggregated trust score as a decimal string (0.0–1.0).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub trust_score: Option<String>,

    /// Uptime percentage over the last 30 days.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub uptime_30d: Option<String>,

    /// Uptime percentage over the last 90 days.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub uptime_90d: Option<String>,

    /// ISO 8601 timestamp of when this entity first became active.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub first_active: Option<String>,

    /// DIDs of entities that have endorsed this entity.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub endorser_dids: Vec<String>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn serde_round_trip() {
        let rep = ReputationSection {
            total_interactions: Some(10_000),
            endorsement_count: Some(42),
            trust_score: Some("0.87".to_string()),
            uptime_30d: Some("99.9%".to_string()),
            uptime_90d: Some("99.5%".to_string()),
            first_active: Some("2026-01-01T00:00:00Z".to_string()),
            endorser_dids: vec!["did:oas:prod:hmr:alice".to_string()],
        };

        let json = serde_json::to_string(&rep).unwrap_or_default();
        let parsed: ReputationSection = serde_json::from_str(&json).unwrap_or_else(|_| rep.clone());
        assert_eq!(rep, parsed);
    }

    #[test]
    fn minimal_section_deserializes() {
        let json = r#"{}"#;
        let rep: Result<ReputationSection, _> = serde_json::from_str(json);
        assert!(rep.is_ok());
    }

    #[test]
    fn optional_fields_omitted() {
        let rep = ReputationSection {
            total_interactions: None,
            endorsement_count: None,
            trust_score: None,
            uptime_30d: None,
            uptime_90d: None,
            first_active: None,
            endorser_dids: vec![],
        };
        let json = serde_json::to_string(&rep).unwrap_or_default();
        assert_eq!(json, "{}");
    }
}