Skip to main content

openapi_to_rust/
extensions.rs

1//! Specification Extensions support — `x-*` fields per OAS §"Specification Extensions".
2//!
3//! `Extensions` is a compatibility-oriented flatten target. It retains `x-*`
4//! specification extensions and other leftover keys so imperfect real-world
5//! documents continue to parse. Callers can inspect [`Extensions::non_extension_keys`]
6//! when they want to diagnose fields outside the OAS extension convention.
7//!
8//! Use it on every spec struct that previously had
9//! `#[serde(flatten)] pub extra: BTreeMap<String, Value>`:
10//!
11//! ```ignore
12//! #[derive(Deserialize)]
13//! struct Foo {
14//!     name: String,
15//!     #[serde(flatten, default)]
16//!     extensions: Extensions,
17//! }
18//! ```
19//!
20//! `Schema` and `SchemaDetails` also retain a loose `extra` map. Their common
21//! JSON Schema 2020-12 keywords are typed, while the open JSON Schema
22//! vocabulary still requires compatibility storage for unknown keywords.
23
24use serde::de::{Deserializer, MapAccess, Visitor};
25use serde::ser::Serializer;
26use serde::{Deserialize, Serialize};
27use serde_json::Value;
28use std::collections::BTreeMap;
29use std::fmt;
30use std::ops::{Deref, DerefMut};
31
32/// Map of specification extensions and other compatibility-preserved fields.
33#[derive(Debug, Clone, Default, PartialEq)]
34pub struct Extensions(pub BTreeMap<String, Value>);
35
36impl Extensions {
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    pub fn get(&self, key: &str) -> Option<&Value> {
42        self.0.get(key)
43    }
44
45    pub fn contains_key(&self, key: &str) -> bool {
46        self.0.contains_key(key)
47    }
48
49    pub fn is_empty(&self) -> bool {
50        self.0.is_empty()
51    }
52
53    pub fn len(&self) -> usize {
54        self.0.len()
55    }
56
57    pub fn iter(&self) -> std::collections::btree_map::Iter<'_, String, Value> {
58        self.0.iter()
59    }
60}
61
62impl Deref for Extensions {
63    type Target = BTreeMap<String, Value>;
64    fn deref(&self) -> &Self::Target {
65        &self.0
66    }
67}
68
69impl DerefMut for Extensions {
70    fn deref_mut(&mut self) -> &mut Self::Target {
71        &mut self.0
72    }
73}
74
75impl Serialize for Extensions {
76    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
77        self.0.serialize(s)
78    }
79}
80
81impl<'de> Deserialize<'de> for Extensions {
82    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
83        struct ExtVisitor;
84
85        impl<'de> Visitor<'de> for ExtVisitor {
86            type Value = Extensions;
87
88            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89                f.write_str("a map of extension and compatibility fields")
90            }
91
92            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
93            where
94                A: MapAccess<'de>,
95            {
96                // We accept any leftover keys here so real-world specs that
97                // sprinkle non-`x-` fields in places they don't belong (we've
98                // observed `produces`, `in`, `type`, `density`, `title`,
99                // `description` on the wrong objects) still parse. Consumers
100                // can inspect non-`x-` keys via `non_extension_keys`.
101                let mut out: BTreeMap<String, Value> = BTreeMap::new();
102                while let Some(key) = map.next_key::<String>()? {
103                    let value: Value = map.next_value()?;
104                    out.insert(key, value);
105                }
106                Ok(Extensions(out))
107            }
108        }
109
110        d.deserialize_map(ExtVisitor)
111    }
112}
113
114impl Extensions {
115    /// Iterate keys that don't follow the OAS `x-*` extension convention.
116    /// These are typically OAS 2.0 leftovers (`produces`/`consumes`) or
117    /// fields placed on the wrong object level. They are retained rather than
118    /// rejected at deserialize time.
119    pub fn non_extension_keys(&self) -> impl Iterator<Item = &str> {
120        self.0
121            .keys()
122            .filter(|k| !k.starts_with("x-"))
123            .map(String::as_str)
124    }
125}