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}