lib

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

mod.rs (9074B)


      1 pub mod draft;
      2 pub mod model;
      3 pub mod mutation;
      4 pub mod price_ext;
      5 pub mod validation;
      6 
      7 use radroots_event::{
      8     envelope::EventEnvelope,
      9     id::{AddressableCoordinateParts, ClassifiedListingAddress, DTag, ParseError},
     10     listing::operational::{OperationalListing, OperationalListingParseError},
     11 };
     12 use radroots_event_codec::decode::operational_listing::operational_listing_from_nostr_event;
     13 use radroots_identity::PublicKey;
     14 
     15 pub use self::draft::{
     16     RadrootsOperationalListingCanonicalEdit, RadrootsOperationalListingEditDocumentV1,
     17     RadrootsOperationalListingEditError, canonicalize_operational_listing_edit,
     18 };
     19 pub use self::model::{RadrootsOperationalListingSubtotal, RadrootsOperationalListingTotal};
     20 pub use self::mutation::build_operational_listing_mutation_plan;
     21 pub use self::mutation::{
     22     RadrootsOperationalListingLifecycleState, RadrootsOperationalListingMutation,
     23     RadrootsOperationalListingMutationError,
     24 };
     25 pub use self::price_ext::BinPricingTryExt;
     26 pub use self::validation::{
     27     RadrootsOperationalListingTradeProjection, validate_operational_listing_event,
     28     validate_operational_listing_model,
     29 };
     30 #[derive(Clone, Debug, PartialEq, Eq)]
     31 pub struct RadrootsClassifiedListingAddressParts {
     32     pub address: ClassifiedListingAddress,
     33     pub kind: u32,
     34     pub seller_pubkey: PublicKey,
     35     pub listing_id: DTag,
     36 }
     37 
     38 impl RadrootsClassifiedListingAddressParts {
     39     pub fn parse(value: impl AsRef<str>) -> Result<Self, ParseError> {
     40         parse_classified_listing_address(value)
     41     }
     42 }
     43 
     44 #[derive(Clone, Debug, PartialEq, Eq)]
     45 pub struct RadrootsPublicClassifiedListingAddress {
     46     pub address: ClassifiedListingAddress,
     47     pub kind: u32,
     48     pub seller_pubkey: PublicKey,
     49     pub listing_id: DTag,
     50 }
     51 
     52 impl RadrootsPublicClassifiedListingAddress {
     53     pub fn parse(value: impl AsRef<str>) -> Result<Self, ParseError> {
     54         parse_public_classified_listing_address(value)
     55     }
     56 }
     57 
     58 pub fn parse_classified_listing_address(
     59     value: impl AsRef<str>,
     60 ) -> Result<RadrootsClassifiedListingAddressParts, ParseError> {
     61     let value = value.as_ref();
     62     let address = ClassifiedListingAddress::parse(value)?;
     63     let parts = AddressableCoordinateParts::parse(address.as_str())
     64         .expect("typed listing address must contain valid coordinate parts");
     65     Ok(RadrootsClassifiedListingAddressParts {
     66         address,
     67         kind: parts.kind,
     68         seller_pubkey: parts.pubkey,
     69         listing_id: parts.d_tag,
     70     })
     71 }
     72 
     73 pub fn parse_public_classified_listing_address(
     74     value: impl AsRef<str>,
     75 ) -> Result<RadrootsPublicClassifiedListingAddress, ParseError> {
     76     let parts = parse_classified_listing_address(value)?;
     77     Ok(RadrootsPublicClassifiedListingAddress {
     78         address: parts.address,
     79         kind: parts.kind,
     80         seller_pubkey: parts.seller_pubkey,
     81         listing_id: parts.listing_id,
     82     })
     83 }
     84 
     85 pub fn parse_operational_listing_event(
     86     event: &EventEnvelope,
     87 ) -> Result<OperationalListing, OperationalListingParseError> {
     88     operational_listing_from_nostr_event(event)
     89 }
     90 
     91 #[cfg(test)]
     92 mod tests {
     93     use super::{
     94         RadrootsClassifiedListingAddressParts, RadrootsPublicClassifiedListingAddress,
     95         parse_classified_listing_address, parse_operational_listing_event,
     96         parse_public_classified_listing_address,
     97     };
     98     use radroots_event::{
     99         envelope::EventEnvelope,
    100         envelope::EventEnvelopeParts,
    101         envelope::kind::{KIND_CLASSIFIED_LISTING, KIND_PROFILE},
    102         id::ClassifiedListingAddress,
    103         listing::operational::OperationalListingParseError,
    104     };
    105 
    106     const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
    107 
    108     fn listing_event() -> EventEnvelope {
    109         EventEnvelope::new(EventEnvelopeParts {
    110             id: "9".repeat(64),
    111             author: SELLER.to_string(),
    112             created_at: 1,
    113             kind: KIND_CLASSIFIED_LISTING,
    114             tags: vec![
    115                 vec!["d".into(), "AAAAAAAAAAAAAAAAAAAAAg".into()],
    116                 vec!["p".into(), SELLER.into()],
    117                 vec!["a".into(), format!("30340:{SELLER}:AAAAAAAAAAAAAAAAAAAAAA")],
    118                 vec!["key".into(), "coffee".into()],
    119                 vec!["title".into(), "Coffee".into()],
    120                 vec!["category".into(), "coffee".into()],
    121                 vec!["summary".into(), "Single origin".into()],
    122                 vec!["radroots:primary_bin".into(), "bin-1".into()],
    123                 vec![
    124                     "radroots:bin".into(),
    125                     "bin-1".into(),
    126                     "1000".into(),
    127                     "g".into(),
    128                 ],
    129                 vec![
    130                     "radroots:price".into(),
    131                     "bin-1".into(),
    132                     "20".into(),
    133                     "USD".into(),
    134                     "1".into(),
    135                     "g".into(),
    136                 ],
    137             ],
    138             content: String::new(),
    139             sig: "f".repeat(128),
    140         })
    141         .expect("listing event")
    142     }
    143 
    144     #[test]
    145     fn parse_operational_listing_event_rejects_non_listing_kind() {
    146         let event = EventEnvelope::new(EventEnvelopeParts {
    147             id: "8".repeat(64),
    148             author: SELLER.to_string(),
    149             created_at: 1,
    150             kind: KIND_PROFILE,
    151             tags: vec![],
    152             content: String::new(),
    153             sig: "f".repeat(128),
    154         })
    155         .expect("profile event");
    156 
    157         assert!(matches!(
    158             parse_operational_listing_event(&event),
    159             Err(OperationalListingParseError::InvalidKind(KIND_PROFILE))
    160         ));
    161     }
    162 
    163     #[test]
    164     fn parse_operational_listing_event_accepts_listing_kind() {
    165         let listing = parse_operational_listing_event(&listing_event()).expect("listing");
    166 
    167         assert_eq!(listing.d_tag.as_str(), "AAAAAAAAAAAAAAAAAAAAAg");
    168         assert_eq!(listing.farm.pubkey, SELLER);
    169         assert_eq!(listing.primary_bin_id.as_str(), "bin-1");
    170     }
    171 
    172     #[test]
    173     fn listing_address_associated_parsers_delegate_to_public_parsers() {
    174         let raw = format!("{KIND_CLASSIFIED_LISTING}:{SELLER}:listing-1");
    175 
    176         let listing =
    177             RadrootsClassifiedListingAddressParts::parse(raw.clone()).expect("listing address");
    178         let public = RadrootsPublicClassifiedListingAddress::parse(&raw).expect("public address");
    179         let typed = parse_public_classified_listing_address(
    180             ClassifiedListingAddress::parse(&raw).expect("typed addr"),
    181         )
    182         .expect("typed public address");
    183 
    184         assert_eq!(listing.address.as_str(), raw);
    185         assert_eq!(public.address.as_str(), raw);
    186         assert_eq!(typed.address.as_str(), raw);
    187         assert_eq!(listing.seller_pubkey.to_hex(), SELLER);
    188         assert_eq!(public.seller_pubkey.to_hex(), SELLER);
    189         assert_eq!(typed.seller_pubkey.to_hex(), SELLER);
    190     }
    191 
    192     #[test]
    193     fn parse_public_classified_listing_address_accepts_public_listing_kind() {
    194         let raw = format!("{KIND_CLASSIFIED_LISTING}:{SELLER}:listing-1");
    195         let parsed = parse_public_classified_listing_address(&raw).expect("public listing address");
    196 
    197         assert_eq!(parsed.address.as_str(), raw);
    198         assert_eq!(parsed.kind, KIND_CLASSIFIED_LISTING);
    199         assert_eq!(parsed.seller_pubkey.to_hex(), SELLER);
    200         assert_eq!(parsed.listing_id.as_str(), "listing-1");
    201     }
    202 
    203     #[test]
    204     fn parse_classified_listing_address_rejects_retired_listing_kind() {
    205         let raw = format!("30403:{SELLER}:listing-1");
    206 
    207         assert!(matches!(
    208             parse_classified_listing_address(&raw),
    209             Err(radroots_event::id::ParseError::UnexpectedKind {
    210                 expected: KIND_CLASSIFIED_LISTING,
    211                 actual: 30403,
    212             })
    213         ));
    214         assert!(matches!(
    215             parse_public_classified_listing_address(&raw),
    216             Err(radroots_event::id::ParseError::UnexpectedKind {
    217                 expected: KIND_CLASSIFIED_LISTING,
    218                 actual: 30403,
    219             })
    220         ));
    221     }
    222 
    223     #[test]
    224     fn parse_public_classified_listing_address_maps_invalid_listing_addresses() {
    225         assert!(matches!(
    226             parse_public_classified_listing_address("not-an-address"),
    227             Err(radroots_event::id::ParseError::InvalidFormat)
    228         ));
    229 
    230         let raw = format!("{KIND_PROFILE}:{SELLER}:listing-1");
    231         assert!(matches!(
    232             parse_public_classified_listing_address(&raw),
    233             Err(radroots_event::id::ParseError::UnexpectedKind {
    234                 expected: KIND_CLASSIFIED_LISTING,
    235                 actual: KIND_PROFILE,
    236             })
    237         ));
    238         assert!(ClassifiedListingAddress::parse(&raw).is_err());
    239     }
    240 
    241     #[test]
    242     fn parse_classified_listing_address_rejects_non_listing_kind() {
    243         let raw = format!("{KIND_PROFILE}:{SELLER}:listing-1");
    244 
    245         assert!(matches!(
    246             parse_classified_listing_address(&raw),
    247             Err(radroots_event::id::ParseError::UnexpectedKind {
    248                 expected: KIND_CLASSIFIED_LISTING,
    249                 actual: KIND_PROFILE,
    250             })
    251         ));
    252     }
    253 }