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;
127#[cfg(test)]
128use pretty_assertions::assert_eq;
129use schemars::{JsonSchema, Schema, SchemaGenerator};
130use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
131#[cfg(test)]
132use serde_json::json;
133use std::{fmt::Display, str::FromStr};
134use uuid::Uuid;
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    percent for crate::shim::percent::Percent,
200    election_proof for crate::blocks::ElectionProof,
201    extended_sector_info for crate::shim::sector::ExtendedSectorInfo,
202    gossip_block for crate::blocks::GossipBlock,
203    key_info for crate::key_management::KeyInfo,
204    message for crate::shim::message::Message,
205    po_st_proof for crate::shim::sector::PoStProof,
206    registered_po_st_proof for crate::shim::sector::RegisteredPoStProof,
207    registered_seal_proof for crate::shim::sector::RegisteredSealProof,
208    sector_info for crate::shim::sector::SectorInfo,
209    sector_size for crate::shim::sector::SectorSize,
210    signature for crate::shim::crypto::Signature,
211    signature_type for crate::shim::crypto::SignatureType,
212    signed_message for  crate::message::SignedMessage,
213    ticket for crate::blocks::Ticket,
214    tipset_keys for crate::blocks::TipsetKey,
215    token_amount for crate::shim::econ::TokenAmount,
216    vec_u8 for Vec<u8>,
217    vrf_proof for crate::blocks::VRFProof,
218);
219
220// If a module cannot be tested normally above, you MAY declare it separately here
221// but you MUST document any tech debt - the reason WHY it cannot be tested above.
222mod actors;
223mod allocation;
224mod arc;
225mod beneficiary_term; // fil_actor_miner_state::v12::BeneficiaryTerm: !quickcheck::Arbitrary
226mod bit_field; //  fil_actors_shared::fvm_ipld_bitfield::BitField: !quickcheck::Arbitrary
227mod bytecode_hash;
228mod entry;
229mod filter_estimate;
230mod hash_map;
231mod ipld; // NaN != NaN
232mod miner_info; // fil_actor_miner_state::v12::MinerInfo: !quickcheck::Arbitrary
233mod miner_power; // actors::miner::MinerInfo: !quickcheck::Arbitrary
234mod nonempty; // can't make snapshots of generic type
235mod opt; // can't make snapshots of generic type
236mod padded_piece_size;
237mod pending_beneficiary_change; // fil_actor_miner_state::v12::PendingBeneficiaryChange: !quickcheck::Arbitrary
238mod power_claim; // actors::power::Claim: !quickcheck::Arbitrary
239mod raw_bytes; // fvm_ipld_encoding::RawBytes: !quickcheck::Arbitrary
240mod receipt; // shim type roundtrip is wrong - see module
241mod token_state;
242mod tombstone;
243mod transient_data;
244mod vec; // can't make snapshots of generic type
245mod verifreg_claim;
246
247pub use vec::*;
248
249#[macro_export]
250macro_rules! test_snapshots {
251    ($ty:ty) => {
252        pastey::paste! {
253            #[test]
254            fn [<snapshots_ $ty:snake>]() {
255                use super::*;
256                assert_all_snapshots::<$ty>();
257            }
258        }
259    };
260
261    ($module:path: $ty:ident: $($version:literal),+ $(,)?) => {
262        $(
263            pastey::paste! {
264                #[test]
265                fn [<snapshots_ $module _v $version _ $ty:lower>]() {
266                    use super::*;
267                    assert_all_snapshots::<$module::[<v $version>]::$ty>();
268                }
269            }
270        )+
271    };
272
273    ($module:path: $nested_path:path: $ty:ident: $($version:literal),+ $(,)?) => {
274        $(
275            pastey::paste! {
276                #[test]
277                fn [<snapshots_ $module _v $version _ $ty:lower>]() {
278                    use super::*;
279                    assert_all_snapshots::<$module::[<v $version>]::$nested_path::$ty>();
280                }
281            }
282        )+
283    };
284}
285
286#[cfg(any(test, doc))]
287pub fn assert_all_snapshots<T>()
288where
289    T: HasLotusJson,
290    <T as HasLotusJson>::LotusJson: PartialEq + std::fmt::Debug,
291{
292    let snapshots = T::snapshots();
293    assert!(!snapshots.is_empty());
294    for (lotus_json, val) in snapshots {
295        assert_one_snapshot(lotus_json, val);
296    }
297}
298
299#[cfg(test)]
300pub fn assert_one_snapshot<T>(lotus_json: serde_json::Value, val: T)
301where
302    T: HasLotusJson,
303    <T as HasLotusJson>::LotusJson: PartialEq + std::fmt::Debug,
304{
305    // T -> T::LotusJson -> lotus_json (Do not clone T as some external types do not implement Clone)
306    let val_lotus_json = val.into_lotus_json();
307    let serialized = serde_json::to_value(&val_lotus_json).unwrap();
308    assert_eq!(
309        serialized.to_string(),
310        lotus_json.to_string(),
311        "snapshot failed for {}",
312        std::any::type_name::<T>()
313    );
314
315    // lotus_json -> T::LotusJson -> T -> T::LotusJson
316    //( Not comparing T because external types may not implement `Eq` and `PartialEq`)
317    let deserialized = match serde_json::from_value::<T::LotusJson>(lotus_json.clone()) {
318        Ok(lotus_json) => T::from_lotus_json(lotus_json).into_lotus_json(),
319        Err(e) => panic!(
320            "couldn't deserialize a {} from {}: {e}",
321            std::any::type_name::<T::LotusJson>(),
322            lotus_json
323        ),
324    };
325    assert_eq!(deserialized, val_lotus_json);
326}
327
328#[cfg(any(test, doc))]
329pub fn assert_unchanged_via_json<T>(val: T)
330where
331    T: HasLotusJson + Clone + PartialEq + std::fmt::Debug,
332    T::LotusJson: Serialize + serde::de::DeserializeOwned,
333{
334    // T -> T::LotusJson -> lotus_json -> T::LotusJson -> T
335
336    // T -> T::LotusJson
337    let temp = val.clone().into_lotus_json();
338    // T::LotusJson -> lotus_json
339    let temp = serde_json::to_value(temp).unwrap();
340    // lotus_json -> T::LotusJson
341    let temp = serde_json::from_value::<T::LotusJson>(temp).unwrap();
342    // T::LotusJson -> T
343    let temp = T::from_lotus_json(temp);
344
345    assert_eq!(val, temp);
346}
347
348/// Usage: `#[serde(with = "stringify")]`
349pub mod stringify {
350    use super::*;
351
352    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
353    where
354        T: Display,
355        S: Serializer,
356    {
357        serializer.collect_str(value)
358    }
359
360    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
361    where
362        T: FromStr,
363        T::Err: Display,
364        D: Deserializer<'de>,
365    {
366        String::deserialize(deserializer)?
367            .parse()
368            .map_err(serde::de::Error::custom)
369    }
370}
371
372/// Usage: `#[serde(with = "hexify_bytes")]`
373pub mod hexify_bytes {
374    use super::*;
375
376    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
377    where
378        T: Display + std::fmt::LowerHex,
379        S: Serializer,
380    {
381        // `ethereum_types` crate serializes bytes as compressed addresses, i.e. `0xff00…03ec`
382        // so we can't just use `serializer.collect_str` here
383        serializer.serialize_str(&format!("{value:#x}"))
384    }
385
386    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
387    where
388        T: FromStr,
389        T::Err: Display,
390        D: Deserializer<'de>,
391    {
392        String::deserialize(deserializer)?
393            .parse()
394            .map_err(serde::de::Error::custom)
395    }
396}
397
398pub mod hexify_vec_bytes {
399    use super::*;
400    use std::borrow::Cow;
401
402    pub fn serialize<S>(value: &[u8], serializer: S) -> Result<S::Ok, S::Error>
403    where
404        S: Serializer,
405    {
406        let mut s = vec![0; 2 + value.len() * 2];
407        s.get_mut(0..2)
408            .expect("len is correct")
409            .copy_from_slice(b"0x");
410        hex::encode_to_slice(value, s.get_mut(2..).expect("len is correct"))
411            .map_err(serde::ser::Error::custom)?;
412        serializer.serialize_str(std::str::from_utf8(&s).expect("valid utf8"))
413    }
414
415    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
416    where
417        D: Deserializer<'de>,
418    {
419        let s = String::deserialize(deserializer)?;
420        let s = Cow::from(s.strip_prefix("0x").unwrap_or(&s));
421
422        // Pad with 0 if odd length. This is necessary because [`hex::decode`] requires an even
423        // number of characters, whereas a valid input is also `0x0`.
424        let s = if s.len() % 2 == 0 {
425            s
426        } else {
427            let mut s = s.into_owned();
428            s.insert(0, '0');
429            Cow::Owned(s)
430        };
431
432        hex::decode(s.as_ref()).map_err(serde::de::Error::custom)
433    }
434}
435
436/// Usage: `#[serde(with = "hexify")]`
437pub mod hexify {
438    use super::*;
439    use num_traits::Num;
440    use serde::{Deserializer, Serializer};
441
442    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
443    where
444        T: Num + std::fmt::LowerHex,
445        S: Serializer,
446    {
447        serializer.serialize_str(format!("{value:#x}").as_str())
448    }
449
450    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
451    where
452        T: Num,
453        <T as Num>::FromStrRadixErr: std::fmt::Display,
454        D: Deserializer<'de>,
455    {
456        let s = String::deserialize(deserializer)?;
457        #[allow(clippy::indexing_slicing)]
458        if s.len() > 2 && &s[..2] == "0x" {
459            T::from_str_radix(&s[2..], 16).map_err(serde::de::Error::custom)
460        } else {
461            Err(serde::de::Error::custom("Invalid hex"))
462        }
463    }
464}
465
466/// Usage: `#[serde(with = "base64_standard")]`
467pub mod base64_standard {
468    use super::*;
469
470    use base64::engine::{Engine as _, general_purpose::STANDARD};
471
472    pub fn serialize<S>(value: &[u8], serializer: S) -> Result<S::Ok, S::Error>
473    where
474        S: Serializer,
475    {
476        STANDARD.encode(value).serialize(serializer)
477    }
478
479    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
480    where
481        D: Deserializer<'de>,
482    {
483        STANDARD
484            .decode(String::deserialize(deserializer)?)
485            .map_err(serde::de::Error::custom)
486    }
487}
488
489/// MUST NOT be used in any `LotusJson` structs
490pub fn serialize<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
491where
492    S: Serializer,
493    T: HasLotusJson + Clone,
494{
495    value.clone().into_lotus_json().serialize(serializer)
496}
497
498/// MUST NOT be used in any `LotusJson` structs.
499pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
500where
501    D: Deserializer<'de>,
502    T: HasLotusJson,
503{
504    Ok(T::from_lotus_json(Deserialize::deserialize(deserializer)?))
505}
506
507/// A domain struct that is (de) serialized through its lotus JSON representation.
508#[derive(
509    Debug, Deserialize, From, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Clone,
510)]
511#[serde(bound = "T: HasLotusJson + Clone", transparent)]
512pub struct LotusJson<T>(#[serde(with = "self")] pub T);
513
514impl<T> JsonSchema for LotusJson<T>
515where
516    T: HasLotusJson,
517    T::LotusJson: JsonSchema,
518{
519    fn schema_name() -> std::borrow::Cow<'static, str> {
520        T::LotusJson::schema_name()
521    }
522
523    fn schema_id() -> std::borrow::Cow<'static, str> {
524        T::LotusJson::schema_id()
525    }
526
527    fn json_schema(g: &mut SchemaGenerator) -> Schema {
528        T::LotusJson::json_schema(g)
529    }
530}
531
532impl<T> LotusJson<T> {
533    pub fn into_inner(self) -> T {
534        self.0
535    }
536}
537
538macro_rules! lotus_json_with_self {
539    ($($domain_ty:ty),* $(,)?) => {
540        $(
541            impl $crate::lotus_json::HasLotusJson for $domain_ty {
542                type LotusJson = Self;
543                #[cfg(test)]
544                fn snapshots() -> Vec<(serde_json::Value, Self)> {
545                    unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
546                }
547                fn into_lotus_json(self) -> Self::LotusJson {
548                    self
549                }
550                fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
551                    lotus_json
552                }
553            }
554        )*
555    }
556}
557pub(crate) use lotus_json_with_self;
558
559lotus_json_with_self!(
560    u32,
561    u64,
562    i64,
563    f64,
564    String,
565    chrono::DateTime<chrono::Utc>,
566    serde_json::Value,
567    (),
568    std::path::PathBuf,
569    bool,
570    DeadlineInfo,
571    PaddedPieceSize,
572    Uuid,
573    std::num::NonZeroUsize,
574);
575
576mod fixme {
577    use super::*;
578
579    impl<T: HasLotusJson> HasLotusJson for (T,) {
580        type LotusJson = (T::LotusJson,);
581        #[cfg(test)]
582        fn snapshots() -> Vec<(serde_json::Value, Self)> {
583            unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
584        }
585        fn into_lotus_json(self) -> Self::LotusJson {
586            (self.0.into_lotus_json(),)
587        }
588        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
589            (HasLotusJson::from_lotus_json(lotus_json.0),)
590        }
591    }
592
593    impl<A: HasLotusJson, B: HasLotusJson> HasLotusJson for (A, B) {
594        type LotusJson = (A::LotusJson, B::LotusJson);
595        #[cfg(test)]
596        fn snapshots() -> Vec<(serde_json::Value, Self)> {
597            unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
598        }
599        fn into_lotus_json(self) -> Self::LotusJson {
600            (self.0.into_lotus_json(), self.1.into_lotus_json())
601        }
602        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
603            (
604                HasLotusJson::from_lotus_json(lotus_json.0),
605                HasLotusJson::from_lotus_json(lotus_json.1),
606            )
607        }
608    }
609
610    impl<A: HasLotusJson, B: HasLotusJson, C: HasLotusJson> HasLotusJson for (A, B, C) {
611        type LotusJson = (A::LotusJson, B::LotusJson, C::LotusJson);
612        #[cfg(test)]
613        fn snapshots() -> Vec<(serde_json::Value, Self)> {
614            unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
615        }
616        fn into_lotus_json(self) -> Self::LotusJson {
617            (
618                self.0.into_lotus_json(),
619                self.1.into_lotus_json(),
620                self.2.into_lotus_json(),
621            )
622        }
623        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
624            (
625                HasLotusJson::from_lotus_json(lotus_json.0),
626                HasLotusJson::from_lotus_json(lotus_json.1),
627                HasLotusJson::from_lotus_json(lotus_json.2),
628            )
629        }
630    }
631
632    impl<A: HasLotusJson, B: HasLotusJson, C: HasLotusJson, D: HasLotusJson> HasLotusJson
633        for (A, B, C, D)
634    {
635        type LotusJson = (A::LotusJson, B::LotusJson, C::LotusJson, D::LotusJson);
636        #[cfg(test)]
637        fn snapshots() -> Vec<(serde_json::Value, Self)> {
638            unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
639        }
640        fn into_lotus_json(self) -> Self::LotusJson {
641            (
642                self.0.into_lotus_json(),
643                self.1.into_lotus_json(),
644                self.2.into_lotus_json(),
645                self.3.into_lotus_json(),
646            )
647        }
648        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
649            (
650                HasLotusJson::from_lotus_json(lotus_json.0),
651                HasLotusJson::from_lotus_json(lotus_json.1),
652                HasLotusJson::from_lotus_json(lotus_json.2),
653                HasLotusJson::from_lotus_json(lotus_json.3),
654            )
655        }
656    }
657}
658
659#[cfg(test)]
660mod tests {
661    use super::*;
662    use ipld_core::serde::SerdeError;
663    use serde::de::{IntoDeserializer, value::StringDeserializer};
664
665    #[derive(Debug, Deserialize, Serialize, PartialEq)]
666    struct HexifyVecBytesTest {
667        #[serde(with = "hexify_vec_bytes")]
668        value: Vec<u8>,
669    }
670
671    #[test]
672    fn test_hexify_vec_bytes_serialize() {
673        let cases = [(vec![], "0x"), (vec![0], "0x00"), (vec![42, 66], "0x2a42")];
674
675        for (input, expected) in cases.into_iter() {
676            let hexify = HexifyVecBytesTest { value: input };
677            let serialized = serde_json::to_string(&hexify).unwrap();
678            self::assert_eq!(serialized, format!("{{\"value\":\"{}\"}}", expected));
679        }
680    }
681
682    #[test]
683    fn test_hexify_vec_bytes_deserialize() {
684        let cases = [
685            ("0x", vec![]),
686            ("0x0", vec![0]),
687            ("0xF", vec![15]),
688            ("0x2a42", vec![42, 66]),
689            ("0x2A42", vec![42, 66]),
690        ];
691
692        for (input, expected) in cases.into_iter() {
693            let deserializer: StringDeserializer<SerdeError> =
694                String::from_str(input).unwrap().into_deserializer();
695            let deserialized = hexify_vec_bytes::deserialize(deserializer).unwrap();
696            self::assert_eq!(deserialized, expected);
697        }
698
699        let fail_cases = ["cthulhu", "x", "0xazathoth"];
700        for input in fail_cases.into_iter() {
701            let deserializer: StringDeserializer<SerdeError> =
702                String::from_str(input).unwrap().into_deserializer();
703            let deserialized = hexify_vec_bytes::deserialize(deserializer);
704            assert!(deserialized.is_err());
705        }
706    }
707}