176 lines
7.6 KiB
Markdown
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.
|