classified_listing.rs (8001B)
1 #[cfg(not(feature = "std"))] 2 use alloc::string::String; 3 #[cfg(feature = "std")] 4 use std::string::String; 5 6 use crate::envelope::{EventTag, EventTags}; 7 8 pub const TAG_RADROOTS_PRICE_UNIT: &str = "radroots:price_unit"; 9 pub const TAG_RADROOTS_QUANTITY: &str = "radroots:quantity"; 10 pub const TAG_RADROOTS_PRIMARY_BIN: &str = "radroots:primary_bin"; 11 pub const TAG_RADROOTS_BIN: &str = "radroots:bin"; 12 pub const TAG_RADROOTS_PRICE: &str = "radroots:price"; 13 14 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 15 /// The marker-selected profile partition for a NIP-99 classified listing. 16 pub enum ClassifiedListingPartition { 17 FocusedFoodAvailability, 18 OperationalListing, 19 GenericNip99, 20 Ambiguous, 21 } 22 23 #[inline] 24 /// Partitions a classified listing by exact raw marker-name presence. 25 /// 26 /// The caller owns kind checks and profile validation. Tag values and arity do 27 /// not affect this partition, so malformed one-element marker tags still count. 28 pub fn classify_classified_listing_tags(tags: &EventTags) -> ClassifiedListingPartition { 29 classify_classified_listing_tag_slice(tags.as_slice()) 30 } 31 32 /// Partitions a borrowed classified-listing tag slice without allocating. 33 /// 34 /// The caller owns kind checks and profile validation. Tag values and arity do 35 /// not affect this partition, so malformed one-element marker tags still count. 36 pub fn classify_classified_listing_tag_slice(tags: &[EventTag]) -> ClassifiedListingPartition { 37 classify_classified_listing_marker_names( 38 tags.iter() 39 .map(|tag| tag.as_slice().first().map(String::as_str)), 40 ) 41 } 42 43 /// Partitions borrowed raw tag names without allocating or validating tag arity. 44 pub fn classify_classified_listing_marker_names<'a>( 45 names: impl IntoIterator<Item = Option<&'a str>>, 46 ) -> ClassifiedListingPartition { 47 let mut has_focused_marker = false; 48 let mut has_operational_marker = false; 49 50 for name in names { 51 let Some(name) = name else { 52 continue; 53 }; 54 55 match name { 56 TAG_RADROOTS_PRICE_UNIT | TAG_RADROOTS_QUANTITY => has_focused_marker = true, 57 TAG_RADROOTS_PRIMARY_BIN | TAG_RADROOTS_BIN | TAG_RADROOTS_PRICE => { 58 has_operational_marker = true; 59 } 60 _ => {} 61 } 62 63 if has_focused_marker && has_operational_marker { 64 return ClassifiedListingPartition::Ambiguous; 65 } 66 } 67 68 match (has_focused_marker, has_operational_marker) { 69 (true, false) => ClassifiedListingPartition::FocusedFoodAvailability, 70 (false, true) => ClassifiedListingPartition::OperationalListing, 71 (false, false) => ClassifiedListingPartition::GenericNip99, 72 (true, true) => ClassifiedListingPartition::Ambiguous, 73 } 74 } 75 76 #[cfg(test)] 77 #[cfg_attr(coverage_nightly, coverage(off))] 78 mod tests { 79 use super::*; 80 81 const FOCUSED_MARKERS: [&str; 2] = [TAG_RADROOTS_PRICE_UNIT, TAG_RADROOTS_QUANTITY]; 82 const OPERATIONAL_MARKERS: [&str; 3] = [ 83 TAG_RADROOTS_PRIMARY_BIN, 84 TAG_RADROOTS_BIN, 85 TAG_RADROOTS_PRICE, 86 ]; 87 88 fn tags(values: &[&[&str]]) -> EventTags { 89 EventTags::new( 90 values 91 .iter() 92 .map(|tag| tag.iter().map(|value| (*value).to_owned()).collect()) 93 .collect(), 94 ) 95 .expect("valid test tags") 96 } 97 98 fn classify(values: &[&[&str]]) -> ClassifiedListingPartition { 99 classify_classified_listing_tags(&tags(values)) 100 } 101 102 #[test] 103 fn exposes_exact_marker_names() { 104 assert_eq!(TAG_RADROOTS_PRICE_UNIT, "radroots:price_unit"); 105 assert_eq!(TAG_RADROOTS_QUANTITY, "radroots:quantity"); 106 assert_eq!(TAG_RADROOTS_PRIMARY_BIN, "radroots:primary_bin"); 107 assert_eq!(TAG_RADROOTS_BIN, "radroots:bin"); 108 assert_eq!(TAG_RADROOTS_PRICE, "radroots:price"); 109 } 110 111 #[test] 112 fn each_focused_marker_selects_focused_food_availability() { 113 for marker in FOCUSED_MARKERS { 114 assert_eq!( 115 classify(&[&[marker, "value"]]), 116 ClassifiedListingPartition::FocusedFoodAvailability, 117 "focused marker {marker}" 118 ); 119 } 120 } 121 122 #[test] 123 fn each_operational_marker_selects_operational_listing() { 124 for marker in OPERATIONAL_MARKERS { 125 assert_eq!( 126 classify(&[&[marker, "value"]]), 127 ClassifiedListingPartition::OperationalListing, 128 "operational marker {marker}" 129 ); 130 } 131 } 132 133 #[test] 134 fn one_element_malformed_markers_still_partition() { 135 for marker in FOCUSED_MARKERS { 136 assert_eq!( 137 classify(&[&[marker]]), 138 ClassifiedListingPartition::FocusedFoodAvailability, 139 "malformed focused marker {marker}" 140 ); 141 } 142 143 for marker in OPERATIONAL_MARKERS { 144 assert_eq!( 145 classify(&[&[marker]]), 146 ClassifiedListingPartition::OperationalListing, 147 "malformed operational marker {marker}" 148 ); 149 } 150 } 151 152 #[test] 153 fn every_focused_and_operational_marker_pair_is_ambiguous_in_either_order() { 154 for focused in FOCUSED_MARKERS { 155 for operational in OPERATIONAL_MARKERS { 156 assert_eq!( 157 classify(&[&[focused], &[operational]]), 158 ClassifiedListingPartition::Ambiguous, 159 "focused {focused} before operational {operational}" 160 ); 161 assert_eq!( 162 classify(&[&[operational], &[focused]]), 163 ClassifiedListingPartition::Ambiguous, 164 "operational {operational} before focused {focused}" 165 ); 166 } 167 } 168 } 169 170 #[test] 171 fn duplicate_markers_do_not_change_the_partition() { 172 assert_eq!( 173 classify(&[ 174 &[TAG_RADROOTS_PRICE_UNIT], 175 &[TAG_RADROOTS_PRICE_UNIT, "lb"], 176 &[TAG_RADROOTS_QUANTITY, "20", "lb"], 177 ]), 178 ClassifiedListingPartition::FocusedFoodAvailability 179 ); 180 assert_eq!( 181 classify(&[ 182 &[TAG_RADROOTS_BIN], 183 &[TAG_RADROOTS_BIN, "bin-1"], 184 &[TAG_RADROOTS_PRICE, "bin-1", "3", "CAD", "1", "lb"], 185 ]), 186 ClassifiedListingPartition::OperationalListing 187 ); 188 } 189 190 #[test] 191 fn marker_matching_is_exact_and_case_sensitive() { 192 for near_match in [ 193 "RADROOTS:PRICE_UNIT", 194 "Radroots:price_unit", 195 "radroots:Price_unit", 196 "radroots:price-unit", 197 "radroots:price_unit ", 198 " radroots:price_unit", 199 "radroots:price_units", 200 "xradroots:price_unit", 201 "radroots:primary_bins", 202 "radroots:binning", 203 "radroots:prices", 204 ] { 205 assert_eq!( 206 classify(&[&[near_match, "value"]]), 207 ClassifiedListingPartition::GenericNip99, 208 "near match {near_match}" 209 ); 210 } 211 } 212 213 #[test] 214 fn marker_names_in_values_do_not_partition() { 215 let values = [ 216 &["summary", TAG_RADROOTS_PRICE_UNIT][..], 217 &["description", TAG_RADROOTS_QUANTITY][..], 218 &["title", TAG_RADROOTS_PRIMARY_BIN][..], 219 &["t", TAG_RADROOTS_BIN][..], 220 &["alt", TAG_RADROOTS_PRICE][..], 221 ]; 222 223 assert_eq!(classify(&values), ClassifiedListingPartition::GenericNip99); 224 } 225 226 #[test] 227 fn empty_tags_are_generic_nip99() { 228 let tags = EventTags::new(Vec::new()).expect("empty tag list is valid"); 229 230 assert_eq!( 231 classify_classified_listing_tags(&tags), 232 ClassifiedListingPartition::GenericNip99 233 ); 234 assert_eq!( 235 classify_classified_listing_tag_slice(tags.as_slice()), 236 ClassifiedListingPartition::GenericNip99 237 ); 238 } 239 }