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