Swap tags and events chapters

This commit is contained in:
Jon Staab
2026-04-16 16:54:28 -07:00
parent 8a29ff39d6
commit 1267477081
3 changed files with 44 additions and 59 deletions
+39 -47
View File
@@ -1,13 +1,11 @@
# Tags
Every nostr event has two halves. The `content` string is for humans:
the text of a note, the prose of an article, the caption of an image.
Everything else that matters to a machine — who this event replies to,
which topics it belongs under, which pubkeys it mentions, which relay
the author recommends, which article it updates — lives in `tags`.
Nostr needs a way to attach structured data to things: who wrote
this, what it replies to, which topics it belongs under, which relay
to find something on, which article it updates. That's what tags are.
A tag is a list of strings. The first string names the tag; the rest
are its values. An event's `tags` field is a list of these. That's the
are its values. A collection of tags is a list of these. That's the
whole definition:
```text
@@ -21,12 +19,12 @@ It looks plainer than it is. Three choices in that shape matter.
**Lists of lists, not maps.** If tags were a dictionary, a key could
only appear once and the order would be lost. Neither property is one
nostr can give up. An event commonly references several pubkeys
(`["p", ...]` repeated), several events (`["e", ...]` repeated), and
several topics (`["t", ...]` repeated), and the order these appear in
sometimes carries meaning — NIP-10 reply threads, for instance, use
position as a fallback when explicit markers are missing. Lists of
lists preserve both.
nostr can give up. A piece of data commonly references several pubkeys
(`["p", ...]` repeated), several other pieces of data (`["e", ...]`
repeated), and several topics (`["t", ...]` repeated), and the order
these appear in sometimes carries meaning — NIP-10 reply threads, for
instance, use position as a fallback when explicit markers are missing.
Lists of lists preserve both.
**Single-letter names are indexed.** Relays index tags whose name is a
single letter (`a` through `z`, `A` through `Z`) and let clients query
@@ -35,24 +33,20 @@ them with `#e`, `#p`, `#t` filters. Multi-character names — `alt`,
distinction matters at design time: if you want something to be
queryable, give it a single-letter name.
**Meaning is kind-dependent.** The `e` tag appears in eight different
NIPs and means eight different things: a reply, a fork, a merge, a
transaction reference, a report target, a list member, an approval,
a mention. There is no way to interpret `["e", ...]` without first
knowing the event's kind. The [building-nostr] philosophy puts this
bluntly: "when resolving the meaning of a tag, always first look at
the specifications for the event's kind." A library type that tries
to parse tags into a taxonomy is betting on the wrong end of that
rule.
**Meaning is context-dependent.** The `e` tag appears in eight
different NIPs and means eight different things: a reply, a fork, a
merge, a transaction reference, a report target, a list member, an
approval, a mention. There is no way to interpret `["e", ...]`
without knowing the context it appears in. The [building-nostr]
philosophy puts this bluntly: "when resolving the meaning of a tag,
always first look at the specifications for the event's kind." A
library type that tries to parse tags into a taxonomy is betting on
the wrong end of that rule.
This chapter therefore introduces a type that is almost nothing. `Tag`
wraps `Vec<String>` and adds five or six convenience methods. A
handful of free functions query a slice of tags by name. That is
enough for every caller we'll meet in the rest of the book. The one
exception is the address tag, `a`, whose payload is three fields
glued with colons — that gets a small `Address` struct, because those
three fields always travel together and parsing them at the edge of
your program is genuinely useful.
wraps `Vec<String>` and adds five or six convenience methods. `Tags`
wraps `Vec<Tag>` and adds query methods. That is enough for every
caller we'll meet in the rest of the book.
[building-nostr]: https://building-nostr.coracle.social
@@ -175,20 +169,19 @@ impl From<Tag> for Vec<String> {
## A collection of tags
When you hold an `Event`, you usually want to ask questions of its
`tags` field: does it have a `p` tag? What's its `d` value? Which
topics is it tagged with? These come up often enough that writing the
filter by hand every time is noise. They belong on a type.
Wherever tags appear, you want to query them by name: does the
collection have a `p` tag? What's its `d` value? Which topics are
present? These come up often enough that writing the filter by hand
every time is noise. They belong on a type.
We introduce `Tags` as a newtype around `Vec<Tag>`. Like `Tag` itself,
the serde impl is transparent, so the wire format and canonical hash
bytes are unchanged. A `Deref<Target = [Tag]>` impl gives iteration,
indexing, and `len` for free, so the wrapper costs almost nothing at
call sites.
`Tags` is a newtype around `Vec<Tag>`. Like `Tag` itself, the serde
impl is transparent, so the wire format is unchanged. A
`Deref<Target = [Tag]>` impl gives iteration, indexing, and `len` for
free, so the wrapper costs almost nothing at call sites.
```rust {file=coracle-lib/src/tags.rs}
/// A collection of tags, usually taken from an [`Event`]'s `tags`
/// field. Serializes transparently as its inner `Vec<Tag>`.
/// A collection of tags. Serializes transparently as its inner
/// `Vec<Tag>`.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Tags(pub Vec<Tag>);
@@ -254,14 +247,13 @@ impl FromIterator<Tag> for Tags {
```
That's the entire query API. No `TagKind`, no marker enum, no parsed
variants. Readers who want to know whether an event mentions a
particular pubkey write `event.tags.values("p").any(|p| p == hex)` and
the code reads the way the question sounds.
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.
## What's next
We now have both halves of an event in their proper types: the
cryptographic core from the last chapter and the structured data half
from this one. The next chapter introduces kinds — the integer that
decides how content and tags on a given event should be read — and
with it the beginning of a taxonomy we have so far resisted building.
Tags are the structured-data building block that every other type in
the library will carry. The next chapter introduces the `Event` type —
the signed, content-addressable JSON object at the heart of the nostr
protocol — which uses `Tags` as one of its seven fields.
+3 -10
View File
@@ -41,13 +41,6 @@ use crate::keys::PublicKey;
use crate::tags::Tags;
```
The `Tag` and `Tags` types are introduced in the next chapter. For
this chapter, treat `Tag` as a transparent wrapper around
`Vec<String>` and `Tags` as a transparent wrapper around `Vec<Tag>`
serde sees them as bare arrays, the canonical form hashes identically,
and you can build them with `Tag::new("t", ["nostr"])` and
`Tags::from(vec![...])`.
## Errors
```rust {file=coracle-lib/src/events.rs}
@@ -489,6 +482,6 @@ without paying for a signature check.
## What's next
The next chapter gives `tags` a proper type. Tags are the structured half of
every event — references to other events, pubkeys, topics, relay hints — and
treating them as bare `Vec<Vec<String>>` gets tiresome fast.
The next chapter introduces kinds — the integer that decides how content
and tags on a given event should be read — and with it the beginning of
a taxonomy we have so far resisted building.
+2 -2
View File
@@ -6,8 +6,8 @@
- [Keys](02-keys.md)
- [Encryption](03-encryption.md)
- [Events](04-events.md)
- [Tags](05-tags.md)
- [Tags](04-tags.md)
- [Events](05-events.md)
- [Kinds](06-kinds.md)
- [Kind Ranges](07-kind-ranges.md)
- [Addresses](08-addresses.md)