lib

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

commit 3a1a072c888551fa1d3bde8f459d41c57436b365
parent b16fd1880772aae5b8b6834c52fdaae60d82cac5
Author: triesap <tyson@radroots.org>
Date:   Sat, 21 Feb 2026 23:34:58 +0000

refactor: remove dead parser branches and extend high-gap tag coverage


- simplify job util and message tag parsing by removing unreachable branch guards while preserving behavior
- gate listing discount serialization by feature and add targeted listing tag branch-path tests
- expand event ref, job result, job util, list set, and resource area helper tests for uncovered edge cases
- verify with cargo check, cargo test, and sdk coverage progress reporting for `radroots-events-codec`

Diffstat:
Mcrates/events-codec/src/job/util.rs | 22+++++++---------------
Mcrates/events-codec/src/listing/tags.rs | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/events-codec/src/message/tags.rs | 18++++++++++++------
Mcrates/events-codec/src/resource_area/list_sets.rs | 10++++++++++
Mcrates/events-codec/tests/event_ref.rs | 21+++++++++++++++++++++
Mcrates/events-codec/tests/job_result.rs | 40++++++++++++++++++++++++++++++++++++++++
Mcrates/events-codec/tests/job_util.rs | 22+++++++++++++++++-----
Mcrates/events-codec/tests/list_set.rs | 12++++++++++++
8 files changed, 215 insertions(+), 26 deletions(-)

