smart_config/de/
alloy_impl.rs

1use serde::{Serialize, de::DeserializeOwned};
2
3use crate::{
4    de::{DeserializeContext, DeserializeParam, Qualified, Serde, WellKnown, WellKnownOption},
5    error::ErrorWithOrigin,
6    metadata::{BasicTypes, ParamMetadata, TypeDescription},
7    value::Value,
8};
9
10const HASH_DE: Qualified<Serde![str]> =
11    Qualified::new(Serde![str], "hex string with optional 0x prefix");
12
13macro_rules! impl_well_known_hash {
14    ($($ty:ident),+) => {
15        $(
16        /// Accepts a hex string with an optional `0x` prefix.
17        #[cfg_attr(docsrs, doc(cfg(feature = "alloy")))]
18        impl WellKnown for alloy::primitives::aliases::$ty {
19            type Deserializer = Qualified<Serde![str]>;
20            const DE: Self::Deserializer = HASH_DE;
21        }
22
23        #[cfg_attr(docsrs, doc(cfg(feature = "alloy")))]
24        impl WellKnownOption for alloy::primitives::aliases::$ty {}
25        )+
26    };
27}
28
29impl_well_known_hash!(B64, B128, B160, B256, B512);
30
31/// Accepts a hex string with an optional `0x` prefix.
32#[cfg_attr(docsrs, doc(cfg(feature = "alloy")))]
33impl WellKnown for alloy::primitives::Address {
34    type Deserializer = Qualified<Serde![str]>;
35    const DE: Self::Deserializer = HASH_DE;
36}
37
38#[cfg_attr(docsrs, doc(cfg(feature = "alloy")))]
39impl WellKnownOption for alloy::primitives::Address {}
40
41/// Hex deserializer enforcing a `0x` prefix. This prefix is not required by `U*` deserializers,
42/// but the value may be ambiguous otherwise (e.g., `34` being equal to 0x34, not decimal 34).
43#[derive(Debug)]
44pub struct HexUintDeserializer;
45
46// This implementation is overly general, but since the struct is private, it's OK.
47impl<T: Serialize + DeserializeOwned> DeserializeParam<T> for HexUintDeserializer {
48    const EXPECTING: BasicTypes = BasicTypes::STRING;
49
50    fn describe(&self, description: &mut TypeDescription) {
51        description.set_details("0x-prefixed hex number");
52    }
53
54    fn deserialize_param(
55        &self,
56        ctx: DeserializeContext<'_>,
57        param: &'static ParamMetadata,
58    ) -> Result<T, ErrorWithOrigin> {
59        let deserializer = ctx.current_value_deserializer(param.name)?;
60        if let Value::String(s) = deserializer.value() {
61            if !s.expose().starts_with("0x") {
62                return Err(deserializer.invalid_type("0x-prefixed hex number"));
63            }
64        }
65        T::deserialize(deserializer)
66    }
67
68    fn serialize_param(&self, param: &T) -> serde_json::Value {
69        serde_json::to_value(param).expect("failed serializing value")
70    }
71}
72
73macro_rules! impl_well_known_uint {
74    ($($ty:ident),+) => {
75        $(
76        /// Accepts a hex string with an **mandatory** `0x` prefix. This prefix is required to clearly signal hex encoding
77        /// so that `"34"` doesn't get mistaken for decimal 34.
78        #[cfg_attr(docsrs, doc(cfg(feature = "primitive-types")))]
79        impl WellKnown for alloy::primitives::$ty {
80            type Deserializer = HexUintDeserializer;
81            const DE: Self::Deserializer = HexUintDeserializer;
82        }
83
84        #[cfg_attr(docsrs, doc(cfg(feature = "primitive-types")))]
85        impl WellKnownOption for alloy::primitives::$ty {}
86        )+
87    };
88}
89
90impl_well_known_uint!(U8, U16, U32, U64, U128, U160, U256, U512);
91
92#[cfg(test)]
93mod tests {
94    use std::collections::HashMap;
95
96    use alloy::primitives::{Address, B256, U128, U160, U256};
97    use smart_config_derive::{DescribeConfig, DeserializeConfig};
98
99    use crate::{
100        config,
101        testing::{test, test_complete},
102    };
103
104    #[derive(Debug, PartialEq, DescribeConfig, DeserializeConfig)]
105    #[config(crate = crate)]
106    struct TestConfig {
107        int: U128,
108        #[config(default)]
109        hash: Option<B256>,
110        #[config(default)]
111        addresses: Vec<Address>,
112        #[config(default)]
113        balances: HashMap<Address, U256>,
114    }
115
116    #[test]
117    fn deserializing_values() {
118        let json = config!(
119            "int": "0x123",
120            "hash": "fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe",
121            "addresses": [
122                "0x0000000000000000000000000000000000001234",
123                "1212121212121212121212121212121212121212",
124            ],
125            "balances": HashMap::from([
126                ("0x0000000000000000000000000000000000004321", "0x3"),
127            ])
128        );
129        let config = test_complete::<TestConfig>(json).unwrap();
130        assert_eq!(config.int, U128::from(0x123));
131        assert_eq!(config.hash, Some(B256::repeat_byte(0xfe)));
132        assert_eq!(
133            config.addresses,
134            [
135                Address::from(U160::from(0x1234)),
136                Address::repeat_byte(0x12)
137            ]
138        );
139        assert_eq!(
140            config.balances,
141            HashMap::from([(Address::from(U160::from(0x4321)), U256::from(3))])
142        );
143    }
144
145    #[test]
146    fn uint_prefix_error() {
147        let json = config!("int": "123");
148        let err = test::<TestConfig>(json).unwrap_err();
149        assert_eq!(err.len(), 1);
150        let err = err.first();
151        assert_eq!(err.path(), "int");
152        let inner = err.inner().to_string();
153        assert!(
154            inner.contains("invalid type") && inner.contains("0x-prefixed hex number"),
155            "{inner}"
156        );
157    }
158}