radrootsd

JSON-RPC bridge for Radroots event publishing
git clone https://radroots.dev/git/radrootsd.git
Log | Files | Refs | README | LICENSE

commit e9622d87a8147721ea0b0d978288a37d92895342
parent 08b11e422e1ed26a146aa66ab17d3f8f0fc1ffc2
Author: triesap <triesap@radroots.dev>
Date:   Sat,  3 Jan 2026 17:26:46 +0000

listing: extract row builder and add tests

- Factor listing event mapping and created_at sorting into build_listing_rows
- Parse listing payload from tags via listing_from_event_parts helper
- Keep handler focused on filter construction and fetch execution
- Add unit tests for ordering and tag-only listing reconstruction

Diffstat:
Msrc/api/jsonrpc/methods/events/listing/list.rs | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 145 insertions(+), 12 deletions(-)

diff --git a/src/api/jsonrpc/methods/events/listing/list.rs b/src/api/jsonrpc/methods/events/listing/list.rs @@ -15,6 +15,7 @@ use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; use radroots_events::kinds::KIND_LISTING; use radroots_events::listing::RadrootsListing; use radroots_nostr::prelude::{ + RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrKind, }; @@ -32,6 +33,25 @@ struct ListingListResponse { listings: Vec<ListingEventFlat>, } +fn build_listing_rows<I>(events: I) -> Vec<ListingEventFlat> +where + I: IntoIterator<Item = RadrootsNostrEvent>, +{ + let mut items = events + .into_iter() + .map(|ev| { + let tags = event_tags(&ev); + let listing = listing_from_event_parts(&tags, &ev.content).ok(); + ListingEventFlat { + event: event_view_with_tags(&ev, tags), + listing, + } + }) + .collect::<Vec<_>>(); + items.sort_by(|a, b| b.event.created_at.cmp(&a.event.created_at)); + items +} + pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { registry.track("events.listing.list"); m.register_async_method("events.listing.list", |params, ctx, _| async move { @@ -70,20 +90,133 @@ pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Res .await .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - let mut items = events - .into_iter() - .map(|ev| { - let tags = event_tags(&ev); - let listing = listing_from_event_parts(&tags, &ev.content).ok(); - ListingEventFlat { - event: event_view_with_tags(&ev, tags), - listing, - } - }) - .collect::<Vec<_>>(); - items.sort_by(|a, b| b.event.created_at.cmp(&a.event.created_at)); + let items = build_listing_rows(events); Ok::<ListingListResponse, RpcError>(ListingListResponse { listings: items }) })?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::build_listing_rows; + use radroots_core::{ + RadrootsCoreCurrency, + RadrootsCoreDecimal, + RadrootsCoreMoney, + RadrootsCoreQuantity, + RadrootsCoreQuantityPrice, + RadrootsCoreUnit, + }; + use radroots_events::kinds::KIND_LISTING; + use radroots_events::listing::{ + RadrootsListing, + RadrootsListingBin, + RadrootsListingFarmRef, + RadrootsListingProduct, + }; + use radroots_nostr::prelude::RadrootsNostrEvent; + use radroots_trade::listing::codec::listing_tags_build; + use serde_json::json; + + fn listing_event( + id: &str, + pubkey: &str, + created_at: u64, + tags: Vec<Vec<String>>, + content: &str, + ) -> RadrootsNostrEvent { + let sig = format!("{:0128x}", 5); + let event_json = json!({ + "id": id, + "pubkey": pubkey, + "created_at": created_at, + "kind": KIND_LISTING, + "tags": tags, + "content": content, + "sig": sig, + }); + serde_json::from_value(event_json).expect("event") + } + + fn sample_listing(farm_pubkey: &str) -> RadrootsListing { + let quantity = RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1_u64), RadrootsCoreUnit::Each); + let price = RadrootsCoreQuantityPrice::new( + RadrootsCoreMoney::new(RadrootsCoreDecimal::from(10_u64), RadrootsCoreCurrency::USD), + quantity.clone(), + ); + let bin = RadrootsListingBin { + bin_id: "bin-1".to_string(), + quantity, + price_per_canonical_unit: price, + display_amount: None, + display_unit: None, + display_label: None, + display_price: None, + display_price_unit: None, + }; + RadrootsListing { + d_tag: "listing-1".to_string(), + farm: RadrootsListingFarmRef { + pubkey: farm_pubkey.to_string(), + d_tag: "farm-1".to_string(), + }, + product: RadrootsListingProduct { + key: "coffee".to_string(), + title: "Coffee".to_string(), + category: "beverage".to_string(), + summary: None, + process: None, + lot: None, + location: None, + profile: None, + year: None, + }, + primary_bin_id: "bin-1".to_string(), + bins: vec![bin], + resource_area: None, + plot: None, + discounts: None, + inventory_available: None, + availability: None, + delivery_method: None, + location: None, + images: None, + } + } + + #[test] + fn listing_list_sorts_by_created_at_desc() { + let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; + let old_id = format!("{:064x}", 1); + let new_id = format!("{:064x}", 2); + let older = listing_event(&old_id, pubkey, 100, Vec::new(), ""); + let newer = listing_event(&new_id, pubkey, 200, Vec::new(), ""); + + let listings = build_listing_rows(vec![older, newer]); + + assert_eq!(listings.len(), 2); + assert_eq!(listings[0].event.id, new_id); + assert_eq!(listings[0].event.created_at, 200); + assert_eq!(listings[1].event.id, old_id); + assert_eq!(listings[1].event.created_at, 100); + } + + #[test] + fn listing_list_builds_from_tags_when_content_empty() { + let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; + let listing = sample_listing(pubkey); + let tags = listing_tags_build(&listing).expect("tags"); + let id = format!("{:064x}", 3); + let event = listing_event(&id, pubkey, 300, tags.clone(), ""); + + let listings = build_listing_rows(vec![event]); + + assert_eq!(listings.len(), 1); + assert_eq!(listings[0].event.tags, tags); + let parsed = listings[0].listing.as_ref().expect("listing"); + assert_eq!(parsed.d_tag, "listing-1"); + assert_eq!(parsed.farm.pubkey, pubkey); + assert_eq!(parsed.primary_bin_id, "bin-1"); + } +}