model.rs (6369B)
1 //! Native trade-domain projections and validated business identifiers. 2 //! 3 //! Protocol trade identity remains owned by [`radroots_event::trade`]. This 4 //! module owns projection state plus the semantically distinct [`OrderId`]. 5 6 pub use crate::trade_contract_v1::{ 7 RadrootsTradeAgreementClaimV1, RadrootsTradeAgreementStateV1, RadrootsTradeAttestationStateV1, 8 RadrootsTradeConflictStateV1, RadrootsTradeFulfillmentStateV1, RadrootsTradeNegotiationStateV1, 9 RadrootsTradePaymentStateV1, RadrootsTradePrivateTermsStateV1, RadrootsTradeProjectionV1, 10 }; 11 12 #[cfg(not(feature = "std"))] 13 use alloc::string::{String, ToString}; 14 #[cfg(feature = "std")] 15 use std::string::String; 16 17 use core::{fmt, str::FromStr}; 18 19 /// Maximum encoded length of a human or business order identifier. 20 pub const ORDER_ID_MAX_LEN: usize = 128; 21 22 /// A human or business-workflow order identifier. 23 /// 24 /// This identifier is deliberately distinct from the canonical protocol 25 /// [`radroots_event::trade::TradeId`]. No conversion exists between them. 26 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 27 pub struct OrderId(String); 28 29 impl OrderId { 30 /// Parses a non-empty, whitespace-free business identifier. 31 pub fn parse(value: impl AsRef<str>) -> Result<Self, OrderIdError> { 32 let value = value.as_ref(); 33 if value.is_empty() { 34 return Err(OrderIdError::Empty); 35 } 36 if value.len() > ORDER_ID_MAX_LEN { 37 return Err(OrderIdError::TooLong { 38 max: ORDER_ID_MAX_LEN, 39 actual: value.len(), 40 }); 41 } 42 if value.trim() != value 43 || value 44 .chars() 45 .any(|character| character.is_control() || character.is_whitespace()) 46 { 47 return Err(OrderIdError::InvalidCharacter); 48 } 49 Ok(Self(value.to_string())) 50 } 51 52 /// Returns the validated business identifier. 53 #[inline] 54 pub fn as_str(&self) -> &str { 55 self.0.as_str() 56 } 57 58 /// Consumes the identifier and returns its owned representation. 59 #[inline] 60 pub fn into_string(self) -> String { 61 self.0 62 } 63 } 64 65 impl AsRef<str> for OrderId { 66 fn as_ref(&self) -> &str { 67 self.as_str() 68 } 69 } 70 71 impl From<OrderId> for String { 72 fn from(value: OrderId) -> Self { 73 value.into_string() 74 } 75 } 76 77 impl fmt::Display for OrderId { 78 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 79 formatter.write_str(self.as_str()) 80 } 81 } 82 83 impl FromStr for OrderId { 84 type Err = OrderIdError; 85 86 fn from_str(value: &str) -> Result<Self, Self::Err> { 87 Self::parse(value) 88 } 89 } 90 91 impl TryFrom<&str> for OrderId { 92 type Error = OrderIdError; 93 94 fn try_from(value: &str) -> Result<Self, Self::Error> { 95 Self::parse(value) 96 } 97 } 98 99 impl TryFrom<String> for OrderId { 100 type Error = OrderIdError; 101 102 fn try_from(value: String) -> Result<Self, Self::Error> { 103 Self::parse(value) 104 } 105 } 106 107 #[cfg(feature = "serde")] 108 impl serde::Serialize for OrderId { 109 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 110 where 111 S: serde::Serializer, 112 { 113 serializer.serialize_str(self.as_str()) 114 } 115 } 116 117 #[cfg(feature = "serde")] 118 impl<'de> serde::Deserialize<'de> for OrderId { 119 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 120 where 121 D: serde::Deserializer<'de>, 122 { 123 let value = <String as serde::Deserialize>::deserialize(deserializer)?; 124 Self::parse(value).map_err(serde::de::Error::custom) 125 } 126 } 127 128 /// Business order identifier validation failure. 129 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 130 pub enum OrderIdError { 131 /// The identifier was empty. 132 Empty, 133 /// The identifier exceeded its bounded encoded length. 134 TooLong { max: usize, actual: usize }, 135 /// The identifier contained whitespace or a control character. 136 InvalidCharacter, 137 } 138 139 impl fmt::Display for OrderIdError { 140 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 141 match self { 142 Self::Empty => formatter.write_str("order identifier is empty"), 143 Self::TooLong { max, actual } => write!( 144 formatter, 145 "order identifier length {actual} exceeds maximum length {max}" 146 ), 147 Self::InvalidCharacter => { 148 formatter.write_str("order identifier contains an invalid character") 149 } 150 } 151 } 152 } 153 154 impl core::error::Error for OrderIdError {} 155 156 #[cfg(test)] 157 mod tests { 158 use core::str::FromStr; 159 160 use super::{ORDER_ID_MAX_LEN, OrderId, OrderIdError}; 161 162 #[test] 163 fn order_id_parses_business_values_without_becoming_a_protocol_trade_id() { 164 let order_id = OrderId::parse("order-1").expect("business order id"); 165 166 assert_eq!(order_id.as_str(), "order-1"); 167 assert_eq!(order_id.as_ref(), "order-1"); 168 assert_eq!(order_id.to_string(), "order-1"); 169 assert_eq!(OrderId::try_from("order-1").unwrap(), order_id); 170 assert_eq!(OrderId::from_str("order-1").unwrap(), order_id); 171 assert_eq!(String::from(order_id.clone()), order_id.into_string()); 172 assert!(radroots_event::trade::TradeId::parse("order-1").is_err()); 173 } 174 175 #[test] 176 fn order_id_rejects_invalid_business_values() { 177 assert_eq!(OrderId::parse("").unwrap_err(), OrderIdError::Empty); 178 assert_eq!( 179 OrderId::parse("x".repeat(ORDER_ID_MAX_LEN + 1)).unwrap_err(), 180 OrderIdError::TooLong { 181 max: ORDER_ID_MAX_LEN, 182 actual: ORDER_ID_MAX_LEN + 1, 183 } 184 ); 185 for value in [" order-1", "order-1 ", "order 1", "order\n1"] { 186 assert_eq!( 187 OrderId::parse(value).unwrap_err(), 188 OrderIdError::InvalidCharacter 189 ); 190 } 191 } 192 193 #[cfg(feature = "json")] 194 #[test] 195 fn order_id_serde_round_trip_preserves_validation() { 196 let order_id = OrderId::parse("order-1").expect("business order id"); 197 let encoded = serde_json::to_string(&order_id).expect("serialize order id"); 198 199 assert_eq!(encoded, "\"order-1\""); 200 assert_eq!( 201 serde_json::from_str::<OrderId>(&encoded).expect("deserialize order id"), 202 order_id 203 ); 204 assert!(serde_json::from_str::<OrderId>("\"bad order\"").is_err()); 205 } 206 }