tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

commit 03a4638735cbd0823e62599f2e1b930fc4ee9386
parent 131fd55a9d8c77632764ba5de690259739aa3d6c
Author: triesap <tyson@radroots.org>
Date:   Fri,  5 Jun 2026 20:49:04 -0700

nips: add single letter tag extraction

Diffstat:
Mcrates/tangle_nips/src/lib.rs | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 105 insertions(+), 2 deletions(-)

diff --git a/crates/tangle_nips/src/lib.rs b/crates/tangle_nips/src/lib.rs @@ -1,6 +1,6 @@ #![forbid(unsafe_code)] -use tangle_protocol::Event; +use tangle_protocol::{Event, TagName}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct ParsedTag { @@ -85,12 +85,60 @@ pub fn repeated_or_missing_policy_boundary( optional_tag_value(event, name) } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SingleLetterTagValue { + name: String, + value: String, +} + +impl SingleLetterTagValue { + pub fn name(&self) -> &str { + &self.name + } + + pub fn value(&self) -> &str { + &self.value + } +} + +pub fn single_letter_tag_values(event: &Event) -> Vec<SingleLetterTagValue> { + event + .unsigned() + .tags() + .iter() + .filter_map(|tag| { + tag.indexed_pair() + .map(|(name, value)| SingleLetterTagValue { + name: name.to_owned(), + value: value.to_owned(), + }) + }) + .collect() +} + +pub fn single_letter_values_for(event: &Event, name: &str) -> Result<Vec<String>, String> { + if !TagName::is_indexable_name(name) { + return Err(format!( + "single-letter tag name `{name}` must be one ASCII letter" + )); + } + Ok(single_letter_tag_values(event) + .into_iter() + .filter(|tag| tag.name() == name) + .map(|tag| tag.value) + .collect()) +} + +pub fn first_single_letter_value(event: &Event, name: &str) -> Result<Option<String>, String> { + Ok(single_letter_values_for(event, name)?.into_iter().next()) +} + #[cfg(test)] mod tests { use super::{ matching_tags, optional_tag_value, optional_tag_values, parse_required_u64_tag, parse_u64_field, repeated_or_missing_policy_boundary, required_tag_value, - required_tag_values, tag_count, + required_tag_values, single_letter_tag_values, single_letter_values_for, tag_count, }; use tangle_protocol::{ Event, EventId, Kind, PublicKeyHex, SignatureHex, Tag, UnixTimestamp, UnsignedEvent, @@ -174,6 +222,61 @@ mod tests { assert_eq!(repeated_or_missing_policy_boundary(&missing, "d"), Ok(None)); } + #[test] + fn single_letter_tag_extraction_indexes_first_values_only() { + let event = event_with_tags(vec![ + Tag::from_parts("e", &["root", "relay"]).expect("e"), + Tag::from_parts("p", &["peer"]).expect("p"), + Tag::from_parts("E", &["uppercase-root"]).expect("E"), + Tag::from_parts("t", &["carrots"]).expect("t"), + Tag::from_parts("alt", &["not indexed"]).expect("alt"), + Tag::from_parts("1", &["not indexed"]).expect("number"), + Tag::from_parts("g", &[]).expect("missing value"), + ]); + let values = single_letter_tag_values(&event); + + assert_eq!(values.len(), 4); + assert_eq!(values[0].name(), "e"); + assert_eq!(values[0].value(), "root"); + assert_eq!(values[2].name(), "E"); + assert_eq!( + single_letter_values_for(&event, "e"), + Ok(vec!["root".to_owned()]) + ); + assert_eq!( + single_letter_values_for(&event, "E"), + Ok(vec!["uppercase-root".to_owned()]) + ); + assert_eq!(single_letter_values_for(&event, "g"), Ok(Vec::new())); + } + + #[test] + fn single_letter_tag_extraction_handles_repeated_missing_and_malformed_names() { + let repeated = event_with_tags(vec![ + Tag::from_parts("t", &["carrots"]).expect("t"), + Tag::from_parts("t", &["greens"]).expect("t"), + ]); + let missing = event_with_tags(Vec::new()); + + assert_eq!( + single_letter_values_for(&repeated, "t"), + Ok(vec!["carrots".to_owned(), "greens".to_owned()]) + ); + assert_eq!( + super::first_single_letter_value(&repeated, "t"), + Ok(Some("carrots".to_owned())) + ); + assert_eq!(super::first_single_letter_value(&missing, "t"), Ok(None)); + assert_eq!( + single_letter_values_for(&missing, "topic").expect_err("long"), + "single-letter tag name `topic` must be one ASCII letter" + ); + assert_eq!( + single_letter_values_for(&missing, "é").expect_err("non ascii"), + "single-letter tag name `é` must be one ASCII letter" + ); + } + fn event_with_tags(tags: Vec<Tag>) -> Event { Event::new( EventId::new(&"a".repeat(EventId::HEX_LENGTH)).expect("id"),