lib

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

encode.rs (4469B)


      1 #![forbid(unsafe_code)]
      2 
      3 #[cfg(not(feature = "std"))]
      4 use alloc::{
      5     string::{String, ToString},
      6     vec,
      7     vec::Vec,
      8 };
      9 
     10 use radroots_event::{post::document::Document, tag::name::TAG_D};
     11 
     12 use crate::d_tag::validate_d_tag;
     13 use crate::error::EventEncodeError;
     14 
     15 #[cfg(feature = "json")]
     16 use radroots_event::envelope::kind::KIND_DOCUMENT;
     17 
     18 #[cfg(feature = "json")]
     19 use radroots_event::wire::Nip01EventWireParts;
     20 
     21 const TAG_T: &str = "t";
     22 const TAG_P: &str = "p";
     23 const TAG_A: &str = "a";
     24 
     25 fn push_tag(tags: &mut Vec<Vec<String>>, key: &str, value: &str) {
     26     tags.push(vec![key.to_string(), value.to_string()]);
     27 }
     28 
     29 pub fn document_build_tags(document: &Document) -> Result<Vec<Vec<String>>, EventEncodeError> {
     30     if document.d_tag.trim().is_empty() {
     31         return Err(EventEncodeError::EmptyRequiredField("d_tag"));
     32     }
     33     validate_d_tag(&document.d_tag, "d_tag")?;
     34     if document.doc_type.trim().is_empty() {
     35         return Err(EventEncodeError::EmptyRequiredField("doc_type"));
     36     }
     37     if document.title.trim().is_empty() {
     38         return Err(EventEncodeError::EmptyRequiredField("title"));
     39     }
     40     if document.version.trim().is_empty() {
     41         return Err(EventEncodeError::EmptyRequiredField("version"));
     42     }
     43     if document.subject.pubkey.trim().is_empty() {
     44         return Err(EventEncodeError::EmptyRequiredField("subject.pubkey"));
     45     }
     46     let mut tags = Vec::new();
     47     push_tag(&mut tags, TAG_D, &document.d_tag);
     48     push_tag(&mut tags, TAG_P, &document.subject.pubkey);
     49     if let Some(address) = document.subject.address.as_ref() {
     50         let address = address.trim();
     51         if address.is_empty() {
     52             return Err(EventEncodeError::EmptyRequiredField("subject.address"));
     53         }
     54         push_tag(&mut tags, TAG_A, address);
     55     }
     56     if let Some(items) = document.tags.as_ref() {
     57         for item in items.iter().filter(|v| !v.trim().is_empty()) {
     58             push_tag(&mut tags, TAG_T, item);
     59         }
     60     }
     61     Ok(tags)
     62 }
     63 
     64 #[cfg(feature = "json")]
     65 pub fn to_wire_parts(document: &Document) -> Result<Nip01EventWireParts, EventEncodeError> {
     66     to_wire_parts_with_kind(document, KIND_DOCUMENT)
     67 }
     68 
     69 #[cfg(feature = "json")]
     70 pub fn to_wire_parts_with_kind(
     71     document: &Document,
     72     kind: u32,
     73 ) -> Result<Nip01EventWireParts, EventEncodeError> {
     74     if kind != KIND_DOCUMENT {
     75         return Err(EventEncodeError::InvalidKind(kind));
     76     }
     77     let tags = document_build_tags(document)?;
     78     let content = serde_json::to_string(document).map_err(|_| EventEncodeError::Json)?;
     79     Ok(Nip01EventWireParts {
     80         kind,
     81         content,
     82         tags,
     83     })
     84 }
     85 
     86 #[cfg(test)]
     87 mod tests {
     88     use super::*;
     89     use crate::test_fixtures::FIXTURE_ALICE_PUBLIC_KEY_HEX;
     90     use radroots_event::post::document::DocumentSubject;
     91 
     92     fn sample_document_address() -> String {
     93         format!("30340:{FIXTURE_ALICE_PUBLIC_KEY_HEX}:AAAAAAAAAAAAAAAAAAAAAA")
     94     }
     95 
     96     fn sample_document() -> Document {
     97         Document {
     98             d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
     99             doc_type: "charter".to_string(),
    100             title: "Charter".to_string(),
    101             version: "1.0.0".to_string(),
    102             summary: None,
    103             effective_at: None,
    104             body_markdown: None,
    105             subject: DocumentSubject {
    106                 pubkey: FIXTURE_ALICE_PUBLIC_KEY_HEX.to_string(),
    107                 address: Some(sample_document_address()),
    108             },
    109             tags: None,
    110         }
    111     }
    112 
    113     #[test]
    114     fn document_build_tags_includes_subject_address_when_present() {
    115         let tags = document_build_tags(&sample_document()).expect("document tags");
    116         assert!(
    117             tags.iter()
    118                 .any(|tag| tag.first().map(|v| v.as_str()) == Some("a"))
    119         );
    120     }
    121 
    122     #[test]
    123     fn document_build_tags_omits_subject_address_when_absent() {
    124         let mut document = sample_document();
    125         document.subject.address = None;
    126         let tags = document_build_tags(&document).expect("document tags");
    127         assert!(
    128             !tags
    129                 .iter()
    130                 .any(|tag| tag.first().map(|v| v.as_str()) == Some("a"))
    131         );
    132     }
    133 
    134     #[test]
    135     fn document_build_tags_rejects_invalid_d_tag() {
    136         let mut document = sample_document();
    137         document.d_tag = "invalid".to_string();
    138         let err = document_build_tags(&document).expect_err("expected invalid d_tag");
    139         assert!(matches!(err, EventEncodeError::InvalidField("d_tag")));
    140     }
    141 }