1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
13pub struct Api {
14 pub name: String,
15 pub version: String,
16 #[serde(default)]
17 pub schemas: Vec<Schema>,
18 #[serde(default)]
19 pub operations: Vec<Operation>,
20 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
23 pub annotations: BTreeMap<String, Value>,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
28pub struct Schema {
29 pub name: String,
30 pub value: SchemaValue,
31}
32
33impl Schema {
34 pub fn new(name: impl Into<String>, value: SchemaValue) -> Self {
35 Self {
36 name: name.into(),
37 value,
38 }
39 }
40}
41
42#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
46pub struct SchemaValue {
47 pub kind: SchemaKind,
48 #[serde(default)]
49 pub nullable: bool,
50 #[serde(default)]
53 pub optional: bool,
54 #[serde(default)]
59 pub nullish: bool,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub format: Option<String>,
62 #[serde(default, skip_serializing_if = "Vec::is_empty")]
63 pub enum_values: Vec<Value>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub const_value: Option<Value>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub default: Option<Value>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub title: Option<String>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub description: Option<String>,
72 #[serde(default)]
73 pub deprecated: bool,
74 #[serde(default)]
75 pub read_only: bool,
76 #[serde(default)]
77 pub write_only: bool,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub discriminator: Option<Discriminator>,
80 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
84 pub constraints: BTreeMap<String, Value>,
85 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
88 pub extensions: BTreeMap<String, Value>,
89}
90
91impl SchemaValue {
92 pub fn new(kind: SchemaKind) -> Self {
93 Self {
94 kind,
95 nullable: false,
96 optional: false,
97 nullish: false,
98 format: None,
99 enum_values: Vec::new(),
100 const_value: None,
101 default: None,
102 title: None,
103 description: None,
104 deprecated: false,
105 read_only: false,
106 write_only: false,
107 discriminator: None,
108 constraints: BTreeMap::new(),
109 extensions: BTreeMap::new(),
110 }
111 }
112
113 pub fn unknown() -> Self {
114 Self::new(SchemaKind::Any)
115 }
116
117 pub fn reference(reference: impl Into<String>) -> Self {
118 Self::new(SchemaKind::Reference {
119 reference: reference.into(),
120 })
121 }
122}
123
124#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
126#[serde(tag = "kind", rename_all = "snake_case")]
127pub enum SchemaKind {
128 Any,
129 Null,
130 Boolean,
131 Integer,
132 Number,
133 String,
134 Array {
135 items: Box<SchemaValue>,
136 },
137 Object {
138 fields: Vec<Field>,
139 additional_properties: AdditionalProperties,
140 },
141 Reference {
145 reference: String,
146 },
147 OneOf {
148 variants: Vec<SchemaValue>,
149 },
150 AnyOf {
151 variants: Vec<SchemaValue>,
152 },
153 AllOf {
154 variants: Vec<SchemaValue>,
155 },
156 Not {
157 schema: Box<SchemaValue>,
158 },
159}
160
161impl SchemaKind {
162 pub fn reference_name(&self) -> Option<&str> {
163 let Self::Reference { reference } = self else {
164 return None;
165 };
166 Some(reference.rsplit('/').next().unwrap_or(reference))
167 }
168}
169
170#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
172#[serde(tag = "kind", rename_all = "snake_case")]
173pub enum AdditionalProperties {
174 #[default]
177 Unspecified,
178 Any,
179 Forbidden,
180 Schema {
181 value: Box<SchemaValue>,
182 },
183}
184
185#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
186pub struct Discriminator {
187 pub property_name: String,
188 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
189 pub mapping: BTreeMap<String, String>,
190}
191
192#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
193pub struct Field {
194 pub name: String,
195 pub value: SchemaValue,
196 #[serde(default)]
197 pub required: bool,
198 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
199 pub annotations: BTreeMap<String, Value>,
200}
201
202#[derive(Clone, Debug, PartialEq, Eq)]
203pub enum HttpMethod {
204 Get,
205 Post,
206 Put,
207 Patch,
208 Delete,
209 Head,
210 Options,
211 Trace,
212 Query,
213 Custom(String),
214}
215
216impl HttpMethod {
217 pub fn as_str(&self) -> &str {
218 match self {
219 Self::Get => "GET",
220 Self::Post => "POST",
221 Self::Put => "PUT",
222 Self::Patch => "PATCH",
223 Self::Delete => "DELETE",
224 Self::Head => "HEAD",
225 Self::Options => "OPTIONS",
226 Self::Trace => "TRACE",
227 Self::Query => "QUERY",
228 Self::Custom(method) => method,
229 }
230 }
231}
232
233impl HttpMethod {
234 pub fn parse(method: &str) -> Result<Self, String> {
237 if method.is_empty()
238 || method.len() > 256
239 || !method
240 .bytes()
241 .all(|byte| byte.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&byte))
242 {
243 return Err("HTTP method must be an ASCII token of 1..=256 bytes".into());
244 }
245 if method != method.to_ascii_uppercase()
246 && matches!(
247 method.to_ascii_uppercase().as_str(),
248 "GET"
249 | "POST"
250 | "PUT"
251 | "PATCH"
252 | "DELETE"
253 | "HEAD"
254 | "OPTIONS"
255 | "TRACE"
256 | "QUERY"
257 )
258 {
259 return Err("standard HTTP methods must use canonical uppercase tokens".into());
260 }
261 Ok(match method {
262 "GET" => Self::Get,
263 "POST" => Self::Post,
264 "PUT" => Self::Put,
265 "PATCH" => Self::Patch,
266 "DELETE" => Self::Delete,
267 "HEAD" => Self::Head,
268 "OPTIONS" => Self::Options,
269 "TRACE" => Self::Trace,
270 "QUERY" => Self::Query,
271 other => Self::Custom(other.to_owned()),
272 })
273 }
274}
275impl Serialize for HttpMethod {
276 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
277 serializer.serialize_str(self.as_str())
278 }
279}
280impl<'de> Deserialize<'de> for HttpMethod {
281 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
282 let method = String::deserialize(deserializer)?;
283 Self::parse(&method).map_err(serde::de::Error::custom)
284 }
285}
286
287#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
288pub struct Operation {
289 pub id: String,
290 pub method: HttpMethod,
291 pub path: String,
292 #[serde(default, skip_serializing_if = "Vec::is_empty")]
293 pub parameters: Vec<OperationParameter>,
294 #[serde(default, skip_serializing_if = "Option::is_none")]
295 pub request_body: Option<OperationRequestBody>,
296 #[serde(default, skip_serializing_if = "Vec::is_empty")]
297 pub responses: Vec<OperationResponse>,
298 #[serde(default, skip_serializing_if = "Vec::is_empty")]
302 pub security: Vec<SecurityRequirement>,
303 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
304 pub annotations: BTreeMap<String, Value>,
305}
306
307impl Default for Operation {
308 fn default() -> Self {
309 Self {
310 id: String::new(),
311 method: HttpMethod::Get,
312 path: String::new(),
313 parameters: Vec::new(),
314 request_body: None,
315 responses: Vec::new(),
316 security: Vec::new(),
317 annotations: BTreeMap::new(),
318 }
319 }
320}
321
322#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
327pub struct OperationParameter {
328 pub name: String,
329 pub location: String,
330 #[serde(default)]
331 pub required: bool,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
333 pub schema: Option<SchemaValue>,
334 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub description: Option<String>,
336 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
337 pub annotations: BTreeMap<String, Value>,
338}
339
340impl Operation {
341 pub fn request_schema(&self) -> Option<&SchemaValue> {
343 preferred_schema(&self.request_body.as_ref()?.media_types)
344 }
345
346 pub fn success_schema(&self) -> Option<&SchemaValue> {
349 preferred_schema(
350 &self
351 .responses
352 .iter()
353 .find(|response| response.status.starts_with('2'))?
354 .media_types,
355 )
356 }
357}
358
359fn preferred_schema(media_types: &[OperationMediaType]) -> Option<&SchemaValue> {
360 media_types
361 .iter()
362 .find(|media| {
363 media.content_type == "application/json" || media.content_type.ends_with("+json")
364 })
365 .or_else(|| media_types.first())?
366 .schema
367 .as_ref()
368}
369
370#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
372pub struct OperationRequestBody {
373 #[serde(default)]
374 pub required: bool,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub description: Option<String>,
377 #[serde(default, skip_serializing_if = "Vec::is_empty")]
378 pub media_types: Vec<OperationMediaType>,
379}
380
381#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
383pub struct OperationResponse {
384 pub status: String,
385 #[serde(default, skip_serializing_if = "Option::is_none")]
386 pub description: Option<String>,
387 #[serde(default, skip_serializing_if = "Vec::is_empty")]
388 pub media_types: Vec<OperationMediaType>,
389}
390
391impl OperationRequestBody {
392 pub fn json(schema: SchemaValue, required: bool) -> Self {
393 Self {
394 required,
395 description: None,
396 media_types: vec![OperationMediaType {
397 content_type: "application/json".into(),
398 schema: Some(schema),
399 }],
400 }
401 }
402}
403
404impl OperationResponse {
405 pub fn json(status: impl Into<String>, schema: SchemaValue) -> Self {
406 Self {
407 status: status.into(),
408 description: None,
409 media_types: vec![OperationMediaType {
410 content_type: "application/json".into(),
411 schema: Some(schema),
412 }],
413 }
414 }
415}
416
417#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
419pub struct OperationMediaType {
420 pub content_type: String,
421 #[serde(default, skip_serializing_if = "Option::is_none")]
422 pub schema: Option<SchemaValue>,
423}
424
425#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
429pub struct SecurityRequirement {
430 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
431 pub schemes: BTreeMap<String, Vec<String>>,
432}
433
434#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
440pub struct SecuritySchemeCatalog {
441 #[serde(default, skip_serializing_if = "Vec::is_empty")]
442 pub schemes: Vec<SecurityScheme>,
443}
444
445#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
447pub struct SecurityScheme {
448 pub name: String,
449 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub description: Option<String>,
451 pub kind: SecuritySchemeKind,
452}
453
454#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
458pub enum SecuritySchemeKind {
459 ApiKey {
460 name: Option<String>,
461 location: Option<String>,
462 },
463 Http {
464 scheme: Option<String>,
465 bearer_format: Option<String>,
466 },
467 OAuth2 {
468 flows: Vec<OAuthFlow>,
469 metadata_url: Option<String>,
470 },
471 OpenIdConnect {
472 discovery_url: Option<String>,
473 },
474 Other {
475 type_name: String,
476 },
477}
478
479#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
481pub struct OAuthFlow {
482 #[serde(default, skip_serializing_if = "Option::is_none")]
483 pub device_authorization_url: Option<String>,
484 pub flow_type: String,
485 #[serde(default, skip_serializing_if = "Option::is_none")]
486 pub authorization_url: Option<String>,
487 #[serde(default, skip_serializing_if = "Option::is_none")]
488 pub token_url: Option<String>,
489 #[serde(default, skip_serializing_if = "Option::is_none")]
490 pub refresh_url: Option<String>,
491 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
492 pub scopes: BTreeMap<String, String>,
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498
499 #[test]
500 fn operation_schema_selection_uses_declared_json_media() {
501 let mut operation = Operation {
502 request_body: Some(OperationRequestBody::json(
503 SchemaValue::reference("Input"),
504 true,
505 )),
506 responses: vec![OperationResponse::json(
507 "200",
508 SchemaValue::reference("Output"),
509 )],
510 ..Operation::default()
511 };
512 operation.request_body.as_mut().unwrap().media_types.insert(
513 0,
514 OperationMediaType {
515 content_type: "text/plain".into(),
516 schema: Some(SchemaValue::new(SchemaKind::String)),
517 },
518 );
519 operation.responses[0].media_types.insert(
520 0,
521 OperationMediaType {
522 content_type: "text/plain".into(),
523 schema: Some(SchemaValue::new(SchemaKind::String)),
524 },
525 );
526 assert_eq!(
527 operation.request_schema().unwrap().kind.reference_name(),
528 Some("Input")
529 );
530 assert_eq!(
531 operation.success_schema().unwrap().kind.reference_name(),
532 Some("Output")
533 );
534 }
535
536 #[test]
537 fn missing_or_empty_success_never_uses_an_error_schema() {
538 let mut operation = Operation {
539 responses: vec![OperationResponse::json(
540 "default",
541 SchemaValue::reference("Error"),
542 )],
543 ..Operation::default()
544 };
545 assert!(operation.success_schema().is_none());
546 operation.responses.insert(
547 0,
548 OperationResponse {
549 status: "204".into(),
550 description: None,
551 media_types: vec![],
552 },
553 );
554 assert!(operation.success_schema().is_none());
555 assert!(operation.request_schema().is_none());
556 }
557
558 #[test]
559 fn preserves_a_recursive_discriminated_union() {
560 let value = SchemaValue {
561 nullable: true,
562 discriminator: Some(Discriminator {
563 property_name: "kind".into(),
564 mapping: BTreeMap::from([("cat".into(), "#/components/schemas/Cat".into())]),
565 }),
566 ..SchemaValue::new(SchemaKind::OneOf {
567 variants: vec![SchemaValue::reference("#/components/schemas/Cat")],
568 })
569 };
570 let round_trip: SchemaValue =
571 serde_json::from_str(&serde_json::to_string(&value).unwrap()).unwrap();
572 assert_eq!(round_trip, value);
573 assert_eq!(value.kind.reference_name(), None);
574 }
575}
576
577#[cfg(test)]
578mod method_tests {
579 use super::*;
580 #[test]
581 fn custom_methods_preserve_case_and_string_serialization() {
582 for token in ["GET", "COPY", "x-Custom", "X!#$%&'*+-.^_`|~"] {
583 let method = HttpMethod::parse(token).unwrap();
584 assert_eq!(method.as_str(), token);
585 let encoded = serde_json::to_string(&method).unwrap();
586 assert_eq!(
587 serde_json::from_str::<HttpMethod>(&encoded).unwrap(),
588 method
589 );
590 }
591 for token in [
592 "",
593 "BAD METHOD",
594 "BAD\r\nMETHOD",
595 "Méthod",
596 "X/Path",
597 "get",
598 "pOst",
599 ] {
600 assert!(HttpMethod::parse(token).is_err());
601 }
602 assert!(HttpMethod::parse(&"X".repeat(257)).is_err());
603 }
604}