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}