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

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

  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

/// 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 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.