Skip to main content

oas_document/
operational.rs

1// OAS Spec §5.11 — Operational Specification
2//! Operational specification section for OAS Identity Documents.
3//!
4//! The operational section describes "how I work" — SLA commitments,
5//! rate limits, supported formats, availability mode, and maintenance windows.
6
7use serde::{Deserialize, Serialize};
8
9use crate::calendar::ScheduleWindow;
10
11/// An SLA commitment for a specific metric.
12///
13/// # Examples
14///
15/// ```
16/// use oas_document::operational::SlaCommitment;
17///
18/// let sla = SlaCommitment {
19///     metric: "response_time_p95".to_string(),
20///     target: "<500ms".to_string(),
21/// };
22/// ```
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct SlaCommitment {
26    /// Metric name: `"response_time_p95"`, `"uptime"`, `"throughput"`.
27    pub metric: String,
28
29    /// Target value: `"<500ms"`, `"99.9%"`, `"1000 req/min"`.
30    pub target: String,
31}
32
33/// A rate limit specification.
34///
35/// # Examples
36///
37/// ```
38/// use oas_document::operational::RateLimit;
39///
40/// let limit = RateLimit {
41///     resource: "requests".to_string(),
42///     limit: 1000,
43///     window: "minute".to_string(),
44/// };
45/// ```
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct RateLimit {
49    /// What is being limited: `"requests"`, `"tokens"`, `"bytes"`.
50    pub resource: String,
51
52    /// Numeric limit value.
53    pub limit: u64,
54
55    /// Time window: `"second"`, `"minute"`, `"hour"`, `"day"`.
56    pub window: String,
57}
58
59/// Operational specification section of an OAS Identity Document.
60///
61/// # Examples
62///
63/// ```
64/// use oas_document::operational::{OperationalSection, SlaCommitment, RateLimit};
65///
66/// let ops = OperationalSection {
67///     availability: Some("24/7".to_string()),
68///     sla: vec![SlaCommitment {
69///         metric: "uptime".to_string(),
70///         target: "99.9%".to_string(),
71///     }],
72///     rate_limits: vec![RateLimit {
73///         resource: "requests".to_string(),
74///         limit: 1000,
75///         window: "minute".to_string(),
76///     }],
77///     supported_input_formats: vec!["application/json".to_string()],
78///     supported_output_formats: vec!["application/json".to_string()],
79///     max_payload_bytes: Some(10_485_760),
80///     max_context_tokens: None,
81///     maintenance_windows: vec![],
82/// };
83/// assert_eq!(ops.sla.len(), 1);
84/// ```
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "camelCase")]
87pub struct OperationalSection {
88    /// Availability mode: `"24/7"`, `"business_hours"`, `"on_demand"`, `"scheduled"`.
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub availability: Option<String>,
91
92    /// SLA commitments.
93    #[serde(default, skip_serializing_if = "Vec::is_empty")]
94    pub sla: Vec<SlaCommitment>,
95
96    /// Rate limits.
97    #[serde(default, skip_serializing_if = "Vec::is_empty")]
98    pub rate_limits: Vec<RateLimit>,
99
100    /// Accepted input MIME types (e.g., `"application/json"`, `"text/plain"`).
101    #[serde(default, skip_serializing_if = "Vec::is_empty")]
102    pub supported_input_formats: Vec<String>,
103
104    /// Produced output MIME types.
105    #[serde(default, skip_serializing_if = "Vec::is_empty")]
106    pub supported_output_formats: Vec<String>,
107
108    /// Maximum request payload size in bytes.
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub max_payload_bytes: Option<u64>,
111
112    /// Maximum context window in tokens (for LLM-backed agents).
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub max_context_tokens: Option<u64>,
115
116    /// Scheduled maintenance windows when the entity is unavailable.
117    #[serde(default, skip_serializing_if = "Vec::is_empty")]
118    pub maintenance_windows: Vec<ScheduleWindow>,
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn serde_round_trip() {
127        let ops = OperationalSection {
128            availability: Some("24/7".to_string()),
129            sla: vec![SlaCommitment {
130                metric: "uptime".to_string(),
131                target: "99.9%".to_string(),
132            }],
133            rate_limits: vec![RateLimit {
134                resource: "requests".to_string(),
135                limit: 1000,
136                window: "minute".to_string(),
137            }],
138            supported_input_formats: vec!["application/json".to_string()],
139            supported_output_formats: vec!["application/json".to_string()],
140            max_payload_bytes: Some(10_485_760),
141            max_context_tokens: Some(128_000),
142            maintenance_windows: vec![],
143        };
144
145        let json = serde_json::to_string_pretty(&ops).unwrap_or_default();
146        let parsed: OperationalSection =
147            serde_json::from_str(&json).unwrap_or_else(|_| ops.clone());
148        assert_eq!(ops, parsed);
149    }
150
151    #[test]
152    fn minimal_section_deserializes() {
153        let json = r#"{}"#;
154        let ops: Result<OperationalSection, _> = serde_json::from_str(json);
155        assert!(ops.is_ok());
156    }
157}