list_sets.rs (11375B)
1 #![forbid(unsafe_code)] 2 3 #[cfg(not(feature = "std"))] 4 use alloc::{ 5 format, 6 string::{String, ToString}, 7 vec, 8 vec::Vec, 9 }; 10 11 use radroots_events::farm::RadrootsFarmRef; 12 use radroots_events::kinds::KIND_FARM; 13 use radroots_events::list::RadrootsListEntry; 14 use radroots_events::list_set::RadrootsListSet; 15 16 use crate::d_tag::validate_d_tag; 17 use crate::error::EventEncodeError; 18 19 const MEMBER_OF_COOPS: &str = "member_of.coops"; 20 21 fn coop_list_set_id(coop_id: &str, suffix: &str) -> Result<String, EventEncodeError> { 22 let coop_id = coop_id.trim(); 23 if coop_id.is_empty() { 24 return Err(EventEncodeError::EmptyRequiredField("coop_id")); 25 } 26 validate_d_tag(coop_id, "coop_id")?; 27 Ok(format!("coop:{coop_id}:{suffix}")) 28 } 29 30 fn list_entries<I, S>(tag: &str, values: I) -> Result<Vec<RadrootsListEntry>, EventEncodeError> 31 where 32 I: IntoIterator<Item = S>, 33 S: AsRef<str>, 34 { 35 let mut entries = Vec::new(); 36 for value in values { 37 let value = value.as_ref().trim(); 38 if value.is_empty() { 39 return Err(EventEncodeError::EmptyRequiredField("entry.values")); 40 } 41 entries.push(RadrootsListEntry { 42 tag: tag.to_string(), 43 values: vec![value.to_string()], 44 }); 45 } 46 Ok(entries) 47 } 48 49 fn farm_address(farm: &RadrootsFarmRef) -> Result<String, EventEncodeError> { 50 if farm.pubkey.trim().is_empty() { 51 return Err(EventEncodeError::EmptyRequiredField("farm.pubkey")); 52 } 53 if farm.d_tag.trim().is_empty() { 54 return Err(EventEncodeError::EmptyRequiredField("farm.d_tag")); 55 } 56 validate_d_tag(&farm.d_tag, "farm.d_tag")?; 57 let mut addr = String::new(); 58 addr.push_str(&KIND_FARM.to_string()); 59 addr.push(':'); 60 addr.push_str(&farm.pubkey); 61 addr.push(':'); 62 addr.push_str(&farm.d_tag); 63 Ok(addr) 64 } 65 66 pub fn coop_members_list_set<I, S>( 67 coop_id: &str, 68 members: I, 69 ) -> Result<RadrootsListSet, EventEncodeError> 70 where 71 I: IntoIterator<Item = S>, 72 S: AsRef<str>, 73 { 74 Ok(RadrootsListSet { 75 d_tag: coop_list_set_id(coop_id, "members")?, 76 content: String::new(), 77 entries: list_entries("p", members)?, 78 title: None, 79 description: None, 80 image: None, 81 }) 82 } 83 84 pub fn coop_members_farms_list_set<I>( 85 coop_id: &str, 86 farms: I, 87 ) -> Result<RadrootsListSet, EventEncodeError> 88 where 89 I: IntoIterator<Item = RadrootsFarmRef>, 90 { 91 let mut entries = Vec::new(); 92 for farm in farms { 93 let address = farm_address(&farm)?; 94 entries.push(RadrootsListEntry { 95 tag: "a".to_string(), 96 values: vec![address], 97 }); 98 entries.push(RadrootsListEntry { 99 tag: "p".to_string(), 100 values: vec![farm.pubkey], 101 }); 102 } 103 Ok(RadrootsListSet { 104 d_tag: coop_list_set_id(coop_id, "members.farms")?, 105 content: String::new(), 106 entries, 107 title: None, 108 description: None, 109 image: None, 110 }) 111 } 112 113 pub fn coop_owners_list_set<I, S>( 114 coop_id: &str, 115 owners: I, 116 ) -> Result<RadrootsListSet, EventEncodeError> 117 where 118 I: IntoIterator<Item = S>, 119 S: AsRef<str>, 120 { 121 Ok(RadrootsListSet { 122 d_tag: coop_list_set_id(coop_id, "members.owners")?, 123 content: String::new(), 124 entries: list_entries("p", owners)?, 125 title: None, 126 description: None, 127 image: None, 128 }) 129 } 130 131 pub fn coop_admins_list_set<I, S>( 132 coop_id: &str, 133 admins: I, 134 ) -> Result<RadrootsListSet, EventEncodeError> 135 where 136 I: IntoIterator<Item = S>, 137 S: AsRef<str>, 138 { 139 Ok(RadrootsListSet { 140 d_tag: coop_list_set_id(coop_id, "members.admins")?, 141 content: String::new(), 142 entries: list_entries("p", admins)?, 143 title: None, 144 description: None, 145 image: None, 146 }) 147 } 148 149 pub fn coop_items_list_set<I, S>( 150 coop_id: &str, 151 item_addresses: I, 152 ) -> Result<RadrootsListSet, EventEncodeError> 153 where 154 I: IntoIterator<Item = S>, 155 S: AsRef<str>, 156 { 157 Ok(RadrootsListSet { 158 d_tag: coop_list_set_id(coop_id, "items")?, 159 content: String::new(), 160 entries: list_entries("a", item_addresses)?, 161 title: None, 162 description: None, 163 image: None, 164 }) 165 } 166 167 pub fn member_of_coops_list_set<I, S>(coop_pubkeys: I) -> Result<RadrootsListSet, EventEncodeError> 168 where 169 I: IntoIterator<Item = S>, 170 S: AsRef<str>, 171 { 172 Ok(RadrootsListSet { 173 d_tag: MEMBER_OF_COOPS.to_string(), 174 content: String::new(), 175 entries: list_entries("p", coop_pubkeys)?, 176 title: None, 177 description: None, 178 image: None, 179 }) 180 } 181 182 #[cfg(test)] 183 mod tests { 184 use super::*; 185 use crate::test_fixtures::FIXTURE_ALICE_PUBLIC_KEY_HEX; 186 187 #[test] 188 fn coop_list_set_id_validates_coop_id() { 189 let err = coop_list_set_id(" ", "members").expect_err("expected coop_id validation error"); 190 assert!(matches!( 191 err, 192 EventEncodeError::EmptyRequiredField("coop_id") 193 )); 194 } 195 196 #[test] 197 fn list_entries_rejects_blank_values() { 198 let err = list_entries("p", [" "]).expect_err("expected blank entry error"); 199 assert!(matches!( 200 err, 201 EventEncodeError::EmptyRequiredField("entry.values") 202 )); 203 } 204 205 #[test] 206 fn list_entries_cover_string_iterators() { 207 let entries = list_entries("p", vec!["member".to_string()]).expect("valid string entries"); 208 assert_eq!(entries.len(), 1); 209 assert_eq!(entries[0].values[0], "member"); 210 211 let err = list_entries("p", vec![" ".to_string()]).expect_err("blank string entry"); 212 assert!(matches!( 213 err, 214 EventEncodeError::EmptyRequiredField("entry.values") 215 )); 216 } 217 218 #[test] 219 fn list_entries_accepts_empty_iterators() { 220 let entries = list_entries("p", Vec::<&str>::new()).expect("empty list entries"); 221 assert!(entries.is_empty()); 222 223 let entries = list_entries("p", vec!["member"]).expect("non-empty vec<&str> entries"); 224 assert_eq!(entries.len(), 1); 225 assert_eq!(entries[0].values[0], "member"); 226 227 let err = list_entries("p", vec![" "]).expect_err("blank vec<&str> entry"); 228 assert!(matches!( 229 err, 230 EventEncodeError::EmptyRequiredField("entry.values") 231 )); 232 } 233 234 #[test] 235 fn farm_address_rejects_empty_and_invalid_d_tag() { 236 let err = farm_address(&RadrootsFarmRef { 237 pubkey: " ".to_string(), 238 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), 239 }) 240 .expect_err("expected empty pubkey error"); 241 assert!(matches!( 242 err, 243 EventEncodeError::EmptyRequiredField("farm.pubkey") 244 )); 245 246 let err = farm_address(&RadrootsFarmRef { 247 pubkey: FIXTURE_ALICE_PUBLIC_KEY_HEX.to_string(), 248 d_tag: " ".to_string(), 249 }) 250 .expect_err("expected empty d_tag error"); 251 assert!(matches!( 252 err, 253 EventEncodeError::EmptyRequiredField("farm.d_tag") 254 )); 255 256 let err = farm_address(&RadrootsFarmRef { 257 pubkey: FIXTURE_ALICE_PUBLIC_KEY_HEX.to_string(), 258 d_tag: "invalid".to_string(), 259 }) 260 .expect_err("expected invalid d_tag error"); 261 assert!(matches!(err, EventEncodeError::InvalidField("farm.d_tag"))); 262 } 263 264 #[test] 265 fn coop_list_set_builders_cover_success_and_error_paths() { 266 let coop_id = "AAAAAAAAAAAAAAAAAAAAAA"; 267 268 let members = coop_members_list_set(coop_id, ["member-a"]).expect("members list set"); 269 assert_eq!(members.d_tag, "coop:AAAAAAAAAAAAAAAAAAAAAA:members"); 270 assert_eq!(members.entries.len(), 1); 271 assert_eq!(members.entries[0].tag, "p"); 272 273 let err = 274 coop_members_list_set("invalid", ["member-a"]).expect_err("expected invalid coop_id"); 275 assert!(matches!(err, EventEncodeError::InvalidField("coop_id"))); 276 277 let err = 278 coop_members_list_set(coop_id, [" "]).expect_err("expected invalid members entry"); 279 assert!(matches!( 280 err, 281 EventEncodeError::EmptyRequiredField("entry.values") 282 )); 283 284 let owners = coop_owners_list_set(coop_id, ["owner-a"]).expect("owners list set"); 285 assert_eq!(owners.d_tag, "coop:AAAAAAAAAAAAAAAAAAAAAA:members.owners"); 286 assert_eq!(owners.entries[0].tag, "p"); 287 let err = coop_owners_list_set("invalid", ["owner-a"]).expect_err("invalid coop_id"); 288 assert!(matches!(err, EventEncodeError::InvalidField("coop_id"))); 289 let err = coop_owners_list_set(coop_id, [" "]).expect_err("expected invalid owner entry"); 290 assert!(matches!( 291 err, 292 EventEncodeError::EmptyRequiredField("entry.values") 293 )); 294 295 let admins = coop_admins_list_set(coop_id, ["admin-a"]).expect("admins list set"); 296 assert_eq!(admins.d_tag, "coop:AAAAAAAAAAAAAAAAAAAAAA:members.admins"); 297 assert_eq!(admins.entries[0].tag, "p"); 298 let err = coop_admins_list_set("invalid", ["admin-a"]).expect_err("invalid coop_id"); 299 assert!(matches!(err, EventEncodeError::InvalidField("coop_id"))); 300 let err = coop_admins_list_set(coop_id, [" "]).expect_err("expected invalid admin entry"); 301 assert!(matches!( 302 err, 303 EventEncodeError::EmptyRequiredField("entry.values") 304 )); 305 306 let items = coop_items_list_set(coop_id, ["30317:author:AAAAAAAAAAAAAAAAAAAAAA"]) 307 .expect("items list set"); 308 assert_eq!(items.d_tag, "coop:AAAAAAAAAAAAAAAAAAAAAA:items"); 309 assert_eq!(items.entries[0].tag, "a"); 310 let err = coop_items_list_set("invalid", ["30317:author:AAAAAAAAAAAAAAAAAAAAAA"]) 311 .expect_err("invalid coop_id"); 312 assert!(matches!(err, EventEncodeError::InvalidField("coop_id"))); 313 let err = coop_items_list_set(coop_id, [" "]).expect_err("expected invalid item entry"); 314 assert!(matches!( 315 err, 316 EventEncodeError::EmptyRequiredField("entry.values") 317 )); 318 319 let member_of = member_of_coops_list_set(["coop-pubkey"]).expect("member_of list set"); 320 assert_eq!(member_of.d_tag, "member_of.coops"); 321 assert_eq!(member_of.entries[0].tag, "p"); 322 let err = 323 member_of_coops_list_set([" "]).expect_err("expected invalid member_of coop entry"); 324 assert!(matches!( 325 err, 326 EventEncodeError::EmptyRequiredField("entry.values") 327 )); 328 } 329 330 #[test] 331 fn coop_members_farms_list_set_covers_success_and_invalid_coop_id() { 332 let farms = vec![RadrootsFarmRef { 333 pubkey: FIXTURE_ALICE_PUBLIC_KEY_HEX.to_string(), 334 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), 335 }]; 336 let list_set = coop_members_farms_list_set("AAAAAAAAAAAAAAAAAAAAAA", farms.clone()) 337 .expect("members farms list set"); 338 assert_eq!(list_set.d_tag, "coop:AAAAAAAAAAAAAAAAAAAAAA:members.farms"); 339 assert_eq!(list_set.entries.len(), 2); 340 assert_eq!(list_set.entries[0].tag, "a"); 341 assert_eq!(list_set.entries[1].tag, "p"); 342 343 let err = coop_members_farms_list_set("invalid", farms) 344 .expect_err("expected invalid coop_id in members farms list set"); 345 assert!(matches!(err, EventEncodeError::InvalidField("coop_id"))); 346 } 347 }