decode.rs (25776B)
1 #![cfg(feature = "serde_json")] 2 3 #[cfg(not(feature = "std"))] 4 use alloc::{ 5 string::{String, ToString}, 6 vec::Vec, 7 }; 8 9 use radroots_core::{ 10 RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreDiscount, RadrootsCoreMoney, 11 RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, 12 }; 13 use radroots_events::{ 14 RadrootsNostrEvent, 15 farm::RadrootsFarmRef, 16 ids::{RadrootsDTag, RadrootsInventoryBinId}, 17 kinds::{KIND_FARM, KIND_PLOT, KIND_RESOURCE_AREA, is_listing_kind}, 18 listing::{ 19 RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, 20 RadrootsListingDeliveryMethod, RadrootsListingImage, RadrootsListingImageSize, 21 RadrootsListingLocation, RadrootsListingProduct, RadrootsListingStatus, 22 }, 23 plot::RadrootsPlotRef, 24 resource_area::RadrootsResourceAreaRef, 25 tags::{TAG_D, TAG_PUBLISHED_AT}, 26 }; 27 28 use crate::d_tag::validate_d_tag_tag; 29 use crate::error::EventParseError; 30 use crate::parsed::{RadrootsParsedData, RadrootsParsedEvent}; 31 32 const EXPECTED_LISTING_KINDS: &str = "30402 or 30403"; 33 const TAG_A: &str = "a"; 34 const TAG_P: &str = "p"; 35 const TAG_PRICE: &str = "price"; 36 const TAG_RADROOTS_BIN: &str = "radroots:bin"; 37 const TAG_RADROOTS_PRICE: &str = "radroots:price"; 38 const TAG_RADROOTS_DISCOUNT: &str = "radroots:discount"; 39 const TAG_RADROOTS_PRIMARY_BIN: &str = "radroots:primary_bin"; 40 const TAG_RADROOTS_RESOURCE_AREA: &str = "radroots:resource_area"; 41 const TAG_RADROOTS_PLOT: &str = "radroots:plot"; 42 const TAG_LOCATION: &str = "location"; 43 const TAG_IMAGE: &str = "image"; 44 const TAG_GEOHASH: &str = "g"; 45 const TAG_INVENTORY: &str = "inventory"; 46 const TAG_DELIVERY: &str = "delivery"; 47 const TAG_RADROOTS_AVAILABILITY_START: &str = "radroots:availability_start"; 48 const TAG_STATUS: &str = "status"; 49 const TAG_EXPIRES_AT: &str = "expires_at"; 50 51 fn parse_decimal(value: &str, field: &'static str) -> Result<RadrootsCoreDecimal, EventParseError> { 52 value 53 .parse::<RadrootsCoreDecimal>() 54 .map_err(|_| EventParseError::InvalidTag(field)) 55 } 56 57 fn parse_currency( 58 value: &str, 59 field: &'static str, 60 ) -> Result<RadrootsCoreCurrency, EventParseError> { 61 let upper = value.trim().to_ascii_uppercase(); 62 RadrootsCoreCurrency::from_str_upper(&upper).map_err(|_| EventParseError::InvalidTag(field)) 63 } 64 65 fn parse_unit(value: &str, field: &'static str) -> Result<RadrootsCoreUnit, EventParseError> { 66 value 67 .parse::<RadrootsCoreUnit>() 68 .map_err(|_| EventParseError::InvalidTag(field)) 69 } 70 71 fn parse_u64_tag_value( 72 value: Option<&String>, 73 field: &'static str, 74 ) -> Result<u64, EventParseError> { 75 value 76 .ok_or(EventParseError::InvalidTag(field))? 77 .parse::<u64>() 78 .map_err(|_| EventParseError::InvalidTag(field)) 79 } 80 81 fn parse_d_tag(tags: &[Vec<String>]) -> Result<String, EventParseError> { 82 let tag = tags 83 .iter() 84 .find(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_D)) 85 .ok_or(EventParseError::MissingTag(TAG_D))?; 86 let value = tag 87 .get(1) 88 .map(|value| value.to_string()) 89 .ok_or(EventParseError::InvalidTag(TAG_D))?; 90 if value.trim().is_empty() { 91 return Err(EventParseError::InvalidTag(TAG_D)); 92 } 93 validate_d_tag_tag(&value, TAG_D)?; 94 Ok(value) 95 } 96 97 fn parse_farm_ref(tags: &[Vec<String>]) -> Result<RadrootsFarmRef, EventParseError> { 98 for tag in tags 99 .iter() 100 .filter(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_A)) 101 { 102 let value = tag 103 .get(1) 104 .map(|value| value.to_string()) 105 .ok_or(EventParseError::InvalidTag(TAG_A))?; 106 let mut parts = value.splitn(3, ':'); 107 let kind = parts 108 .next() 109 .and_then(|raw| raw.parse::<u32>().ok()) 110 .ok_or(EventParseError::InvalidTag(TAG_A))?; 111 if kind != KIND_FARM { 112 continue; 113 } 114 let pubkey = parts 115 .next() 116 .ok_or(EventParseError::InvalidTag(TAG_A))? 117 .to_string(); 118 let d_tag = parts 119 .next() 120 .ok_or(EventParseError::InvalidTag(TAG_A))? 121 .to_string(); 122 if pubkey.trim().is_empty() || d_tag.trim().is_empty() { 123 return Err(EventParseError::InvalidTag(TAG_A)); 124 } 125 validate_d_tag_tag(&d_tag, TAG_A)?; 126 return Ok(RadrootsFarmRef { pubkey, d_tag }); 127 } 128 Err(EventParseError::MissingTag(TAG_A)) 129 } 130 131 fn parse_farm_pubkey(tags: &[Vec<String>]) -> Result<String, EventParseError> { 132 let tag = tags 133 .iter() 134 .find(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_P)) 135 .ok_or(EventParseError::MissingTag(TAG_P))?; 136 let value = tag 137 .get(1) 138 .map(|value| value.to_string()) 139 .ok_or(EventParseError::InvalidTag(TAG_P))?; 140 if value.trim().is_empty() { 141 return Err(EventParseError::InvalidTag(TAG_P)); 142 } 143 Ok(value) 144 } 145 146 fn parse_resource_area( 147 tags: &[Vec<String>], 148 ) -> Result<Option<RadrootsResourceAreaRef>, EventParseError> { 149 let tag = tags 150 .iter() 151 .find(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_RADROOTS_RESOURCE_AREA)); 152 let Some(tag) = tag else { 153 return Ok(None); 154 }; 155 let value = tag 156 .get(1) 157 .map(|value| value.to_string()) 158 .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA))?; 159 let mut parts = value.splitn(3, ':'); 160 let kind = parts 161 .next() 162 .and_then(|raw| raw.parse::<u32>().ok()) 163 .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA))?; 164 if kind != KIND_RESOURCE_AREA { 165 return Err(EventParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA)); 166 } 167 let pubkey = parts 168 .next() 169 .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA))? 170 .to_string(); 171 let d_tag = parts 172 .next() 173 .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA))? 174 .to_string(); 175 if pubkey.trim().is_empty() || d_tag.trim().is_empty() { 176 return Err(EventParseError::InvalidTag(TAG_RADROOTS_RESOURCE_AREA)); 177 } 178 validate_d_tag_tag(&d_tag, TAG_RADROOTS_RESOURCE_AREA)?; 179 Ok(Some(RadrootsResourceAreaRef { pubkey, d_tag })) 180 } 181 182 fn parse_plot_ref(tags: &[Vec<String>]) -> Result<Option<RadrootsPlotRef>, EventParseError> { 183 let tag = tags 184 .iter() 185 .find(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_RADROOTS_PLOT)); 186 let Some(tag) = tag else { 187 return Ok(None); 188 }; 189 let value = tag 190 .get(1) 191 .map(|value| value.to_string()) 192 .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_PLOT))?; 193 let mut parts = value.splitn(3, ':'); 194 let kind = parts 195 .next() 196 .and_then(|raw| raw.parse::<u32>().ok()) 197 .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_PLOT))?; 198 if kind != KIND_PLOT { 199 return Err(EventParseError::InvalidTag(TAG_RADROOTS_PLOT)); 200 } 201 let pubkey = parts 202 .next() 203 .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_PLOT))? 204 .to_string(); 205 let d_tag = parts 206 .next() 207 .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_PLOT))? 208 .to_string(); 209 if pubkey.trim().is_empty() || d_tag.trim().is_empty() { 210 return Err(EventParseError::InvalidTag(TAG_RADROOTS_PLOT)); 211 } 212 validate_d_tag_tag(&d_tag, TAG_RADROOTS_PLOT)?; 213 Ok(Some(RadrootsPlotRef { pubkey, d_tag })) 214 } 215 216 pub fn listing_from_event( 217 kind: u32, 218 tags: &[Vec<String>], 219 content: &str, 220 ) -> Result<RadrootsListing, EventParseError> { 221 if !is_listing_kind(kind) { 222 return Err(EventParseError::InvalidKind { 223 expected: EXPECTED_LISTING_KINDS, 224 got: kind, 225 }); 226 } 227 listing_from_event_parts(tags, content) 228 } 229 230 pub fn listing_from_event_parts( 231 tags: &[Vec<String>], 232 _content: &str, 233 ) -> Result<RadrootsListing, EventParseError> { 234 let d_tag = parse_d_tag(tags)?; 235 let farm_ref = parse_farm_ref(tags)?; 236 let farm_pubkey = parse_farm_pubkey(tags)?; 237 let resource_area = parse_resource_area(tags)?; 238 let plot = parse_plot_ref(tags)?; 239 240 let mut product = RadrootsListingProduct { 241 key: String::new(), 242 title: String::new(), 243 category: String::new(), 244 summary: None, 245 process: None, 246 lot: None, 247 location: None, 248 profile: None, 249 year: None, 250 }; 251 let mut primary_bin_id: Option<String> = None; 252 let mut bin_drafts: Vec<BinDraft> = Vec::new(); 253 let mut bin_order = 0usize; 254 let mut discounts: Vec<RadrootsCoreDiscount> = Vec::new(); 255 let mut location: Option<RadrootsListingLocation> = None; 256 let mut inventory_available: Option<RadrootsCoreDecimal> = None; 257 let mut availability_status: Option<RadrootsListingStatus> = None; 258 let mut availability_start: Option<u64> = None; 259 let mut availability_end: Option<u64> = None; 260 let mut delivery_method: Option<RadrootsListingDeliveryMethod> = None; 261 let mut images: Vec<RadrootsListingImage> = Vec::new(); 262 let mut geohash: Option<String> = None; 263 let mut published_at: Option<u64> = None; 264 265 let has_structured_location = tags 266 .iter() 267 .any(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_LOCATION) && tag.len() >= 3); 268 269 for tag in tags { 270 if tag.is_empty() { 271 continue; 272 } 273 match tag[0].as_str() { 274 "key" => set_if_empty(&mut product.key, tag.get(1)), 275 "title" => set_if_empty(&mut product.title, tag.get(1)), 276 "category" => set_if_empty(&mut product.category, tag.get(1)), 277 "summary" => set_optional(&mut product.summary, tag.get(1)), 278 TAG_PUBLISHED_AT => { 279 published_at = Some(parse_u64_tag_value(tag.get(1), TAG_PUBLISHED_AT)?); 280 } 281 "process" => set_optional(&mut product.process, tag.get(1)), 282 "lot" => set_optional(&mut product.lot, tag.get(1)), 283 "location" => { 284 let parse_structured_location = match tag.len() { 285 0 | 1 => false, 286 2 => !has_structured_location && location.is_none(), 287 _ => true, 288 }; 289 if parse_structured_location { 290 let primary = tag 291 .get(1) 292 .and_then(|value| clean_value(value)) 293 .ok_or(EventParseError::InvalidTag(TAG_LOCATION))?; 294 let mut parsed = RadrootsListingLocation { 295 primary, 296 city: None, 297 region: None, 298 country: None, 299 lat: None, 300 lng: None, 301 geohash: None, 302 }; 303 if let Some(city) = tag.get(2).and_then(|value| clean_value(value)) { 304 parsed.city = Some(city); 305 } 306 if let Some(region) = tag.get(3).and_then(|value| clean_value(value)) { 307 parsed.region = Some(region); 308 } 309 if let Some(country) = tag.get(4).and_then(|value| clean_value(value)) { 310 parsed.country = Some(country); 311 } 312 location = Some(parsed); 313 } else { 314 set_optional(&mut product.location, tag.get(1)); 315 } 316 } 317 "profile" => set_optional(&mut product.profile, tag.get(1)), 318 "year" => set_optional(&mut product.year, tag.get(1)), 319 TAG_PRICE => {} 320 TAG_RADROOTS_PRIMARY_BIN => { 321 let value = tag 322 .get(1) 323 .and_then(|value| clean_value(value)) 324 .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_PRIMARY_BIN))?; 325 if let Some(existing) = primary_bin_id.as_ref() { 326 if existing != &value { 327 return Err(EventParseError::InvalidTag(TAG_RADROOTS_PRIMARY_BIN)); 328 } 329 } else { 330 primary_bin_id = Some(value); 331 } 332 } 333 TAG_RADROOTS_BIN => { 334 if tag.len() < 4 || tag.len() > 7 { 335 return Err(EventParseError::InvalidTag(TAG_RADROOTS_BIN)); 336 } 337 let bin_id = tag 338 .get(1) 339 .and_then(|value| clean_value(value)) 340 .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_BIN))?; 341 let amount = parse_decimal(&tag[2], TAG_RADROOTS_BIN)?; 342 let unit = parse_unit(&tag[3], TAG_RADROOTS_BIN)?; 343 if unit != unit.canonical_unit() { 344 return Err(EventParseError::InvalidTag(TAG_RADROOTS_BIN)); 345 } 346 let bin = upsert_bin(&mut bin_drafts, &bin_id, &mut bin_order); 347 if bin.quantity.is_some() { 348 return Err(EventParseError::InvalidTag(TAG_RADROOTS_BIN)); 349 } 350 bin.quantity = Some(RadrootsCoreQuantity::new(amount, unit)); 351 352 match tag.as_slice() { 353 [_, _, _, _, display_amount_raw, display_unit_raw] 354 | [_, _, _, _, display_amount_raw, display_unit_raw, _] => { 355 let display_amount = parse_decimal(display_amount_raw, TAG_RADROOTS_BIN)?; 356 let display_unit = parse_unit(display_unit_raw, TAG_RADROOTS_BIN)?; 357 bin.display_amount = Some(display_amount); 358 bin.display_unit = Some(display_unit); 359 if let [_, _, _, _, _, _, label] = tag.as_slice() { 360 bin.display_label = clean_value(label); 361 } 362 } 363 [_, _, _, _, _] => return Err(EventParseError::InvalidTag(TAG_RADROOTS_BIN)), 364 _ => {} 365 } 366 } 367 TAG_RADROOTS_PRICE => { 368 if tag.len() < 6 || tag.len() > 8 { 369 return Err(EventParseError::InvalidTag(TAG_RADROOTS_PRICE)); 370 } 371 let bin_id = tag 372 .get(1) 373 .and_then(|value| clean_value(value)) 374 .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_PRICE))?; 375 let amount = parse_decimal(&tag[2], TAG_RADROOTS_PRICE)?; 376 let currency = parse_currency(&tag[3], TAG_RADROOTS_PRICE)?; 377 let per_amount = parse_decimal(&tag[4], TAG_RADROOTS_PRICE)?; 378 let per_unit = parse_unit(&tag[5], TAG_RADROOTS_PRICE)?; 379 let price_per_canonical_unit = RadrootsCoreQuantityPrice::new( 380 RadrootsCoreMoney::new(amount, currency), 381 RadrootsCoreQuantity::new(per_amount, per_unit), 382 ); 383 if !price_per_canonical_unit.is_price_per_canonical_unit() { 384 return Err(EventParseError::InvalidTag(TAG_RADROOTS_PRICE)); 385 } 386 let bin = upsert_bin(&mut bin_drafts, &bin_id, &mut bin_order); 387 if bin.price_per_canonical_unit.is_some() { 388 return Err(EventParseError::InvalidTag(TAG_RADROOTS_PRICE)); 389 } 390 bin.price_per_canonical_unit = Some(price_per_canonical_unit); 391 392 match tag.as_slice() { 393 [_, _, _, _, _, _, _] => { 394 return Err(EventParseError::InvalidTag(TAG_RADROOTS_PRICE)); 395 } 396 [_, _, _, _, _, _, display_price_raw, display_unit_raw] => { 397 let display_price = parse_decimal(display_price_raw, TAG_RADROOTS_PRICE)?; 398 let display_unit = parse_unit(display_unit_raw, TAG_RADROOTS_PRICE)?; 399 bin.display_price = Some(RadrootsCoreMoney::new(display_price, currency)); 400 bin.display_price_unit = Some(display_unit); 401 } 402 _ => {} 403 } 404 } 405 TAG_RADROOTS_DISCOUNT => { 406 let payload = tag 407 .get(1) 408 .ok_or(EventParseError::InvalidTag(TAG_RADROOTS_DISCOUNT))?; 409 discounts.push(parse_discount(payload)?); 410 } 411 TAG_GEOHASH => { 412 if let Some(value) = tag.get(1).and_then(|value| clean_value(value)) { 413 geohash = Some(value); 414 } 415 } 416 TAG_INVENTORY => { 417 let value = tag 418 .get(1) 419 .ok_or(EventParseError::InvalidTag(TAG_INVENTORY))?; 420 inventory_available = Some(parse_decimal(value, TAG_INVENTORY)?); 421 } 422 TAG_RADROOTS_AVAILABILITY_START => { 423 availability_start = Some(parse_u64_tag_value( 424 tag.get(1), 425 TAG_RADROOTS_AVAILABILITY_START, 426 )?); 427 } 428 TAG_EXPIRES_AT => { 429 availability_end = Some(parse_u64_tag_value(tag.get(1), TAG_EXPIRES_AT)?); 430 } 431 TAG_STATUS => { 432 let status = tag 433 .get(1) 434 .and_then(|value| clean_value(value)) 435 .unwrap_or_default(); 436 availability_status = Some(parse_status(&status)); 437 } 438 TAG_DELIVERY => { 439 let method = tag 440 .get(1) 441 .and_then(|value| clean_value(value)) 442 .unwrap_or_default(); 443 delivery_method = Some(match method.as_str() { 444 "pickup" => RadrootsListingDeliveryMethod::Pickup, 445 "local_delivery" => RadrootsListingDeliveryMethod::LocalDelivery, 446 "shipping" => RadrootsListingDeliveryMethod::Shipping, 447 "other" => RadrootsListingDeliveryMethod::Other { 448 method: tag 449 .get(2) 450 .and_then(|value| clean_value(value)) 451 .unwrap_or_default(), 452 }, 453 other => RadrootsListingDeliveryMethod::Other { 454 method: other.to_string(), 455 }, 456 }); 457 } 458 TAG_IMAGE => { 459 let url = tag.get(1).ok_or(EventParseError::InvalidTag(TAG_IMAGE))?; 460 if url.trim().is_empty() { 461 continue; 462 } 463 images.push(RadrootsListingImage { 464 url: url.to_string(), 465 size: tag.get(2).and_then(|value| parse_image_size(value)), 466 }); 467 } 468 _ => {} 469 } 470 } 471 472 let availability = match availability_status { 473 Some(status) => Some(RadrootsListingAvailability::Status { status }), 474 None => match (availability_start, availability_end) { 475 (None, None) => None, 476 (start, end) => Some(RadrootsListingAvailability::Window { start, end }), 477 }, 478 }; 479 480 let location = location.map(|mut location| { 481 location.geohash = location.geohash.or(geohash); 482 location 483 }); 484 485 if farm_pubkey != farm_ref.pubkey { 486 return Err(EventParseError::InvalidTag(TAG_P)); 487 } 488 489 let primary_bin_id = 490 primary_bin_id.ok_or(EventParseError::MissingTag(TAG_RADROOTS_PRIMARY_BIN))?; 491 let bins = build_bins(bin_drafts)?; 492 if !bins.iter().any(|bin| bin.bin_id == primary_bin_id) { 493 return Err(EventParseError::InvalidTag(TAG_RADROOTS_PRIMARY_BIN)); 494 } 495 496 let d_tag = RadrootsDTag::parse(&d_tag).map_err(|_| EventParseError::InvalidTag(TAG_D))?; 497 let primary_bin_id = RadrootsInventoryBinId::parse(&primary_bin_id) 498 .map_err(|_| EventParseError::InvalidTag(TAG_RADROOTS_PRIMARY_BIN))?; 499 500 Ok(RadrootsListing { 501 d_tag, 502 published_at, 503 farm: farm_ref, 504 product, 505 primary_bin_id, 506 bins, 507 resource_area, 508 plot, 509 discounts: if discounts.is_empty() { 510 None 511 } else { 512 Some(discounts) 513 }, 514 inventory_available, 515 availability, 516 delivery_method, 517 location, 518 images: if images.is_empty() { 519 None 520 } else { 521 Some(images) 522 }, 523 }) 524 } 525 526 pub fn data_from_event( 527 id: String, 528 author: String, 529 published_at: u32, 530 kind: u32, 531 content: String, 532 tags: Vec<Vec<String>>, 533 ) -> Result<RadrootsParsedData<RadrootsListing>, EventParseError> { 534 let listing = listing_from_event(kind, &tags, &content)?; 535 Ok(RadrootsParsedData::new( 536 id, 537 author, 538 published_at, 539 kind, 540 listing, 541 )) 542 } 543 544 pub fn parsed_from_event( 545 id: String, 546 author: String, 547 published_at: u32, 548 kind: u32, 549 content: String, 550 tags: Vec<Vec<String>>, 551 sig: String, 552 ) -> Result<RadrootsParsedEvent<RadrootsListing>, EventParseError> { 553 let data = data_from_event( 554 id.clone(), 555 author.clone(), 556 published_at, 557 kind, 558 content.clone(), 559 tags.clone(), 560 )?; 561 Ok(RadrootsParsedEvent::from_parts( 562 id, 563 author, 564 published_at, 565 kind, 566 content, 567 tags, 568 sig, 569 data.data, 570 )) 571 } 572 573 pub fn data_from_nostr_event( 574 event: &RadrootsNostrEvent, 575 ) -> Result<RadrootsParsedData<RadrootsListing>, EventParseError> { 576 data_from_event( 577 event.id.clone(), 578 event.author.clone(), 579 event.created_at, 580 event.kind, 581 event.content.clone(), 582 event.tags.clone(), 583 ) 584 } 585 586 pub fn parsed_from_nostr_event( 587 event: &RadrootsNostrEvent, 588 ) -> Result<RadrootsParsedEvent<RadrootsListing>, EventParseError> { 589 parsed_from_event( 590 event.id.clone(), 591 event.author.clone(), 592 event.created_at, 593 event.kind, 594 event.content.clone(), 595 event.tags.clone(), 596 event.sig.clone(), 597 ) 598 } 599 600 fn clean_value(value: &str) -> Option<String> { 601 let trimmed = value.trim(); 602 if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("null") { 603 None 604 } else { 605 Some(trimmed.to_string()) 606 } 607 } 608 609 fn set_if_empty(target: &mut String, value: Option<&String>) { 610 if target.trim().is_empty() 611 && let Some(value) = value.and_then(|value| clean_value(value)) 612 { 613 *target = value; 614 } 615 } 616 617 fn set_optional(target: &mut Option<String>, value: Option<&String>) { 618 if target.is_none() 619 && let Some(value) = value.and_then(|value| clean_value(value)) 620 { 621 *target = Some(value); 622 } 623 } 624 625 fn parse_status(value: &str) -> RadrootsListingStatus { 626 match value.trim().to_ascii_lowercase().as_str() { 627 "active" => RadrootsListingStatus::Active, 628 "sold" => RadrootsListingStatus::Sold, 629 other => RadrootsListingStatus::Other { 630 value: other.to_string(), 631 }, 632 } 633 } 634 635 fn parse_image_size(value: &str) -> Option<RadrootsListingImageSize> { 636 let (w_raw, h_raw) = value.split_once('x')?; 637 let w = w_raw.parse::<u32>().ok()?; 638 let h = h_raw.parse::<u32>().ok()?; 639 Some(RadrootsListingImageSize { w, h }) 640 } 641 642 fn parse_discount(payload: &str) -> Result<RadrootsCoreDiscount, EventParseError> { 643 serde_json::from_str(payload).map_err(|_| EventParseError::InvalidTag(TAG_RADROOTS_DISCOUNT)) 644 } 645 646 #[derive(Clone, Debug)] 647 struct BinDraft { 648 bin_id: String, 649 order_index: usize, 650 quantity: Option<RadrootsCoreQuantity>, 651 display_amount: Option<RadrootsCoreDecimal>, 652 display_unit: Option<RadrootsCoreUnit>, 653 display_label: Option<String>, 654 price_per_canonical_unit: Option<RadrootsCoreQuantityPrice>, 655 display_price: Option<RadrootsCoreMoney>, 656 display_price_unit: Option<RadrootsCoreUnit>, 657 } 658 659 fn upsert_bin<'a>( 660 bins: &'a mut Vec<BinDraft>, 661 bin_id: &str, 662 order_index: &mut usize, 663 ) -> &'a mut BinDraft { 664 if let Some(position) = bins.iter().position(|bin| bin.bin_id == bin_id) { 665 return &mut bins[position]; 666 } 667 bins.push(BinDraft { 668 bin_id: bin_id.to_string(), 669 order_index: *order_index, 670 quantity: None, 671 display_amount: None, 672 display_unit: None, 673 display_label: None, 674 price_per_canonical_unit: None, 675 display_price: None, 676 display_price_unit: None, 677 }); 678 *order_index += 1; 679 let index = bins.len() - 1; 680 &mut bins[index] 681 } 682 683 fn build_bins(mut drafts: Vec<BinDraft>) -> Result<Vec<RadrootsListingBin>, EventParseError> { 684 drafts.sort_by_key(|draft| draft.order_index); 685 let mut bins = Vec::with_capacity(drafts.len()); 686 for draft in drafts { 687 let quantity = draft 688 .quantity 689 .ok_or(EventParseError::MissingTag(TAG_RADROOTS_BIN))?; 690 let price = draft 691 .price_per_canonical_unit 692 .ok_or(EventParseError::MissingTag(TAG_RADROOTS_PRICE))?; 693 if quantity.unit != price.quantity.unit { 694 return Err(EventParseError::InvalidTag(TAG_RADROOTS_PRICE)); 695 } 696 bins.push(RadrootsListingBin { 697 bin_id: RadrootsInventoryBinId::parse(&draft.bin_id) 698 .map_err(|_| EventParseError::InvalidTag(TAG_RADROOTS_BIN))?, 699 quantity, 700 price_per_canonical_unit: price, 701 display_amount: draft.display_amount, 702 display_unit: draft.display_unit, 703 display_label: draft.display_label, 704 display_price: draft.display_price, 705 display_price_unit: draft.display_price_unit, 706 }); 707 } 708 Ok(bins) 709 }