ocpi_kit/types/extensions.rs
1//! `Extensions` — the map that keeps JSON fields this crate has never heard of.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7use super::validate::{Validate, Validator};
8
9/// Undocumented JSON fields found on an OCPI object, preserved verbatim.
10///
11/// OCPI 2.3.0 is explicit:
12///
13/// > *An OCPI Platform SHALL NOT reject request or response payloads based on the presence of
14/// > JSON object field names that are not documented in this specification.*
15/// >
16/// > *OCPI implementers are encouraged to extend OCPI with new fields to address needs that are
17/// > not foreseen by the specification.*
18///
19/// Every OCPI object in this crate carries an `extensions` field marked `#[serde(flatten)]`, so
20/// a vendor field arrives, survives, and is written back out unchanged. A hub built on this
21/// crate can therefore sit between two parties that have agreed on an extension without knowing
22/// anything about it — which is the whole point of that paragraph, and the thing generated type
23/// sets get wrong.
24///
25/// Keys are kept in a [`BTreeMap`], so serialisation order is deterministic.
26///
27/// Every wire object in this crate carries one as a `#[serde(flatten)]` field, so the undocumented
28/// members of the object it came from land here and are written straight back:
29///
30/// ```
31/// use ocpi_kit::types::Extensions;
32///
33/// let json = r#"{"acme_note":"kerbside","nltnm_accuracy_m":3}"#;
34/// let extensions: Extensions = serde_json::from_str(json).unwrap();
35///
36/// assert_eq!(extensions.get::<u32>("nltnm_accuracy_m").unwrap(), Some(3));
37/// assert_eq!(serde_json::to_string(&extensions).unwrap(), json);
38/// ```
39///
40/// In place, on a real object:
41///
42#[cfg_attr(feature = "v2_3_0", doc = "```rust")]
43#[cfg_attr(not(feature = "v2_3_0"), doc = "```rust,ignore")]
44/// # use ocpi_kit::v2_3_0::locations::GeoLocation;
45/// let json = r#"{"latitude":"52.010","longitude":"4.350","nltnm_accuracy_m":3}"#;
46/// let geo: GeoLocation = serde_json::from_str(json).unwrap();
47/// assert_eq!(geo.extensions.get::<u32>("nltnm_accuracy_m").unwrap(), Some(3));
48/// assert_eq!(serde_json::to_string(&geo).unwrap(), json);
49/// ```
50///
51/// Spec: 2.3.0 §transport_and_format — Non-specified JSON fields
52#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
53#[serde(transparent)]
54pub struct Extensions(BTreeMap<String, serde_json::Value>);
55
56impl Extensions {
57 /// An empty set of extensions.
58 #[must_use]
59 pub fn new() -> Self {
60 Self::default()
61 }
62
63 /// Whether no undocumented field was present.
64 ///
65 /// Objects skip serialising their `extensions` field when this is true, so an object that
66 /// carried no extensions is written back byte-identically.
67 #[must_use]
68 pub fn is_empty(&self) -> bool {
69 self.0.is_empty()
70 }
71
72 /// How many undocumented fields are present.
73 #[must_use]
74 pub fn len(&self) -> usize {
75 self.0.len()
76 }
77
78 /// The raw JSON value stored under `key`, if any.
79 #[must_use]
80 pub fn get_raw(&self, key: &str) -> Option<&serde_json::Value> {
81 self.0.get(key)
82 }
83
84 /// Deserialises the value stored under `key` into `T`.
85 ///
86 /// Returns `Ok(None)` when the key is absent, and `Err` when it is present but does not
87 /// deserialise into `T`.
88 ///
89 /// # Errors
90 ///
91 /// Propagates the `serde_json` error describing why the value did not fit `T`.
92 pub fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Result<Option<T>, serde_json::Error> {
93 self.0.get(key).cloned().map(serde_json::from_value).transpose()
94 }
95
96 /// Stores `value` under `key`, replacing any previous value.
97 ///
98 /// # Errors
99 ///
100 /// Propagates the `serde_json` error if `value` cannot be serialised.
101 pub fn insert<T: Serialize>(
102 &mut self,
103 key: impl Into<String>,
104 value: T,
105 ) -> Result<(), serde_json::Error> {
106 self.0.insert(key.into(), serde_json::to_value(value)?);
107 Ok(())
108 }
109
110 /// Removes and returns the raw value stored under `key`.
111 pub fn remove(&mut self, key: &str) -> Option<serde_json::Value> {
112 self.0.remove(key)
113 }
114
115 /// Whether `key` is present.
116 #[must_use]
117 pub fn contains_key(&self, key: &str) -> bool {
118 self.0.contains_key(key)
119 }
120
121 /// The undocumented fields, in key order.
122 pub fn iter(&self) -> impl Iterator<Item = (&String, &serde_json::Value)> {
123 self.0.iter()
124 }
125
126 /// The field names, in order.
127 pub fn keys(&self) -> impl Iterator<Item = &String> {
128 self.0.keys()
129 }
130}
131
132impl Validate for Extensions {
133 // Undocumented fields carry no spec constraints by definition.
134 fn validate_in(&self, _v: &mut Validator) {}
135}
136
137impl<K: Into<String>, V: Into<serde_json::Value>> FromIterator<(K, V)> for Extensions {
138 fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
139 Self(iter.into_iter().map(|(k, v)| (k.into(), v.into())).collect())
140 }
141}
142
143impl<'a> IntoIterator for &'a Extensions {
144 type Item = (&'a String, &'a serde_json::Value);
145 type IntoIter = std::collections::btree_map::Iter<'a, String, serde_json::Value>;
146 fn into_iter(self) -> Self::IntoIter {
147 self.0.iter()
148 }
149}
150
151#[cfg(feature = "schema")]
152impl schemars::JsonSchema for Extensions {
153 fn schema_name() -> std::borrow::Cow<'static, str> {
154 "Extensions".into()
155 }
156 fn json_schema(_g: &mut schemars::SchemaGenerator) -> schemars::Schema {
157 schemars::json_schema!({
158 "type": "object",
159 "additionalProperties": true,
160 "description": "Undocumented JSON fields, preserved verbatim",
161 })
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn typed_access_round_trips() {
171 let mut ext = Extensions::new();
172 ext.insert("nltnm_rank", 7u32).unwrap();
173 assert_eq!(ext.get::<u32>("nltnm_rank").unwrap(), Some(7));
174 assert_eq!(ext.get::<u32>("absent").unwrap(), None);
175 assert!(ext.get::<String>("nltnm_rank").is_err(), "type mismatch is an error");
176 }
177
178 #[test]
179 fn empty_extensions_are_invisible() {
180 let ext = Extensions::new();
181 assert!(ext.is_empty());
182 assert_eq!(serde_json::to_string(&ext).unwrap(), "{}");
183 }
184}