jacquard_common/types/
value.rs

1use crate::{
2    IntoStatic,
3    types::{DataModelType, LexiconStringType, UriType, blob::Blob, string::*},
4};
5use bytes::Bytes;
6use ipld_core::ipld::Ipld;
7use smol_str::{SmolStr, ToSmolStr};
8use std::collections::BTreeMap;
9
10/// Conversion utilities for Data types
11pub mod convert;
12/// String parsing for AT Protocol types
13pub mod parsing;
14/// Serde implementations for Data types
15pub mod serde_impl;
16
17#[cfg(test)]
18mod tests;
19
20/// AT Protocol data model value
21///
22/// Represents any valid value in the AT Protocol data model, which supports JSON and CBOR
23/// serialization with specific constraints (no floats, CID links, blobs with metadata).
24///
25/// This is the generic "unknown data" type used for lexicon values, extra fields captured
26/// by `#[lexicon]`, and IPLD data structures.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum Data<'s> {
29    /// Null value
30    Null,
31    /// Boolean value
32    Boolean(bool),
33    /// Integer value (no floats in AT Protocol)
34    Integer(i64),
35    /// String value (parsed into specific AT Protocol types when possible)
36    String(AtprotoStr<'s>),
37    /// Raw bytes
38    Bytes(Bytes),
39    /// CID link reference
40    CidLink(Cid<'s>),
41    /// Array of values
42    Array(Array<'s>),
43    /// Object/map of values
44    Object(Object<'s>),
45    /// Blob reference with metadata
46    Blob(Blob<'s>),
47}
48
49/// Errors that can occur when working with AT Protocol data
50#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
51pub enum AtDataError {
52    /// Floating point numbers are not allowed in AT Protocol
53    #[error("floating point numbers not allowed in AT protocol data")]
54    FloatNotAllowed,
55}
56
57impl<'s> Data<'s> {
58    /// Get the data model type of this value
59    pub fn data_type(&self) -> DataModelType {
60        match self {
61            Data::Null => DataModelType::Null,
62            Data::Boolean(_) => DataModelType::Boolean,
63            Data::Integer(_) => DataModelType::Integer,
64            Data::String(s) => match s {
65                AtprotoStr::Datetime(_) => DataModelType::String(LexiconStringType::Datetime),
66                AtprotoStr::Language(_) => DataModelType::String(LexiconStringType::Language),
67                AtprotoStr::Tid(_) => DataModelType::String(LexiconStringType::Tid),
68                AtprotoStr::Nsid(_) => DataModelType::String(LexiconStringType::Nsid),
69                AtprotoStr::Did(_) => DataModelType::String(LexiconStringType::Did),
70                AtprotoStr::Handle(_) => DataModelType::String(LexiconStringType::Handle),
71                AtprotoStr::AtIdentifier(_) => {
72                    DataModelType::String(LexiconStringType::AtIdentifier)
73                }
74                AtprotoStr::AtUri(_) => DataModelType::String(LexiconStringType::AtUri),
75                AtprotoStr::Uri(uri) => match uri {
76                    Uri::Did(_) => DataModelType::String(LexiconStringType::Uri(UriType::Did)),
77                    Uri::At(_) => DataModelType::String(LexiconStringType::Uri(UriType::At)),
78                    Uri::Https(_) => DataModelType::String(LexiconStringType::Uri(UriType::Https)),
79                    Uri::Wss(_) => DataModelType::String(LexiconStringType::Uri(UriType::Wss)),
80                    Uri::Cid(_) => DataModelType::String(LexiconStringType::Uri(UriType::Cid)),
81                    Uri::Any(_) => DataModelType::String(LexiconStringType::Uri(UriType::Any)),
82                },
83                AtprotoStr::Cid(_) => DataModelType::String(LexiconStringType::Cid),
84                AtprotoStr::RecordKey(_) => DataModelType::String(LexiconStringType::RecordKey),
85                AtprotoStr::String(_) => DataModelType::String(LexiconStringType::String),
86            },
87            Data::Bytes(_) => DataModelType::Bytes,
88            Data::CidLink(_) => DataModelType::CidLink,
89            Data::Array(_) => DataModelType::Array,
90            Data::Object(_) => DataModelType::Object,
91            Data::Blob(_) => DataModelType::Blob,
92        }
93    }
94    /// Parse a Data value from a JSON value
95    pub fn from_json(json: &'s serde_json::Value) -> Result<Self, AtDataError> {
96        Ok(if let Some(value) = json.as_bool() {
97            Self::Boolean(value)
98        } else if let Some(value) = json.as_i64() {
99            Self::Integer(value)
100        } else if let Some(value) = json.as_str() {
101            Self::String(parsing::parse_string(value))
102        } else if let Some(value) = json.as_array() {
103            Self::Array(Array::from_json(value)?)
104        } else if let Some(value) = json.as_object() {
105            Object::from_json(value)?
106        } else if json.is_f64() {
107            return Err(AtDataError::FloatNotAllowed);
108        } else {
109            Self::Null
110        })
111    }
112
113    /// Parse a Data value from an IPLD value (CBOR)
114    pub fn from_cbor(cbor: &'s Ipld) -> Result<Self, AtDataError> {
115        Ok(match cbor {
116            Ipld::Null => Data::Null,
117            Ipld::Bool(bool) => Data::Boolean(*bool),
118            Ipld::Integer(int) => Data::Integer(*int as i64),
119            Ipld::Float(_) => {
120                return Err(AtDataError::FloatNotAllowed);
121            }
122            Ipld::String(string) => Self::String(parsing::parse_string(string)),
123            Ipld::Bytes(items) => Self::Bytes(Bytes::copy_from_slice(items.as_slice())),
124            Ipld::List(iplds) => Self::Array(Array::from_cbor(iplds)?),
125            Ipld::Map(btree_map) => Object::from_cbor(btree_map)?,
126            Ipld::Link(cid) => Self::CidLink(Cid::ipld(*cid)),
127        })
128    }
129}
130
131impl IntoStatic for Data<'_> {
132    type Output = Data<'static>;
133    fn into_static(self) -> Data<'static> {
134        match self {
135            Data::Null => Data::Null,
136            Data::Boolean(bool) => Data::Boolean(bool),
137            Data::Integer(int) => Data::Integer(int),
138            Data::String(string) => Data::String(string.into_static()),
139            Data::Bytes(bytes) => Data::Bytes(bytes),
140            Data::Array(array) => Data::Array(array.into_static()),
141            Data::Object(object) => Data::Object(object.into_static()),
142            Data::CidLink(cid) => Data::CidLink(cid.into_static()),
143            Data::Blob(blob) => Data::Blob(blob.into_static()),
144        }
145    }
146}
147
148/// Array of AT Protocol data values
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct Array<'s>(pub Vec<Data<'s>>);
151
152impl IntoStatic for Array<'_> {
153    type Output = Array<'static>;
154    fn into_static(self) -> Array<'static> {
155        Array(self.0.into_static())
156    }
157}
158
159impl<'s> Array<'s> {
160    /// Parse an array from JSON values
161    pub fn from_json(json: &'s Vec<serde_json::Value>) -> Result<Self, AtDataError> {
162        let mut array = Vec::with_capacity(json.len());
163        for item in json {
164            array.push(Data::from_json(item)?);
165        }
166        Ok(Self(array))
167    }
168    /// Parse an array from IPLD values (CBOR)
169    pub fn from_cbor(cbor: &'s Vec<Ipld>) -> Result<Self, AtDataError> {
170        let mut array = Vec::with_capacity(cbor.len());
171        for item in cbor {
172            array.push(Data::from_cbor(item)?);
173        }
174        Ok(Self(array))
175    }
176}
177
178/// Object/map of AT Protocol data values
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct Object<'s>(pub BTreeMap<SmolStr, Data<'s>>);
181
182impl IntoStatic for Object<'_> {
183    type Output = Object<'static>;
184    fn into_static(self) -> Object<'static> {
185        Object(self.0.into_static())
186    }
187}
188
189impl<'s> Object<'s> {
190    /// Parse an object from a JSON map with type inference
191    ///
192    /// Uses key names to infer the appropriate AT Protocol types for values.
193    pub fn from_json(
194        json: &'s serde_json::Map<String, serde_json::Value>,
195    ) -> Result<Data<'s>, AtDataError> {
196        if let Some(type_field) = json.get("$type").and_then(|v| v.as_str()) {
197            if parsing::infer_from_type(type_field) == DataModelType::Blob {
198                if let Some(blob) = parsing::json_to_blob(json) {
199                    return Ok(Data::Blob(blob));
200                }
201            }
202        }
203        let mut map = BTreeMap::new();
204
205        for (key, value) in json {
206            if key == "$type" {
207                map.insert(key.to_smolstr(), Data::from_json(value)?);
208            }
209            match parsing::string_key_type_guess(key) {
210                DataModelType::Null if value.is_null() => {
211                    map.insert(key.to_smolstr(), Data::Null);
212                }
213                DataModelType::Boolean if value.is_boolean() => {
214                    map.insert(key.to_smolstr(), Data::Boolean(value.as_bool().unwrap()));
215                }
216                DataModelType::Integer if value.is_i64() => {
217                    map.insert(key.to_smolstr(), Data::Integer(value.as_i64().unwrap()));
218                }
219                DataModelType::Bytes if value.is_string() => {
220                    map.insert(
221                        key.to_smolstr(),
222                        parsing::decode_bytes(value.as_str().unwrap()),
223                    );
224                }
225                DataModelType::CidLink => {
226                    if let Some(value) = value.as_object() {
227                        if let Some(value) = value.get("$link").and_then(|v| v.as_str()) {
228                            map.insert(key.to_smolstr(), Data::CidLink(Cid::Str(value.into())));
229                        } else {
230                            map.insert(key.to_smolstr(), Object::from_json(value)?);
231                        }
232                    } else {
233                        map.insert(key.to_smolstr(), Data::from_json(value)?);
234                    }
235                }
236                DataModelType::Blob if value.is_object() => {
237                    map.insert(
238                        key.to_smolstr(),
239                        Object::from_json(value.as_object().unwrap())?,
240                    );
241                }
242                DataModelType::Array if value.is_array() => {
243                    map.insert(
244                        key.to_smolstr(),
245                        Data::Array(Array::from_json(value.as_array().unwrap())?),
246                    );
247                }
248                DataModelType::Object if value.is_object() => {
249                    map.insert(
250                        key.to_smolstr(),
251                        Object::from_json(value.as_object().unwrap())?,
252                    );
253                }
254                DataModelType::String(string_type) if value.is_string() => {
255                    parsing::insert_string(&mut map, key, value.as_str().unwrap(), string_type)?;
256                }
257                _ => {
258                    map.insert(key.to_smolstr(), Data::from_json(value)?);
259                }
260            }
261        }
262
263        Ok(Data::Object(Object(map)))
264    }
265
266    /// Parse an object from IPLD (CBOR) with type inference
267    ///
268    /// Uses key names to infer the appropriate AT Protocol types for values.
269    pub fn from_cbor(cbor: &'s BTreeMap<String, Ipld>) -> Result<Data<'s>, AtDataError> {
270        if let Some(Ipld::String(type_field)) = cbor.get("$type") {
271            if parsing::infer_from_type(type_field) == DataModelType::Blob {
272                if let Some(blob) = parsing::cbor_to_blob(cbor) {
273                    return Ok(Data::Blob(blob));
274                }
275            }
276        }
277        let mut map = BTreeMap::new();
278
279        for (key, value) in cbor {
280            if key == "$type" {
281                map.insert(key.to_smolstr(), Data::from_cbor(value)?);
282            }
283            match (parsing::string_key_type_guess(key), value) {
284                (DataModelType::Null, Ipld::Null) => {
285                    map.insert(key.to_smolstr(), Data::Null);
286                }
287                (DataModelType::Boolean, Ipld::Bool(value)) => {
288                    map.insert(key.to_smolstr(), Data::Boolean(*value));
289                }
290                (DataModelType::Integer, Ipld::Integer(int)) => {
291                    map.insert(key.to_smolstr(), Data::Integer(*int as i64));
292                }
293                (DataModelType::Bytes, Ipld::Bytes(value)) => {
294                    map.insert(key.to_smolstr(), Data::Bytes(Bytes::copy_from_slice(value)));
295                }
296                (DataModelType::Blob, Ipld::Map(value)) => {
297                    map.insert(key.to_smolstr(), Object::from_cbor(value)?);
298                }
299                (DataModelType::Array, Ipld::List(value)) => {
300                    map.insert(key.to_smolstr(), Data::Array(Array::from_cbor(value)?));
301                }
302                (DataModelType::Object, Ipld::Map(value)) => {
303                    map.insert(key.to_smolstr(), Object::from_cbor(value)?);
304                }
305                (DataModelType::String(string_type), Ipld::String(value)) => {
306                    parsing::insert_string(&mut map, key, value, string_type)?;
307                }
308                _ => {
309                    map.insert(key.to_smolstr(), Data::from_cbor(value)?);
310                }
311            }
312        }
313
314        Ok(Data::Object(Object(map)))
315    }
316}
317
318/// Level 1 deserialization of raw atproto data
319///
320/// Maximally permissive with zero inference for cases where you just want to pass through the data
321/// and don't necessarily care if it's totally valid, or you want to validate later.
322/// E.g. lower-level services, PDS implementations, firehose indexers, relay implementations.
323#[derive(Debug, Clone, PartialEq, Eq)]
324pub enum RawData<'s> {
325    /// Null value
326    Null,
327    /// Boolean value
328    Boolean(bool),
329    /// Signed integer
330    SignedInt(i64),
331    /// Unsigned integer
332    UnsignedInt(u64),
333    /// String value (no type inference)
334    String(CowStr<'s>),
335    /// Raw bytes
336    Bytes(Bytes),
337    /// CID link reference
338    CidLink(Cid<'s>),
339    /// Array of raw values
340    Array(Vec<RawData<'s>>),
341    /// Object/map of raw values
342    Object(BTreeMap<SmolStr, RawData<'s>>),
343    /// Valid blob reference
344    Blob(Blob<'s>),
345    /// Invalid blob structure (captured for debugging)
346    InvalidBlob(Box<RawData<'s>>),
347    /// Invalid number format, generally a floating point number (captured as bytes)
348    InvalidNumber(Bytes),
349    /// Invalid/unknown data (captured as bytes)
350    InvalidData(Bytes),
351}