oas-document 1.1.1

OAS Identity Document types and validation for the Open Agent Specification
Documentation
// OAS Spec §5.11 — Operational Specification
//! Operational specification section for OAS Identity Documents.
//!
//! The operational section describes "how I work" — SLA commitments,
//! rate limits, supported formats, availability mode, and maintenance windows.

use serde::{Deserialize, Serialize};

use crate::calendar::ScheduleWindow;

/// An SLA commitment for a specific metric.
///
/// # Examples
///
/// ```
/// use oas_document::operational::SlaCommitment;
///
/// let sla = SlaCommitment {
///     metric: "response_time_p95".to_string(),
///     target: "<500ms".to_string(),
/// };
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SlaCommitment {
    /// Metric name: `"response_time_p95"`, `"uptime"`, `"throughput"`.
    pub metric: String,

    /// Target value: `"<500ms"`, `"99.9%"`, `"1000 req/min"`.
    pub target: String,
}

/// A rate limit specification.
///
/// # Examples
///
/// ```
/// use oas_document::operational::RateLimit;
///
/// let limit = RateLimit {
///     resource: "requests".to_string(),
///     limit: 1000,
///     window: "minute".to_string(),
/// };
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RateLimit {
    /// What is being limited: `"requests"`, `"tokens"`, `"bytes"`.
    pub resource: String,

    /// Numeric limit value.
    pub limit: u64,

    /// Time window: `"second"`, `"minute"`, `"hour"`, `"day"`.
    pub window: String,
}

/// Operational specification section of an OAS Identity Document.
///
/// # Examples
///
/// ```
/// use oas_document::operational::{OperationalSection, SlaCommitment, RateLimit};
///
/// let ops = OperationalSection {
///     availability: Some("24/7".to_string()),
///     sla: vec![SlaCommitment {
///         metric: "uptime".to_string(),
///         target: "99.9%".to_string(),
///     }],
///     rate_limits: vec![RateLimit {
///         resource: "requests".to_string(),
///         limit: 1000,
///         window: "minute".to_string(),
///     }],
///     supported_input_formats: vec!["application/json".to_string()],
///     supported_output_formats: vec!["application/json".to_string()],
///     max_payload_bytes: Some(10_485_760),
///     max_context_tokens: None,
///     maintenance_windows: vec![],
/// };
/// assert_eq!(ops.sla.len(), 1);
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OperationalSection {
    /// Availability mode: `"24/7"`, `"business_hours"`, `"on_demand"`, `"scheduled"`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub availability: Option<String>,

    /// SLA commitments.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub sla: Vec<SlaCommitment>,

    /// Rate limits.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub rate_limits: Vec<RateLimit>,

    /// Accepted input MIME types (e.g., `"application/json"`, `"text/plain"`).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub supported_input_formats: Vec<String>,

    /// Produced output MIME types.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub supported_output_formats: Vec<String>,

    /// Maximum request payload size in bytes.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_payload_bytes: Option<u64>,

    /// Maximum context window in tokens (for LLM-backed agents).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_context_tokens: Option<u64>,

    /// Scheduled maintenance windows when the entity is unavailable.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub maintenance_windows: Vec<ScheduleWindow>,
}

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

    #[test]
    fn serde_round_trip() {
        let ops = OperationalSection {
            availability: Some("24/7".to_string()),
            sla: vec![SlaCommitment {
                metric: "uptime".to_string(),
                target: "99.9%".to_string(),
            }],
            rate_limits: vec![RateLimit {
                resource: "requests".to_string(),
                limit: 1000,
                window: "minute".to_string(),
            }],
            supported_input_formats: vec!["application/json".to_string()],
            supported_output_formats: vec!["application/json".to_string()],
            max_payload_bytes: Some(10_485_760),
            max_context_tokens: Some(128_000),
            maintenance_windows: vec![],
        };

        let json = serde_json::to_string_pretty(&ops).unwrap_or_default();
        let parsed: OperationalSection =
            serde_json::from_str(&json).unwrap_or_else(|_| ops.clone());
        assert_eq!(ops, parsed);
    }

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