Skip to main content

kynos_openapi/model/
extensions.rs

1//! Specification extensions (`x-` prefixed fields).
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::Map;
7
8/// The prefix every specification extension field name must carry.
9pub const EXTENSION_PREFIX: &str = "x-";
10
11/// Prefixes reserved by the OpenAPI Initiative.
12///
13/// A description that is not itself an OAI publication must not use these.
14pub const RESERVED_EXTENSION_PREFIXES: &[&str] = &["x-oai-", "x-oas-"];
15
16/// Implementation-defined fields attached to an object.
17///
18/// Most objects in the model carry one of these flattened into their
19/// serialization. Two do not, because the specification forbids it: the
20/// Reference Object and the Security Requirement Object.
21///
22/// Keys are *not* checked on construction — [`crate::validate`] reports
23/// non-conforming names, so that a description parsed from an external source
24/// round-trips rather than being silently rewritten.
25#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(transparent)]
27pub struct Extensions(pub Map<Value>);
28
29impl Extensions {
30    /// Creates an empty set of extensions.
31    #[must_use]
32    pub fn new() -> Self {
33        Self(Map::new())
34    }
35
36    /// Returns `true` when no extension is present.
37    #[must_use]
38    pub fn is_empty(&self) -> bool {
39        self.0.is_empty()
40    }
41
42    /// Inserts an extension, returning the previous value for that key.
43    ///
44    /// The `x-` prefix is not added for you; pass the full field name.
45    pub fn insert(&mut self, key: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
46        self.0.insert(key.into(), value.into())
47    }
48
49    /// Looks up an extension by its full field name.
50    #[must_use]
51    pub fn get(&self, key: &str) -> Option<&Value> {
52        self.0.get(key)
53    }
54
55    /// Removes an extension, returning its value.
56    ///
57    /// Removal preserves the order of the remaining entries, which is what
58    /// keeps an emitted description byte-stable across an edit. Owning that
59    /// choice here is the point: a caller reaching through to the map would
60    /// have to make it, and could make it differently each time.
61    pub fn remove(&mut self, key: &str) -> Option<Value> {
62        self.0.shift_remove(key)
63    }
64
65    /// Returns `true` when `name` is a well-formed extension field name that is
66    /// not reserved by the OpenAPI Initiative.
67    #[must_use]
68    pub fn is_valid_name(name: &str) -> bool {
69        name.starts_with(EXTENSION_PREFIX)
70            && !RESERVED_EXTENSION_PREFIXES
71                .iter()
72                .any(|reserved| name.starts_with(reserved))
73    }
74}
75
76#[cfg(test)]
77mod tests;