tangle


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

commit 5cf43cdba919da0bced17d785c9c7765d405b018
parent 8e9da170911d8d531f39579e6c8ffd089f504342
Author: triesap <tyson@radroots.org>
Date:   Fri,  5 Jun 2026 20:54:21 -0700

nips: add nip50 search parser

Diffstat:
MCargo.lock | 1+
Mcrates/tangle_nips/Cargo.toml | 3+++
Mcrates/tangle_nips/src/lib.rs | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
3 files changed, 96 insertions(+), 2 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -386,6 +386,7 @@ dependencies = [ name = "tangle_nips" version = "0.1.0" dependencies = [ + "serde_json", "tangle_protocol", ] diff --git a/crates/tangle_nips/Cargo.toml b/crates/tangle_nips/Cargo.toml @@ -10,5 +10,8 @@ description = "MVP NIP parsers and projection contracts for tangle" [dependencies] tangle_protocol = { path = "../tangle_protocol" } +[dev-dependencies] +serde_json = "1" + [lints] workspace = true diff --git a/crates/tangle_nips/src/lib.rs b/crates/tangle_nips/src/lib.rs @@ -1,7 +1,9 @@ #![forbid(unsafe_code)] use core::str::FromStr; -use tangle_protocol::{AddressCoordinate, Event, EventId, PublicKeyHex, TagName, UnixTimestamp}; +use tangle_protocol::{ + AddressCoordinate, Event, EventId, Filter, PublicKeyHex, TagName, UnixTimestamp, +}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct ParsedTag { @@ -230,16 +232,56 @@ pub fn parse_relay_auth_event(event: &Event) -> Result<Option<RelayAuthEvent>, S })) } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Nip50SearchQuery { + text: String, + terms: Vec<String>, +} + +impl Nip50SearchQuery { + pub fn text(&self) -> &str { + &self.text + } + + pub fn terms(&self) -> &[String] { + &self.terms + } +} + +pub fn parse_nip50_search(search: &str) -> Result<Option<Nip50SearchQuery>, String> { + let terms = search + .split_whitespace() + .filter(|term| !term.contains(':')) + .map(str::to_owned) + .collect::<Vec<_>>(); + if terms.is_empty() { + return Ok(None); + } + Ok(Some(Nip50SearchQuery { + text: terms.join(" "), + terms, + })) +} + +pub fn parse_nip50_filter_search(filter: &Filter) -> Result<Option<Nip50SearchQuery>, String> { + match filter.search() { + Some(search) => parse_nip50_search(search), + None => Ok(None), + } +} + #[cfg(test)] mod tests { use super::{ DeletionTarget, matching_tags, optional_tag_value, optional_tag_values, - parse_deletion_request, parse_relay_auth_event, parse_required_u64_tag, parse_u64_field, + parse_deletion_request, parse_nip50_filter_search, parse_nip50_search, + parse_relay_auth_event, parse_required_u64_tag, parse_u64_field, repeated_or_missing_policy_boundary, required_tag_value, required_tag_values, single_letter_tag_values, single_letter_values_for, tag_count, }; use tangle_protocol::{ Event, EventId, Kind, PublicKeyHex, SignatureHex, Tag, UnixTimestamp, UnsignedEvent, + filter_from_value, }; #[test] @@ -550,6 +592,54 @@ mod tests { ); } + #[test] + fn nip50_search_parser_extracts_plain_terms_and_ignores_extensions() { + let query = parse_nip50_search(" fresh seller:ignored carrots status:ignored greens ") + .expect("parse") + .expect("query"); + + assert_eq!(query.text(), "fresh carrots greens"); + assert_eq!( + query.terms(), + &[ + "fresh".to_owned(), + "carrots".to_owned(), + "greens".to_owned() + ] + ); + } + + #[test] + fn nip50_search_parser_treats_empty_and_extension_only_queries_as_absent() { + assert_eq!(parse_nip50_search(" "), Ok(None)); + assert_eq!( + parse_nip50_search("seller:ignored status:ignored"), + Ok(None) + ); + } + + #[test] + fn nip50_search_parser_reads_filter_search_field() { + let filter = filter_from_value(&serde_json::json!({ + "search": "farmstand tomatoes", + "kinds": [1] + })) + .expect("filter"); + let missing = filter_from_value(&serde_json::json!({ + "kinds": [1] + })) + .expect("missing"); + + assert_eq!( + parse_nip50_filter_search(&filter) + .expect("filter") + .expect("query") + .text(), + "farmstand tomatoes" + ); + assert_eq!(parse_nip50_filter_search(&missing), Ok(None)); + } + fn event_with_tags(tags: Vec<Tag>) -> Event { event_with_kind_and_tags(30_402, tags) }