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 }