Skip to main content

forest/lotus_json/
mod.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4//! In the Filecoin ecosystem, there are TWO different ways to present a domain object:
5//! - CBOR (defined in [`fvm_ipld_encoding`]).
6//!   This is the wire format.
7//! - JSON (see [`serde_json`]).
8//!   This is used in e.g RPC code, or in lotus printouts
9//!
10//! We care about compatibility with lotus/the Filecoin ecosystem for both.
11//! This module defines traits and types for handling both.
12//!
13//! # Terminology and background
14//! - A "domain object" is the _concept_ of an object.
15//!   E.g `"a CID with version = 1, codec = 0, and a multihash which is all zero"`
16//!   (This happens to be the default CID).
17//! - The "in memory" representation is how (rust) lays that out in memory.
18//!   See the definition of [`struct Cid { .. }`](`::cid::Cid`).
19//! - The "lotus JSON" is how [lotus](https://github.com/filecoin-project/lotus),
20//!   the reference Filecoin implementation, displays that object in JSON.
21//!   ```json
22//!   { "/": "baeaaaaa" }
23//!   ```
24//! - The "lotus CBOR" is how lotus represents that object on the wire.
25//!   ```rust
26//!   let in_memory = ::cid::Cid::default();
27//!   let cbor = fvm_ipld_encoding::to_vec(&in_memory).unwrap();
28//!   assert_eq!(
29//!       cbor,
30//!       0b_11011000_00101010_01000101_00000000_00000001_00000000_00000000_00000000_u64.to_be_bytes(),
31//!   );
32//!   ```
33//!
34//! In rust, the most common serialization framework is [`serde`].
35//! It has ONE (de)serialization model for each struct - the serialization code _cannot_ know
36//! if it's writing JSON or CBOR.
37//!
38//! The cleanest way handle the distinction would be a serde-compatible trait:
39//! ```rust
40//! # use serde::Serializer;
41//! pub trait LotusSerialize {
42//!     fn serialize_cbor<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
43//!     where
44//!         S: Serializer;
45//!
46//!     fn serialize_json<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
47//!     where
48//!         S: Serializer;
49//! }
50//! pub trait LotusDeserialize<'de> { /* ... */ }
51//! ```
52//!
53//! However, that would require writing and maintaining a custom derive macro - can we lean on
54//! [`macro@serde::Serialize`] and [`macro@serde::Deserialize`] instead?
55//!
56//! # Lotus JSON in Forest
57//! - Have a struct which represents a domain object: e.g [`GossipBlock`](crate::blocks::GossipBlock).
58//! - Implement [`serde::Serialize`] on that object, normally using [`fvm_ipld_encoding::tuple::Serialize_tuple`].
59//!   This corresponds to the CBOR representation.
60//! - Implement [`HasLotusJson`] on the domain object.
61//!   This attaches a separate JSON type, which should implement (`#[derive(...)]`) [`serde::Serialize`] and [`serde::Deserialize`] AND conversions to and from the domain object
62//!   E.g [`gossip_block`]
63//!
64//! Whenever you need the lotus JSON of an object, use the [`LotusJson`] wrapper.
65//! Note that the actual [`HasLotusJson::LotusJson`] types should be private - we don't want these names
66//! proliferating over the codebase.
67//!
68//! ## Implementation notes
69//! ### Illegal states are unrepresentable
70//! Consider [Address](crate::shim::address::Address) - it is represented as a simple string in JSON,
71//! so there are two possible definitions of `AddressLotusJson`:
72//! ```rust
73//! # use serde::{Deserialize, Serialize};
74//! # #[derive(Serialize, Deserialize)] enum Address {}
75//! # mod stringify {
76//! #     pub fn serialize<T, S: serde::Serializer>(_: &T, _: S) -> Result<S::Ok, S::Error> { unimplemented!() }
77//! #     pub fn deserialize<'de, T, D: serde::Deserializer<'de>>(_: D) -> Result<T, D::Error> { unimplemented!() }
78//! # }
79//! #[derive(Serialize, Deserialize)]
80//! pub struct AddressLotusJson(#[serde(with = "stringify")] Address);
81//! ```
82//! ```rust
83//! # use serde::{Deserialize, Serialize};
84//! #[derive(Serialize, Deserialize)]
85//! pub struct AddressLotusJson(String);
86//! ```
87//! However, with the second implementation, `impl From<AddressLotusJson> for Address` would involve unwrapping
88//! a call to [std::primitive::str::parse], which is unacceptable - malformed JSON could cause a crash!
89//!
90//! ### Location
91//! Prefer implementing in this module, as [`decl_and_test`] will handle `quickcheck`-ing and snapshot testing.
92//!
93//! If you require access to private fields, consider:
94//! - implementing an exhaustive helper method, e.g [`crate::beacon::BeaconEntry::into_parts`].
95//! - moving implementation to the module where the struct is defined, e.g [`crate::blocks::tipset::lotus_json`].
96//!   If you do this, you MUST manually add snapshot and `quickcheck` tests.
97//!
98//! ### Compound structs
99//! - Each field of a struct should be wrapped with [`LotusJson`].
100//! - Implementations of [`HasLotusJson::into_lotus_json`] and [`HasLotusJson::from_lotus_json`]
101//!   should use [`Into`] and [`LotusJson::into_inner`] calls
102//! - Use destructuring to ensure exhaustiveness
103//!
104//! ### Optional fields
105//! It's not clear if optional fields should be serialized as `null` or not.
106//! See e.g `LotusJson<Receipt>`.
107//!
108//! For now, fields are recommended to have the following annotations:
109//! ```rust,ignore
110//! # struct Foo {
111//! #[serde(skip_serializing_if = "LotusJson::is_none", default)]
112//! foo: LotusJson<Option<usize>>,
113//! # }
114//! ```
115//!
116//! # API hazards
117//! - Avoid using `#[serde(with = ...)]` except for leaf types
118//! - There is a hazard if the same type can be de/serialized in multiple ways.
119//!
120//! # Future work
121//! - use [`proptest`](https://docs.rs/proptest/) to test the parser pipeline
122//! - use a derive macro for simple compound structs
123
124use crate::shim::actors::miner::DeadlineInfo;
125use derive_more::From;
126use fvm_shared4::piece::PaddedPieceSize;
127use schemars::{JsonSchema, Schema, SchemaGenerator};
128use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
129#[cfg(test)]
130use serde_json::json;
131use std::{fmt::Display, str::FromStr};
132use uuid::Uuid;
133#[cfg(test)]
134use {pretty_assertions::assert_eq, quickcheck::quickcheck};
135
136pub trait HasLotusJson: Sized {
137    /// The struct representing JSON. You should `#[derive(Deserialize, Serialize)]` on it.
138    type LotusJson: Serialize + DeserializeOwned;
139    /// To ensure code quality, conversion to/from lotus JSON MUST be tested.
140    /// Provide snapshots of the JSON, and the domain type it should serialize to.
141    ///
142    /// Serialization and de-serialization of the domain type should match the snapshot.
143    ///
144    /// If using [`decl_and_test`], this test is automatically run for you, but if the test
145    /// is out-of-module, you must call [`assert_all_snapshots`] manually.
146    #[cfg(test)]
147    fn snapshots() -> Vec<(serde_json::Value, Self)>;
148    fn into_lotus_json(self) -> Self::LotusJson;
149    fn from_lotus_json(lotus_json: Self::LotusJson) -> Self;
150    fn into_lotus_json_value(self) -> serde_json::Result<serde_json::Value> {
151        serde_json::to_value(self.into_lotus_json())
152    }
153    fn into_lotus_json_string(self) -> serde_json::Result<String> {
154        serde_json::to_string(&self.into_lotus_json())
155    }
156    fn into_lotus_json_string_pretty(self) -> serde_json::Result<String> {
157        serde_json::to_string_pretty(&self.into_lotus_json())
158    }
159}
160
161macro_rules! decl_and_test {
162    ($($mod_name:ident for $domain_ty:ty),* $(,)?) => {
163        $(
164            mod $mod_name;
165        )*
166        #[test]
167        fn all_snapshots() {
168            $(
169                print!("test snapshots for {}...", std::any::type_name::<$domain_ty>());
170                std::io::Write::flush(&mut std::io::stdout()).unwrap();
171                // ^ make sure the above line is flushed in case the test fails
172                assert_all_snapshots::<$domain_ty>();
173                println!("ok.");
174            )*
175        }
176        #[test]
177        fn all_quickchecks() {
178            $(
179                print!("quickcheck for {}...", std::any::type_name::<$domain_ty>());
180                std::io::Write::flush(&mut std::io::stdout()).unwrap();
181                // ^ make sure the above line is flushed in case the test fails
182                ::quickcheck::quickcheck(assert_unchanged_via_json::<$domain_ty> as fn(_));
183                println!("ok.");
184            )*
185        }
186    }
187}
188#[cfg(doc)]
189pub(crate) use decl_and_test;
190
191decl_and_test!(
192    actor_state for crate::shim::state_tree::ActorState,
193    address for crate::shim::address::Address,
194    beacon_entry for crate::beacon::BeaconEntry,
195    big_int for num::BigInt,
196    block_header for crate::blocks::CachingBlockHeader,
197    cid for ::cid::Cid,
198    duration for std::time::Duration,
199    election_proof for crate::blocks::ElectionProof,
200    extended_sector_info for crate::shim::sector::ExtendedSectorInfo,
201    gossip_block for crate::blocks::GossipBlock,
202    key_info for crate::key_management::KeyInfo,
203    message for crate::shim::message::Message,
204    po_st_proof for crate::shim::sector::PoStProof,
205    registered_po_st_proof for crate::shim::sector::RegisteredPoStProof,
206    registered_seal_proof for crate::shim::sector::RegisteredSealProof,
207    sector_info for crate::shim::sector::SectorInfo,
208    sector_size for crate::shim::sector::SectorSize,
209    signature for crate::shim::crypto::Signature,
210    signature_type for crate::shim::crypto::SignatureType,
211    signed_message for  crate::message::SignedMessage,
212    ticket for crate::blocks::Ticket,
213    tipset_keys for crate::blocks::TipsetKey,
214    token_amount for crate::shim::econ::TokenAmount,
215    vec_u8 for Vec<u8>,
216    vrf_proof for crate::blocks::VRFProof,
217);
218
219// If a module cannot be tested normally above, you MAY declare it separately here
220// but you MUST document any tech debt - the reason WHY it cannot be tested above.
221mod actors;
222mod allocation;
223mod arc;
224mod beneficiary_term; // fil_actor_miner_state::v12::BeneficiaryTerm: !quickcheck::Arbitrary
225mod bit_field; //  fil_actors_shared::fvm_ipld_bitfield::BitField: !quickcheck::Arbitrary
226mod bytecode_hash;
227mod entry;
228mod filter_estimate;
229mod hash_map;
230mod ipld; // NaN != NaN
231mod miner_info; // fil_actor_miner_state::v12::MinerInfo: !quickcheck::Arbitrary
232mod miner_power; // actors::miner::MinerInfo: !quickcheck::Arbitrary
233mod nonempty; // can't make snapshots of generic type
234mod opt; // can't make snapshots of generic type
235mod padded_piece_size;
236mod pending_beneficiary_change; // fil_actor_miner_state::v12::PendingBeneficiaryChange: !quickcheck::Arbitrary
237mod power_claim; // actors::power::Claim: !quickcheck::Arbitrary
238mod raw_bytes; // fvm_ipld_encoding::RawBytes: !quickcheck::Arbitrary
239mod receipt; // shim type roundtrip is wrong - see module
240mod token_state;
241mod tombstone;
242mod transient_data;
243mod vec; // can't make snapshots of generic type
244mod verifreg_claim;
245
246pub use vec::*;
247
248#[macro_export]
249macro_rules! test_snapshots {
250    ($ty:ty) => {
251        pastey::paste! {
252            #[test]
253            fn [<snapshots_ $ty:snake>]() {
254                use super::*;
255                assert_all_snapshots::<$ty>();
256            }
257        }
258    };
259
260    ($module:path: $ty:ident: $($version:literal),+ $(,)?) => {
261        $(
262            pastey::paste! {
263                #[test]
264                fn [<snapshots_ $module _v $version _ $ty:lower>]() {
265                    use super::*;
266                    assert_all_snapshots::<$module::[<v $version>]::$ty>();
267                }
268            }
269        )+
270    };
271
272    ($module:path: $nested_path:path: $ty:ident: $($version:literal),+ $(,)?) => {
273        $(
274            pastey::paste! {
275                #[test]
276                fn [<snapshots_ $module _v $version _ $ty:lower>]() {
277                    use super::*;
278                    assert_all_snapshots::<$module::[<v $version>]::$nested_path::$ty>();
279                }
280            }
281        )+
282    };
283}
284
285#[cfg(any(test, doc))]
286pub fn assert_all_snapshots<T>()
287where
288    T: HasLotusJson,
289    <T as HasLotusJson>::LotusJson: PartialEq + std::fmt::Debug,
290{
291    let snapshots = T::snapshots();
292    assert!(!snapshots.is_empty());
293    for (lotus_json, val) in snapshots {
294        assert_one_snapshot(lotus_json, val);
295    }
296}
297
298#[cfg(test)]
299pub fn assert_one_snapshot<T>(lotus_json: serde_json::Value, val: T)
300where
301    T: HasLotusJson,
302    <T as HasLotusJson>::LotusJson: PartialEq + std::fmt::Debug,
303{
304    // T -> T::LotusJson -> lotus_json (Do not clone T as some external types do not implement Clone)
305    let val_lotus_json = val.into_lotus_json();
306    let serialized = serde_json::to_value(&val_lotus_json).unwrap();
307    assert_eq!(
308        serialized.to_string(),
309        lotus_json.to_string(),
310        "snapshot failed for {}",
311        std::any::type_name::<T>()
312    );
313
314    // lotus_json -> T::LotusJson -> T -> T::LotusJson
315    //( Not comparing T because external types may not implement `Eq` and `PartialEq`)
316    let deserialized = match serde_json::from_value::<T::LotusJson>(lotus_json.clone()) {
317        Ok(lotus_json) => T::from_lotus_json(lotus_json).into_lotus_json(),
318        Err(e) => panic!(
319            "couldn't deserialize a {} from {}: {e}",
320            std::any::type_name::<T::LotusJson>(),
321            lotus_json
322        ),
323    };
324    assert_eq!(deserialized, val_lotus_json);
325}
326
327#[cfg(any(test, doc))]
328pub fn assert_unchanged_via_json<T>(val: T)
329where
330    T: HasLotusJson + Clone + PartialEq + std::fmt::Debug,
331    T::LotusJson: Serialize + serde::de::DeserializeOwned,
332{
333    // T -> T::LotusJson -> lotus_json -> T::LotusJson -> T
334
335    // T -> T::LotusJson
336    let temp = val.clone().into_lotus_json();
337    // T::LotusJson -> lotus_json
338    let temp = serde_json::to_value(temp).unwrap();
339    // lotus_json -> T::LotusJson
340    let temp = serde_json::from_value::<T::LotusJson>(temp).unwrap();
341    // T::LotusJson -> T
342    let temp = T::from_lotus_json(temp);
343
344    assert_eq!(val, temp);
345}
346
347/// Usage: `#[serde(with = "stringify")]`
348pub mod stringify {
349    use super::*;
350
351    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
352    where
353        T: Display,
354        S: Serializer,
355    {
356        serializer.collect_str(value)
357    }
358
359    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
360    where
361        T: FromStr,
362        T::Err: Display,
363        D: Deserializer<'de>,
364    {
365        String::deserialize(deserializer)?
366            .parse()
367            .map_err(serde::de::Error::custom)
368    }
369}
370
371/// Usage: `#[serde(with = "hexify_bytes")]`
372pub mod hexify_bytes {
373    use super::*;
374
375    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
376    where
377        T: Display + std::fmt::LowerHex,
378        S: Serializer,
379    {
380        // `ethereum_types` crate serializes bytes as compressed addresses, i.e. `0xff00…03ec`
381        // so we can't just use `serializer.collect_str` here
382        serializer.serialize_str(&format!("{value:#x}"))
383    }
384
385    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
386    where
387        T: FromStr,
388        T::Err: Display,
389        D: Deserializer<'de>,
390    {
391        String::deserialize(deserializer)?
392            .parse()
393            .map_err(serde::de::Error::custom)
394    }
395}
396
397pub mod hexify_vec_bytes {
398    use super::*;
399    use std::borrow::Cow;
400
401    pub fn serialize<S>(value: &[u8], serializer: S) -> Result<S::Ok, S::Error>
402    where
403        S: Serializer,
404    {
405        let mut s = vec![0; 2 + value.len() * 2];
406        s.get_mut(0..2)
407            .expect("len is correct")
408            .copy_from_slice(b"0x");
409        hex::encode_to_slice(value, s.get_mut(2..).expect("len is correct"))
410            .map_err(serde::ser::Error::custom)?;
411        serializer.serialize_str(std::str::from_utf8(&s).expect("valid utf8"))
412    }
413
414    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
415    where
416        D: Deserializer<'de>,
417    {
418        let s = String::deserialize(deserializer)?;
419        let s = Cow::from(s.strip_prefix("0x").unwrap_or(&s));
420
421        // Pad with 0 if odd length. This is necessary because [`hex::decode`] requires an even
422        // number of characters, whereas a valid input is also `0x0`.
423        let s = if s.len() % 2 == 0 {
424            s
425        } else {
426            let mut s = s.into_owned();
427            s.insert(0, '0');
428            Cow::Owned(s)
429        };
430
431        hex::decode(s.as_ref()).map_err(serde::de::Error::custom)
432    }
433}
434
435/// Usage: `#[serde(with = "hexify")]`
436pub mod hexify {
437    use super::*;
438    use num_traits::Num;
439    use serde::{Deserializer, Serializer};
440
441    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
442    where
443        T: Num + std::fmt::LowerHex,
444        S: Serializer,
445    {
446        serializer.serialize_str(format!("{value:#x}").as_str())
447    }
448
449    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
450    where
451        T: Num,
452        <T as Num>::FromStrRadixErr: std::fmt::Display,
453        D: Deserializer<'de>,
454    {
455        let s = String::deserialize(deserializer)?;
456        #[allow(clippy::indexing_slicing)]
457        if s.len() > 2 && &s[..2] == "0x" {
458            T::from_str_radix(&s[2..], 16).map_err(serde::de::Error::custom)
459        } else {
460            Err(serde::de::Error::custom("Invalid hex"))
461        }
462    }
463}
464
465/// Usage: `#[serde(with = "base64_standard")]`
466pub mod base64_standard {
467    use super::*;
468
469    use base64::engine::{Engine as _, general_purpose::STANDARD};
470
471    pub fn serialize<S>(value: &[u8], serializer: S) -> Result<S::Ok, S::Error>
472    where
473        S: Serializer,
474    {
475        STANDARD.encode(value).serialize(serializer)
476    }
477
478    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
479    where
480        D: Deserializer<'de>,
481    {
482        STANDARD
483            .decode(String::deserialize(deserializer)?)
484            .map_err(serde::de::Error::custom)
485    }
486}
487
488/// MUST NOT be used in any `LotusJson` structs
489pub fn serialize<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
490where
491    S: Serializer,
492    T: HasLotusJson + Clone,
493{
494    value.clone().into_lotus_json().serialize(serializer)
495}
496
497/// MUST NOT be used in any `LotusJson` structs.
498pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
499where
500    D: Deserializer<'de>,
501    T: HasLotusJson,
502{
503    Ok(T::from_lotus_json(Deserialize::deserialize(deserializer)?))
504}
505
506/// A domain struct that is (de) serialized through its lotus JSON representation.
507#[derive(
508    Debug, Deserialize, From, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Clone,
509)]
510#[serde(bound = "T: HasLotusJson + Clone", transparent)]
511pub struct LotusJson<T>(#[serde(with = "self")] pub T);
512
513impl<T> JsonSchema for LotusJson<T>
514where
515    T: HasLotusJson,
516    T::LotusJson: JsonSchema,
517{
518    fn schema_name() -> std::borrow::Cow<'static, str> {
519        T::LotusJson::schema_name()
520    }
521
522    fn schema_id() -> std::borrow::Cow<'static, str> {
523        T::LotusJson::schema_id()
524    }
525
526    fn json_schema(g: &mut SchemaGenerator) -> Schema {
527        T::LotusJson::json_schema(g)
528    }
529}
530
531impl<T> LotusJson<T> {
532    pub fn into_inner(self) -> T {
533        self.0
534    }
535}
536
537macro_rules! lotus_json_with_self {
538    ($($domain_ty:ty),* $(,)?) => {
539        $(
540            impl $crate::lotus_json::HasLotusJson for $domain_ty {
541                type LotusJson = Self;
542                #[cfg(test)]
543                fn snapshots() -> Vec<(serde_json::Value, Self)> {
544                    unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
545                }
546                fn into_lotus_json(self) -> Self::LotusJson {
547                    self
548                }
549                fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
550                    lotus_json
551                }
552            }
553        )*
554    }
555}
556pub(crate) use lotus_json_with_self;
557
558lotus_json_with_self!(
559    u32,
560    u64,
561    i64,
562    f64,
563    String,
564    chrono::DateTime<chrono::Utc>,
565    serde_json::Value,
566    (),
567    std::path::PathBuf,
568    bool,
569    DeadlineInfo,
570    PaddedPieceSize,
571    Uuid,
572    std::num::NonZeroUsize,
573);
574
575mod fixme {
576    use super::*;
577
578    impl<T: HasLotusJson> HasLotusJson for (T,) {
579        type LotusJson = (T::LotusJson,);
580        #[cfg(test)]
581        fn snapshots() -> Vec<(serde_json::Value, Self)> {
582            unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
583        }
584        fn into_lotus_json(self) -> Self::LotusJson {
585            (self.0.into_lotus_json(),)
586        }
587        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
588            (HasLotusJson::from_lotus_json(lotus_json.0),)
589        }
590    }
591
592    impl<A: HasLotusJson, B: HasLotusJson> HasLotusJson for (A, B) {
593        type LotusJson = (A::LotusJson, B::LotusJson);
594        #[cfg(test)]
595        fn snapshots() -> Vec<(serde_json::Value, Self)> {
596            unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
597        }
598        fn into_lotus_json(self) -> Self::LotusJson {
599            (self.0.into_lotus_json(), self.1.into_lotus_json())
600        }
601        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
602            (
603                HasLotusJson::from_lotus_json(lotus_json.0),
604                HasLotusJson::from_lotus_json(lotus_json.1),
605            )
606        }
607    }
608
609    impl<A: HasLotusJson, B: HasLotusJson, C: HasLotusJson> HasLotusJson for (A, B, C) {
610        type LotusJson = (A::LotusJson, B::LotusJson, C::LotusJson);
611        #[cfg(test)]
612        fn snapshots() -> Vec<(serde_json::Value, Self)> {
613            unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
614        }
615        fn into_lotus_json(self) -> Self::LotusJson {
616            (
617                self.0.into_lotus_json(),
618                self.1.into_lotus_json(),
619                self.2.into_lotus_json(),
620            )
621        }
622        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
623            (
624                HasLotusJson::from_lotus_json(lotus_json.0),
625                HasLotusJson::from_lotus_json(lotus_json.1),
626                HasLotusJson::from_lotus_json(lotus_json.2),
627            )
628        }
629    }
630
631    impl<A: HasLotusJson, B: HasLotusJson, C: HasLotusJson, D: HasLotusJson> HasLotusJson
632        for (A, B, C, D)
633    {
634        type LotusJson = (A::LotusJson, B::LotusJson, C::LotusJson, D::LotusJson);
635        #[cfg(test)]
636        fn snapshots() -> Vec<(serde_json::Value, Self)> {
637            unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
638        }
639        fn into_lotus_json(self) -> Self::LotusJson {
640            (
641                self.0.into_lotus_json(),
642                self.1.into_lotus_json(),
643                self.2.into_lotus_json(),
644                self.3.into_lotus_json(),
645            )
646        }
647        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
648            (
649                HasLotusJson::from_lotus_json(lotus_json.0),
650                HasLotusJson::from_lotus_json(lotus_json.1),
651                HasLotusJson::from_lotus_json(lotus_json.2),
652                HasLotusJson::from_lotus_json(lotus_json.3),
653            )
654        }
655    }
656}
657
658#[cfg(test)]
659mod tests {
660    use super::*;
661    use ipld_core::serde::SerdeError;
662    use serde::de::{IntoDeserializer, value::StringDeserializer};
663
664    #[derive(Debug, Deserialize, Serialize, PartialEq)]
665    struct HexifyVecBytesTest {
666        #[serde(with = "hexify_vec_bytes")]
667        value: Vec<u8>,
668    }
669
670    #[test]
671    fn test_hexify_vec_bytes_serialize() {
672        let cases = [(vec![], "0x"), (vec![0], "0x00"), (vec![42, 66], "0x2a42")];
673
674        for (input, expected) in cases.into_iter() {
675            let hexify = HexifyVecBytesTest { value: input };
676            let serialized = serde_json::to_string(&hexify).unwrap();
677            self::assert_eq!(serialized, format!("{{\"value\":\"{}\"}}", expected));
678        }
679    }
680
681    #[test]
682    fn test_hexify_vec_bytes_deserialize() {
683        let cases = [
684            ("0x", vec![]),
685            ("0x0", vec![0]),
686            ("0xF", vec![15]),
687            ("0x2a42", vec![42, 66]),
688            ("0x2A42", vec![42, 66]),
689        ];
690
691        for (input, expected) in cases.into_iter() {
692            let deserializer: StringDeserializer<SerdeError> =
693                String::from_str(input).unwrap().into_deserializer();
694            let deserialized = hexify_vec_bytes::deserialize(deserializer).unwrap();
695            self::assert_eq!(deserialized, expected);
696        }
697
698        let fail_cases = ["cthulhu", "x", "0xazathoth"];
699        for input in fail_cases.into_iter() {
700            let deserializer: StringDeserializer<SerdeError> =
701                String::from_str(input).unwrap().into_deserializer();
702            let deserialized = hexify_vec_bytes::deserialize(deserializer);
703            assert!(deserialized.is_err());
704        }
705    }
706}