Use traits to keep event methods dry

This commit is contained in:
Jon Staab
2026-05-20 14:27:26 -07:00
parent 8ad0bc393b
commit d0709e1811
7 changed files with 138 additions and 61 deletions
+80
View File
@@ -525,6 +525,86 @@ Deserialization does not verify the signature. Parsing is cheap; `verify()`
is not, and there are many places where you want to read an event off disk
without paying for a signature check.
## A shared surface across event stages
There is a lot of overlap between various event structs. A few minimal accessor
traits capture read access to various fields so that we can define extensions
to events without duplication.
```rust {file=coracle-lib/src/events.rs}
pub trait HasTags {
fn tags(&self) -> &Tags;
}
pub trait HasId {
fn id(&self) -> &[u8; 32];
}
impl HasTags for HashedEvent {
fn tags(&self) -> &Tags {
&self.tags
}
}
impl HasTags for Event {
fn tags(&self) -> &Tags {
&self.tags
}
}
impl HasId for HashedEvent {
fn id(&self) -> &[u8; 32] {
&self.id
}
}
impl HasId for Event {
fn id(&self) -> &[u8; 32] {
&self.id
}
}
```
Subsequent chapters will add *extension traits* bounded on these accessors,
supply the method bodies as defaults, and close with a blanket impl over every
type that satisfies the bound. The logic is written once and reaches both event
types — and any event type added later — without another line per struct.
The expiration chapter's version reads like this:
```rust
pub trait EventExtensionExpiration: HasTags {
fn is_expired(&self) -> bool {
is_expired(self.tags())
}
// ...the rest of the expiration queries
}
impl<T: HasTags> EventExtensionExpiration for T {}
```
A trait's methods are only callable where the trait is in scope, so the
library gathers these extension traits in a `prelude`. One glob import brings
the whole behavior-query surface along:
```rust
use coracle_lib::prelude::*;
```
```rust {file=coracle-lib/src/lib.rs}
pub mod prelude;
```
```rust {file=coracle-lib/src/prelude.rs}
//! One-stop import for the extension traits that add behavior-tag queries to
//! events. With `coracle_lib::prelude::*` in scope, methods like
//! `is_expired`, `is_protected`, and `get_pow` become callable on
//! `HashedEvent` and `Event`.
pub use crate::events::{HasId, HasTags};
```
Each later chapter that defines an extension trait adds it to this prelude.
## What's next
The next chapter introduces kinds — the integer that decides how content
+17 -17
View File
@@ -44,7 +44,7 @@ pub mod pow;
use sha2::{Digest, Sha256};
use crate::events::{Event, HashedEvent, OwnedEvent};
use crate::events::{HasId, HasTags, HashedEvent, OwnedEvent};
use crate::tags::{Tag, Tags};
```
@@ -119,31 +119,31 @@ commitment was made.
### Methods on event types
Both `HashedEvent` and `Event` carry an id and tags, so both get a
`get_pow` method that delegates to the free function. This lets callers
write `event.get_pow() >= 20` regardless of whether the event has been
signed yet.
Proof of work is the first query that needs the event id as well as its
tags, so its extension trait is bounded on both `HasId` and `HasTags` from
the events chapter. The method body delegates to the free function through
those accessors, and the blanket impl puts `get_pow` on every type that
satisfies the bound — both `HashedEvent` and `Event` do. Callers write
`event.get_pow() >= 20` regardless of whether the event has been signed yet,
as long as the trait is in scope (via `coracle_lib::prelude::*`).
```rust {file=coracle-lib/src/pow.rs}
impl HashedEvent {
/// Proof-of-work queries on any event that has an id and tags.
pub trait EventExtensionProofOfWork: HasId + HasTags {
/// Return the validated proof-of-work difficulty of this event.
///
/// The result is the minimum of the actual leading zero bits in the
/// event id and the difficulty claimed in the nonce tag.
pub fn get_pow(&self) -> u8 {
get_pow(&self.id, &self.tags)
fn get_pow(&self) -> u8 {
get_pow(self.id(), self.tags())
}
}
impl Event {
/// Return the validated proof-of-work difficulty of this event.
///
/// The result is the minimum of the actual leading zero bits in the
/// event id and the difficulty claimed in the nonce tag.
pub fn get_pow(&self) -> u8 {
get_pow(&self.id, &self.tags)
}
}
impl<T: HasId + HasTags> EventExtensionProofOfWork for T {}
```
```rust {file=coracle-lib/src/prelude.rs}
pub use crate::pow::EventExtensionProofOfWork;
```
## Mining
+24 -32
View File
@@ -33,13 +33,13 @@ pub mod expiration;
//! NIP-40 expiring events: reading the `expiration` tag and checking
//! whether an event has expired.
//!
//! The free functions in this module operate on [`Tags`] so they can
//! be used wherever a tag slice is in hand. Method versions on
//! [`HashedEvent`] and [`Event`] delegate to them for a readable call
//! site. The tag itself is built with [`Tag::expiration`] from the
//! tags module.
//! The free functions in this module operate on [`Tags`] so they can be
//! used wherever a tag slice is in hand. The [`EventExtensionExpiration`]
//! trait layers method-call sugar on top for any event type, reachable
//! through the crate prelude. The tag itself is built with
//! [`Tag::expiration`] from the tags module.
use crate::events::{Event, HashedEvent};
use crate::events::HasTags;
use crate::tags::Tags;
use crate::util::now;
```
@@ -128,45 +128,37 @@ impl Tag {
## Methods on event types
Both `HashedEvent` and `Event` carry a `Tags` field, and callers who
have an event in hand almost always want to ask questions of its tags
without a separate import. The method impls delegate to the free
functions verbatim.
Rather than repeat these queries on each event struct, expiration exposes
them through an extension trait bounded on [`HasTags`]. The method bodies
live once, as defaults, and a blanket impl makes them available on every
type that can hand back its tags — `HashedEvent`, `Event`, and anything
added later. With `coracle_lib::prelude::*` in scope, `event.is_expired()`
reads the same whether the event is hashed or signed.
```rust {file=coracle-lib/src/expiration.rs}
impl HashedEvent {
/// Expiration queries on any event that carries tags.
pub trait EventExtensionExpiration: HasTags {
/// Return the expiration timestamp of this event, if any.
pub fn get_expiration(&self) -> Option<u64> {
get_expiration(&self.tags)
fn get_expiration(&self) -> Option<u64> {
get_expiration(self.tags())
}
/// Whether this event has expired relative to the given timestamp.
pub fn is_expired_at(&self, now: u64) -> bool {
is_expired_at(&self.tags, now)
fn is_expired_at(&self, now: u64) -> bool {
is_expired_at(self.tags(), now)
}
/// Whether this event has expired relative to the system clock.
pub fn is_expired(&self) -> bool {
is_expired(&self.tags)
fn is_expired(&self) -> bool {
is_expired(self.tags())
}
}
impl Event {
/// Return the expiration timestamp of this event, if any.
pub fn get_expiration(&self) -> Option<u64> {
get_expiration(&self.tags)
}
impl<T: HasTags> EventExtensionExpiration for T {}
```
/// Whether this event has expired relative to the given timestamp.
pub fn is_expired_at(&self, now: u64) -> bool {
is_expired_at(&self.tags, now)
}
/// Whether this event has expired relative to the system clock.
pub fn is_expired(&self) -> bool {
is_expired(&self.tags)
}
}
```rust {file=coracle-lib/src/prelude.rs}
pub use crate::expiration::EventExtensionExpiration;
```
## Usage patterns
+14 -12
View File
@@ -48,7 +48,7 @@ pub mod protected;
//! challenge followed by a pubkey comparison) needs a live, authenticated
//! connection and is handled in the relay layer.
use crate::events::{Event, HashedEvent};
use crate::events::HasTags;
use crate::tags::Tags;
```
@@ -91,23 +91,25 @@ impl Tag {
## Methods on event types
A caller holding an event wants to ask whether it is protected without
reaching for its tags directly. Both `HashedEvent` and `Event` carry a
`Tags` field, so the method facades delegate to the free function.
reaching for its tags directly. Following the pattern from expiration, the
query is an extension trait bounded on [`HasTags`], so one default method
serves every event type. With `coracle_lib::prelude::*` in scope,
`event.is_protected()` works on both `HashedEvent` and `Event`.
```rust {file=coracle-lib/src/protected.rs}
impl HashedEvent {
/// Protected-event queries on any event that carries tags.
pub trait EventExtensionProtected: HasTags {
/// Whether this event carries the NIP-70 protected marker.
pub fn is_protected(&self) -> bool {
is_protected(&self.tags)
fn is_protected(&self) -> bool {
is_protected(self.tags())
}
}
impl Event {
/// Whether this event carries the NIP-70 protected marker.
pub fn is_protected(&self) -> bool {
is_protected(&self.tags)
}
}
impl<T: HasTags> EventExtensionProtected for T {}
```
```rust {file=coracle-lib/src/prelude.rs}
pub use crate::protected::EventExtensionProtected;
```
## Usage patterns
+1
View File
@@ -1,5 +1,6 @@
use coracle_lib::events::{EventContent, HashedEvent};
use coracle_lib::expiration::{get_expiration, is_expired, is_expired_at};
use coracle_lib::prelude::*;
use coracle_lib::keys::SecretKey;
use coracle_lib::tags::{Tag, Tags};
+1
View File
@@ -1,5 +1,6 @@
use coracle_lib::events::{EventContent, OwnedEvent};
use coracle_lib::keys::SecretKey;
use coracle_lib::prelude::*;
use coracle_lib::pow::{get_leading_zero_bits, get_pow, mine_pow, mine_pow_batch};
use coracle_lib::tags::{Tag, Tags};
+1
View File
@@ -1,5 +1,6 @@
use coracle_lib::events::{EventContent, HashedEvent};
use coracle_lib::keys::SecretKey;
use coracle_lib::prelude::*;
use coracle_lib::protected::is_protected;
use coracle_lib::tags::{Tag, Tags};