lib

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

commit 33c02a4ad3b1e9400f5614c2ba05c9884e308683
parent 6df2858a4101aa78008a3fe64a100c548f4f17b6
Author: triesap <tyson@radroots.org>
Date:   Wed, 15 Apr 2026 21:03:37 +0000

events: align farm refs and optional farm geo

Diffstat:
Mcrates/events/src/farm.rs | 5+++--
Mcrates/events/src/listing.rs | 26+++++---------------------
Mcrates/events_codec/src/farm/encode.rs | 8++++++--
Mcrates/events_codec/src/farm/mod.rs | 30+++++++++++++++++++++---------
Mcrates/events_codec/src/listing/decode.rs | 10+++++-----
Mcrates/events_codec/src/listing/tags.rs | 17+++++++++--------
Mcrates/events_codec/tests/domain_encode_non_serde.rs | 24+++++++++++++++++++-----
Mcrates/events_codec/tests/listing.rs | 10+++++-----
Mcrates/events_codec/tests/structured_encode_default.rs | 26++++++++++++++++++++------
Mcrates/events_codec/tests/tag_builders.rs | 8++++----
Mcrates/events_codec_wasm/src/lib.rs | 7+++----
Mcrates/replica_sync/src/emit.rs | 49+++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/replica_sync/src/ingest.rs | 69++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mcrates/replica_sync/tests/ingest_roundtrip.rs | 4++--
Mcrates/sdk/tests/client.rs | 8++++----
Mcrates/sdk/tests/facade.rs | 8++++----
Mcrates/sdk/tests/radrootsd.rs | 7++++---
Mcrates/sdk/tests/relay_direct.rs | 7++++---
Mcrates/trade/src/listing/codec.rs | 19++++++++++---------
Mcrates/trade/src/listing/overlay.rs | 9+++++----
Mcrates/trade/src/listing/projection.rs | 18++++++++++--------
Mcrates/trade/src/listing/publish.rs | 6+++---
Mcrates/trade/src/listing/validation.rs | 6+++---
Mspec/conformance/vectors/farm/build_draft.v1.json | 20++++++++++++++++++++
24 files changed, 262 insertions(+), 139 deletions(-)

