lib

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

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 }