r402_core/wire/
extensions.rs1use std::collections::HashMap;
10
11use compact_str::CompactString;
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(transparent)]
34pub struct Extensions(HashMap<CompactString, ExtensionEntry>);
35
36impl Extensions {
37 #[must_use]
39 pub fn new() -> Self {
40 Self(HashMap::new())
41 }
42
43 #[must_use]
45 pub fn is_empty(&self) -> bool {
46 self.0.is_empty()
47 }
48
49 #[must_use]
51 pub fn len(&self) -> usize {
52 self.0.len()
53 }
54
55 #[must_use]
57 pub fn get(&self, id: &str) -> Option<&ExtensionEntry> {
58 self.0.get(id)
59 }
60
61 pub fn insert(&mut self, id: impl Into<CompactString>, entry: ExtensionEntry) {
63 let _ = self.0.insert(id.into(), entry);
64 }
65
66 #[must_use]
68 pub fn remove(&mut self, id: &str) -> Option<ExtensionEntry> {
69 self.0.remove(id)
70 }
71
72 pub fn iter(&self) -> impl Iterator<Item = (&CompactString, &ExtensionEntry)> {
74 self.0.iter()
75 }
76}
77
78impl<K, V> FromIterator<(K, V)> for Extensions
79where
80 K: Into<CompactString>,
81 V: Into<ExtensionEntry>,
82{
83 fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
84 Self(
85 iter.into_iter()
86 .map(|(k, v)| (k.into(), v.into()))
87 .collect(),
88 )
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(untagged)]
107pub enum ExtensionEntry {
108 Structured {
110 info: Value,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
114 schema: Option<Value>,
115 },
116 Raw(Value),
118}
119
120impl ExtensionEntry {
121 #[must_use]
123 pub const fn info(info: Value) -> Self {
124 Self::Structured { info, schema: None }
125 }
126
127 #[must_use]
129 pub const fn with_schema(info: Value, schema: Value) -> Self {
130 Self::Structured {
131 info,
132 schema: Some(schema),
133 }
134 }
135
136 #[must_use]
138 pub const fn raw(value: Value) -> Self {
139 Self::Raw(value)
140 }
141
142 #[must_use]
144 pub const fn as_info(&self) -> Option<&Value> {
145 match self {
146 Self::Structured { info, .. } => Some(info),
147 Self::Raw(_) => None,
148 }
149 }
150
151 #[must_use]
153 pub const fn as_schema(&self) -> Option<&Value> {
154 match self {
155 Self::Structured { schema, .. } => schema.as_ref(),
156 Self::Raw(_) => None,
157 }
158 }
159
160 #[must_use]
162 pub fn to_value(&self) -> Value {
163 match self {
164 Self::Structured { info, schema } => {
165 let mut obj = serde_json::Map::new();
166 let _ = obj.insert("info".to_owned(), info.clone());
167 if let Some(schema) = schema {
168 let _ = obj.insert("schema".to_owned(), schema.clone());
169 }
170 Value::Object(obj)
171 }
172 Self::Raw(value) => value.clone(),
173 }
174 }
175}
176
177impl From<Value> for ExtensionEntry {
178 fn from(value: Value) -> Self {
179 Self::Raw(value)
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use serde_json::json;
186
187 use super::*;
188
189 #[test]
190 fn extensions_empty_by_default() {
191 let ext = Extensions::new();
192 assert!(ext.is_empty());
193 assert_eq!(serde_json::to_value(&ext).unwrap(), json!({}));
194 }
195
196 #[test]
197 fn structured_entry_roundtrip() {
198 let mut ext = Extensions::new();
199 ext.insert(
200 "bazaar",
201 ExtensionEntry::with_schema(json!({"registered": true}), json!({"type": "object"})),
202 );
203 let encoded = serde_json::to_value(&ext).unwrap();
204 assert_eq!(encoded["bazaar"]["info"]["registered"], true);
205 assert_eq!(encoded["bazaar"]["schema"]["type"], "object");
206 let decoded: Extensions = serde_json::from_value(encoded).unwrap();
207 assert_eq!(decoded, ext);
208 }
209
210 #[test]
211 fn raw_entry_roundtrip() {
212 let mut ext = Extensions::new();
213 ext.insert("custom", ExtensionEntry::raw(json!([1, 2, 3])));
214 let encoded = serde_json::to_value(&ext).unwrap();
215 assert_eq!(encoded["custom"], json!([1, 2, 3]));
216 let decoded: Extensions = serde_json::from_value(encoded).unwrap();
217 assert_eq!(decoded, ext);
218 }
219}