Add kinds chapter

This commit is contained in:
Jon Staab
2026-04-16 17:37:21 -07:00
parent e42d7efd6e
commit c7ec9f18fa
4 changed files with 780 additions and 0 deletions
+272
View File
@@ -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 |
|-------|------|----------|
| 09999 | Regular | Stored by relays, no replacement logic |
| 1000019999 | Replaceable | Relay keeps only the latest event per author and kind |
| 2000029999 | Ephemeral | Relays broadcast but do not store |
| 3000039999 | 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.
+175
View File
@@ -0,0 +1,175 @@
# 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.
+256
View File
@@ -0,0 +1,256 @@
# Research: Kinds
## Topic Summary
The kinds chapter should explain what kinds are without going into detail on any kind in
particular (that's saved for domain chapters). Instead, the chapter should create some kind
of factory/registry/enum that can be used to add behavior to a given kind — validation,
event template factories, interfaces for decryption, methods for converting an event of a
particular kind into a domain-specific struct, etc. Applesauce's factory pattern is preferred
over welshman's ad-hoc utilities approach.
## Philosophy
From building-nostr, several key principles inform this chapter:
**Numbers Over Names:** Event kinds are 16-bit integers. Using numbers instead of names is
deliberate — in a distributed system built on signed data, names can't change. Numbers are
meaningless, which allows subjective interpretations while keeping meaning intact. This frees
implementers to assign their own terms based on understanding.
**Kind Ranges Encode Behavior:** The protocol defines ranges with storage/replacement semantics:
- Regular (1-9999, with exceptions): standard events stored by relays
- Replaceable (0, 3, 10000-19999): only latest per (pubkey, kind) kept
- Ephemeral (20000-29999): not stored, just broadcast
- Addressable (30000-39999): latest per (pubkey, kind, d-tag) kept
The author criticizes this design as coupling kind numbers with behavior — these policies are
orthogonal to content type and "should have belonged in behavior tags."
**More Kinds, Less Ambiguity:** Prefer creating specific event kinds over overloading existing
ones. "Overloading a single kind for multiple purposes is always a recipe for disaster."
**Implementation-First:** Nostr takes an implementation-first approach. It's only through
implementation that what is actually needed can be understood.
## Reference Implementation Analysis
### applesauce
The most sophisticated factory pattern found. Uses a layered architecture:
**EventFactory base class** extends `Promise<T>` for fluent chaining. Core capabilities:
- `EventFactory.fromKind<K>(kind)` — create from kind number
- `EventFactory.fromEvent<K>(event)` — create from existing event (modify)
- `.content()`, `.created()`, `.meta()` — base operations
- `.modifyPublicTags()`, `.modifyHiddenTags()` — tag manipulation
- `.encryptedContent()` — NIP-44 encryption
- `.as(signer)` — set signer, propagated through chain
- `.stamp()`, `.sign()` — finalize event
**Typed Factory subclasses** per kind:
```typescript
export class NoteFactory extends EventFactory<kinds.ShortTextNote, NoteTemplate> {
static create(content?: string): NoteFactory
static reply(parent: NostrEvent, content?: string): NoteFactory
text(content, options?)
mention(pubkey)
subject(subject)
addHashtag(hashtag)
}
```
**Dual system for create vs consume:**
- **Factories** — for creating/modifying events (builder pattern)
- **Casts** (`EventCast<KnownEvent<KIND>>`) — for consuming events, providing typed
properties, validation, and observable streams for relationships
**List factory hierarchy** for NIP-51:
- `ListFactory``NIP51RelayListFactory` / `NIP51UserListFactory` / `NIP51ItemListFactory`
- Concrete: `BookmarkSetFactory extends NIP51ItemListFactory`
**Validation** via type guard functions:
```typescript
function isValidComment(event): event is CommentEvent {
return event.kind === COMMENT_KIND && hasRoot(event) && hasReply(event);
}
```
**No centralized registry** — factories distributed across modules, aggregated via index.ts exports.
### ndk
Uses class-based kind wrappers with inheritance:
**NDKKind enum** with 200+ named variants. Each kind gets a dedicated wrapper class extending
`NDKEvent`:
```typescript
export class NDKArticle extends NDKEvent {
static kind = NDKKind.Article;
static kinds = [NDKKind.Article]; // supports multi-kind classes
static from(event: NDKEvent): NDKArticle
get title(): string | undefined
set title(title: string | undefined)
}
```
**Multi-kind classes:** `NDKVideo` handles kinds 21, 34235, 34236, 22. `NDKRepost` handles 6
and 16.
**40+ kind wrapper files** in `core/src/events/kinds/`.
Kind-aware behavior methods on base `NDKEvent`: `isReplaceable()`, `isEphemeral()`,
`isParamReplaceable()`.
### nostr-gadgets
Functional/storage-oriented approach:
**Higher-order factory functions:**
```typescript
export const loadFollowsList = makeListFetcher<string>(3, RELAYS, itemsFromTags(...))
export const loadFollowSets = makeSetFetcher<string>(30000, itemsFromTags(...))
```
**Generic fetcher factories** take kind number + processor function, return typed fetchers.
Storage interface returns different shapes based on kind classification (addressable vs
replaceable).
### nostrlib
Go library with multi-layered kind awareness:
**`Kind` type** is `uint16` with categorization methods: `IsRegular()`, `IsReplaceable()`,
`IsEphemeral()`, `IsAddressable()`.
**Factory map pattern** for domain objects:
```go
var moderationActionFactories = map[nostr.Kind]func(nostr.Event) (Action, error){
nostr.KindSimpleGroupPutUser: func(evt nostr.Event) (Action, error) { ... },
}
func PrepareModerationAction(evt nostr.Event) (Action, error) {
factory, ok := moderationActionFactories[evt.Kind]
if !ok { return nil, fmt.Errorf("unsupported kind %d", evt.Kind) }
return factory(evt)
}
```
**Schema-based validation** from YAML specs per kind (content format, required tags, d-tag rules).
**Generic list/set patterns** with `fetchGenericList[V, I](kind, index, parseTag, cache)`.
**Dataloader registries** — index-based arrays for 18 replaceable and 4 addressable kinds.
### nostr-tools
Foundational kind system:
**Const + typeof pattern:**
```typescript
export const ShortTextNote = 1;
export type ShortTextNote = typeof ShortTextNote;
```
**Classification functions:** `classifyKind()`, `isRegularKind()`, `isReplaceableKind()`,
`isEphemeralKind()`, `isAddressableKind()`.
**Type guard:** `isKind<T>(event, kind): event is NostrEvent & { kind: T }`.
65+ kind constants. No factory pattern — this is the lowest-level library.
### rust-nostr
Most comprehensive Rust implementation:
**Macro-generated `Kind` enum** with 130+ named variants plus `Custom(u16)` fallback:
```rust
pub enum Kind { Metadata, TextNote, ContactList, ..., Custom(u16) }
```
**Kind range constants:**
```rust
pub const REPLACEABLE_RANGE: Range<u16> = 10_000..20_000;
pub const EPHEMERAL_RANGE: Range<u16> = 20_000..30_000;
pub const ADDRESSABLE_RANGE: Range<u16> = 30_000..40_000;
```
**Behavior methods:** `is_regular()`, `is_replaceable()`, `is_ephemeral()`, `is_addressable()`.
**EventBuilder** with 140+ kind-specific factory methods:
```rust
pub fn text_note(content) -> Self
pub fn metadata(metadata) -> Self
pub fn job_request(kind) -> Result<Self, Error> // validates kind range
```
**Coordinate validation** enforces kind constraints (replaceable vs addressable, d-tag rules).
**No central registry** — behavior statically encoded in kind ranges and builder methods.
### welshman
Ad-hoc utilities approach:
**Kind constants** in centralized `Kinds.ts` (60+ constants).
**Classification predicates:** `isRegularKind()`, `isPlainReplaceableKind()`,
`isParameterizedReplaceableKind()`, `isEphemeralKind()`, `isDVMKind()`.
**Domain types per kind** with read/create/edit functions:
- `readProfile(event) -> PublishedProfile`
- `createProfile(profile) -> EventTemplate`
- `editProfile(profile) -> EventTemplate`
- `readRoomMeta(event) -> PublishedRoomMeta` (validates kind in reader)
- `readList(event) -> PublishedList` (generic across list kinds)
**Store integration:** `deriveItemsByKey({ eventToItem: readProfile, filters: [{kinds: [PROFILE]}] })`
**Encryptable wrapper** for events with encrypted content (lists with private tags).
Pattern: each kind gets constants + types + read/create/edit functions + store wiring. Scattered
across files rather than unified in a registry.
## Common Patterns
1. **Kind as integer wrapper** — all implementations use u16/number with named constants
2. **Range-based classification** — universal: regular/replaceable/ephemeral/addressable predicates
3. **Factory methods for creation** — builder/factory pattern to construct events of specific kinds
4. **Parser functions for consumption** — event → domain struct conversion
5. **Validation at boundaries** — kind checks when parsing events, kind range validation for coordinates
6. **No centralized registry** — most implementations distribute behavior across modules rather
than maintaining a central kind→behavior map
**Divergences:**
- applesauce uses class hierarchy with fluent builder; welshman uses standalone functions
- rust-nostr uses macro-generated enum; Go uses typed constants
- NDK couples kind behavior to event subclasses; others keep them separate
- Only nostrlib uses external schema-based validation
## Considerations for Our Implementation
1. **Trait-based factory pattern** — Rust traits map well to the factory/cast dual pattern from
applesauce. A `KindHandler` trait could define validation, parsing, and creation methods.
2. **Kind enum with Custom fallback** — Follow rust-nostr's `Kind` enum pattern with named
variants + `Custom(u16)`. This gives compile-time safety for known kinds while remaining
extensible.
3. **Range classification as methods**`Kind::is_replaceable()` etc. are universally needed
and straightforward to implement.
4. **Separate create vs parse** — The applesauce pattern of factories (create) vs casts (parse)
maps to Rust traits: one trait for building `EventTemplate` from domain data, another for
extracting domain data from `Event`.
5. **Registry for extensibility** — A `HashMap<Kind, Box<dyn KindHandler>>` or similar registry
would let domain chapters register their handlers without modifying the kinds chapter.
Alternatively, use static dispatch with an enum if the set of kinds is known at compile time.
6. **Literate programming flow** — The chapter should introduce kinds conceptually, define the
`Kind` type, implement classification methods, then build the trait/registry infrastructure
that later chapters will use. Keep actual kind implementations (profiles, lists, etc.) for
domain chapters.
7. **Dependencies** — This chapter depends on the events chapter (for `Event`, `EventTemplate`
types). It should not depend on any domain-specific types.
+77
View File
@@ -0,0 +1,77 @@
use coracle_lib::kinds::{is_addressable, is_ephemeral, is_regular, is_replaceable};
use coracle_lib::kinds::KindError;
// --- Free function classification tests ---
#[test]
fn regular_kind_1() {
assert!(is_regular(1));
}
#[test]
fn regular_kind_9999() {
assert!(is_regular(9999));
}
#[test]
fn kind_0_is_replaceable_not_regular() {
assert!(!is_regular(0));
assert!(is_replaceable(0));
}
#[test]
fn kind_3_is_replaceable_not_regular() {
assert!(!is_regular(3));
assert!(is_replaceable(3));
}
#[test]
fn replaceable_range() {
assert!(is_replaceable(10_000));
assert!(is_replaceable(19_999));
assert!(!is_replaceable(20_000));
}
#[test]
fn ephemeral_range() {
assert!(is_ephemeral(20_000));
assert!(is_ephemeral(29_999));
assert!(!is_ephemeral(30_000));
}
#[test]
fn addressable_range() {
assert!(is_addressable(30_000));
assert!(is_addressable(39_999));
assert!(!is_addressable(40_000));
}
#[test]
fn ranges_are_mutually_exclusive_for_regular() {
for k in [1, 100, 5000, 9999] {
assert!(is_regular(k));
assert!(!is_replaceable(k));
assert!(!is_ephemeral(k));
assert!(!is_addressable(k));
}
}
#[test]
fn high_kinds_are_none_of_the_above() {
assert!(!is_regular(40_000));
assert!(!is_replaceable(40_000));
assert!(!is_ephemeral(40_000));
assert!(!is_addressable(40_000));
}
#[test]
fn kind_error_display() {
let err = KindError::WrongKind {
expected: 5,
got: 1,
};
assert_eq!(err.to_string(), "expected kind 5, got 1");
let err = KindError::InvalidContent("bad json".into());
assert_eq!(err.to_string(), "invalid content for kind: bad json");
}