lib

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

encode.rs (4503B)


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