Skip to main content

oas_document/
profile.rs

1// OAS Spec ยง5.8 โ€” Entity Profile
2//! Profile section for OAS Identity Documents.
3//!
4//! The profile section provides the "business card" presentation layer
5//! for any OAS entity โ€” avatar, tagline, categories, languages, timezone.
6
7use serde::{Deserialize, Serialize};
8
9/// Profile section of an OAS Identity Document.
10///
11/// Provides human-readable presentation data for the entity.
12/// All fields are optional โ€” entities expose only what they choose to.
13///
14/// # Examples
15///
16/// ```
17/// use oas_document::profile::ProfileSection;
18///
19/// let profile = ProfileSection {
20///     avatar_url: Some("https://cdn.example.com/agent-42/avatar.png".to_string()),
21///     banner_url: None,
22///     tagline: Some("AI-powered financial analysis agent".to_string()),
23///     categories: vec!["finance".to_string(), "analytics".to_string()],
24///     languages: vec!["en".to_string(), "es".to_string()],
25///     timezone: Some("America/New_York".to_string()),
26///     accent_color: Some("#3B82F6".to_string()),
27/// };
28/// assert_eq!(profile.languages.len(), 2);
29/// ```
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "camelCase")]
32pub struct ProfileSection {
33    /// URL to the entity's avatar image.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub avatar_url: Option<String>,
36
37    /// URL to the entity's banner/header image.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub banner_url: Option<String>,
40
41    /// Short tagline or bio (max 280 chars, like a tweet).
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub tagline: Option<String>,
44
45    /// Category tags for discovery (e.g., "finance", "customer-support", "research").
46    #[serde(default, skip_serializing_if = "Vec::is_empty")]
47    pub categories: Vec<String>,
48
49    /// Supported languages as ISO 639-1 codes (e.g., "en", "es", "zh").
50    #[serde(default, skip_serializing_if = "Vec::is_empty")]
51    pub languages: Vec<String>,
52
53    /// IANA timezone identifier (e.g., "America/New_York", "UTC").
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub timezone: Option<String>,
56
57    /// Accent/brand color as hex (e.g., "#3B82F6").
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub accent_color: Option<String>,
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn serde_round_trip() {
68        let profile = ProfileSection {
69            avatar_url: Some("https://example.com/avatar.png".to_string()),
70            banner_url: None,
71            tagline: Some("Test agent".to_string()),
72            categories: vec!["test".to_string()],
73            languages: vec!["en".to_string()],
74            timezone: Some("UTC".to_string()),
75            accent_color: None,
76        };
77        let json = serde_json::to_string(&profile).unwrap_or_default();
78        let parsed: ProfileSection =
79            serde_json::from_str(&json).unwrap_or_else(|_| profile.clone());
80        assert_eq!(profile, parsed);
81    }
82
83    #[test]
84    fn minimal_profile_deserializes() {
85        let json = r#"{"tagline":"Hello"}"#;
86        let profile: Result<ProfileSection, _> = serde_json::from_str(json);
87        assert!(profile.is_ok());
88        let profile = profile.unwrap_or_else(|_| ProfileSection {
89            avatar_url: None,
90            banner_url: None,
91            tagline: None,
92            categories: vec![],
93            languages: vec![],
94            timezone: None,
95            accent_color: None,
96        });
97        assert_eq!(profile.tagline.as_deref(), Some("Hello"));
98        assert!(profile.categories.is_empty());
99    }
100
101    #[test]
102    fn empty_vecs_omitted_in_serialization() {
103        let profile = ProfileSection {
104            avatar_url: None,
105            banner_url: None,
106            tagline: Some("Minimal".to_string()),
107            categories: vec![],
108            languages: vec![],
109            timezone: None,
110            accent_color: None,
111        };
112        let json = serde_json::to_string(&profile).unwrap_or_default();
113        assert!(!json.contains("categories"));
114        assert!(!json.contains("languages"));
115    }
116}