lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }