templar_proxy_oracle_kernel/primitive/
price_id.rs

1#[cfg(feature = "schemars")]
2use alloc::string::String;
3#[cfg(any(feature = "borsh", feature = "schemars"))]
4use alloc::string::ToString;
5
6const PRICE_IDENTIFIER_BYTES: usize = 32;
7#[cfg(feature = "schemars")]
8const PRICE_IDENTIFIER_HEX_LENGTH: u32 = 64;
9#[cfg(feature = "schemars")]
10const PRICE_IDENTIFIER_HEX_PATTERN: &str = "^[0-9A-Fa-f]{64}$";
11
12#[cfg_attr(
13    feature = "borsh",
14    derive(
15        ::borsh::BorshSerialize,
16        ::borsh::BorshDeserialize,
17        ::borsh::BorshSchema
18    )
19)]
20#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))]
21#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct PriceIdentifier(
23    #[cfg_attr(
24        feature = "serde",
25        serde(
26            serialize_with = "hex::serde::serialize",
27            deserialize_with = "hex::serde::deserialize"
28        )
29    )]
30    pub [u8; PRICE_IDENTIFIER_BYTES],
31);
32
33#[cfg(feature = "schemars")]
34impl schemars::JsonSchema for PriceIdentifier {
35    fn schema_name() -> String {
36        "PriceIdentifier".to_string()
37    }
38
39    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
40        let mut schema = gen.subschema_for::<String>().into_object();
41        let validation = schema.string();
42        validation.min_length = Some(PRICE_IDENTIFIER_HEX_LENGTH);
43        validation.max_length = Some(PRICE_IDENTIFIER_HEX_LENGTH);
44        validation.pattern = Some(PRICE_IDENTIFIER_HEX_PATTERN.to_string());
45        schema.into()
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    #[cfg(any(feature = "serde", feature = "schemars", feature = "borsh"))]
52    use super::PriceIdentifier;
53    #[cfg(feature = "borsh")]
54    use super::PRICE_IDENTIFIER_BYTES;
55    #[cfg(feature = "schemars")]
56    use super::{PRICE_IDENTIFIER_HEX_LENGTH, PRICE_IDENTIFIER_HEX_PATTERN};
57
58    #[cfg(feature = "serde")]
59    #[test]
60    fn serde_round_trip_uses_lowercase_hex_string() {
61        let id = PriceIdentifier([
62            0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
63            0xee, 0xff, 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe, 0x01, 0x23, 0x45, 0x67,
64            0x89, 0xab, 0xcd, 0xef,
65        ]);
66
67        let serialized = serde_json::to_string(&id).unwrap();
68        assert_eq!(
69            serialized,
70            r#""00112233445566778899aabbccddeeff1032547698badcfe0123456789abcdef""#
71        );
72
73        let deserialized: PriceIdentifier = serde_json::from_str(&serialized).unwrap();
74        assert_eq!(deserialized, id);
75    }
76
77    #[cfg(feature = "serde")]
78    #[test]
79    fn serde_rejects_malformed_hex() {
80        assert!(serde_json::from_str::<PriceIdentifier>(r#""xyz""#).is_err());
81        assert!(serde_json::from_str::<PriceIdentifier>(r#""0011""#).is_err());
82    }
83
84    #[cfg(feature = "serde")]
85    #[test]
86    fn hal_26_serde_accepts_exact_case_insensitive_hex_only() {
87        let valid = [
88            "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
89            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
90            "AaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAa",
91        ];
92        for value in valid {
93            let encoded = alloc::format!("\"{value}\"");
94            assert!(serde_json::from_str::<PriceIdentifier>(&encoded).is_ok());
95        }
96
97        let invalid = [
98            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
99            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
100            "gaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
101            "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
102            " aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
103            "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
104        ];
105        for value in invalid {
106            let encoded = alloc::format!("\"{value}\"");
107            assert!(serde_json::from_str::<PriceIdentifier>(&encoded).is_err());
108        }
109        assert!(serde_json::from_str::<PriceIdentifier>("[0, 1]").is_err());
110    }
111
112    #[cfg(feature = "schemars")]
113    #[test]
114    fn hal_26_schema_describes_the_hex_wire_format() {
115        let schema = schemars::schema_for!(PriceIdentifier);
116        let validation = schema.schema.string.as_ref().unwrap();
117        assert_eq!(validation.min_length, Some(PRICE_IDENTIFIER_HEX_LENGTH));
118        assert_eq!(validation.max_length, Some(PRICE_IDENTIFIER_HEX_LENGTH));
119        assert_eq!(
120            validation.pattern.as_deref(),
121            Some(PRICE_IDENTIFIER_HEX_PATTERN)
122        );
123        assert!(schema.schema.array.is_none());
124    }
125
126    #[cfg(feature = "borsh")]
127    #[test]
128    fn hal_26_borsh_bytes_remain_unchanged() {
129        let bytes = core::array::from_fn(|index| {
130            u8::try_from(index).unwrap_or_else(|_| unreachable!("array length is below u8::MAX"))
131        });
132        let id = PriceIdentifier(bytes);
133        assert_eq!(borsh::to_vec(&id).unwrap(), bytes);
134        assert_eq!(borsh::to_vec(&id).unwrap().len(), PRICE_IDENTIFIER_BYTES);
135    }
136}