templar_proxy_oracle_kernel/primitive/
price_id.rs1#[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}