reliakit-derive 0.1.2

Derive macros for reliakit traits, implemented with only the standard library proc-macro API and no third-party dependencies.
Documentation

reliakit-derive

Crates.io Crates.io Downloads Docs.rs CI codecov License: MIT

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-named reliakit-codec (binary) traits,
  • #[derive(JsonEncode)] and #[derive(JsonDecode)] for the same-named reliakit-json traits,
  • 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:

[dependencies]
reliakit-codec = "0.2"
reliakit-derive = "0.1"
use reliakit_codec::{decode_from_slice_exact, encode_to_vec};
use reliakit_derive::{CanonicalDecode, CanonicalEncode};

#[derive(Debug, PartialEq, CanonicalEncode, CanonicalDecode)]
struct Point {
    x: u16,
    y: u16,
}

let encoded = encode_to_vec(&Point { x: 10, y: 20 }).unwrap();
assert_eq!(encoded, [10, 0, 20, 0]);
assert_eq!(decode_from_slice_exact::<Point>(&encoded).unwrap(), Point { x: 10, y: 20 });

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:

#[derive(CanonicalEncode, CanonicalDecode)]
enum Message {
    Ping, // tag 0u32 => 00 00 00 00
    Pong, // tag 1u32 => 01 00 00 00
}

Tuple variants:

#[derive(CanonicalEncode, CanonicalDecode)]
enum Command {
    SetPort(u16),   // tag 0, then the u16
    SetName(String), // tag 1, then the string
}

Struct variants:

#[derive(CanonicalEncode, CanonicalDecode)]
enum Event {
    UserCreated { id: u64, name: String }, // tag 0, then id, then name
    UserDeleted { id: u64 },               // tag 1, then id
}

A complete runnable example — all three variant kinds plus a nested derived struct inside a struct variant — is in examples/protocol.rs:

cargo run -p reliakit-derive --example protocol

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 = N discriminant would silently change the encoding and make the format depend on values the derive does not read.
  • #[repr(...)] enums. repr controls the in-memory discriminant type, not this crate's wire format (always a u32 tag), so accepting it would be misleading.
  • Generic enums, where clauses, lifetimes, and const generics. The derive reads only names and shapes, not bounds, so it cannot generate correct where clauses; 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