diff --git a/crates/events/src/farm.rs b/crates/events/src/farm.rs @@ -28,7 +28,7 @@ pub struct RadrootsFarm { #[cfg_attr(feature = "ts-rs", derive(TS))] #[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct RadrootsFarmRef { pub pubkey: String, pub d_tag: String, @@ -107,5 +107,6 @@ pub struct RadrootsFarmLocation { pub region: Option<String>, #[cfg_attr(feature = "ts-rs", ts(optional, type = "string | null"))] pub country: Option<String>, - pub gcs: RadrootsGcsLocation, + #[cfg_attr(feature = "ts-rs", ts(optional, type = "RadrootsGcsLocation | null"))] + pub gcs: Option<RadrootsGcsLocation>, } diff --git a/crates/events/src/listing.rs b/crates/events/src/listing.rs @@ -5,6 +5,7 @@ use radroots_core::{ #[cfg(feature = "ts-rs")] use ts_rs::TS; +use crate::farm::RadrootsFarmRef; use crate::plot::RadrootsPlotRef; use crate::resource_area::RadrootsResourceAreaRef; @@ -67,7 +68,7 @@ pub enum RadrootsListingDeliveryMethod { pub struct RadrootsListing { pub d_tag: String, #[cfg_attr(feature = "serde", serde(default))] - pub farm: RadrootsListingFarmRef, + pub farm: RadrootsFarmRef, pub product: RadrootsListingProduct, pub primary_bin_id: String, pub bins: Vec<RadrootsListingBin>, @@ -111,24 +112,6 @@ pub struct RadrootsListing { #[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug)] -pub struct RadrootsListingFarmRef { - pub pubkey: String, - pub d_tag: String, -} - -impl Default for RadrootsListingFarmRef { - fn default() -> Self { - Self { - pubkey: String::new(), - d_tag: String::new(), - } - } -} - -#[cfg_attr(feature = "ts-rs", derive(TS))] -#[cfg_attr(feature = "ts-rs", ts(export, export_to = "types.ts"))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug)] pub struct RadrootsListingProduct { pub key: String, pub title: String, @@ -228,7 +211,8 @@ pub struct RadrootsListingImageSize { #[cfg(all(test, feature = "ts-rs", feature = "std"))] mod constants_tests { - use super::{RADROOTS_LISTING_PRODUCT_TAG_KEYS, RadrootsListingFarmRef}; + use super::RADROOTS_LISTING_PRODUCT_TAG_KEYS; + use crate::farm::RadrootsFarmRef; use std::{ fs, path::{Path, PathBuf}, @@ -286,7 +270,7 @@ mod constants_tests { #[test] fn defaults_listing_farm_ref_to_empty_values() { - let farm_ref = RadrootsListingFarmRef::default(); + let farm_ref = RadrootsFarmRef::default(); assert!(farm_ref.pubkey.is_empty()); assert!(farm_ref.d_tag.is_empty()); } diff --git a/crates/events_codec/src/farm/encode.rs b/crates/events_codec/src/farm/encode.rs @@ -41,8 +41,12 @@ pub fn farm_build_tags(farm: &RadrootsFarm) -> Result<Vec<Vec<String>>, EventEnc push_tag(&mut tags, TAG_T, item); } } - if let Some(location) = farm.location.as_ref() { - let geohash = location.gcs.geohash.trim(); + if let Some(geohash) = farm + .location + .as_ref() + .and_then(|location| location.gcs.as_ref()) + .map(|gcs| gcs.geohash.trim()) + { if geohash.is_empty() { return Err(EventEncodeError::EmptyRequiredField("location.gcs.geohash")); } diff --git a/crates/events_codec/src/farm/mod.rs b/crates/events_codec/src/farm/mod.rs @@ -18,9 +18,7 @@ mod tests { RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef, RadrootsGcsLocation, RadrootsGeoJsonPoint, RadrootsGeoJsonPolygon, }; - use radroots_events::listing::{ - RadrootsListing, RadrootsListingBin, RadrootsListingFarmRef, RadrootsListingProduct, - }; + use radroots_events::listing::{RadrootsListing, RadrootsListingBin, RadrootsListingProduct}; use radroots_events::plot::RadrootsPlot; #[test] @@ -37,7 +35,7 @@ mod tests { city: None, region: None, country: None, - gcs: RadrootsGcsLocation { + gcs: Some(RadrootsGcsLocation { lat: 37.0, lng: -122.0, geohash: "9q8yy".to_string(), @@ -68,7 +66,7 @@ mod tests { gc_admin1_name: None, gc_country_id: None, gc_country_name: None, - }, + }), }), tags: Some(vec!["orchard".to_string()]), }; @@ -165,7 +163,7 @@ mod tests { city: None, region: None, country: None, - gcs: RadrootsGcsLocation { + gcs: Some(RadrootsGcsLocation { lat: 37.0, lng: -122.0, geohash: "9q8yy".to_string(), @@ -196,7 +194,7 @@ mod tests { gc_admin1_name: None, gc_country_id: None, gc_country_name: None, - }, + }), }), tags: None, }; @@ -211,13 +209,27 @@ mod tests { assert!(matches!(err, EventEncodeError::EmptyRequiredField("name"))); farm.name = "Test Farm".to_string(); - farm.location.as_mut().expect("location").gcs.geohash = " ".to_string(); + farm.location + .as_mut() + .expect("location") + .gcs + .as_mut() + .expect("gcs") + .geohash = " ".to_string(); let err = farm_build_tags(&farm).expect_err("expected empty geohash"); assert!(matches!( err, EventEncodeError::EmptyRequiredField("location.gcs.geohash") )); + farm.location.as_mut().expect("location").gcs = None; + let tags = farm_build_tags(&farm).expect("string-only farm location should be allowed"); + assert!( + !tags + .iter() + .any(|tag| tag.get(0).map(|v| v.as_str()) == Some("g")) + ); + let err = farm_ref_tags(&RadrootsFarmRef { pubkey: " ".to_string(), d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), @@ -283,7 +295,7 @@ mod tests { fn farm_listings_list_set_uses_listing_addresses() { let listings = vec![RadrootsListing { d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), - farm: RadrootsListingFarmRef { + farm: RadrootsFarmRef { pubkey: "farm_pubkey".to_string(), d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), }, diff --git a/crates/events_codec/src/listing/decode.rs b/crates/events_codec/src/listing/decode.rs @@ -12,12 +12,12 @@ use radroots_core::{ }; use radroots_events::{ RadrootsNostrEvent, + farm::RadrootsFarmRef, kinds::{KIND_FARM, KIND_PLOT, KIND_RESOURCE_AREA, is_listing_kind}, listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingImage, - RadrootsListingImageSize, RadrootsListingLocation, RadrootsListingProduct, - RadrootsListingStatus, + RadrootsListingDeliveryMethod, RadrootsListingImage, RadrootsListingImageSize, + RadrootsListingLocation, RadrootsListingProduct, RadrootsListingStatus, }, plot::RadrootsPlotRef, resource_area::RadrootsResourceAreaRef, @@ -93,7 +93,7 @@ fn parse_d_tag(tags: &[Vec<String>]) -> Result<String, EventParseError> { Ok(value) } -fn parse_farm_ref(tags: &[Vec<String>]) -> Result<RadrootsListingFarmRef, EventParseError> { +fn parse_farm_ref(tags: &[Vec<String>]) -> Result<RadrootsFarmRef, EventParseError> { for tag in tags .iter() .filter(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_A)) @@ -122,7 +122,7 @@ fn parse_farm_ref(tags: &[Vec<String>]) -> Result<RadrootsListingFarmRef, EventP return Err(EventParseError::InvalidTag(TAG_A)); } validate_d_tag_tag(&d_tag, TAG_A)?; - return Ok(RadrootsListingFarmRef { pubkey, d_tag }); + return Ok(RadrootsFarmRef { pubkey, d_tag }); } Err(EventParseError::MissingTag(TAG_A)) } diff --git a/crates/events_codec/src/listing/tags.rs b/crates/events_codec/src/listing/tags.rs @@ -13,11 +13,12 @@ use core::cmp; #[cfg(any(feature = "serde_json", test))] use radroots_core::RadrootsCoreDiscount; use radroots_core::RadrootsCoreMoney; +use radroots_events::farm::RadrootsFarmRef; use radroots_events::kinds::{KIND_FARM, KIND_PLOT, KIND_RESOURCE_AREA}; use radroots_events::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingImage, - RadrootsListingLocation, RadrootsListingStatus, + RadrootsListingDeliveryMethod, RadrootsListingImage, RadrootsListingLocation, + RadrootsListingStatus, }; use radroots_events::plot::RadrootsPlotRef; use radroots_events::resource_area::RadrootsResourceAreaRef; @@ -266,7 +267,7 @@ pub fn listing_tags_with_options( fn push_farm_tags( tags: &mut Vec<Vec<String>>, - farm: &RadrootsListingFarmRef, + farm: &RadrootsFarmRef, ) -> Result<(), EventEncodeError> { if farm.pubkey.trim().is_empty() { return Err(EventEncodeError::EmptyRequiredField("farm.pubkey")); @@ -679,7 +680,7 @@ mod tests { fn base_listing() -> RadrootsListing { RadrootsListing { d_tag: TEST_D_TAG.to_string(), - farm: RadrootsListingFarmRef { + farm: RadrootsFarmRef { pubkey: TEST_PUBKEY_HEX.to_string(), d_tag: TEST_FARM_D_TAG.to_string(), }, @@ -1007,7 +1008,7 @@ mod tests { let mut tags = Vec::new(); push_farm_tags( &mut tags, - &RadrootsListingFarmRef { + &RadrootsFarmRef { pubkey: TEST_PUBKEY_HEX.to_string(), d_tag: TEST_FARM_D_TAG.to_string(), }, @@ -1018,7 +1019,7 @@ mod tests { let err = push_farm_tags( &mut Vec::new(), - &RadrootsListingFarmRef { + &RadrootsFarmRef { pubkey: "".to_string(), d_tag: TEST_FARM_D_TAG.to_string(), }, @@ -1031,7 +1032,7 @@ mod tests { let err = push_farm_tags( &mut Vec::new(), - &RadrootsListingFarmRef { + &RadrootsFarmRef { pubkey: TEST_PUBKEY_HEX.to_string(), d_tag: "".to_string(), }, @@ -1044,7 +1045,7 @@ mod tests { let err = push_farm_tags( &mut Vec::new(), - &RadrootsListingFarmRef { + &RadrootsFarmRef { pubkey: TEST_PUBKEY_HEX.to_string(), d_tag: "farm:invalid".to_string(), }, diff --git a/crates/events_codec/tests/domain_encode_non_serde.rs b/crates/events_codec/tests/domain_encode_non_serde.rs @@ -16,8 +16,7 @@ use radroots_events::{ }, listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingLocation, - RadrootsListingProduct, + RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, }, plot::{RadrootsPlot, RadrootsPlotLocation, RadrootsPlotRef}, resource_area::{RadrootsResourceArea, RadrootsResourceAreaLocation, RadrootsResourceAreaRef}, @@ -123,7 +122,7 @@ fn sample_farm() -> RadrootsFarm { city: None, region: None, country: None, - gcs: sample_gcs("9q8yy"), + gcs: Some(sample_gcs("9q8yy")), }), tags: Some(vec!["orchard".to_string()]), } @@ -159,7 +158,7 @@ fn sample_listing() -> RadrootsListing { RadrootsListing { d_tag: VALID_DOC_D_TAG.to_string(), - farm: RadrootsListingFarmRef { + farm: RadrootsFarmRef { pubkey: VALID_PUBKEY.to_string(), d_tag: VALID_FARM_D_TAG.to_string(), }, @@ -463,13 +462,28 @@ fn farm_encode_and_list_set_paths() { assert!(matches!(err, EventEncodeError::EmptyRequiredField("name"))); let mut farm = sample_farm(); - farm.location.as_mut().expect("location").gcs.geohash = " ".to_string(); + farm.location + .as_mut() + .expect("location") + .gcs + .as_mut() + .expect("gcs") + .geohash = " ".to_string(); let err = farm_build_tags(&farm).expect_err("empty geohash"); assert!(matches!( err, EventEncodeError::EmptyRequiredField("location.gcs.geohash") )); + let mut farm = sample_farm(); + farm.location.as_mut().expect("location").gcs = None; + let tags = farm_build_tags(&farm).expect("farm location without geo"); + assert!( + !tags + .iter() + .any(|tag| tag.first().map(|v| v.as_str()) == Some("g")) + ); + let tags = farm_ref_tags(&RadrootsFarmRef { pubkey: VALID_PUBKEY.to_string(), d_tag: VALID_FARM_D_TAG.to_string(), diff --git a/crates/events_codec/tests/listing.rs b/crates/events_codec/tests/listing.rs @@ -7,12 +7,12 @@ use radroots_core::{ }; use radroots_events::tags::TAG_D; use radroots_events::{ + farm::RadrootsFarmRef, kinds::{KIND_LISTING, KIND_LISTING_DRAFT, KIND_POST}, listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingImage, - RadrootsListingImageSize, RadrootsListingLocation, RadrootsListingProduct, - RadrootsListingStatus, + RadrootsListingDeliveryMethod, RadrootsListingImage, RadrootsListingImageSize, + RadrootsListingLocation, RadrootsListingProduct, RadrootsListingStatus, }, }; use radroots_events_codec::error::{EventEncodeError, EventParseError}; @@ -35,7 +35,7 @@ fn sample_listing(d_tag: &str) -> RadrootsListing { RadrootsListing { d_tag: d_tag.to_string(), - farm: RadrootsListingFarmRef { + farm: RadrootsFarmRef { pubkey: "farm_pubkey".to_string(), d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), }, @@ -80,7 +80,7 @@ fn sample_listing_full(d_tag: &str) -> RadrootsListing { RadrootsListing { d_tag: d_tag.to_string(), - farm: RadrootsListingFarmRef { + farm: RadrootsFarmRef { pubkey: "farm_pubkey".to_string(), d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), }, diff --git a/crates/events_codec/tests/structured_encode_default.rs b/crates/events_codec/tests/structured_encode_default.rs @@ -12,9 +12,7 @@ use radroots_events::farm::{ RadrootsGeoJsonPolygon, }; use radroots_events::list_set::RadrootsListSet; -use radroots_events::listing::{ - RadrootsListing, RadrootsListingBin, RadrootsListingFarmRef, RadrootsListingProduct, -}; +use radroots_events::listing::{RadrootsListing, RadrootsListingBin, RadrootsListingProduct}; use radroots_events::plot::{RadrootsPlot, RadrootsPlotLocation, RadrootsPlotRef}; use radroots_events::resource_area::{ RadrootsResourceArea, RadrootsResourceAreaLocation, RadrootsResourceAreaRef, @@ -90,7 +88,7 @@ fn sample_listing(d_tag: &str) -> RadrootsListing { ); RadrootsListing { d_tag: d_tag.to_string(), - farm: RadrootsListingFarmRef { + farm: RadrootsFarmRef { pubkey: TEST_PUBKEY_HEX.to_string(), d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), }, @@ -141,7 +139,7 @@ fn structured_build_tags_cover_optional_and_error_paths() { city: None, region: None, country: None, - gcs: sample_gcs(), + gcs: Some(sample_gcs()), }), tags: Some(vec!["organic".to_string(), " ".to_string()]), }; @@ -155,13 +153,29 @@ fn structured_build_tags_cover_optional_and_error_paths() { assert!(farm_tags.iter().any(|tag| tag[0] == "g")); let mut invalid_farm = farm.clone(); - invalid_farm.location.as_mut().unwrap().gcs.geohash = " ".to_string(); + invalid_farm + .location + .as_mut() + .unwrap() + .gcs + .as_mut() + .unwrap() + .geohash = " ".to_string(); let err = farm_build_tags(&invalid_farm).unwrap_err(); assert!(matches!( err, EventEncodeError::EmptyRequiredField("location.gcs.geohash") )); + let mut string_only_farm = farm.clone(); + string_only_farm.location.as_mut().unwrap().gcs = None; + let string_only_tags = farm_build_tags(&string_only_farm).unwrap(); + assert!( + !string_only_tags + .iter() + .any(|tag| tag.first().map(|v| v.as_str()) == Some("g")) + ); + let farm_ref_tags = farm_ref_tags(&RadrootsFarmRef { pubkey: TEST_PUBKEY_HEX.to_string(), d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), diff --git a/crates/events_codec/tests/tag_builders.rs b/crates/events_codec/tests/tag_builders.rs @@ -29,9 +29,9 @@ use radroots_events::kinds::{ use radroots_events::list::{RadrootsList, RadrootsListEntry}; use radroots_events::list_set::RadrootsListSet; use radroots_events::listing::{ - RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, RadrootsListingFarmRef, - RadrootsListingImage, RadrootsListingImageSize, RadrootsListingLocation, - RadrootsListingProduct, RadrootsListingStatus, + RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, RadrootsListingImage, + RadrootsListingImageSize, RadrootsListingLocation, RadrootsListingProduct, + RadrootsListingStatus, }; use radroots_events::message::{RadrootsMessage, RadrootsMessageRecipient}; use radroots_events::message_file::RadrootsMessageFile; @@ -115,7 +115,7 @@ fn sample_listing() -> RadrootsListing { RadrootsListing { d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), - farm: RadrootsListingFarmRef { + farm: RadrootsFarmRef { pubkey: TEST_NPUB.to_string(), d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), }, diff --git a/crates/events_codec_wasm/src/lib.rs b/crates/events_codec_wasm/src/lib.rs @@ -189,11 +189,10 @@ mod tests { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; + use radroots_events::farm::RadrootsFarmRef; use radroots_events::job::JobInputType; use radroots_events::job_request::{RadrootsJobInput, RadrootsJobParam}; - use radroots_events::listing::{ - RadrootsListingBin, RadrootsListingFarmRef, RadrootsListingProduct, - }; + use radroots_events::listing::{RadrootsListingBin, RadrootsListingProduct}; fn sample_listing() -> RadrootsListing { let quantity = @@ -205,7 +204,7 @@ mod tests { RadrootsListing { d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), - farm: RadrootsListingFarmRef { + farm: RadrootsFarmRef { pubkey: "farm_pubkey".to_string(), d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), }, diff --git a/crates/replica_sync/src/emit.rs b/crates/replica_sync/src/emit.rs @@ -482,8 +482,20 @@ fn load_farm_location( exec: &dyn SqlExecutor, farm: &Farm, ) -> Result<Option<RadrootsFarmLocation>, RadrootsReplicaEventsError> { - let location = load_gcs_location_for_farm(exec, &farm.id)?; - Ok(location.map(|gcs| RadrootsFarmLocation { + let gcs = load_gcs_location_for_farm(exec, &farm.id)?; + let has_strings = [ + farm.location_primary.as_deref(), + farm.location_city.as_deref(), + farm.location_region.as_deref(), + farm.location_country.as_deref(), + ] + .into_iter() + .flatten() + .any(|value| !value.trim().is_empty()); + if !has_strings && gcs.is_none() { + return Ok(None); + } + Ok(Some(RadrootsFarmLocation { primary: farm.location_primary.clone(), city: farm.location_city.clone(), region: farm.location_region.clone(), @@ -1873,6 +1885,39 @@ mod tests { } #[test] + fn load_farm_location_preserves_string_only_locations() { + let exec = SqliteExecutor::open_memory().expect("db"); + migrations::run_all_up(&exec).expect("migrations"); + let farm_row = farm::create( + &exec, + &IFarmFields { + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), + pubkey: "f".repeat(64), + name: "string-only farm".to_string(), + about: None, + website: None, + picture: None, + banner: None, + location_primary: Some("San Francisco, CA".to_string()), + location_city: Some("San Francisco".to_string()), + location_region: Some("CA".to_string()), + location_country: Some("US".to_string()), + }, + ) + .expect("farm") + .result; + + let location = load_farm_location(&exec, &farm_row) + .expect("location query") + .expect("string-only location"); + assert_eq!(location.primary.as_deref(), Some("San Francisco, CA")); + assert_eq!(location.city.as_deref(), Some("San Francisco")); + assert_eq!(location.region.as_deref(), Some("CA")); + assert_eq!(location.country.as_deref(), Some("US")); + assert!(location.gcs.is_none()); + } + + #[test] fn emit_propagates_queryfail_and_builder_errors() { let exec = SqliteExecutor::open_memory().expect("db"); let (farm_row, _, _) = seed(&exec); diff --git a/crates/replica_sync/src/ingest.rs b/crates/replica_sync/src/ingest.rs @@ -711,13 +711,15 @@ fn upsert_farm_location( ) -> Result<(), RadrootsReplicaEventsError> { clear_farm_locations(exec, farm_id)?; if let Some(location) = location { - let gcs_id = create_gcs_location(exec, location.gcs, factory)?; - let fields = IFarmGcsLocationFields { - farm_id: farm_id.to_string(), - gcs_location_id: gcs_id, - role: ROLE_PRIMARY.to_string(), - }; - let _ = farm_gcs_location::create(exec, &fields)?; + if let Some(gcs) = location.gcs { + let gcs_id = create_gcs_location(exec, gcs, factory)?; + let fields = IFarmGcsLocationFields { + farm_id: farm_id.to_string(), + gcs_location_id: gcs_id, + role: ROLE_PRIMARY.to_string(), + }; + let _ = farm_gcs_location::create(exec, &fields)?; + } } Ok(()) } @@ -1697,7 +1699,7 @@ mod tests { city: Some("city".to_string()), region: Some("region".to_string()), country: Some("country".to_string()), - gcs: sample_gcs(10.0, 20.0, "s0"), + gcs: Some(sample_gcs(10.0, 20.0, "s0")), }), Some(vec![ "coffee".to_string(), @@ -1912,7 +1914,7 @@ mod tests { city: Some("c".to_string()), region: Some("r".to_string()), country: Some("k".to_string()), - gcs: sample_gcs(12.0, 22.0, "s2"), + gcs: Some(sample_gcs(12.0, 22.0, "s2")), }; assert_eq!( unpack_farm_location_strings(Some(&location)).0, @@ -2053,12 +2055,33 @@ mod tests { city: None, region: None, country: None, - gcs: sample_gcs(1.0, 2.0, "s4"), + gcs: Some(sample_gcs(1.0, 2.0, "s4")), + }), + &FixedFactory, + ) + .is_ok() + ); + assert!( + upsert_farm_location( + &exec, + &farm_id, + Some(RadrootsFarmLocation { + primary: Some("manual".to_string()), + city: Some("San Francisco".to_string()), + region: Some("CA".to_string()), + country: Some("US".to_string()), + gcs: None, }), &FixedFactory, ) .is_ok() ); + assert!( + farm_gcs_location::find_many(&exec, &IFarmGcsLocationFindMany { filter: None }) + .expect("farm locations after string-only upsert") + .results + .is_empty() + ); let not_found_plot_locations = DeleteErrorExecutor { inner: &exec, @@ -2173,7 +2196,7 @@ mod tests { city: None, region: None, country: None, - gcs: sample_gcs(2.0, 3.0, "s6"), + gcs: Some(sample_gcs(2.0, 3.0, "s6")), }), &FixedFactory, ) @@ -2274,7 +2297,7 @@ mod tests { city: Some("city".to_string()), region: Some("region".to_string()), country: Some("country".to_string()), - gcs: sample_gcs(10.0, 20.0, "s0"), + gcs: Some(sample_gcs(10.0, 20.0, "s0")), }), Some(vec!["coffee".to_string(), "coffee".to_string()]), ); @@ -2404,7 +2427,7 @@ mod tests { city: None, region: None, country: None, - gcs: sample_gcs(10.0, 20.0, "s0"), + gcs: Some(sample_gcs(10.0, 20.0, "s0")), }), Some(vec!["coffee".to_string()]), ); @@ -2529,7 +2552,7 @@ mod tests { city: None, region: None, country: None, - gcs: sample_gcs(12.0, 22.0, "s2"), + gcs: Some(sample_gcs(12.0, 22.0, "s2")), }), Some(vec!["coffee".to_string()]), ); @@ -2584,7 +2607,7 @@ mod tests { city: None, region: None, country: None, - gcs: sample_gcs(15.0, 25.0, "s5"), + gcs: Some(sample_gcs(15.0, 25.0, "s5")), }), &FixedFactory, ) @@ -2650,7 +2673,7 @@ mod tests { city: None, region: None, country: None, - gcs: sample_gcs(15.0, 25.0, "s5"), + gcs: Some(sample_gcs(15.0, 25.0, "s5")), }), &FixedFactory, ) @@ -2777,7 +2800,7 @@ mod tests { city: Some("city".to_string()), region: Some("region".to_string()), country: Some("country".to_string()), - gcs: sample_gcs(10.0, 20.0, "s0"), + gcs: Some(sample_gcs(10.0, 20.0, "s0")), }), Some(vec!["seed".to_string()]), ); @@ -2870,7 +2893,7 @@ mod tests { city: None, region: None, country: None, - gcs: sample_gcs(11.0, 21.0, "s1"), + gcs: Some(sample_gcs(11.0, 21.0, "s1")), }), None, ); @@ -2892,7 +2915,7 @@ mod tests { city: None, region: None, country: None, - gcs: sample_gcs(12.0, 22.0, "s2"), + gcs: Some(sample_gcs(12.0, 22.0, "s2")), }), None, ); @@ -2927,7 +2950,7 @@ mod tests { city: None, region: None, country: None, - gcs: bad_point, + gcs: Some(bad_point), }), None, ); @@ -2946,7 +2969,7 @@ mod tests { city: None, region: None, country: None, - gcs: bad_polygon, + gcs: Some(bad_polygon), }), None, ); @@ -3367,7 +3390,7 @@ mod tests { city: None, region: None, country: None, - gcs: sample_gcs(31.0, 41.0, "s8"), + gcs: Some(sample_gcs(31.0, 41.0, "s8")), }), &FixedFactory, ) @@ -3388,7 +3411,7 @@ mod tests { city: None, region: None, country: None, - gcs: sample_gcs(32.0, 42.0, "s9"), + gcs: Some(sample_gcs(32.0, 42.0, "s9")), }), &FixedFactory, ) diff --git a/crates/replica_sync/tests/ingest_roundtrip.rs b/crates/replica_sync/tests/ingest_roundtrip.rs @@ -823,7 +823,7 @@ fn ingest_reports_query_fail_paths_for_profile_farm_plot_and_list_sets() { city: None, region: None, country: None, - gcs: sample_gcs(37.7, -122.4, "9q8yy"), + gcs: Some(sample_gcs(37.7, -122.4, "9q8yy")), }), Some(vec!["coffee".to_string()]), ); @@ -1167,7 +1167,7 @@ fn ingest_event_paths_cover_profile_farm_plot_and_list_set_variants() { city: Some("city".to_string()), region: Some("region".to_string()), country: Some("country".to_string()), - gcs: sample_gcs(37.7, -122.4, "9q8yy"), + gcs: Some(sample_gcs(37.7, -122.4, "9q8yy")), }; let farm_create = farm_event( 200, diff --git a/crates/sdk/tests/client.rs b/crates/sdk/tests/client.rs @@ -2,12 +2,12 @@ use radroots_core::{ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; -use radroots_events::farm::RadrootsFarm; +use radroots_events::farm::{RadrootsFarm, RadrootsFarmRef}; use radroots_events::kinds::{KIND_LISTING, KIND_TRADE_LISTING_VALIDATE_REQ}; use radroots_events::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingLocation, - RadrootsListingProduct, RadrootsListingStatus, + RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, + RadrootsListingStatus, }; use radroots_events::trade::{RadrootsTradeListingValidateRequest, RadrootsTradeMessagePayload}; use radroots_sdk::{ @@ -32,7 +32,7 @@ fn sample_farm() -> RadrootsFarm { fn sample_listing() -> RadrootsListing { RadrootsListing { d_tag: "AAAAAAAAAAAAAAAAAAAAAg".into(), - farm: RadrootsListingFarmRef { + farm: RadrootsFarmRef { pubkey: "seller".into(), d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), }, diff --git a/crates/sdk/tests/facade.rs b/crates/sdk/tests/facade.rs @@ -2,14 +2,14 @@ use radroots_core::{ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; -use radroots_events::farm::RadrootsFarm; +use radroots_events::farm::{RadrootsFarm, RadrootsFarmRef}; use radroots_events::kinds::{ KIND_FARM, KIND_LISTING, KIND_PROFILE, KIND_TRADE_LISTING_VALIDATE_REQ, }; use radroots_events::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingLocation, - RadrootsListingProduct, RadrootsListingStatus, + RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, + RadrootsListingStatus, }; use radroots_events::profile::{RadrootsProfile, RadrootsProfileType}; use radroots_events::trade::{RadrootsTradeListingValidateRequest, RadrootsTradeMessagePayload}; @@ -46,7 +46,7 @@ fn sample_farm() -> RadrootsFarm { fn sample_listing() -> RadrootsListing { RadrootsListing { d_tag: "AAAAAAAAAAAAAAAAAAAAAg".into(), - farm: RadrootsListingFarmRef { + farm: RadrootsFarmRef { pubkey: "seller".into(), d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), }, diff --git a/crates/sdk/tests/radrootsd.rs b/crates/sdk/tests/radrootsd.rs @@ -4,6 +4,7 @@ use radroots_core::{ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; +use radroots_events::farm::RadrootsFarmRef; use radroots_events::kinds::{KIND_LISTING, KIND_LISTING_DRAFT}; use radroots_sdk::adapters::radrootsd::{ SdkRadrootsdBridgeJob, SdkRadrootsdBridgePublishResponse, SdkRadrootsdListingPublishRequest, @@ -12,8 +13,8 @@ use radroots_sdk::adapters::radrootsd::{ }; use radroots_sdk::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingLocation, - RadrootsListingProduct, RadrootsListingStatus, RadrootsTradeListingParseError, + RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, + RadrootsListingStatus, RadrootsTradeListingParseError, }; use radroots_sdk::trade::{ RadrootsTradeDiscountDecision, RadrootsTradeMessagePayload, RadrootsTradeMessageType, @@ -319,7 +320,7 @@ async fn write_http_response( fn sample_listing() -> RadrootsListing { RadrootsListing { d_tag: "AAAAAAAAAAAAAAAAAAAAAg".into(), - farm: RadrootsListingFarmRef { + farm: RadrootsFarmRef { pubkey: "seller".into(), d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), }, diff --git a/crates/sdk/tests/relay_direct.rs b/crates/sdk/tests/relay_direct.rs @@ -10,11 +10,12 @@ use radroots_core::{ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; +use radroots_sdk::farm::RadrootsFarmRef; use radroots_sdk::identity::RadrootsIdentity; use radroots_sdk::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingLocation, - RadrootsListingProduct, RadrootsListingStatus, + RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, + RadrootsListingStatus, }; use radroots_sdk::{ RadrootsSdkClient, RadrootsSdkConfig, RelayConfig, SdkEnvironment, SdkPublishError, @@ -101,7 +102,7 @@ impl Drop for AckRelay { fn sample_listing() -> RadrootsListing { RadrootsListing { d_tag: "AAAAAAAAAAAAAAAAAAAAAg".into(), - farm: RadrootsListingFarmRef { + farm: RadrootsFarmRef { pubkey: "seller".into(), d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), }, diff --git a/crates/trade/src/listing/codec.rs b/crates/trade/src/listing/codec.rs @@ -7,12 +7,12 @@ use radroots_core::{ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreDiscount, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; +use radroots_events::farm::RadrootsFarmRef; use radroots_events::kinds::{KIND_FARM, KIND_PLOT, KIND_RESOURCE_AREA}; use radroots_events::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingImage, - RadrootsListingImageSize, RadrootsListingLocation, RadrootsListingProduct, - RadrootsListingStatus, + RadrootsListingDeliveryMethod, RadrootsListingImage, RadrootsListingImageSize, + RadrootsListingLocation, RadrootsListingProduct, RadrootsListingStatus, }; use radroots_events::plot::RadrootsPlotRef; use radroots_events::resource_area::RadrootsResourceAreaRef; @@ -161,7 +161,7 @@ fn map_listing_tags_error(err: EventEncodeError) -> TradeListingParseError { fn listing_from_tags( tags: &[Vec<String>], d_tag: String, - farm_ref: RadrootsListingFarmRef, + farm_ref: RadrootsFarmRef, farm_pubkey: String, resource_area: Option<RadrootsResourceAreaRef>, plot: Option<RadrootsPlotRef>, @@ -485,7 +485,7 @@ fn listing_from_tags( }) } -fn parse_farm_ref(tags: &[Vec<String>]) -> Result<RadrootsListingFarmRef, TradeListingParseError> { +fn parse_farm_ref(tags: &[Vec<String>]) -> Result<RadrootsFarmRef, TradeListingParseError> { for tag in tags .iter() .filter(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_A)) @@ -516,7 +516,7 @@ fn parse_farm_ref(tags: &[Vec<String>]) -> Result<RadrootsListingFarmRef, TradeL if !is_d_tag_base64url(&d_tag) { return Err(TradeListingParseError::InvalidTag(TAG_A.to_string())); } - return Ok(RadrootsListingFarmRef { pubkey, d_tag }); + return Ok(RadrootsFarmRef { pubkey, d_tag }); } Err(TradeListingParseError::MissingTag(TAG_A.to_string())) } @@ -631,10 +631,11 @@ mod tests { RadrootsCoreDiscountThreshold, RadrootsCoreDiscountValue, RadrootsCoreMoney, RadrootsCorePercent, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; - use radroots_events::listing::{RadrootsListing, RadrootsListingFarmRef}; + use radroots_events::farm::RadrootsFarmRef; + use radroots_events::listing::RadrootsListing; - fn farm_ref() -> RadrootsListingFarmRef { - RadrootsListingFarmRef { + fn farm_ref() -> RadrootsFarmRef { + RadrootsFarmRef { pubkey: "seller".to_string(), d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), } diff --git a/crates/trade/src/listing/overlay.rs b/crates/trade/src/listing/overlay.rs @@ -527,10 +527,11 @@ mod tests { RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; use radroots_events::RadrootsNostrEventPtr; + use radroots_events::farm::RadrootsFarmRef; use radroots_events::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingLocation, - RadrootsListingProduct, RadrootsListingStatus, + RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, + RadrootsListingStatus, }; #[derive(Clone, Debug)] @@ -647,7 +648,7 @@ mod tests { fn base_listing() -> RadrootsListing { RadrootsListing { d_tag: "AAAAAAAAAAAAAAAAAAAAAg".into(), - farm: RadrootsListingFarmRef { + farm: RadrootsFarmRef { pubkey: "farm-pubkey".into(), d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), }, @@ -712,7 +713,7 @@ mod tests { fn alternate_listing() -> RadrootsListing { RadrootsListing { d_tag: "AAAAAAAAAAAAAAAAAAAAAw".into(), - farm: RadrootsListingFarmRef { + farm: RadrootsFarmRef { pubkey: "farm-pubkey-2".into(), d_tag: "AAAAAAAAAAAAAAAAAAAABA".into(), }, diff --git a/crates/trade/src/listing/projection.rs b/crates/trade/src/listing/projection.rs @@ -10,11 +10,12 @@ use std::collections::BTreeMap; use radroots_core::{RadrootsCoreDecimal, RadrootsCoreDiscount, RadrootsCoreDiscountValue}; use radroots_events::{ RadrootsNostrEvent, RadrootsNostrEventPtr, + farm::RadrootsFarmRef, kinds::{KIND_LISTING, is_listing_kind}, listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingImage, - RadrootsListingLocation, RadrootsListingProduct, + RadrootsListingDeliveryMethod, RadrootsListingImage, RadrootsListingLocation, + RadrootsListingProduct, }, plot::RadrootsPlotRef, resource_area::RadrootsResourceAreaRef, @@ -56,8 +57,8 @@ pub struct RadrootsTradeListingProjection { pub listing_addr: String, pub seller_pubkey: String, pub listing_id: String, - #[cfg_attr(feature = "ts-rs", ts(type = "RadrootsListingFarmRef"))] - pub farm: RadrootsListingFarmRef, + #[cfg_attr(feature = "ts-rs", ts(type = "RadrootsFarmRef"))] + pub farm: RadrootsFarmRef, #[cfg_attr(feature = "ts-rs", ts(type = "RadrootsListingProduct"))] pub product: RadrootsListingProduct, pub primary_bin_id: String, @@ -1769,10 +1770,11 @@ mod tests { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCorePercent, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; + use radroots_events::farm::RadrootsFarmRef; use radroots_events::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingLocation, - RadrootsListingProduct, RadrootsListingStatus, + RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, + RadrootsListingStatus, }; use radroots_events::{RadrootsNostrEvent, RadrootsNostrEventPtr, kinds::KIND_LISTING}; @@ -1890,7 +1892,7 @@ mod tests { fn base_listing() -> RadrootsListing { RadrootsListing { d_tag: "AAAAAAAAAAAAAAAAAAAAAg".into(), - farm: RadrootsListingFarmRef { + farm: RadrootsFarmRef { pubkey: "farm-pubkey".into(), d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), }, @@ -1995,7 +1997,7 @@ mod tests { fn alternate_listing() -> RadrootsListing { RadrootsListing { d_tag: "AAAAAAAAAAAAAAAAAAAAAw".into(), - farm: RadrootsListingFarmRef { + farm: RadrootsFarmRef { pubkey: "farm-pubkey-2".into(), d_tag: "AAAAAAAAAAAAAAAAAAAABA".into(), }, diff --git a/crates/trade/src/listing/publish.rs b/crates/trade/src/listing/publish.rs @@ -63,10 +63,10 @@ mod tests { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; + use radroots_events::farm::RadrootsFarmRef; use radroots_events::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingLocation, - RadrootsListingProduct, + RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, }; use super::{ @@ -76,7 +76,7 @@ mod tests { fn base_listing() -> RadrootsListing { RadrootsListing { d_tag: "AAAAAAAAAAAAAAAAAAAAAg".into(), - farm: RadrootsListingFarmRef { + farm: RadrootsFarmRef { pubkey: String::new(), d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), }, diff --git a/crates/trade/src/listing/validation.rs b/crates/trade/src/listing/validation.rs @@ -179,18 +179,18 @@ mod tests { }; use radroots_events::{ RadrootsNostrEvent, + farm::RadrootsFarmRef, kinds::{KIND_LISTING, KIND_LISTING_DRAFT}, listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingLocation, - RadrootsListingProduct, + RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, }, }; fn base_listing() -> RadrootsListing { RadrootsListing { d_tag: "AAAAAAAAAAAAAAAAAAAAAg".into(), - farm: RadrootsListingFarmRef { + farm: RadrootsFarmRef { pubkey: "seller".into(), d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), }, diff --git a/spec/conformance/vectors/farm/build_draft.v1.json b/spec/conformance/vectors/farm/build_draft.v1.json @@ -18,6 +18,26 @@ "tags_shape": "d_tag_required" } } + }, + { + "id": "farm_build_draft_location_without_geo_002", + "kind": "farm.build_draft", + "input": { + "farm": { + "d_tag": "AAAAAAAAAAAAAAAAAAAAAQ", + "name": "example farm with local location", + "location": { + "primary": "San Francisco, CA" + } + } + }, + "expected": { + "wire_parts": { + "kind": "farm", + "content_shape": "farm_json", + "tags_shape": "d_tag_required" + } + } } ] }