diff --git a/crates/events-codec/src/job/util.rs b/crates/events-codec/src/job/util.rs @@ -132,27 +132,19 @@ pub fn parse_i_tags(tags: &[Vec<String>]) -> Vec<RadrootsJobInput> { let v = &t[3]; if looks_like_ws_relay(v) { relay = Some(v.clone()); - } else if marker.is_none() { + } else { marker = Some(v.clone()); } } _ => { data = t[1].clone(); input_type = job_input_type_from_tag(t[2].as_str()).unwrap_or(JobInputType::Text); - if let Some(v) = t.get(3) { - if looks_like_ws_relay(v) { - relay = Some(v.clone()); - if let Some(m) = t.get(4) { - marker = Some(m.clone()); - } - } else { - marker = Some(v.clone()); - } - } - if marker.is_none() { - if let Some(m) = t.get(4) { - marker = Some(m.clone()); - } + let relay_or_marker = &t[3]; + if looks_like_ws_relay(relay_or_marker) { + relay = Some(relay_or_marker.clone()); + marker = Some(t[4].clone()); + } else { + marker = Some(relay_or_marker.clone()); } } } diff --git a/crates/events-codec/src/listing/tags.rs b/crates/events-codec/src/listing/tags.rs @@ -166,12 +166,17 @@ pub fn listing_tags_with_options( tags.push(tag_listing_price_generic(&total)); } + #[cfg(feature = "serde_json")] if let Some(discounts) = &listing.discounts { for discount in discounts { let payload = discount_tag_payload(discount)?; tags.push(vec![TAG_RADROOTS_DISCOUNT.to_string(), payload]); } } + #[cfg(not(feature = "serde_json"))] + if listing.discounts.as_ref().is_some() { + return Err(EventEncodeError::Json); + } if options.include_inventory { if let Some(inventory) = &listing.inventory_available { @@ -871,6 +876,23 @@ mod tests { }, ); assert!(find_tag(&geohash_only_tags, "g").is_some()); + + let mut invalid_with_geohash_enabled = Vec::new(); + push_location_geotags( + &mut invalid_with_geohash_enabled, + &invalid_geohash, + ListingTagOptions { + include_geohash: true, + include_gps: true, + ..ListingTagOptions::default() + }, + ); + assert!(find_tag(&invalid_with_geohash_enabled, "g").is_some()); + assert!( + !invalid_with_geohash_enabled + .iter() + .any(|tag| tag.first().map(|v| v.as_str()) == Some("l")) + ); } #[test] @@ -1194,6 +1216,34 @@ mod tests { ) .expect("delivery option without value"); assert!(find_tag(&no_delivery_tags, "delivery").is_none()); + + let mut no_inventory = base_listing(); + no_inventory.discounts = None; + no_inventory.inventory_available = None; + let no_inventory_tags = listing_tags_with_options( + &no_inventory, + ListingTagOptions { + include_inventory: true, + ..ListingTagOptions::default() + }, + ) + .expect("inventory option without value"); + assert!(find_tag(&no_inventory_tags, "inventory").is_none()); + + let mut no_geo = base_listing(); + no_geo.discounts = None; + let no_geo_tags = listing_tags_with_options( + &no_geo, + ListingTagOptions { + include_geohash: false, + include_gps: false, + ..ListingTagOptions::default() + }, + ) + .expect("location without geotags"); + assert!(find_tag(&no_geo_tags, "location").is_some()); + assert!(find_tag(&no_geo_tags, "g").is_none()); + assert!(find_tag(&no_geo_tags, "l").is_none()); } #[test] @@ -1332,6 +1382,52 @@ mod tests { } #[test] + fn listing_tags_reorders_primary_bin_and_falls_back_to_quantity_label() { + let mut listing = base_listing(); + listing.discounts = None; + + let mut primary = base_bin(); + primary.bin_id = "bin-1".to_string(); + primary.display_label = None; + primary.quantity = primary.quantity.clone().with_label("fallback-label"); + + let mut secondary = base_bin(); + secondary.bin_id = "bin-2".to_string(); + listing.primary_bin_id = "bin-1".to_string(); + listing.bins = vec![secondary, primary]; + + let tags = listing_tags(&listing).expect("listing tags"); + let first_bin = tags + .iter() + .find(|tag| tag.first().map(|v| v.as_str()) == Some("radroots:bin")) + .expect("first bin tag"); + assert_eq!(first_bin.get(1).map(|v| v.as_str()), Some("bin-1")); + assert_eq!(first_bin.get(6).map(|v| v.as_str()), Some("fallback-label")); + } + + #[test] + fn listing_tags_location_handles_partial_optional_components() { + let mut listing = base_listing(); + listing.discounts = None; + listing.location = Some(RadrootsListingLocation { + primary: "Moyobamba".to_string(), + city: Some(" ".to_string()), + region: Some("San Martin".to_string()), + country: Some(" ".to_string()), + lat: Some(-6.03), + lng: Some(-76.97), + geohash: None, + }); + + let tags = listing_tags(&listing).expect("listing tags"); + let location = find_tag(&tags, "location").expect("location tag"); + assert_eq!(location.get(0).map(|v| v.as_str()), Some("location")); + assert_eq!(location.get(1).map(|v| v.as_str()), Some("Moyobamba")); + assert_eq!(location.get(2).map(|v| v.as_str()), Some("San Martin")); + assert_eq!(location.len(), 3); + } + + #[test] fn listing_tags_supports_npub_farm_pubkey() { let mut listing = base_listing(); listing.discounts = None; diff --git a/crates/events-codec/src/message/tags.rs b/crates/events-codec/src/message/tags.rs @@ -125,9 +125,6 @@ pub(crate) fn parse_reply_tag( Some(tag) => tag, None => return Ok(None), }; - if tag.get(0).map(|s| s.as_str()) != Some("e") { - return Err(EventParseError::InvalidTag("e")); - } let id = tag.get(1).ok_or(EventParseError::InvalidTag("e"))?; if id.trim().is_empty() { return Err(EventParseError::InvalidTag("e")); @@ -151,12 +148,21 @@ pub(crate) fn parse_subject_tag(tags: &[Vec<String>]) -> Result<Option<String>, Some(tag) => tag, None => return Ok(None), }; - if tag.get(0).map(|s| s.as_str()) != Some("subject") { - return Err(EventParseError::InvalidTag("subject")); - } let subject = tag.get(1).ok_or(EventParseError::InvalidTag("subject"))?; if subject.trim().is_empty() { return Err(EventParseError::InvalidTag("subject")); } Ok(Some(subject.clone())) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_recipient_tag_rejects_non_p_tag() { + let err = parse_recipient_tag(&["x".to_string(), "pub".to_string()]) + .expect_err("expected invalid tag"); + assert!(matches!(err, EventParseError::InvalidTag("p"))); + } +} diff --git a/crates/events-codec/src/resource_area/list_sets.rs b/crates/events-codec/src/resource_area/list_sets.rs @@ -184,6 +184,16 @@ mod tests { #[test] fn farm_and_plot_address_helpers_reject_empty_d_tags() { let err = farm_address(&RadrootsFarmRef { + pubkey: " ".to_string(), + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), + }) + .expect_err("expected farm pubkey error"); + assert!(matches!( + err, + EventEncodeError::EmptyRequiredField("farm.pubkey") + )); + + let err = farm_address(&RadrootsFarmRef { pubkey: "farm_pubkey".to_string(), d_tag: " ".to_string(), }) diff --git a/crates/events-codec/tests/event_ref.rs b/crates/events-codec/tests/event_ref.rs @@ -56,6 +56,17 @@ fn parse_event_ref_tag_allows_relay_only_fifth_entry() { let parsed = parse_event_ref_tag(&tag, "e").unwrap(); assert!(parsed.d_tag.is_none()); assert_eq!(parsed.relays, Some(vec!["wss://relay".to_string()])); + + let ws_tag = vec![ + "e".to_string(), + "id".to_string(), + "author".to_string(), + KIND_POST.to_string(), + "ws://relay".to_string(), + ]; + let parsed = parse_event_ref_tag(&ws_tag, "e").unwrap(); + assert!(parsed.d_tag.is_none()); + assert_eq!(parsed.relays, Some(vec!["ws://relay".to_string()])); } #[test] @@ -229,4 +240,14 @@ fn parse_nip10_ref_tags_skips_invalid_a_tags_until_match() { parsed.relays, Some(vec!["wss://relay.empty-d.example.com".to_string()]) ); + + let tags = vec![ + vec!["e".to_string(), "id".to_string()], + vec!["p".to_string(), "author".to_string()], + vec!["k".to_string(), KIND_POST.to_string()], + vec!["a".to_string(), format!("{}:{}", KIND_POST, "author")], + ]; + let parsed = parse_nip10_ref_tags(&tags, "e", "p", "k", "a").unwrap(); + assert!(parsed.d_tag.is_none()); + assert!(parsed.relays.is_none()); } diff --git a/crates/events-codec/tests/job_result.rs b/crates/events-codec/tests/job_result.rs @@ -97,6 +97,46 @@ fn job_result_encrypted_adds_flag_and_rejects_inputs() { } #[test] +fn job_result_build_tags_supports_minimal_optional_fields() { + let mut res = sample_result(); + res.request_json = None; + res.inputs.clear(); + res.customer_pubkey = None; + res.payment = None; + let parts = to_wire_parts(&res, "payload").unwrap(); + assert!( + parts + .tags + .iter() + .any(|tag| tag.first().map(|v| v.as_str()) == Some("e")) + ); + assert!( + !parts + .tags + .iter() + .any(|tag| tag.first().map(|v| v.as_str()) == Some("request")) + ); + assert!( + !parts + .tags + .iter() + .any(|tag| tag.first().map(|v| v.as_str()) == Some("i")) + ); + assert!( + !parts + .tags + .iter() + .any(|tag| tag.first().map(|v| v.as_str()) == Some("p")) + ); + assert!( + !parts + .tags + .iter() + .any(|tag| tag.first().map(|v| v.as_str()) == Some("amount")) + ); +} + +#[test] fn job_result_requires_request_event_tag() { let tags = vec![vec!["p".to_string(), "customer".to_string()]]; let err = job_result_from_tags(KIND_JOB_RESULT_MIN + 1, &tags, "payload").unwrap_err(); diff --git a/crates/events-codec/tests/job_util.rs b/crates/events-codec/tests/job_util.rs @@ -75,6 +75,10 @@ fn parse_i_tags_handles_multiple_shapes() { ], vec![ "i".to_string(), + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), + ], + vec![ + "i".to_string(), "job-id".to_string(), "job".to_string(), "wss://relay".to_string(), @@ -83,7 +87,7 @@ fn parse_i_tags_handles_multiple_shapes() { ]; let inputs = parse_i_tags(&tags); - assert_eq!(inputs.len(), 4); + assert_eq!(inputs.len(), 5); assert_eq!(inputs[0].data, "https://example.com"); assert_eq!(inputs[0].input_type, JobInputType::Url); @@ -98,10 +102,18 @@ fn parse_i_tags_handles_multiple_shapes() { assert!(inputs[2].relay.is_none()); assert!(inputs[2].marker.is_none()); - assert_eq!(inputs[3].data, "job-id"); - assert_eq!(inputs[3].input_type, JobInputType::Job); - assert_eq!(inputs[3].relay.as_deref(), Some("wss://relay")); - assert_eq!(inputs[3].marker.as_deref(), Some("marker")); + assert_eq!( + inputs[3].data, + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + ); + assert_eq!(inputs[3].input_type, JobInputType::Event); + assert!(inputs[3].relay.is_none()); + assert!(inputs[3].marker.is_none()); + + assert_eq!(inputs[4].data, "job-id"); + assert_eq!(inputs[4].input_type, JobInputType::Job); + assert_eq!(inputs[4].relay.as_deref(), Some("wss://relay")); + assert_eq!(inputs[4].marker.as_deref(), Some("marker")); } #[test] diff --git a/crates/events-codec/tests/list_set.rs b/crates/events-codec/tests/list_set.rs @@ -203,3 +203,15 @@ fn list_set_decode_keeps_first_d_tag() { let decoded = list_set_from_tags(KIND_LIST_SET_FOLLOW, "private".to_string(), &tags).unwrap(); assert_eq!(decoded.d_tag, "members.owners"); } + +#[test] +fn list_set_build_tags_omits_blank_optional_metadata() { + let mut list_set = sample_list_set(); + list_set.title = Some(" ".to_string()); + list_set.description = Some(" ".to_string()); + list_set.image = Some(" ".to_string()); + let tags = list_set_build_tags(&list_set).unwrap(); + assert!(!tags.iter().any(|tag| tag[0] == "title")); + assert!(!tags.iter().any(|tag| tag[0] == "description")); + assert!(!tags.iter().any(|tag| tag[0] == "image")); +}