7.6 KiB
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
-
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.
-
The module — Add
pub mod kinds;to lib.rs, set up the file. -
The
Kindtrait — 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. -
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. -
Parsing events — The
from_eventassociated function on the trait. Takes an&Event, checksevent.kind == Self::kind_number(), then delegates to a requiredparsemethod. ReturnsResult<Self, KindError>. This gives domain types a uniform entry point:Profile::from_event(&event). -
Creating events — A
to_templatemethod that converts a domain struct back into anEventTemplate. Required, no default — each kind defines its own serialization. -
A concrete example — A trivial
Deletionkind (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. -
What's next — Tease kind ranges chapter (deeper dive into the range system and its implications for relay storage and query planning).
API Design
/// 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):
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
eventsmodule (usesEvent,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/parsesplit mirrors applesauce's factory/cast pattern but in Rust idiom:from_eventis the public API that does the kind check,parseis 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
-
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.
-
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. -
Events keep
u16— Per user preference. TheKindtrait lives alongside events, not inside them. Events are protocol-level; kinds are application-level. -
Free functions mirror trait methods — Not all code has a
Kindimpl. Relay logic, filter building, and storage layers need classification from a rawu16. The free functions are the source of truth; the trait defaults call them. -
from_eventwith kind check as default — Implementors only writeparse, which can assume the kind is correct. The trait provides the boilerplate. -
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
-
Associated const vs function — Should it be
const KIND: u16 = 5;orfn kind_number() -> u16 { 5 }? Const is more idiomatic for a fixed value. Try const during writing and see if defaults can reference it cleanly. -
Should
to_templatereturnEventTemplateorEventContent? — EventTemplate includes the kind, which the trait already knows. Returning EventContent and having a defaultto_templatethat adds the kind could reduce boilerplate. Explore during writing.