Add kinds chapter
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
# Kinds
|
||||
|
||||
Every event carries a `kind` field — a 16-bit integer that says what the
|
||||
event means. Kind 0 is a profile. Kind 1 is a short text note. Kind 5 is
|
||||
a deletion request. Kind 1984 is a content report. The number is the only
|
||||
thing that distinguishes a follow list from a zap receipt; the rest of the
|
||||
event structure is identical.
|
||||
|
||||
Using integers instead of human-readable names is deliberate. In a protocol
|
||||
built on signed, immutable data, names can never change once chosen. Numbers
|
||||
carry no inherent meaning, which lets every implementer assign their own
|
||||
terminology based on their understanding of a kind's purpose. The protocol
|
||||
says "kind 0"; whether your library calls that `Metadata`, `Profile`, or
|
||||
`UserInfo` is your business.
|
||||
|
||||
This chapter does not catalog individual kinds — a future series of chapters
|
||||
will do that. Instead it builds the infrastructure that those chapters will use:
|
||||
a trait for attaching behavior to a kind number, free functions for
|
||||
classifying raw kind values, and a concrete example to prove the pattern
|
||||
works.
|
||||
|
||||
## The module
|
||||
|
||||
```rust {file=coracle-lib/src/lib.rs}
|
||||
pub mod kinds;
|
||||
```
|
||||
|
||||
```rust {file=coracle-lib/src/kinds.rs}
|
||||
//! Event kind classification and the [`Kind`] trait for attaching
|
||||
//! domain-specific behavior to kind numbers.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use crate::events::{Event, EventContent, EventTemplate};
|
||||
```
|
||||
|
||||
## Classifying a kind number
|
||||
|
||||
The nostr protocol divides the 16-bit kind space into four ranges, each
|
||||
with different storage and replacement semantics:
|
||||
|
||||
| Range | Name | Behavior |
|
||||
|-------|------|----------|
|
||||
| 0–9999 | Regular | Stored by relays, no replacement logic |
|
||||
| 10000–19999 | Replaceable | Relay keeps only the latest event per author and kind |
|
||||
| 20000–29999 | Ephemeral | Relays broadcast but do not store |
|
||||
| 30000–39999 | Addressable | Like replaceable, but partitioned by a `d` tag |
|
||||
|
||||
Two exceptions break the pattern: kinds 0 and 3 sit below 10000 but are
|
||||
replaceable. A user's profile (kind 0) and follow list (kind 3) predate
|
||||
the range system and were grandfathered in.
|
||||
|
||||
These four functions encode the rules so that the rest of the library doesn't
|
||||
have to remember them:
|
||||
|
||||
```rust {file=coracle-lib/src/kinds.rs}
|
||||
/// A regular event: stored by relays with no replacement logic.
|
||||
pub fn is_regular(kind: u16) -> bool {
|
||||
kind != 0 && kind != 3 && kind < 10_000
|
||||
}
|
||||
|
||||
/// A replaceable event: relays keep only the latest per (author, kind).
|
||||
/// Kinds 0 and 3 are replaceable despite falling below 10000.
|
||||
pub fn is_replaceable(kind: u16) -> bool {
|
||||
kind == 0 || kind == 3 || (10_000 <= kind && kind < 20_000)
|
||||
}
|
||||
|
||||
/// An ephemeral event: relays broadcast it but do not store it.
|
||||
pub fn is_ephemeral(kind: u16) -> bool {
|
||||
20_000 <= kind && kind < 30_000
|
||||
}
|
||||
|
||||
/// An addressable event: like replaceable, but the relay partitions by
|
||||
/// the event's `d` tag in addition to author and kind.
|
||||
pub fn is_addressable(kind: u16) -> bool {
|
||||
30_000 <= kind && kind < 40_000
|
||||
}
|
||||
```
|
||||
|
||||
These are free functions rather than methods on a type because plenty of
|
||||
code needs to classify a raw `u16` without having a `Kind` implementor
|
||||
in hand. Relay logic, filter construction, and storage layers all work
|
||||
with kind numbers directly.
|
||||
|
||||
## Errors
|
||||
|
||||
Parsing an event into a kind-specific type can fail for two reasons: the
|
||||
event's kind doesn't match what the parser expects, or the content and
|
||||
tags don't conform to the kind's format.
|
||||
|
||||
```rust {file=coracle-lib/src/kinds.rs}
|
||||
/// Errors that can occur when parsing an event into a kind-specific
|
||||
/// domain type.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum KindError {
|
||||
/// The event's kind field does not match the expected kind number.
|
||||
WrongKind {
|
||||
expected: u16,
|
||||
got: u16,
|
||||
},
|
||||
/// The event's content or tags could not be parsed for this kind.
|
||||
InvalidContent(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for KindError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
KindError::WrongKind { expected, got } => {
|
||||
write!(f, "expected kind {expected}, got {got}")
|
||||
}
|
||||
KindError::InvalidContent(msg) => {
|
||||
write!(f, "invalid content for kind: {msg}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for KindError {}
|
||||
```
|
||||
|
||||
## The `Kind` trait
|
||||
|
||||
A kind is more than a number — it's a contract about what an event's
|
||||
content and tags mean and how to work with them. The `Kind` trait captures
|
||||
that contract. Implement it for a domain type and you get:
|
||||
|
||||
- A kind number constant that ties the type to a specific integer, plus
|
||||
a `kind()` method to access it from an instance.
|
||||
- A `from_event` entry point that checks the kind and delegates to your
|
||||
parser.
|
||||
- A `to_template` method that serializes your type back into an event.
|
||||
|
||||
The set of kinds is open-ended — every NIP can introduce new ones — so
|
||||
a trait is the right abstraction. Each module that handles a specific kind
|
||||
implements the trait for its own type without touching a central enum or
|
||||
registry.
|
||||
|
||||
```rust {file=coracle-lib/src/kinds.rs}
|
||||
/// A domain type that corresponds to a specific event kind.
|
||||
///
|
||||
/// Implementors provide a kind number, a parser that reads an event's
|
||||
/// content and tags into the domain type, and a serializer that
|
||||
/// converts it back into an event template. The trait provides a
|
||||
/// `from_event` entry point that checks the kind number before parsing.
|
||||
pub trait Kind: Sized {
|
||||
/// The kind number this type handles.
|
||||
const KIND: u16;
|
||||
|
||||
/// Parse an event's content and tags into this domain type.
|
||||
///
|
||||
/// This is called by [`from_event`](Kind::from_event) after the kind
|
||||
/// number check passes, so implementations can assume the kind is
|
||||
/// correct.
|
||||
fn parse(event: &Event) -> Result<Self, KindError>;
|
||||
|
||||
/// Convert this domain type into an event's content and tags.
|
||||
fn to_content(&self) -> EventContent;
|
||||
|
||||
/// The kind number for this instance.
|
||||
fn kind(&self) -> u16 {
|
||||
Self::KIND
|
||||
}
|
||||
|
||||
/// Convert this domain type into an event template.
|
||||
///
|
||||
/// The default implementation calls [`to_content`](Kind::to_content)
|
||||
/// and attaches the kind number. Override this only if you need
|
||||
/// non-standard behavior.
|
||||
fn to_template(&self) -> EventTemplate {
|
||||
self.to_content().kind(Self::KIND)
|
||||
}
|
||||
|
||||
/// Parse an event into this domain type, checking the kind number
|
||||
/// first.
|
||||
fn from_event(event: &Event) -> Result<Self, KindError> {
|
||||
if event.kind != Self::KIND {
|
||||
return Err(KindError::WrongKind {
|
||||
expected: Self::KIND,
|
||||
got: event.kind,
|
||||
});
|
||||
}
|
||||
Self::parse(event)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The trait has one associated constant and two required methods.
|
||||
Implementors provide `parse` (event to domain type) and `to_content`
|
||||
(domain type to event content). The trait provides `kind()` for
|
||||
instance access, `to_template()` to attach the kind number, and
|
||||
`from_event()` to check the kind before parsing.
|
||||
|
||||
Classification lives in the free functions from earlier in the chapter.
|
||||
Given any `Kind` implementor, `is_replaceable(deletion.kind())` tells
|
||||
you what you need to know.
|
||||
|
||||
The `to_content` / `to_template` split keeps implementors from having to
|
||||
repeat the kind number. `to_content` returns the content and tags;
|
||||
`to_template` attaches the kind. Since the kind is already known from
|
||||
the constant, there's no reason to ask every implementor to specify it
|
||||
again.
|
||||
|
||||
## A concrete example
|
||||
|
||||
Kind 5 is a deletion request. Its content is an optional human-readable
|
||||
reason, and its tags reference the events or addresses being deleted. It
|
||||
makes a good first implementation of the `Kind` trait: simple enough to
|
||||
fit in a few lines, real enough to exercise every part of the interface.
|
||||
|
||||
```rust
|
||||
use coracle_lib::kinds::{Kind, KindError};
|
||||
use coracle_lib::events::{Event, EventContent};
|
||||
use coracle_lib::tags::Tags;
|
||||
|
||||
/// A deletion request (kind 5). The content is an optional reason
|
||||
/// string, and the tags reference the events or addresses to delete.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Deletion {
|
||||
/// An optional human-readable reason for the deletion.
|
||||
pub reason: String,
|
||||
/// The tags from the original event, which reference the events
|
||||
/// or addresses being deleted.
|
||||
pub tags: Tags,
|
||||
}
|
||||
|
||||
impl Kind for Deletion {
|
||||
const KIND: u16 = 5;
|
||||
|
||||
fn parse(event: &Event) -> Result<Self, KindError> {
|
||||
Ok(Deletion {
|
||||
reason: event.content.clone(),
|
||||
tags: event.tags.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn to_content(&self) -> EventContent {
|
||||
EventContent::new(self.reason.clone(), self.tags.clone())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With this implementation, parsing a deletion event is:
|
||||
|
||||
```rust
|
||||
let deletion = Deletion::from_event(&event)?;
|
||||
```
|
||||
|
||||
And creating an event template from a `Deletion` value is:
|
||||
|
||||
```rust
|
||||
let template = deletion.to_template();
|
||||
assert_eq!(template.kind, 5);
|
||||
```
|
||||
|
||||
Classification uses the free functions with the instance's kind number:
|
||||
|
||||
```rust
|
||||
use coracle_lib::kinds;
|
||||
|
||||
assert!(kinds::is_regular(deletion.kind()));
|
||||
assert!(!kinds::is_replaceable(deletion.kind()));
|
||||
```
|
||||
|
||||
Later chapters will follow this same pattern for profiles, follow lists,
|
||||
reactions, zaps, and everything else. The shape is always the same:
|
||||
define a struct, implement `Kind`, and the trait provides the rest.
|
||||
|
||||
## What's next
|
||||
|
||||
The next chapter looks more closely at kind ranges — the system that
|
||||
ties storage behavior to kind numbers — and what that means for relay
|
||||
queries and event addressing.
|
||||
Reference in New Issue
Block a user