lib

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

mod.rs (9776B)


      1 mod codec;
      2 pub mod draft;
      3 pub mod model;
      4 pub mod mutation;
      5 pub mod price_ext;
      6 pub mod validation;
      7 
      8 use radroots_events::{
      9     RadrootsNostrEvent,
     10     ids::{
     11         RadrootsAddressableCoordinateParts, RadrootsDTag, RadrootsIdParseError,
     12         RadrootsListingAddress, RadrootsPublicKey,
     13     },
     14     kinds::{KIND_LISTING, is_listing_kind},
     15     listing::RadrootsListing,
     16 };
     17 use thiserror::Error;
     18 
     19 pub use self::draft::{
     20     RadrootsCanonicalListingDraft, RadrootsListingDraftDocumentV1, RadrootsListingDraftError,
     21     canonicalize_listing_draft,
     22 };
     23 #[cfg(feature = "serde_json")]
     24 pub use self::mutation::build_listing_mutation_draft;
     25 pub use self::mutation::{
     26     RadrootsListingLifecycleState, RadrootsListingMutation, RadrootsListingMutationError,
     27 };
     28 pub use radroots_events::order::RadrootsListingParseError as ListingParseError;
     29 
     30 #[derive(Clone, Debug, Error, PartialEq, Eq)]
     31 pub enum RadrootsListingAddressError {
     32     #[error("invalid listing address: {0}")]
     33     InvalidAddress(RadrootsIdParseError),
     34     #[error("listing address must reference a listing kind")]
     35     InvalidKind { actual: u32 },
     36 }
     37 
     38 #[derive(Clone, Debug, Error, PartialEq, Eq)]
     39 pub enum RadrootsPublicListingAddressError {
     40     #[error("invalid listing address: {0}")]
     41     InvalidAddress(RadrootsIdParseError),
     42     #[error("listing address must reference a listing kind")]
     43     InvalidListingKind { actual: u32 },
     44     #[error("listing address must reference a public NIP-99 listing")]
     45     InvalidKind { actual: u32 },
     46 }
     47 
     48 #[derive(Clone, Debug, PartialEq, Eq)]
     49 pub struct RadrootsListingAddressParts {
     50     pub address: RadrootsListingAddress,
     51     pub kind: u32,
     52     pub seller_pubkey: RadrootsPublicKey,
     53     pub listing_id: RadrootsDTag,
     54 }
     55 
     56 impl RadrootsListingAddressParts {
     57     pub fn parse(value: impl AsRef<str>) -> Result<Self, RadrootsListingAddressError> {
     58         parse_listing_address(value)
     59     }
     60 }
     61 
     62 #[derive(Clone, Debug, PartialEq, Eq)]
     63 pub struct RadrootsPublicListingAddress {
     64     pub address: RadrootsListingAddress,
     65     pub kind: u32,
     66     pub seller_pubkey: RadrootsPublicKey,
     67     pub listing_id: RadrootsDTag,
     68 }
     69 
     70 impl RadrootsPublicListingAddress {
     71     pub fn parse(value: impl AsRef<str>) -> Result<Self, RadrootsPublicListingAddressError> {
     72         parse_public_listing_address(value)
     73     }
     74 }
     75 
     76 pub fn parse_listing_address(
     77     value: impl AsRef<str>,
     78 ) -> Result<RadrootsListingAddressParts, RadrootsListingAddressError> {
     79     let value = value.as_ref();
     80     let address = RadrootsListingAddress::parse(value)
     81         .map_err(RadrootsListingAddressError::InvalidAddress)?;
     82     let parts = RadrootsAddressableCoordinateParts::parse(address.as_str())
     83         .map_err(RadrootsListingAddressError::InvalidAddress)?;
     84     if !is_listing_kind(parts.kind) {
     85         return Err(RadrootsListingAddressError::InvalidKind { actual: parts.kind });
     86     }
     87     Ok(RadrootsListingAddressParts {
     88         address,
     89         kind: parts.kind,
     90         seller_pubkey: parts.pubkey,
     91         listing_id: parts.d_tag,
     92     })
     93 }
     94 
     95 pub fn parse_public_listing_address(
     96     value: impl AsRef<str>,
     97 ) -> Result<RadrootsPublicListingAddress, RadrootsPublicListingAddressError> {
     98     let parts = parse_listing_address(value).map_err(|error| match error {
     99         RadrootsListingAddressError::InvalidAddress(error) => {
    100             RadrootsPublicListingAddressError::InvalidAddress(error)
    101         }
    102         RadrootsListingAddressError::InvalidKind { actual } => {
    103             RadrootsPublicListingAddressError::InvalidListingKind { actual }
    104         }
    105     })?;
    106     if parts.kind != KIND_LISTING {
    107         return Err(RadrootsPublicListingAddressError::InvalidKind { actual: parts.kind });
    108     }
    109     Ok(RadrootsPublicListingAddress {
    110         address: parts.address,
    111         kind: parts.kind,
    112         seller_pubkey: parts.seller_pubkey,
    113         listing_id: parts.listing_id,
    114     })
    115 }
    116 
    117 pub fn parse_listing_event(
    118     event: &RadrootsNostrEvent,
    119 ) -> Result<RadrootsListing, ListingParseError> {
    120     if !is_listing_kind(event.kind) {
    121         return Err(ListingParseError::InvalidKind(event.kind));
    122     }
    123     self::codec::listing_from_event_parts(&event.tags, &event.content)
    124 }
    125 
    126 #[cfg(test)]
    127 mod tests {
    128     use super::{
    129         RadrootsListingAddressError, RadrootsListingAddressParts, RadrootsPublicListingAddress,
    130         RadrootsPublicListingAddressError, parse_listing_address, parse_listing_event,
    131         parse_public_listing_address,
    132     };
    133     use radroots_events::{
    134         RadrootsNostrEvent,
    135         ids::RadrootsListingAddress,
    136         kinds::{KIND_LISTING, KIND_LISTING_DRAFT, KIND_PROFILE},
    137         order::RadrootsListingParseError,
    138     };
    139 
    140     const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
    141 
    142     fn listing_event() -> RadrootsNostrEvent {
    143         RadrootsNostrEvent {
    144             id: "event-1".into(),
    145             author: SELLER.into(),
    146             created_at: 1,
    147             kind: KIND_LISTING,
    148             tags: vec![
    149                 vec!["d".into(), "AAAAAAAAAAAAAAAAAAAAAg".into()],
    150                 vec!["p".into(), SELLER.into()],
    151                 vec!["a".into(), format!("30340:{SELLER}:AAAAAAAAAAAAAAAAAAAAAA")],
    152                 vec!["key".into(), "coffee".into()],
    153                 vec!["title".into(), "Coffee".into()],
    154                 vec!["category".into(), "coffee".into()],
    155                 vec!["summary".into(), "Single origin".into()],
    156                 vec!["radroots:primary_bin".into(), "bin-1".into()],
    157                 vec![
    158                     "radroots:bin".into(),
    159                     "bin-1".into(),
    160                     "1000".into(),
    161                     "g".into(),
    162                 ],
    163                 vec![
    164                     "radroots:price".into(),
    165                     "bin-1".into(),
    166                     "20".into(),
    167                     "USD".into(),
    168                     "1".into(),
    169                     "g".into(),
    170                 ],
    171             ],
    172             content: String::new(),
    173             sig: String::new(),
    174         }
    175     }
    176 
    177     #[test]
    178     fn parse_listing_event_rejects_non_listing_kind() {
    179         let event = RadrootsNostrEvent {
    180             id: "event-1".into(),
    181             author: "seller".into(),
    182             created_at: 1,
    183             kind: KIND_PROFILE,
    184             tags: vec![],
    185             content: String::new(),
    186             sig: String::new(),
    187         };
    188 
    189         assert!(matches!(
    190             parse_listing_event(&event),
    191             Err(RadrootsListingParseError::InvalidKind(KIND_PROFILE))
    192         ));
    193     }
    194 
    195     #[test]
    196     fn parse_listing_event_accepts_listing_kind() {
    197         let listing = parse_listing_event(&listing_event()).expect("listing");
    198 
    199         assert_eq!(listing.d_tag.as_str(), "AAAAAAAAAAAAAAAAAAAAAg");
    200         assert_eq!(listing.farm.pubkey, SELLER);
    201         assert_eq!(listing.primary_bin_id.as_str(), "bin-1");
    202     }
    203 
    204     #[test]
    205     fn listing_address_associated_parsers_delegate_to_public_parsers() {
    206         let raw = format!("{KIND_LISTING}:{SELLER}:listing-1");
    207 
    208         let listing = RadrootsListingAddressParts::parse(raw.clone()).expect("listing address");
    209         let public = RadrootsPublicListingAddress::parse(&raw).expect("public address");
    210         let typed =
    211             parse_public_listing_address(RadrootsListingAddress::parse(&raw).expect("typed addr"))
    212                 .expect("typed public address");
    213 
    214         assert_eq!(listing.address.as_str(), raw);
    215         assert_eq!(public.address.as_str(), raw);
    216         assert_eq!(typed.address.as_str(), raw);
    217         assert_eq!(listing.seller_pubkey.as_str(), SELLER);
    218         assert_eq!(public.seller_pubkey.as_str(), SELLER);
    219         assert_eq!(typed.seller_pubkey.as_str(), SELLER);
    220     }
    221 
    222     #[test]
    223     fn parse_public_listing_address_accepts_public_listing_kind() {
    224         let raw = format!("{KIND_LISTING}:{SELLER}:listing-1");
    225         let parsed = parse_public_listing_address(&raw).expect("public listing address");
    226 
    227         assert_eq!(parsed.address.as_str(), raw);
    228         assert_eq!(parsed.kind, KIND_LISTING);
    229         assert_eq!(parsed.seller_pubkey.as_str(), SELLER);
    230         assert_eq!(parsed.listing_id.as_str(), "listing-1");
    231     }
    232 
    233     #[test]
    234     fn parse_listing_address_accepts_draft_listing_kind() {
    235         let raw = format!("{KIND_LISTING_DRAFT}:{SELLER}:listing-1");
    236         let parsed = parse_listing_address(&raw).expect("listing address");
    237 
    238         assert_eq!(parsed.address.as_str(), raw);
    239         assert_eq!(parsed.kind, KIND_LISTING_DRAFT);
    240         assert_eq!(parsed.seller_pubkey.as_str(), SELLER);
    241         assert_eq!(parsed.listing_id.as_str(), "listing-1");
    242     }
    243 
    244     #[test]
    245     fn parse_public_listing_address_rejects_draft_listing_kind() {
    246         let raw = format!("{KIND_LISTING_DRAFT}:{SELLER}:listing-1");
    247 
    248         assert!(matches!(
    249             parse_public_listing_address(&raw),
    250             Err(RadrootsPublicListingAddressError::InvalidKind {
    251                 actual: KIND_LISTING_DRAFT
    252             })
    253         ));
    254     }
    255 
    256     #[test]
    257     fn parse_public_listing_address_maps_invalid_listing_addresses() {
    258         assert!(matches!(
    259             parse_public_listing_address("not-an-address"),
    260             Err(RadrootsPublicListingAddressError::InvalidAddress(_))
    261         ));
    262 
    263         let raw = format!("{KIND_PROFILE}:{SELLER}:listing-1");
    264         assert!(matches!(
    265             parse_public_listing_address(&raw),
    266             Err(RadrootsPublicListingAddressError::InvalidListingKind {
    267                 actual: KIND_PROFILE
    268             })
    269         ));
    270     }
    271 
    272     #[test]
    273     fn parse_listing_address_rejects_non_listing_kind() {
    274         let raw = format!("{KIND_PROFILE}:{SELLER}:listing-1");
    275 
    276         assert!(matches!(
    277             parse_listing_address(&raw),
    278             Err(RadrootsListingAddressError::InvalidKind {
    279                 actual: KIND_PROFILE
    280             })
    281         ));
    282     }
    283 }