oas-document 1.1.1

OAS Identity Document types and validation for the Open Agent Specification
Documentation
// OAS Spec ยง5.8 โ€” Entity Profile
//! Profile section for OAS Identity Documents.
//!
//! The profile section provides the "business card" presentation layer
//! for any OAS entity โ€” avatar, tagline, categories, languages, timezone.

use serde::{Deserialize, Serialize};

/// Profile section of an OAS Identity Document.
///
/// Provides human-readable presentation data for the entity.
/// All fields are optional โ€” entities expose only what they choose to.
///
/// # Examples
///
/// ```
/// use oas_document::profile::ProfileSection;
///
/// let profile = ProfileSection {
///     avatar_url: Some("https://cdn.example.com/agent-42/avatar.png".to_string()),
///     banner_url: None,
///     tagline: Some("AI-powered financial analysis agent".to_string()),
///     categories: vec!["finance".to_string(), "analytics".to_string()],
///     languages: vec!["en".to_string(), "es".to_string()],
///     timezone: Some("America/New_York".to_string()),
///     accent_color: Some("#3B82F6".to_string()),
/// };
/// assert_eq!(profile.languages.len(), 2);
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProfileSection {
    /// URL to the entity's avatar image.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub avatar_url: Option<String>,

    /// URL to the entity's banner/header image.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub banner_url: Option<String>,

    /// Short tagline or bio (max 280 chars, like a tweet).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tagline: Option<String>,

    /// Category tags for discovery (e.g., "finance", "customer-support", "research").
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub categories: Vec<String>,

    /// Supported languages as ISO 639-1 codes (e.g., "en", "es", "zh").
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub languages: Vec<String>,

    /// IANA timezone identifier (e.g., "America/New_York", "UTC").
    #[serde(skip_serializing_if = "Option::is_none")]
    pub timezone: Option<String>,

    /// Accent/brand color as hex (e.g., "#3B82F6").
    #[serde(skip_serializing_if = "Option::is_none")]
    pub accent_color: Option<String>,
}

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

    #[test]
    fn serde_round_trip() {
        let profile = ProfileSection {
            avatar_url: Some("https://example.com/avatar.png".to_string()),
            banner_url: None,
            tagline: Some("Test agent".to_string()),
            categories: vec!["test".to_string()],
            languages: vec!["en".to_string()],
            timezone: Some("UTC".to_string()),
            accent_color: None,
        };
        let json = serde_json::to_string(&profile).unwrap_or_default();
        let parsed: ProfileSection =
            serde_json::from_str(&json).unwrap_or_else(|_| profile.clone());
        assert_eq!(profile, parsed);
    }

    #[test]
    fn minimal_profile_deserializes() {
        let json = r#"{"tagline":"Hello"}"#;
        let profile: Result<ProfileSection, _> = serde_json::from_str(json);
        assert!(profile.is_ok());
        let profile = profile.unwrap_or_else(|_| ProfileSection {
            avatar_url: None,
            banner_url: None,
            tagline: None,
            categories: vec![],
            languages: vec![],
            timezone: None,
            accent_color: None,
        });
        assert_eq!(profile.tagline.as_deref(), Some("Hello"));
        assert!(profile.categories.is_empty());
    }

    #[test]
    fn empty_vecs_omitted_in_serialization() {
        let profile = ProfileSection {
            avatar_url: None,
            banner_url: None,
            tagline: Some("Minimal".to_string()),
            categories: vec![],
            languages: vec![],
            timezone: None,
            accent_color: None,
        };
        let json = serde_json::to_string(&profile).unwrap_or_default();
        assert!(!json.contains("categories"));
        assert!(!json.contains("languages"));
    }
}