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;