Skip to main content

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}