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 }