atrium_api/
types.rs

1//! Definitions for AT Protocol's data models.
2//! <https://atproto.com/specs/data-model>
3
4use crate::error::Error;
5use ipld_core::ipld::Ipld;
6use ipld_core::serde::to_ipld;
7use serde::{Deserialize, Serialize};
8use serde::{de, ser};
9use std::collections::BTreeMap;
10use std::fmt;
11use std::ops::{Deref, DerefMut};
12
13mod cid_link;
14pub use cid_link::CidLink;
15
16mod integer;
17pub use integer::*;
18
19pub mod string;
20use string::RecordKey;
21
22/// Trait for a collection of records that can be stored in a repository.
23///
24/// The records all have the same Lexicon schema.
25pub trait Collection: fmt::Debug {
26    /// The NSID for the Lexicon that defines the schema of records in this collection.
27    const NSID: &'static str;
28
29    /// This collection's record type.
30    type Record: fmt::Debug + de::DeserializeOwned + Serialize;
31
32    /// Returns the [`Nsid`] for the Lexicon that defines the schema of records in this
33    /// collection.
34    ///
35    /// This is a convenience method that parses [`Self::NSID`].
36    ///
37    /// # Panics
38    ///
39    /// Panics if [`Self::NSID`] is not a valid NSID.
40    ///
41    /// [`Nsid`]: string::Nsid
42    fn nsid() -> string::Nsid {
43        Self::NSID.parse().expect("Self::NSID should be a valid NSID")
44    }
45
46    /// Returns the repo path for a record in this collection with the given record key.
47    ///
48    /// Per the [Repo Data Structure v3] specification:
49    /// > Repo paths currently have a fixed structure of `<collection>/<record-key>`. This
50    /// > means a valid, normalized [`Nsid`], followed by a `/`, followed by a valid
51    /// > [`RecordKey`].
52    ///
53    /// [Repo Data Structure v3]: https://atproto.com/specs/repository#repo-data-structure-v3
54    /// [`Nsid`]: string::Nsid
55    fn repo_path(rkey: &RecordKey) -> String {
56        format!("{}/{}", Self::NSID, rkey.as_str())
57    }
58}
59
60/// Definitions for Blob types.
61/// Usually a map with `$type` is used, but deprecated legacy formats are also supported for parsing.
62/// <https://atproto.com/specs/data-model#blob-type>
63#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
64#[serde(untagged)]
65pub enum BlobRef {
66    Typed(TypedBlobRef),
67    Untyped(UnTypedBlobRef),
68}
69
70/// Current, typed blob reference.
71#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
72#[serde(tag = "$type", rename_all = "lowercase")]
73pub enum TypedBlobRef {
74    Blob(Blob),
75}
76
77/// An untyped blob reference.
78/// Some records in the wild still contain this format, but should never write them.
79#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
80#[serde(rename_all = "camelCase")]
81pub struct UnTypedBlobRef {
82    pub cid: String,
83    pub mime_type: String,
84}
85
86#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
87#[serde(rename_all = "camelCase")]
88pub struct Blob {
89    pub r#ref: CidLink,
90    pub mime_type: String,
91    pub size: usize, // TODO
92}
93
94/// A generic object type.
95#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
96pub struct Object<T> {
97    #[serde(flatten)]
98    pub data: T,
99    #[serde(flatten)]
100    pub extra_data: Ipld,
101}
102
103impl<T> From<T> for Object<T> {
104    fn from(data: T) -> Self {
105        Self { data, extra_data: Ipld::Map(std::collections::BTreeMap::new()) }
106    }
107}
108
109impl<T> Deref for Object<T> {
110    type Target = T;
111
112    fn deref(&self) -> &Self::Target {
113        &self.data
114    }
115}
116
117impl<T> DerefMut for Object<T> {
118    fn deref_mut(&mut self) -> &mut Self::Target {
119        &mut self.data
120    }
121}
122
123/// An "open" union type.
124#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
125#[serde(untagged)]
126pub enum Union<T> {
127    Refs(T),
128    Unknown(UnknownData),
129}
130
131/// Data with an unknown schema in an open [`Union`].
132///
133/// The data of variants represented by a map and include a `$type` field indicating the variant type.
134#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
135pub struct UnknownData {
136    #[serde(rename = "$type")]
137    pub r#type: String,
138    #[serde(flatten)]
139    pub data: Ipld,
140}
141
142/// Arbitrary data with no specific validation and no type-specific fields.
143///
144/// Corresponds to [the `unknown` field type].
145///
146/// [the `unknown` field type]: https://atproto.com/specs/lexicon#unknown
147///
148/// By using the [`TryFromUnknown`] trait, it is possible to convert to any type
149/// that implements [`DeserializeOwned`](serde::de::DeserializeOwned).
150///
151/// ```
152/// use atrium_api::types::{TryFromUnknown, Unknown};
153///
154/// #[derive(Debug, serde::Deserialize)]
155/// struct Foo {
156///     bar: i32,
157/// }
158///
159/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
160/// let value: Unknown = serde_json::from_str(r#"{"bar": 42}"#)?;
161/// println!("{value:?}"); // Object({"bar": DataModel(42)})
162///
163/// let foo = Foo::try_from_unknown(value)?;
164/// println!("{foo:?}"); // Foo { bar: 42 }
165/// #     Ok(())
166/// # }
167/// ```
168#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
169#[serde(untagged)]
170pub enum Unknown {
171    Object(BTreeMap<String, DataModel>),
172    Null,
173    Other(DataModel),
174}
175
176#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
177#[serde(try_from = "Ipld")]
178pub struct DataModel(#[serde(serialize_with = "serialize_data_model")] Ipld);
179
180fn serialize_data_model<S>(ipld: &Ipld, serializer: S) -> Result<S::Ok, S::Error>
181where
182    S: ser::Serializer,
183{
184    match ipld {
185        Ipld::Float(_) => Err(ser::Error::custom("float values are not allowed in ATProtocol")),
186        Ipld::List(list) => {
187            if list.iter().any(|value| matches!(value, Ipld::Float(_))) {
188                Err(ser::Error::custom("float values are not allowed in ATProtocol"))
189            } else {
190                list.iter().cloned().map(DataModel).collect::<Vec<_>>().serialize(serializer)
191            }
192        }
193        Ipld::Map(map) => {
194            if map.values().any(|value| matches!(value, Ipld::Float(_))) {
195                Err(ser::Error::custom("float values are not allowed in ATProtocol"))
196            } else {
197                map.iter()
198                    .map(|(k, v)| (k, DataModel(v.clone())))
199                    .collect::<BTreeMap<_, _>>()
200                    .serialize(serializer)
201            }
202        }
203        Ipld::Link(link) => CidLink(*link).serialize(serializer),
204        _ => ipld.serialize(serializer),
205    }
206}
207
208impl Deref for DataModel {
209    type Target = Ipld;
210
211    fn deref(&self) -> &Self::Target {
212        &self.0
213    }
214}
215
216impl DerefMut for DataModel {
217    fn deref_mut(&mut self) -> &mut Self::Target {
218        &mut self.0
219    }
220}
221
222impl TryFrom<Ipld> for DataModel {
223    type Error = Error;
224
225    fn try_from(value: Ipld) -> Result<Self, Self::Error> {
226        // Enforce the ATProto data model.
227        // https://atproto.com/specs/data-model
228        match value {
229            Ipld::Float(_) => Err(Error::NotAllowed),
230            Ipld::List(list) => {
231                if list.iter().any(|value| matches!(value, Ipld::Float(_))) {
232                    Err(Error::NotAllowed)
233                } else {
234                    Ok(DataModel(Ipld::List(list)))
235                }
236            }
237            Ipld::Map(map) => {
238                if map.values().any(|value| matches!(value, Ipld::Float(_))) {
239                    Err(Error::NotAllowed)
240                } else {
241                    Ok(DataModel(Ipld::Map(map)))
242                }
243            }
244            data => Ok(DataModel(data)),
245        }
246    }
247}
248
249/// Trait for types that can be deserialized from an [`Unknown`] value.
250pub trait TryFromUnknown: Sized {
251    type Error;
252
253    fn try_from_unknown(value: Unknown) -> Result<Self, Self::Error>;
254}
255
256impl<T> TryFromUnknown for T
257where
258    T: de::DeserializeOwned,
259{
260    type Error = Error;
261
262    fn try_from_unknown(value: Unknown) -> Result<Self, Self::Error> {
263        // TODO: Fix this
264        // In the current latest `ipld-core` 0.4.1, deserialize to structs containing untagged/internal tagged does not work correctly when `Ipld::Integer` is included.
265        // https://github.com/ipld/rust-ipld-core/issues/19
266        // (It should be possible to convert as follows)
267        // ```
268        // Ok(match value {
269        //     Unknown::Object(map) => {
270        //         T::deserialize(Ipld::Map(map.into_iter().map(|(k, v)| (k, v.0)).collect()))?
271        //     }
272        //     Unknown::Null => T::deserialize(Ipld::Null)?,
273        //     Unknown::Other(data) => T::deserialize(data.0)?,
274        // })
275        // ```
276        //
277        // For the time being, until this problem is resolved, use the workaround of serializing once to a json string and then deserializing it.
278        let json = serde_json::to_vec(&value).unwrap();
279        Ok(serde_json::from_slice(&json).unwrap())
280    }
281}
282
283/// Trait for types that can be serialized into an [`Unknown`] value.
284pub trait TryIntoUnknown {
285    type Error;
286
287    fn try_into_unknown(self) -> Result<Unknown, Self::Error>;
288}
289
290impl<T> TryIntoUnknown for T
291where
292    T: Serialize,
293{
294    type Error = Error;
295
296    fn try_into_unknown(self) -> Result<Unknown, Self::Error> {
297        Ok(Unknown::Other(to_ipld(self)?.try_into()?))
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use ipld_core::cid::Cid;
305    use serde_json::{from_str, to_string};
306
307    const CID_LINK_JSON: &str =
308        r#"{"$link":"bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy"}"#;
309
310    #[test]
311    fn cid_link_serde_json() {
312        let deserialized =
313            from_str::<CidLink>(CID_LINK_JSON).expect("failed to deserialize cid-link");
314        let serialized = to_string(&deserialized).expect("failed to serialize cid-link");
315        assert_eq!(serialized, CID_LINK_JSON);
316    }
317
318    #[test]
319    fn blob_ref_typed_deserialize_json() {
320        let json = format!(
321            r#"{{"$type":"blob","ref":{},"mimeType":"text/plain","size":0}}"#,
322            CID_LINK_JSON
323        );
324        let deserialized = from_str::<BlobRef>(&json).expect("failed to deserialize blob-ref");
325        assert_eq!(
326            deserialized,
327            BlobRef::Typed(TypedBlobRef::Blob(Blob {
328                r#ref: CidLink::try_from(
329                    "bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy"
330                )
331                .expect("failed to create cid-link"),
332                mime_type: "text/plain".into(),
333                size: 0
334            }))
335        );
336    }
337
338    #[test]
339    fn blob_ref_untyped_deserialize_json() {
340        let json = r#"{"cid":"bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy","mimeType":"text/plain"}"#;
341        let deserialized = from_str::<BlobRef>(json).expect("failed to deserialize blob-ref");
342        assert_eq!(
343            deserialized,
344            BlobRef::Untyped(UnTypedBlobRef {
345                cid: "bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy".into(),
346                mime_type: "text/plain".into(),
347            })
348        );
349    }
350
351    #[test]
352    fn blob_ref_serialize_json() {
353        let blob_ref = BlobRef::Typed(TypedBlobRef::Blob(Blob {
354            r#ref: CidLink::try_from("bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy")
355                .expect("failed to create cid-link"),
356            mime_type: "text/plain".into(),
357            size: 0,
358        }));
359        let serialized = to_string(&blob_ref).expect("failed to serialize blob-ref");
360        assert_eq!(
361            serialized,
362            format!(
363                r#"{{"$type":"blob","ref":{},"mimeType":"text/plain","size":0}}"#,
364                CID_LINK_JSON
365            )
366        );
367    }
368
369    #[test]
370    fn blob_ref_deserialize_dag_cbor() {
371        // {"$type": "blob", "mimeType": "text/plain", "ref": bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy, "size": 0}
372        let dag_cbor = [
373            0xa4, 0x65, 0x24, 0x74, 0x79, 0x70, 0x65, 0x64, 0x62, 0x6c, 0x6f, 0x62, 0x63, 0x72,
374            0x65, 0x66, 0xd8, 0x2a, 0x58, 0x25, 0x00, 0x01, 0x55, 0x12, 0x20, 0x2c, 0x26, 0xb4,
375            0x6b, 0x68, 0xff, 0xc6, 0x8f, 0xf9, 0x9b, 0x45, 0x3c, 0x1d, 0x30, 0x41, 0x34, 0x13,
376            0x42, 0x2d, 0x70, 0x64, 0x83, 0xbf, 0xa0, 0xf9, 0x8a, 0x5e, 0x88, 0x62, 0x66, 0xe7,
377            0xae, 0x68, 0x6d, 0x69, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, 0x6a, 0x74, 0x65, 0x78,
378            0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x64, 0x73, 0x69, 0x7a, 0x65, 0x00,
379        ];
380        let deserialized = serde_ipld_dagcbor::from_slice::<BlobRef>(dag_cbor.as_slice())
381            .expect("failed to deserialize blob-ref");
382        assert_eq!(
383            deserialized,
384            BlobRef::Typed(TypedBlobRef::Blob(Blob {
385                r#ref: CidLink::try_from(
386                    "bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy"
387                )
388                .expect("failed to create cid-link"),
389                mime_type: "text/plain".into(),
390                size: 0,
391            }))
392        );
393    }
394
395    #[test]
396    fn data_model() {
397        assert!(DataModel::try_from(Ipld::Null).is_ok());
398        assert!(DataModel::try_from(Ipld::Bool(true)).is_ok());
399        assert!(DataModel::try_from(Ipld::Integer(1)).is_ok());
400        assert!(DataModel::try_from(Ipld::Float(1.5)).is_err(), "float value should fail");
401        assert!(DataModel::try_from(Ipld::String("s".into())).is_ok());
402        assert!(DataModel::try_from(Ipld::Bytes(vec![0x01])).is_ok());
403        assert!(DataModel::try_from(Ipld::List(vec![Ipld::Bool(true)])).is_ok());
404        assert!(
405            DataModel::try_from(Ipld::List(vec![Ipld::Bool(true), Ipld::Float(1.5)])).is_err(),
406            "list with float value should fail"
407        );
408        assert!(
409            DataModel::try_from(Ipld::Map(BTreeMap::from_iter([(
410                String::from("k"),
411                Ipld::Bool(true)
412            )])))
413            .is_ok()
414        );
415        assert!(
416            DataModel::try_from(Ipld::Map(BTreeMap::from_iter([(
417                String::from("k"),
418                Ipld::Float(1.5)
419            )])))
420            .is_err(),
421            "map with float value should fail"
422        );
423        assert!(
424            DataModel::try_from(Ipld::Link(
425                Cid::try_from("bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy")
426                    .expect("failed to create cid")
427            ))
428            .is_ok()
429        );
430    }
431
432    #[test]
433    fn union() {
434        #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
435        #[serde(tag = "$type")]
436        enum FooRefs {
437            #[serde(rename = "example.com#bar")]
438            Bar(Box<Bar>),
439            #[serde(rename = "example.com#baz")]
440            Baz(Box<Baz>),
441        }
442
443        #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
444        struct Bar {
445            bar: String,
446        }
447
448        #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
449        struct Baz {
450            baz: i32,
451        }
452
453        type Foo = Union<FooRefs>;
454
455        let foo = serde_json::from_str::<Foo>(r#"{"$type":"example.com#bar","bar":"bar"}"#)
456            .expect("failed to deserialize foo");
457        assert_eq!(foo, Union::Refs(FooRefs::Bar(Box::new(Bar { bar: String::from("bar") }))));
458
459        let foo = serde_json::from_str::<Foo>(r#"{"$type":"example.com#baz","baz":42}"#)
460            .expect("failed to deserialize foo");
461        assert_eq!(foo, Union::Refs(FooRefs::Baz(Box::new(Baz { baz: 42 }))));
462
463        let foo = serde_json::from_str::<Foo>(r#"{"$type":"example.com#foo","foo":true}"#)
464            .expect("failed to deserialize foo");
465        assert_eq!(
466            foo,
467            Union::Unknown(UnknownData {
468                r#type: String::from("example.com#foo"),
469                data: Ipld::Map(BTreeMap::from_iter([(String::from("foo"), Ipld::Bool(true))]))
470            })
471        );
472    }
473
474    #[test]
475    fn unknown_serialize() {
476        #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
477        struct Foo {
478            foo: Unknown,
479        }
480
481        let foo = Foo {
482            foo: Unknown::Object(BTreeMap::from_iter([(
483                String::from("bar"),
484                DataModel(Ipld::String(String::from("bar"))),
485            )])),
486        };
487        let serialized = to_string(&foo).expect("failed to serialize foo");
488        assert_eq!(serialized, r#"{"foo":{"bar":"bar"}}"#);
489    }
490
491    #[test]
492    fn unknown_deserialize() {
493        #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
494        struct Foo {
495            foo: Unknown,
496        }
497
498        // valid: data object
499        {
500            let json = r#"{
501                "foo": {
502                    "$type": "example.com#foo",
503                    "bar": "bar"
504                }
505            }"#;
506            let deserialized = from_str::<Foo>(json).expect("failed to deserialize foo");
507            assert_eq!(
508                deserialized,
509                Foo {
510                    foo: Unknown::Object(BTreeMap::from_iter([
511                        (String::from("bar"), DataModel(Ipld::String(String::from("bar")))),
512                        (
513                            String::from("$type"),
514                            DataModel(Ipld::String(String::from("example.com#foo")))
515                        )
516                    ]))
517                }
518            );
519        }
520        // valid(?): empty object
521        {
522            let json = r#"{
523                "foo": {}
524            }"#;
525            let deserialized = from_str::<Foo>(json).expect("failed to deserialize foo");
526            assert_eq!(deserialized, Foo { foo: Unknown::Object(BTreeMap::default()) });
527        }
528        // valid(?): object with no `$type`
529        {
530            let json = r#"{
531                "foo": {
532                    "bar": "bar"
533                }
534            }"#;
535            let deserialized = from_str::<Foo>(json).expect("failed to deserialize foo");
536            assert_eq!(
537                deserialized,
538                Foo {
539                    foo: Unknown::Object(BTreeMap::from_iter([(
540                        String::from("bar"),
541                        DataModel(Ipld::String(String::from("bar")))
542                    )]))
543                }
544            );
545        }
546        // valid(?): null
547        {
548            let json = r#"{
549                "foo": null
550            }"#;
551            let deserialized = from_str::<Foo>(json).expect("failed to deserialize foo");
552            assert_eq!(deserialized, Foo { foo: Unknown::Null });
553        }
554        // valid(?): primitive types
555        {
556            let json = r#"{
557                "foo": 42
558            }"#;
559            let deserialized = from_str::<Foo>(json).expect("failed to deserialize foo");
560            assert_eq!(deserialized, Foo { foo: Unknown::Other(DataModel(Ipld::Integer(42))) });
561        }
562        // invalid: float (not allowed)
563        {
564            let json = r#"{
565                "foo": 42.195
566            }"#;
567            assert!(from_str::<Foo>(json).is_err());
568        }
569    }
570
571    #[test]
572    fn unknown_try_from() {
573        #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
574        #[serde(tag = "$type")]
575        enum Foo {
576            #[serde(rename = "example.com#bar")]
577            Bar(Box<Bar>),
578            #[serde(rename = "example.com#baz")]
579            Baz(Box<Baz>),
580        }
581
582        #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
583        struct Bar {
584            bar: String,
585        }
586
587        #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
588        struct Baz {
589            baz: i32,
590        }
591
592        {
593            let unknown = Unknown::Object(BTreeMap::from_iter([
594                (String::from("$type"), DataModel(Ipld::String(String::from("example.com#bar")))),
595                (String::from("bar"), DataModel(Ipld::String(String::from("barbar")))),
596            ]));
597            let bar = Bar::try_from_unknown(unknown.clone()).expect("failed to convert to Bar");
598            assert_eq!(bar, Bar { bar: String::from("barbar") });
599            let barbaz = Foo::try_from_unknown(unknown).expect("failed to convert to Bar");
600            assert_eq!(barbaz, Foo::Bar(Box::new(Bar { bar: String::from("barbar") })));
601        }
602        {
603            let unknown = Unknown::Object(BTreeMap::from_iter([
604                (String::from("$type"), DataModel(Ipld::String(String::from("example.com#baz")))),
605                (String::from("baz"), DataModel(Ipld::Integer(42))),
606            ]));
607            let baz = Baz::try_from_unknown(unknown.clone()).expect("failed to convert to Baz");
608            assert_eq!(baz, Baz { baz: 42 });
609            let barbaz = Foo::try_from_unknown(unknown).expect("failed to convert to Bar");
610            assert_eq!(barbaz, Foo::Baz(Box::new(Baz { baz: 42 })));
611        }
612    }
613
614    #[test]
615    fn serialize_unknown_from_cid_link() {
616        // cid link
617        {
618            let cid_link =
619                CidLink::try_from("bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy")
620                    .expect("failed to create cid-link");
621            let unknown = cid_link.try_into_unknown().expect("failed to convert to unknown");
622            assert_eq!(
623                serde_json::to_string(&unknown).expect("failed to serialize unknown"),
624                r#"{"$link":"bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy"}"#
625            );
626        }
627        // blob ref (includes cid link)
628        {
629            let cid_link =
630                CidLink::try_from("bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy")
631                    .expect("failed to create cid-link");
632            let blob_ref = BlobRef::Typed(TypedBlobRef::Blob(Blob {
633                r#ref: cid_link,
634                mime_type: "text/plain".into(),
635                size: 0,
636            }));
637            let unknown = blob_ref.try_into_unknown().expect("failed to convert to unknown");
638            let serialized = serde_json::to_string(&unknown).expect("failed to serialize unknown");
639            assert!(
640                serialized.contains("bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy"),
641                "serialized unknown should contain cid string: {serialized}"
642            );
643        }
644    }
645}