lib

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

article.rs (7105B)


      1 #![cfg(feature = "serde_json")]
      2 
      3 use radroots_events::{
      4     article::RadrootsArticle,
      5     farm::RadrootsFarmRef,
      6     kinds::{KIND_ARTICLE, KIND_POST},
      7     social::{RadrootsSocialFarmAnchor, RadrootsSocialLocation},
      8     tags::{TAG_A, TAG_D, TAG_G, TAG_IMAGE, TAG_LOCATION, TAG_PUBLISHED_AT, TAG_T, TAG_TITLE},
      9 };
     10 use radroots_events_codec::{
     11     article::{
     12         decode::{article_from_event, data_from_event, parsed_from_event},
     13         encode::{article_build_tags, to_wire_parts, to_wire_parts_with_kind},
     14     },
     15     error::{EventEncodeError, EventParseError},
     16 };
     17 
     18 const VALID_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
     19 const FARM_D_TAG: &str = "BBBBBBBBBBBBBBBBBBBBBA";
     20 const FARM_PUBKEY: &str = "farm_pubkey";
     21 
     22 fn sample_article() -> RadrootsArticle {
     23     RadrootsArticle {
     24         d_tag: VALID_D_TAG.to_string(),
     25         title: "Spring soil notes".to_string(),
     26         content: "# Spring soil notes".to_string(),
     27         summary: Some("Field update".to_string()),
     28         image: Some("https://media.example.test/soil.jpg".to_string()),
     29         published_at: Some(1_781_895_600),
     30         farm: Some(RadrootsSocialFarmAnchor {
     31             farm: RadrootsFarmRef {
     32                 pubkey: FARM_PUBKEY.to_string(),
     33                 d_tag: FARM_D_TAG.to_string(),
     34             },
     35             relays: None,
     36         }),
     37         location: Some(RadrootsSocialLocation {
     38             name: Some("North field".to_string()),
     39             geohash: Some("c23nb62w20st".to_string()),
     40         }),
     41         topics: Some(vec!["soil".to_string(), "cover-crops".to_string()]),
     42     }
     43 }
     44 
     45 fn has_tag(tags: &[Vec<String>], key: &str, value: &str) -> bool {
     46     tags.iter().any(|tag| {
     47         tag.first().map(|entry| entry.as_str()) == Some(key)
     48             && tag.get(1).map(|entry| entry.as_str()) == Some(value)
     49     })
     50 }
     51 
     52 #[test]
     53 fn article_to_wire_parts_roundtrips_social_metadata() {
     54     let article = sample_article();
     55     let parts = to_wire_parts(&article).unwrap();
     56 
     57     assert_eq!(parts.kind, KIND_ARTICLE);
     58     assert_eq!(parts.content, article.content);
     59     assert!(has_tag(&parts.tags, TAG_D, VALID_D_TAG));
     60     assert!(has_tag(&parts.tags, TAG_TITLE, "Spring soil notes"));
     61     assert!(has_tag(
     62         &parts.tags,
     63         TAG_IMAGE,
     64         "https://media.example.test/soil.jpg"
     65     ));
     66     assert!(has_tag(&parts.tags, TAG_PUBLISHED_AT, "1781895600"));
     67     assert!(has_tag(&parts.tags, TAG_LOCATION, "North field"));
     68     assert!(has_tag(&parts.tags, TAG_G, "c23nb62w20st"));
     69     assert!(has_tag(
     70         &parts.tags,
     71         TAG_A,
     72         "30340:farm_pubkey:BBBBBBBBBBBBBBBBBBBBBA"
     73     ));
     74     assert!(has_tag(&parts.tags, TAG_T, "soil"));
     75     assert!(has_tag(&parts.tags, TAG_T, "cover-crops"));
     76 
     77     let decoded = article_from_event(parts.kind, &parts.tags, &parts.content).unwrap();
     78     assert_eq!(decoded.d_tag, VALID_D_TAG);
     79     assert_eq!(decoded.title, "Spring soil notes");
     80     assert_eq!(decoded.content, "# Spring soil notes");
     81     assert_eq!(decoded.summary.as_deref(), Some("Field update"));
     82     assert_eq!(decoded.published_at, Some(1_781_895_600));
     83     assert_eq!(
     84         decoded.farm.as_ref().map(|farm| farm.farm.pubkey.as_str()),
     85         Some(FARM_PUBKEY)
     86     );
     87     assert_eq!(
     88         decoded
     89             .location
     90             .as_ref()
     91             .and_then(|location| location.name.as_deref()),
     92         Some("North field")
     93     );
     94     assert_eq!(decoded.topics.as_ref().map(Vec::len), Some(2));
     95 }
     96 
     97 #[test]
     98 fn article_codec_requires_kind_required_fields_and_valid_d_tag() {
     99     let mut article = sample_article();
    100     article.title = " ".to_string();
    101     assert!(matches!(
    102         article_build_tags(&article),
    103         Err(EventEncodeError::EmptyRequiredField("title"))
    104     ));
    105 
    106     let mut article = sample_article();
    107     article.d_tag = "bad".to_string();
    108     assert!(matches!(
    109         to_wire_parts(&article),
    110         Err(EventEncodeError::InvalidField("d_tag"))
    111     ));
    112 
    113     assert!(matches!(
    114         to_wire_parts_with_kind(&sample_article(), KIND_POST),
    115         Err(EventEncodeError::InvalidKind(KIND_POST))
    116     ));
    117 
    118     let mut tags = article_build_tags(&sample_article()).unwrap();
    119     tags.retain(|tag| tag.first().map(|value| value.as_str()) != Some(TAG_TITLE));
    120     assert!(matches!(
    121         article_from_event(KIND_ARTICLE, &tags, "# Spring soil notes"),
    122         Err(EventParseError::MissingTag(TAG_TITLE))
    123     ));
    124 
    125     let err = article_from_event(KIND_POST, &tags, "# Spring soil notes").unwrap_err();
    126     assert!(matches!(
    127         err,
    128         EventParseError::InvalidKind {
    129             expected: "30023",
    130             got: KIND_POST
    131         }
    132     ));
    133 }
    134 
    135 #[test]
    136 fn article_decode_handles_minimal_and_invalid_optional_tags() {
    137     let tags = vec![
    138         vec![TAG_D.to_string(), VALID_D_TAG.to_string()],
    139         vec![TAG_TITLE.to_string(), "Minimal article".to_string()],
    140     ];
    141     let decoded = article_from_event(KIND_ARTICLE, &tags, "Body").unwrap();
    142     assert_eq!(decoded.d_tag, VALID_D_TAG);
    143     assert_eq!(decoded.title, "Minimal article");
    144     assert!(decoded.farm.is_none());
    145     assert_eq!(decoded.topics, None);
    146     assert_eq!(decoded.published_at, None);
    147 
    148     let mut tags = tags.clone();
    149     tags.push(vec![TAG_PUBLISHED_AT.to_string(), "not-a-time".to_string()]);
    150     assert!(matches!(
    151         article_from_event(KIND_ARTICLE, &tags, "Body"),
    152         Err(EventParseError::InvalidNumber(TAG_PUBLISHED_AT, _))
    153     ));
    154 
    155     assert!(matches!(
    156         article_from_event(KIND_ARTICLE, &tags, " "),
    157         Err(EventParseError::InvalidTag("content"))
    158     ));
    159 }
    160 
    161 #[test]
    162 fn article_build_tags_handles_absent_optional_metadata() {
    163     let article = RadrootsArticle {
    164         d_tag: VALID_D_TAG.to_string(),
    165         title: "Minimal article".to_string(),
    166         content: "Body".to_string(),
    167         summary: None,
    168         image: None,
    169         published_at: None,
    170         farm: None,
    171         location: None,
    172         topics: None,
    173     };
    174 
    175     let tags = article_build_tags(&article).unwrap();
    176     assert!(has_tag(&tags, TAG_D, VALID_D_TAG));
    177     assert!(has_tag(&tags, TAG_TITLE, "Minimal article"));
    178     assert!(!tags.iter().any(|tag| {
    179         matches!(
    180             tag.first().map(String::as_str),
    181             Some(TAG_PUBLISHED_AT | TAG_A | TAG_LOCATION | TAG_G | TAG_T)
    182         )
    183     }));
    184 }
    185 
    186 #[test]
    187 fn article_wrappers_preserve_event_metadata() {
    188     let article = sample_article();
    189     let parts = to_wire_parts(&article).unwrap();
    190     let data = data_from_event(
    191         "event_id".to_string(),
    192         "author".to_string(),
    193         42,
    194         parts.kind,
    195         parts.content.clone(),
    196         parts.tags.clone(),
    197     )
    198     .unwrap();
    199 
    200     assert_eq!(data.id, "event_id");
    201     assert_eq!(data.author, "author");
    202     assert_eq!(data.published_at, 42);
    203     assert_eq!(data.kind, KIND_ARTICLE);
    204     assert_eq!(data.data.title, "Spring soil notes");
    205 
    206     let parsed = parsed_from_event(
    207         "event_id".to_string(),
    208         "author".to_string(),
    209         42,
    210         parts.kind,
    211         parts.content,
    212         parts.tags,
    213         "sig".to_string(),
    214     )
    215     .unwrap();
    216 
    217     assert_eq!(parsed.event.sig, "sig");
    218     assert_eq!(parsed.data.data.d_tag, VALID_D_TAG);
    219 }