reliakit-derive
Derive macros for reliakit traits, implemented with only the standard library
proc-macro API and no third-party dependencies.
reliakit-derive generates the same trait implementations a handwritten one
would: it reads only the type name and field shape it needs, emits one
encode/decode call per field in declaration order, and rejects anything
outside its supported subset with a clear compile error rather than guessing.
Introduction
Some reliakit traits are deliberately implemented by hand so field order and
validation stay visible in normal Rust. For plain data structs where the
implementation is purely mechanical — one call per field, in order — writing it
out by hand is repetitive without adding clarity. This crate provides an opt-in
derive for exactly those cases.
It does not parse the full Rust grammar. It reads the type name and its field or variant shape, which is all the generated code needs, and stops with a descriptive error on constructs it does not handle.
What This Crate Does
This crate provides:
#[derive(CanonicalEncode)]and#[derive(CanonicalDecode)]for the same-namedreliakit-codec(binary) traits,#[derive(JsonEncode)]and#[derive(JsonDecode)]for the same-namedreliakit-jsontraits,- generated implementations identical to a handwritten one — each field encoded and decoded in declaration order,
- clear compile errors for unsupported inputs.
One #[derive(...)] line can target both formats at once, so the same struct
round-trips through canonical binary and through JSON.
Supported Types
Structs:
- structs with named fields
- tuple structs
- unit structs
Enums:
- unit variants
- tuple variants
- struct variants
The enum or struct itself may be public or private. Outer attributes (such as
#[doc = "..."] or #[allow(...)]) on the type, its fields, or its variants are
ignored.
Unions, generic types (including generic enums and where clauses), enums with
explicit discriminants or a #[repr(...)], and empty enums are rejected with a
compile error that names exactly what is unsupported.
The JsonEncode/JsonDecode derives currently cover structs only (named fields
become a JSON object in declaration order, a tuple struct an array, a unit
struct null); enums are rejected for now. The CanonicalEncode/
CanonicalDecode derives cover both structs and enums as listed above.
When To Use
Use this derive when a type's encoding is the mechanical field-by-field default and a handwritten implementation would add nothing:
- plain data structs at protocol or storage boundaries,
- fixtures and cache-key types,
- aggregates of already-
Canonical*fields.
When Not To Use
Write the implementation by hand when the encoding is not a straight
field-by-field pass: custom field order, enum tag schemes, versioning,
validation on decode, or skipped/derived fields. reliakit-codec is designed
for those to be explicit, and this derive does not try to express them.
Installation
The generated code refers to reliakit-codec, so the two crates are used
together:
[]
= "0.2"
= "0.1"
use ;
use ;
let encoded = encode_to_vec.unwrap;
assert_eq!;
assert_eq!;
The bytes are exactly what the handwritten implementation in the reliakit-codec
documentation produces.
Enums
An enum value is encoded as a variant tag followed by that variant's fields:
- the tag is the variant's zero-based declaration index, encoded as a
little-endian
u32; - a unit variant encodes only the tag;
- a tuple variant encodes the tag, then its fields in declaration order;
- a struct variant encodes the tag, then its named fields in declaration order.
Decoding reads the u32 tag first, then decodes the matching variant's payload.
An unknown tag is an InvalidValue codec error; a field decode error propagates
unchanged. Trailing bytes are the caller's concern — use
decode_from_slice_exact to reject them — not the generated impl's.
Tags follow declaration order, so reordering variants is a wire-format change. Adding new variants at the end is backward compatible for decoding older payloads.
Unit variants:
Tuple variants:
Struct variants:
A complete runnable example — all three variant kinds plus a nested derived
struct inside a struct variant — is in examples/protocol.rs:
Unsupported enum forms
These are rejected with a compile error rather than encoded with a guessed meaning:
- Explicit discriminants (
A = 1). The wire tag is always the declaration index; honoring a= Ndiscriminant would silently change the encoding and make the format depend on values the derive does not read. #[repr(...)]enums.reprcontrols the in-memory discriminant type, not this crate's wire format (always au32tag), so accepting it would be misleading.- Generic enums,
whereclauses, lifetimes, and const generics. The derive reads only names and shapes, not bounds, so it cannot generate correctwhereclauses; these are rejected rather than half-supported. - Empty enums (
enum Never {}). There is no variant to encode or decode.
A tuple field whose type contains a top-level comma inside angle brackets (for
example Result<A, B>) is also not supported in tuple structs or tuple
variants, because the field counter splits on those commas; use a type alias.
How It Works
The derive reads the derive input as a token stream and extracts two things: the type name and the field shape (named field identifiers, tuple field count, or unit) — for enums, that shape is read per variant along with its declaration index. For decoding it calls the trait through its fully qualified path, so it never needs to parse or reproduce field types — only their names or positions. The generated implementation is then built as source text and parsed back into tokens. This keeps the crate small enough to need no parsing library.
Feature Flags
This crate has no feature flags.
Safety
This crate uses #![forbid(unsafe_code)].
It runs only at compile time and performs no I/O. On unsupported input it emits a
compile_error! with a specific message instead of generating questionable code.
MSRV
The minimum supported Rust version is Rust 1.85.
License
MIT