lib

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

commit 3bdc3c4683bf6807c8febcc9077ce4ced2aa6ed8
parent d586c196ba8d00dc501a37e55210e385865763a1
Author: triesap <tyson@radroots.org>
Date:   Fri,  2 Jan 2026 15:32:43 +0000

listing: support generic price tags and prefer extended


- Emit generic price tags alongside extended price tags
- Parse generic and extended price tags into separate price lists
- Prefer extended price tags when present, fallback to generic
- Add tests for price tag precedence and generic parsing

Diffstat:
Mevents-codec/src/listing/tags.rs | 9+++++++++
Mtrade/src/listing/codec.rs | 70+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 76 insertions(+), 3 deletions(-)

diff --git a/events-codec/src/listing/tags.rs b/events-codec/src/listing/tags.rs @@ -129,6 +129,7 @@ pub fn listing_tags_with_options( } for price in &listing.prices { + tags.push(tag_listing_price_generic(price)); tags.push(tag_listing_price(price)); } @@ -270,6 +271,14 @@ fn tag_listing_price(price: &RadrootsCoreQuantityPrice) -> Vec<String> { tag } +fn tag_listing_price_generic(price: &RadrootsCoreQuantityPrice) -> Vec<String> { + let mut tag = Vec::with_capacity(4); + tag.push(TAG_PRICE.to_string()); + tag.push(price.amount.amount.to_string()); + tag.push(price.amount.currency.as_str().to_ascii_lowercase()); + tag +} + fn tag_listing_image(image: &RadrootsListingImage) -> Option<Vec<String>> { let url = clean_value(&image.url)?; let mut tag = Vec::with_capacity(3); diff --git a/trade/src/listing/codec.rs b/trade/src/listing/codec.rs @@ -164,7 +164,8 @@ fn listing_from_tags( }; let mut quantities: Vec<RadrootsListingQuantity> = Vec::new(); - let mut prices: Vec<RadrootsCoreQuantityPrice> = Vec::new(); + let mut prices_extended: Vec<RadrootsCoreQuantityPrice> = Vec::new(); + let mut prices_generic: Vec<RadrootsCoreQuantityPrice> = Vec::new(); let mut discounts: Vec<RadrootsListingDiscount> = Vec::new(); let mut location: Option<RadrootsListingLocation> = None; let mut inventory_available: Option<RadrootsCoreDecimal> = None; @@ -250,7 +251,7 @@ fn listing_from_tags( let unit = parse_unit(unit)?; let label = tag.get(5).and_then(|v| clean_value(v)); let quantity = RadrootsCoreQuantity::new(quantity_amount, unit).with_optional_label(label); - prices.push(RadrootsCoreQuantityPrice { + prices_extended.push(RadrootsCoreQuantityPrice { amount: RadrootsCoreMoney::new(amount, currency), quantity, }); @@ -258,7 +259,7 @@ fn listing_from_tags( let amount = parse_decimal(amount, TAG_PRICE)?; let currency = parse_currency(currency)?; let quantity = RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::Each); - prices.push(RadrootsCoreQuantityPrice { + prices_generic.push(RadrootsCoreQuantityPrice { amount: RadrootsCoreMoney::new(amount, currency), quantity, }); @@ -337,6 +338,12 @@ fn listing_from_tags( }, }; + let prices = if prices_extended.is_empty() { + prices_generic + } else { + prices_extended + }; + let location = location.map(|mut loc| { if loc.geohash.is_none() { loc.geohash = geohash; @@ -409,6 +416,63 @@ fn parse_farm_pubkey(tags: &[Vec<String>]) -> Result<String, TradeListingParseEr Ok(value) } +#[cfg(test)] +mod tests { + use super::*; + use radroots_core::RadrootsCoreUnit; + use radroots_events::listing::RadrootsListingFarmRef; + + fn farm_ref() -> RadrootsListingFarmRef { + RadrootsListingFarmRef { + pubkey: "seller".to_string(), + d_tag: "farm-1".to_string(), + } + } + + #[test] + fn listing_prefers_extended_price_tags() { + let tags = vec![ + vec!["key".into(), "coffee".into()], + vec!["title".into(), "Coffee".into()], + vec!["category".into(), "coffee".into()], + vec!["price".into(), "20".into(), "usd".into()], + vec!["price".into(), "20".into(), "usd".into(), "1".into(), "lb".into()], + ]; + + let listing = listing_from_tags( + &tags, + "listing-1".to_string(), + farm_ref(), + "seller".to_string(), + ) + .expect("listing"); + + assert_eq!(listing.prices.len(), 1); + assert_eq!(listing.prices[0].quantity.unit, RadrootsCoreUnit::MassLb); + } + + #[test] + fn listing_accepts_generic_price_tags() { + let tags = vec![ + vec!["key".into(), "coffee".into()], + vec!["title".into(), "Coffee".into()], + vec!["category".into(), "coffee".into()], + vec!["price".into(), "20".into(), "usd".into()], + ]; + + let listing = listing_from_tags( + &tags, + "listing-1".to_string(), + farm_ref(), + "seller".to_string(), + ) + .expect("listing"); + + assert_eq!(listing.prices.len(), 1); + assert_eq!(listing.prices[0].quantity.unit, RadrootsCoreUnit::Each); + } +} + fn clean_value(value: &str) -> Option<String> { let trimmed = value.trim(); if trimmed.is_empty() {