Add some utility methods to tags

This commit is contained in:
Jon Staab
2026-04-22 10:39:03 -07:00
parent a8a57a3d77
commit 8773017198
4 changed files with 79 additions and 6 deletions
+47
View File
@@ -218,6 +218,38 @@ impl Tags {
pub fn has(&self, name: &str) -> bool {
self.find(name).is_some()
}
/// Append a tag built from a name and a list of values. Returns
/// `self` so the call can chain.
pub fn add<N, V, S>(mut self, name: N, values: V) -> Self
where
N: Into<String>,
V: IntoIterator<Item = S>,
S: Into<String>,
{
self.0.push(Tag::new(name, values));
self
}
/// Remove every tag with the given name. Returns `self` so the
/// call can chain.
pub fn remove(mut self, name: &str) -> Self {
self.0.retain(|t| t.name() != name);
self
}
/// Replace every tag with the given name by a single new tag
/// built from `name` and `values`. Equivalent to `remove(name)`
/// followed by `add(Tag::new(name, values))`.
pub fn set<N, V, S>(self, name: N, values: V) -> Self
where
N: Into<String>,
V: IntoIterator<Item = S>,
S: Into<String>,
{
let name = name.into();
self.remove(&name).add(name, values)
}
}
impl std::ops::Deref for Tags {
@@ -251,6 +283,21 @@ variants. Checking whether a collection mentions a particular pubkey
is `tags.values("p").any(|p| p == hex)` — the code reads the way the
question sounds.
Mutation is just as flat. `add` appends a tag; `remove` drops every
tag with a given name; `set` replaces every tag of a given name with
a single fresh one, which is the common case when a replaceable kind
is rewriting one of its own singleton fields. All three consume and
return `self`, so assembling or rewriting a collection reads as one
expression:
```rust
let tags = Tags::new()
.add("t", ["nostr"]) // [["t", "nostr"]]
.add("t", ["rust"]) // [["t", "nostr"], ["t", "rust"]]
.set("t", ["nothing"]) // [["t", "nothing"]]
.remove("t"); // []
```
## What's next
Tags are the structured-data building block that every other type in
+26 -4
View File
@@ -100,17 +100,38 @@ other events, pubkeys, topics, and whatever else. Tags and content travel
together because tags are part of what the hash commits to — they are not
metadata in the "could be added later" sense.
`EventContent` is a builder. `new` hands back an empty payload and
chainable `content` and `tags` setters fill it in. Neither field is
required at construction time — a zero-length note with no tags is a
perfectly legal kind-1 event — and the builder shape means callers
don't have to pass `""` or `vec![]` placeholders when they only care
about one of the two.
```rust {file=coracle-lib/src/events.rs}
/// The content of an event: the human-readable body plus its tags.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct EventContent {
pub content: String,
pub tags: Tags,
}
impl EventContent {
pub fn new(content: impl Into<String>, tags: impl Into<Tags>) -> Self {
EventContent { content: content.into(), tags: tags.into() }
/// An empty payload: no content, no tags.
pub fn new() -> Self {
Self::default()
}
/// Set the human-readable content. Returns `self` so the call can
/// chain.
pub fn content(mut self, content: impl Into<String>) -> Self {
self.content = content.into();
self
}
/// Set the tags. Returns `self` so the call can chain.
pub fn tags(mut self, tags: impl Into<Tags>) -> Self {
self.tags = tags.into();
self
}
}
```
@@ -324,7 +345,8 @@ With that in place, the full pipeline from "hello nostr" to a signed event is
one expression:
```rust
let hashed = EventContent::new("hello nostr", vec![])
let hashed = EventContent::new()
.content("hello nostr")
.kind(1)
.stamp(1_700_000_000)
.own(secret.public_key())
+3 -1
View File
@@ -234,7 +234,9 @@ impl Kind for Deletion {
}
fn to_content(&self) -> EventContent {
EventContent::new(self.reason.clone(), self.tags.clone())
EventContent::new()
.content(self.reason.clone())
.tags(self.tags.clone())
}
}
```