Skip to main content

openapi_to_rust/
extensions.rs

1//! Specification Extensions support — `x-*` fields per OAS §"Specification Extensions".
2//!
3//! `Extensions` is a flatten target that accepts only keys with the `x-` prefix.
4//! Any other unknown key triggers a deserialize error, surfacing what the audit
5//! found to be the architectural root cause: every spec struct silently
6//! swallowed unknown fields into a generic `BTreeMap`.
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//! Note: `Schema` and `SchemaDetails` are intentionally left with the loose
21//! `extra: BTreeMap<...>` for now — analysis.rs reads JSON Schema 2020-12
22//! keywords (`patternProperties`, `propertyNames`, `dependentRequired`,
23//! `if`/`then`/`else`, etc.) directly from there. They are graduated to typed
24//! fields under the J5–J8 beads (Phase 2b).
25
26use serde::de::{Deserializer, MapAccess, Visitor};
27use serde::ser::Serializer;
28use serde::{Deserialize, Serialize};
29use serde_json::Value;
30use std::collections::BTreeMap;
31use std::fmt;
32use std::ops::{Deref, DerefMut};
33
34/// Map of `x-*` specification extensions. Any non-`x-`-prefixed key fails to
35/// deserialize.
36#[derive(Debug, Clone, Default, PartialEq)]
37pub struct Extensions(pub BTreeMap<String, Value>);
38
39impl Extensions {
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    pub fn get(&self, key: &str) -> Option<&Value> {
45        self.0.get(key)
46    }
47
48    pub fn contains_key(&self, key: &str) -> bool {
49        self.0.contains_key(key)
50    }
51
52    pub fn is_empty(&self) -> bool {
53        self.0.is_empty()
54    }
55
56    pub fn len(&self) -> usize {
57        self.0.len()
58    }
59
60    pub fn iter(&self) -> std::collections::btree_map::Iter<'_, String, Value> {
61        self.0.iter()
62    }
63}
64
65impl Deref for Extensions {
66    type Target = BTreeMap<String, Value>;
67    fn deref(&self) -> &Self::Target {
68        &self.0
69    }
70}
71
72impl DerefMut for Extensions {
73    fn deref_mut(&mut self) -> &mut Self::Target {
74        &mut self.0
75    }
76}
77
78impl Serialize for Extensions {
79    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
80        self.0.serialize(s)
81    }
82}
83
84impl<'de> Deserialize<'de> for Extensions {
85    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
86        struct ExtVisitor;
87
88        impl<'de> Visitor<'de> for ExtVisitor {
89            type Value = Extensions;
90
91            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92                f.write_str("a map of OAS specification extensions (`x-*` keys)")
93            }
94
95            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
96            where
97                A: MapAccess<'de>,
98            {
99                // We accept any leftover keys here so real-world specs that
100                // sprinkle non-`x-` fields in places they don't belong (we've
101                // observed `produces`, `in`, `type`, `density`, `title`,
102                // `description` on the wrong objects) still parse. The CLI
103                // surfaces non-`x-` keys as warnings via `non_extension_keys`
104                // so silent drops still get noticed.
105                let mut out: BTreeMap<String, Value> = BTreeMap::new();
106                while let Some(key) = map.next_key::<String>()? {
107                    let value: Value = map.next_value()?;
108                    out.insert(key, value);
109                }
110                Ok(Extensions(out))
111            }
112        }
113
114        d.deserialize_map(ExtVisitor)
115    }
116}
117
118impl Extensions {
119    /// Iterate keys that don't follow the OAS `x-*` extension convention.
120    /// These are typically OAS 2.0 leftovers (`produces`/`consumes`) or
121    /// fields placed on the wrong object level. The CLI prints them as a
122    /// warning so silent drops remain visible even though we no longer
123    /// reject them at deserialize time.
124    pub fn non_extension_keys(&self) -> impl Iterator<Item = &str> {
125        self.0
126            .keys()
127            .filter(|k| !k.starts_with("x-"))
128            .map(String::as_str)
129    }
130}