Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Serialization and Macros

The optional integration crates keep the core dependency-free. Add only the boundary your application needs:

[dependencies]
specification-core = "0.1"
specification-core-serde = "0.1"
specification-core-macros = "0.1"

Versioned rule documents

specification-core-serde stores a declarative rule graph. It does not and cannot serialize arbitrary closures, captured state, or trait objects. The schema is versioned independently from crate SemVer and unknown versions are rejected.

The complete JSON round-trip is covered by the serde crate tests and the versioned_document.rs example:

use specification_core_serde::{RuleDocument, RuleNode};

let document = RuleDocument::new(RuleNode::AllOf {
    rules: vec![
        RuleNode::Flag { key: "premium".into() },
        RuleNode::Not {
            rule: Box::new(RuleNode::MaxCount {
                key: "attempts".into(),
                maximum: 3,
            }),
        },
    ],
});

The wire shape is tagged and deterministic:

{"schema_version":1,"rule":{"kind":"flag","key":"premium"}}

RuleDocument is data, not an evaluator. Applications must bind the decoded graph to their own typed context and clock policy.

Predicate macro

#[specification(Name)] turns one free function into a named stateless Specification type:

use specification_core::Specification;
use specification_core_macros::specification;

struct User { age: u8 }

#[specification(Adult)]
fn is_adult(user: &User) -> bool {
    user.age >= 18
}

assert!(Adult.is_satisfied_by(&User { age: 21 }));

The macro validates the immutable &Candidate argument and bool return type. It does not add context lookup, allocation, async behavior, or generated composition. The compiled integration example is tests/usage.rs.