Files
coracle-rust/book/plan/kinds.md
T
2026-04-16 17:37:21 -07:00

176 lines
7.6 KiB
Markdown

# Plan: Kinds
## Topic Summary
The kinds chapter explains what event kinds are — 16-bit integers that determine how an
event's content and tags should be interpreted — without going into detail on any particular
kind. It introduces a `Kind` trait that domain chapters will implement to attach behavior
(validation, parsing, creation) to specific kind numbers. Classification methods like
`is_replaceable()` get default implementations derived from the kind number.
## Chapter Outline
1. **Introduction** — What kinds are and why they're integers not names. Draw on
building-nostr philosophy: numbers are meaningless, which allows subjective naming while
keeping technical meaning intact. Mention that this chapter builds infrastructure; the
domain chapters fill it in.
2. **The module** — Add `pub mod kinds;` to lib.rs, set up the file.
3. **The `Kind` trait** — The core abstraction. One required method: `fn kind_number() -> u16`.
This is an associated function (not `&self`) since kind number is a property of the type,
not an instance. Default methods for classification derive from it.
4. **Classification methods** — Default implementations for `is_regular()`,
`is_replaceable()`, `is_ephemeral()`, `is_addressable()`. Explain the ranges and the
special cases (kinds 0 and 3 are replaceable despite being < 10000). Mention the
building-nostr critique that coupling storage behavior to kind ranges is a design flaw,
but we implement the protocol as-is.
5. **Parsing events** — The `from_event` associated function on the trait. Takes an `&Event`,
checks `event.kind == Self::kind_number()`, then delegates to a required `parse` method.
Returns `Result<Self, KindError>`. This gives domain types a uniform entry point:
`Profile::from_event(&event)`.
6. **Creating events** — A `to_template` method that converts a domain struct back into an
`EventTemplate`. Required, no default — each kind defines its own serialization.
7. **A concrete example** — A trivial `Deletion` kind (kind 5) as a proof-of-concept
implementation, showing how domain chapters will use the trait. Simple enough to not
steal thunder from the domain chapters but real enough to prove the pattern works.
8. **What's next** — Tease kind ranges chapter (deeper dive into the range system and its
implications for relay storage and query planning).
## API Design
```rust
/// Errors when parsing an event into a domain type.
pub enum KindError {
/// The event's kind field doesn't match the expected kind number.
WrongKind { expected: u16, got: u16 },
/// The event's content or tags couldn't be parsed for this kind.
InvalidContent(String),
}
/// The core trait for attaching behavior to a kind number.
pub trait Kind: Sized {
/// The kind number this type handles.
fn kind_number() -> u16;
/// Parse an event's content and tags into this domain type.
/// Called by `from_event` after the kind number check passes.
fn parse(event: &Event) -> Result<Self, KindError>;
/// Convert this domain type back into an event template.
fn to_template(&self) -> EventTemplate;
/// Parse an event into this type, checking the kind number first.
fn from_event(event: &Event) -> Result<Self, KindError> {
if event.kind != Self::kind_number() {
return Err(KindError::WrongKind {
expected: Self::kind_number(),
got: event.kind,
});
}
Self::parse(event)
}
// --- Classification defaults ---
fn is_regular() -> bool {
let k = Self::kind_number();
k != 0 && k != 3 && k < 10_000
}
fn is_replaceable() -> bool {
let k = Self::kind_number();
k == 0 || k == 3 || (10_000 <= k && k < 20_000)
}
fn is_ephemeral() -> bool {
let k = Self::kind_number();
20_000 <= k && k < 30_000
}
fn is_addressable() -> bool {
let k = Self::kind_number();
30_000 <= k && k < 40_000
}
}
```
Also provide free functions for the same classification on raw `u16` values, for code that
doesn't have a `Kind` impl handy (e.g. relay logic, filters):
```rust
pub fn is_regular(kind: u16) -> bool { ... }
pub fn is_replaceable(kind: u16) -> bool { ... }
pub fn is_ephemeral(kind: u16) -> bool { ... }
pub fn is_addressable(kind: u16) -> bool { ... }
```
## Code Organization
- All code in `coracle-lib/src/kinds.rs`
- Module declared in `coracle-lib/src/lib.rs`
- Depends on `events` module (uses `Event`, `EventTemplate`, `EventContent`)
- No dependency on any domain crate
## Dependencies
No new external crates needed. Only uses std and types from earlier chapters.
## Narrative Notes
- Lead with the philosophy of why kinds are numbers. The building-nostr quote about
meaningless integers allowing subjective naming is compelling.
- Explain the trait design choice: why a trait instead of an enum. The set of kinds is
open-ended and grows with every NIP. An enum would require updating a central file for
every new kind. A trait lets domain chapters add kinds independently.
- The classification defaults are a nice teaching moment: "implement one method, get four
for free" demonstrates Rust's default method pattern.
- The `from_event`/`parse` split mirrors applesauce's factory/cast pattern but in Rust
idiom: `from_event` is the public API that does the kind check, `parse` is the
kind-specific logic implementors provide.
- Keep the Deletion example minimal — just enough to show the trait in action.
- Note the building-nostr critique of kind ranges coupling content type with storage
behavior, but don't dwell on it. We implement the protocol.
## Design Decisions
1. **Trait, not enum** — The set of kinds is open and grows with each NIP. An enum would
centralize all kind definitions. A trait lets each domain module add its own kinds
independently. This follows the distributed pattern seen in applesauce (no centralized
registry) and is idiomatic Rust.
2. **`kind_number()` as associated function, not const** — An associated const
(`const KIND: u16`) would also work but associated functions compose better with
default methods. Either would be fine; the chapter can use whichever reads better.
Actually, a const may be cleaner since it can't vary per call. Decide during writing.
3. **Events keep `u16`** — Per user preference. The `Kind` trait lives alongside events,
not inside them. Events are protocol-level; kinds are application-level.
4. **Free functions mirror trait methods** — Not all code has a `Kind` impl. Relay logic,
filter building, and storage layers need classification from a raw `u16`. The free
functions are the source of truth; the trait defaults call them.
5. **`from_event` with kind check as default** — Implementors only write `parse`, which
can assume the kind is correct. The trait provides the boilerplate.
6. **Deletion as example** — Kind 5 is simple (content is a reason string, tags are
references to deleted events) and universally understood. It won't conflict with
domain chapter content since deletion is a protocol-level concept.
## Open Questions
1. **Associated const vs function** — Should it be `const KIND: u16 = 5;` or
`fn kind_number() -> u16 { 5 }`? Const is more idiomatic for a fixed value.
Try const during writing and see if defaults can reference it cleanly.
2. **Should `to_template` return `EventTemplate` or `EventContent`?** — EventTemplate
includes the kind, which the trait already knows. Returning EventContent and having
a default `to_template` that adds the kind could reduce boilerplate. Explore during
writing.