oas-document 1.1.1

OAS Identity Document types and validation for the Open Agent Specification
Documentation
// OAS Spec §5.7 — Field Visibility
//! Visibility levels for access-controlled fields in OAS Identity Documents.
//!
//! DID documents are stored on the public Weave DHT. Fields marked with a
//! visibility level other than [`Visibility::Public`] are redacted in the
//! DHT-stored copy. The full values are served via an authenticated
//! `OASPrivateEndpointService` service endpoint on the entity.
//!
//! # Access Control Model
//!
//! | Level          | Who Can Read                          | DHT Behavior               |
//! |----------------|---------------------------------------|----------------------------|
//! | `public`       | Anyone resolving the DID              | Full value stored           |
//! | `authenticated`| Any agent proving identity via DID-Auth| Capability flag only        |
//! | `organization` | Same AO/ENR members (lineage check)   | Capability flag only        |
//! | `private`      | Only the entity itself                | Never stored on DHT         |

use std::fmt;
use std::str::FromStr;

use serde::{Deserialize, Serialize};

/// Visibility level for access-controlled fields.
///
/// Controls who can read a field's actual value versus seeing only a
/// capability advertisement (e.g., "this agent has a calendar" without
/// revealing the booking URL).
///
/// # Examples
///
/// ```
/// use oas_document::visibility::Visibility;
///
/// let vis = Visibility::Organization;
/// assert_eq!(vis.as_str(), "organization");
/// assert!(!vis.is_public());
/// ```
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Visibility {
    /// Visible to anyone resolving the DID. Full value stored on DHT.
    #[default]
    Public,

    /// Visible to any agent that can prove identity via DID-Auth.
    /// DHT stores only a capability flag; actual value served via
    /// authenticated endpoint.
    Authenticated,

    /// Visible only to members of the same Autonomous Organization or
    /// Entity Name Record (verified via lineage chain).
    /// DHT stores only a capability flag.
    Organization,

    /// Visible only to the entity itself. Never stored on the DHT.
    Private,
}

impl Visibility {
    /// Returns the string representation.
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Public => "public",
            Self::Authenticated => "authenticated",
            Self::Organization => "organization",
            Self::Private => "private",
        }
    }

    /// Returns `true` if this field should be fully visible on the public DHT.
    pub fn is_public(&self) -> bool {
        matches!(self, Self::Public)
    }

    /// Returns `true` if this field should be redacted (capability flag only)
    /// or hidden entirely in the DHT-stored copy.
    pub fn is_redacted_on_dht(&self) -> bool {
        !self.is_public()
    }
}

impl fmt::Display for Visibility {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

impl FromStr for Visibility {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "public" => Ok(Self::Public),
            "authenticated" => Ok(Self::Authenticated),
            "organization" => Ok(Self::Organization),
            "private" => Ok(Self::Private),
            _ => Err(format!(
                "unknown visibility '{s}'; expected one of: public, authenticated, organization, private"
            )),
        }
    }
}

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

    #[test]
    fn default_is_public() {
        assert_eq!(Visibility::default(), Visibility::Public);
    }

    #[test]
    fn public_is_not_redacted() {
        assert!(!Visibility::Public.is_redacted_on_dht());
    }

    #[test]
    fn non_public_is_redacted() {
        assert!(Visibility::Authenticated.is_redacted_on_dht());
        assert!(Visibility::Organization.is_redacted_on_dht());
        assert!(Visibility::Private.is_redacted_on_dht());
    }

    #[test]
    fn round_trip_all_variants() {
        for vis in [
            Visibility::Public,
            Visibility::Authenticated,
            Visibility::Organization,
            Visibility::Private,
        ] {
            let s = vis.as_str();
            let parsed = Visibility::from_str(s);
            assert!(parsed.is_ok());
            assert_eq!(parsed.unwrap_or(Visibility::Public), vis);
        }
    }

    #[test]
    fn serde_round_trip() {
        let vis = Visibility::Organization;
        let json = serde_json::to_string(&vis).unwrap_or_default();
        assert_eq!(json, r#""organization""#);
        let parsed: Visibility = serde_json::from_str(&json).unwrap_or(Visibility::Public);
        assert_eq!(parsed, vis);
    }

    #[test]
    fn unknown_returns_error() {
        assert!(Visibility::from_str("secret").is_err());
    }
}