forest/lotus_json/
mod.rs

1// Copyright 2019-2025 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    ($module:path: $ty:ident: $($version:literal),+ $(,)?) => {
251        $(
252            pastey::paste! {
253                #[test]
254                fn [<snapshots_ $module _v $version _ $ty:lower>]() {
255                    use super::*;
256                    assert_all_snapshots::<$module::[<v $version>]::$ty>();
257                }
258            }
259        )+
260    };
261
262    ($module:path: $nested_path:path: $ty:ident: $($version:literal),+ $(,)?) => {
263        $(
264            pastey::paste! {
265                #[test]
266                fn [<snapshots_ $module _v $version _ $ty:lower>]() {
267                    use super::*;
268                    assert_all_snapshots::<$module::[<v $version>]::$nested_path::$ty>();
269                }
270            }
271        )+
272    };
273}
274
275#[cfg(any(test, doc))]
276pub fn assert_all_snapshots<T>()
277where
278    T: HasLotusJson + PartialEq + std::fmt::Debug + Clone,
279{
280    let snapshots = T::snapshots();
281    assert!(!snapshots.is_empty());
282    for (lotus_json, val) in snapshots {
283        assert_one_snapshot(lotus_json, val);
284    }
285}
286
287#[cfg(test)]
288pub fn assert_one_snapshot<T>(lotus_json: serde_json::Value, val: T)
289where
290    T: HasLotusJson + PartialEq + std::fmt::Debug + Clone,
291{
292    // T -> T::LotusJson -> lotus_json
293    let serialized = val.clone().into_lotus_json_value().unwrap();
294    assert_eq!(
295        serialized.to_string(),
296        lotus_json.to_string(),
297        "snapshot failed for {}",
298        std::any::type_name::<T>()
299    );
300
301    // lotus_json -> T::LotusJson -> T
302    let deserialized = match serde_json::from_value::<T::LotusJson>(lotus_json.clone()) {
303        Ok(lotus_json) => T::from_lotus_json(lotus_json),
304        Err(e) => panic!(
305            "couldn't deserialize a {} from {}: {e}",
306            std::any::type_name::<T::LotusJson>(),
307            lotus_json
308        ),
309    };
310    assert_eq!(deserialized, val);
311}
312
313#[cfg(any(test, doc))]
314pub fn assert_unchanged_via_json<T>(val: T)
315where
316    T: HasLotusJson + Clone + PartialEq + std::fmt::Debug,
317    T::LotusJson: Serialize + serde::de::DeserializeOwned,
318{
319    // T -> T::LotusJson -> lotus_json -> T::LotusJson -> T
320
321    // T -> T::LotusJson
322    let temp = val.clone().into_lotus_json();
323    // T::LotusJson -> lotus_json
324    let temp = serde_json::to_value(temp).unwrap();
325    // lotus_json -> T::LotusJson
326    let temp = serde_json::from_value::<T::LotusJson>(temp).unwrap();
327    // T::LotusJson -> T
328    let temp = T::from_lotus_json(temp);
329
330    assert_eq!(val, temp);
331}
332
333/// Usage: `#[serde(with = "stringify")]`
334pub mod stringify {
335    use super::*;
336
337    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
338    where
339        T: Display,
340        S: Serializer,
341    {
342        serializer.collect_str(value)
343    }
344
345    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
346    where
347        T: FromStr,
348        T::Err: Display,
349        D: Deserializer<'de>,
350    {
351        String::deserialize(deserializer)?
352            .parse()
353            .map_err(serde::de::Error::custom)
354    }
355}
356
357/// Usage: `#[serde(with = "hexify_bytes")]`
358pub mod hexify_bytes {
359    use super::*;
360
361    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
362    where
363        T: Display + std::fmt::LowerHex,
364        S: Serializer,
365    {
366        // `ethereum_types` crate serializes bytes as compressed addresses, i.e. `0xff00…03ec`
367        // so we can't just use `serializer.collect_str` here
368        serializer.serialize_str(&format!("{value:#x}"))
369    }
370
371    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
372    where
373        T: FromStr,
374        T::Err: Display,
375        D: Deserializer<'de>,
376    {
377        String::deserialize(deserializer)?
378            .parse()
379            .map_err(serde::de::Error::custom)
380    }
381}
382
383pub mod hexify_vec_bytes {
384    use super::*;
385    use std::borrow::Cow;
386
387    pub fn serialize<S>(value: &[u8], serializer: S) -> Result<S::Ok, S::Error>
388    where
389        S: Serializer,
390    {
391        let mut s = vec![0; 2 + value.len() * 2];
392        s.get_mut(0..2)
393            .expect("len is correct")
394            .copy_from_slice(b"0x");
395        hex::encode_to_slice(value, s.get_mut(2..).expect("len is correct"))
396            .map_err(serde::ser::Error::custom)?;
397        serializer.serialize_str(std::str::from_utf8(&s).expect("valid utf8"))
398    }
399
400    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
401    where
402        D: Deserializer<'de>,
403    {
404        let s = String::deserialize(deserializer)?;
405        let s = Cow::from(s.strip_prefix("0x").unwrap_or(&s));
406
407        // Pad with 0 if odd length. This is necessary because [`hex::decode`] requires an even
408        // number of characters, whereas a valid input is also `0x0`.
409        let s = if s.len() % 2 == 0 {
410            s
411        } else {
412            let mut s = s.into_owned();
413            s.insert(0, '0');
414            Cow::Owned(s)
415        };
416
417        hex::decode(s.as_ref()).map_err(serde::de::Error::custom)
418    }
419}
420
421/// Usage: `#[serde(with = "hexify")]`
422pub mod hexify {
423    use super::*;
424    use num_traits::Num;
425    use serde::{Deserializer, Serializer};
426
427    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
428    where
429        T: Num + std::fmt::LowerHex,
430        S: Serializer,
431    {
432        serializer.serialize_str(format!("{value:#x}").as_str())
433    }
434
435    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
436    where
437        T: Num,
438        <T as Num>::FromStrRadixErr: std::fmt::Display,
439        D: Deserializer<'de>,
440    {
441        let s = String::deserialize(deserializer)?;
442        #[allow(clippy::indexing_slicing)]
443        if s.len() > 2 && &s[..2] == "0x" {
444            T::from_str_radix(&s[2..], 16).map_err(serde::de::Error::custom)
445        } else {
446            Err(serde::de::Error::custom("Invalid hex"))
447        }
448    }
449}
450
451/// Usage: `#[serde(with = "base64_standard")]`
452pub mod base64_standard {
453    use super::*;
454
455    use base64::engine::{Engine as _, general_purpose::STANDARD};
456
457    pub fn serialize<S>(value: &[u8], serializer: S) -> Result<S::Ok, S::Error>
458    where
459        S: Serializer,
460    {
461        STANDARD.encode(value).serialize(serializer)
462    }
463
464    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
465    where
466        D: Deserializer<'de>,
467    {
468        STANDARD
469            .decode(String::deserialize(deserializer)?)
470            .map_err(serde::de::Error::custom)
471    }
472}
473
474/// MUST NOT be used in any `LotusJson` structs
475pub fn serialize<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
476where
477    S: Serializer,
478    T: HasLotusJson + Clone,
479{
480    value.clone().into_lotus_json().serialize(serializer)
481}
482
483/// MUST NOT be used in any `LotusJson` structs.
484pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
485where
486    D: Deserializer<'de>,
487    T: HasLotusJson,
488{
489    Ok(T::from_lotus_json(Deserialize::deserialize(deserializer)?))
490}
491
492/// A domain struct that is (de) serialized through its lotus JSON representation.
493#[derive(
494    Debug, Deserialize, From, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Clone,
495)]
496#[serde(bound = "T: HasLotusJson + Clone", transparent)]
497pub struct LotusJson<T>(#[serde(with = "self")] pub T);
498
499impl<T> JsonSchema for LotusJson<T>
500where
501    T: HasLotusJson,
502    T::LotusJson: JsonSchema,
503{
504    fn schema_name() -> std::borrow::Cow<'static, str> {
505        T::LotusJson::schema_name()
506    }
507
508    fn schema_id() -> std::borrow::Cow<'static, str> {
509        T::LotusJson::schema_id()
510    }
511
512    fn json_schema(g: &mut SchemaGenerator) -> Schema {
513        T::LotusJson::json_schema(g)
514    }
515}
516
517impl<T> LotusJson<T> {
518    pub fn into_inner(self) -> T {
519        self.0
520    }
521}
522
523macro_rules! lotus_json_with_self {
524    ($($domain_ty:ty),* $(,)?) => {
525        $(
526            impl $crate::lotus_json::HasLotusJson for $domain_ty {
527                type LotusJson = Self;
528                #[cfg(test)]
529                fn snapshots() -> Vec<(serde_json::Value, Self)> {
530                    unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
531                }
532                fn into_lotus_json(self) -> Self::LotusJson {
533                    self
534                }
535                fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
536                    lotus_json
537                }
538            }
539        )*
540    }
541}
542pub(crate) use lotus_json_with_self;
543
544lotus_json_with_self!(
545    u32,
546    u64,
547    i64,
548    f64,
549    String,
550    chrono::DateTime<chrono::Utc>,
551    serde_json::Value,
552    (),
553    std::path::PathBuf,
554    bool,
555    DeadlineInfo,
556    PaddedPieceSize,
557    Uuid,
558    std::num::NonZeroUsize,
559);
560
561mod fixme {
562    use super::*;
563
564    impl<T: HasLotusJson> HasLotusJson for (T,) {
565        type LotusJson = (T::LotusJson,);
566        #[cfg(test)]
567        fn snapshots() -> Vec<(serde_json::Value, Self)> {
568            unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
569        }
570        fn into_lotus_json(self) -> Self::LotusJson {
571            (self.0.into_lotus_json(),)
572        }
573        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
574            (HasLotusJson::from_lotus_json(lotus_json.0),)
575        }
576    }
577
578    impl<A: HasLotusJson, B: HasLotusJson> HasLotusJson for (A, B) {
579        type LotusJson = (A::LotusJson, B::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(), self.1.into_lotus_json())
586        }
587        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
588            (
589                HasLotusJson::from_lotus_json(lotus_json.0),
590                HasLotusJson::from_lotus_json(lotus_json.1),
591            )
592        }
593    }
594
595    impl<A: HasLotusJson, B: HasLotusJson, C: HasLotusJson> HasLotusJson for (A, B, C) {
596        type LotusJson = (A::LotusJson, B::LotusJson, C::LotusJson);
597        #[cfg(test)]
598        fn snapshots() -> Vec<(serde_json::Value, Self)> {
599            unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
600        }
601        fn into_lotus_json(self) -> Self::LotusJson {
602            (
603                self.0.into_lotus_json(),
604                self.1.into_lotus_json(),
605                self.2.into_lotus_json(),
606            )
607        }
608        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
609            (
610                HasLotusJson::from_lotus_json(lotus_json.0),
611                HasLotusJson::from_lotus_json(lotus_json.1),
612                HasLotusJson::from_lotus_json(lotus_json.2),
613            )
614        }
615    }
616
617    impl<A: HasLotusJson, B: HasLotusJson, C: HasLotusJson, D: HasLotusJson> HasLotusJson
618        for (A, B, C, D)
619    {
620        type LotusJson = (A::LotusJson, B::LotusJson, C::LotusJson, D::LotusJson);
621        #[cfg(test)]
622        fn snapshots() -> Vec<(serde_json::Value, Self)> {
623            unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
624        }
625        fn into_lotus_json(self) -> Self::LotusJson {
626            (
627                self.0.into_lotus_json(),
628                self.1.into_lotus_json(),
629                self.2.into_lotus_json(),
630                self.3.into_lotus_json(),
631            )
632        }
633        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
634            (
635                HasLotusJson::from_lotus_json(lotus_json.0),
636                HasLotusJson::from_lotus_json(lotus_json.1),
637                HasLotusJson::from_lotus_json(lotus_json.2),
638                HasLotusJson::from_lotus_json(lotus_json.3),
639            )
640        }
641    }
642}
643
644#[cfg(test)]
645mod tests {
646    use super::*;
647    use ipld_core::serde::SerdeError;
648    use serde::de::{IntoDeserializer, value::StringDeserializer};
649
650    #[derive(Debug, Deserialize, Serialize, PartialEq)]
651    struct HexifyVecBytesTest {
652        #[serde(with = "hexify_vec_bytes")]
653        value: Vec<u8>,
654    }
655
656    #[test]
657    fn test_hexify_vec_bytes_serialize() {
658        let cases = [(vec![], "0x"), (vec![0], "0x00"), (vec![42, 66], "0x2a42")];
659
660        for (input, expected) in cases.into_iter() {
661            let hexify = HexifyVecBytesTest { value: input };
662            let serialized = serde_json::to_string(&hexify).unwrap();
663            self::assert_eq!(serialized, format!("{{\"value\":\"{}\"}}", expected));
664        }
665    }
666
667    #[test]
668    fn test_hexify_vec_bytes_deserialize() {
669        let cases = [
670            ("0x", vec![]),
671            ("0x0", vec![0]),
672            ("0xF", vec![15]),
673            ("0x2a42", vec![42, 66]),
674            ("0x2A42", vec![42, 66]),
675        ];
676
677        for (input, expected) in cases.into_iter() {
678            let deserializer: StringDeserializer<SerdeError> =
679                String::from_str(input).unwrap().into_deserializer();
680            let deserialized = hexify_vec_bytes::deserialize(deserializer).unwrap();
681            self::assert_eq!(deserialized, expected);
682        }
683
684        let fail_cases = ["cthulhu", "x", "0xazathoth"];
685        for input in fail_cases.into_iter() {
686            let deserializer: StringDeserializer<SerdeError> =
687                String::from_str(input).unwrap().into_deserializer();
688            let deserialized = hexify_vec_bytes::deserialize(deserializer);
689            assert!(deserialized.is_err());
690        }
691    }
692}