Holy
Holy
A proc-macro library that ships derive macros for everyday struct chores: getters, setters, observers, fuzz constructors, and input sanitization. Pulling it as a path / crates.io dep keeps each consumer free of hand-rolled boilerplate and centralizes policy changes (new sanitize rules, observer hooks, etc.) in one crate.
Available derives
Getters— auto-generatesget_<field>()accessors.Setters— auto-generatesset_<field>()mutators.Observer— emits change events on field updates.Fuzz— generatesrandom()constructors for tests.Sanitize— generates.sanitize()+ per-field.sanitize_<field>()methods driven by#[holy(sanitize = "rule1, rule2(arg)")]attributes.
Getters & Setters
#[derive(holy::Getters, holy::Setters)]pub struct User { pub name: String, pub age: u32,}
let mut user = User { name: "test".into(), age: 25 };let name: &String = user.name(); // getteruser.set_age(26); // setterSupports generic structs:
#[derive(holy::Getters, holy::Setters)]pub struct Container<T> { pub value: T,}Attributes
Control visibility and behavior per-field with #[holy(...)]:
#[holy(public)]— make the generated getter/setterpubregardless of field visibility#[holy(private)]— make the generated getter/setter private regardless of field visibility#[holy(skip)]— skip generating getter/setter for this field#[holy(observe)]— mark field for observer pattern (used withObserverderive)
Observer
Derive Observer to generate a companion struct for the observer pattern:
#[derive(holy::Observer)]pub struct Sensor { #[holy(observe)] pub temperature: f64, pub name: String,}
// Generates `SensorObservers` companion structlet mut observers = SensorObservers::new();observers.add_temperature_observer(|s: &Sensor| { println!("temp: {}", s.temperature);});observers.notify_temperature_observers(&sensor);Sanitize
Derive Sanitize to generate .sanitize() plus per-field
.sanitize_<field>() methods. Each rule is declared inline on the
field via #[holy(sanitize = "...")] and runs in the order written.
| Rule | Field type | Effect |
|---|---|---|
trim | String | .trim().to_string() |
lowercase | String | .to_lowercase() |
uppercase | String | .to_uppercase() |
alphanumeric | String | retain only char::is_alphanumeric() |
escape_html | String | replace & < > " ' with HTML entities |
nul_strip | String | drop every \0 byte (0.2.1) |
control_strip | String | drop ASCII/Unicode control chars + bidi overrides (U+202A..U+202E, U+2066..U+2069) + zero-width chars (U+200B..U+200D, U+FEFF) (0.2.1) |
slug | String | lowercase + ASCII alphanumerics + collapse separator runs into single - + trim leading/trailing - (0.2.1) |
truncate(N) | String | UTF-8-safe byte truncate to N; walks back to nearest char boundary so multi-byte codepoints never panic |
clamp(min, max) | numeric | .clamp(min, max) |
Use control_strip only on inline text fields (titles, signatures,
slugs). It removes \n and \t so it is not appropriate for
markdown bodies — those should be rendered through a markdown sanitizer
on the read path instead.
Option<String> fields work the same way: rules run only when the
field is Some(_) and None passes through. Useful for partial-update
DTOs (e.g. pub bio: Option<String>).
#[derive(holy::Sanitize, serde::Deserialize)]pub struct CreateThreadBody { #[holy(sanitize = "trim, control_strip, escape_html, truncate(180)")] pub title: String, #[holy(sanitize = "trim, lowercase, slug, truncate(50)")] pub space_slug: String, #[holy(sanitize = "nul_strip, truncate(50000)")] pub body: String,}After payload.sanitize() the struct is safe to forward into downstream
RPCs without per-field length / control-char checks.
Fuzz
Derive Fuzz to generate random() constructors for tests:
#[derive(holy::Fuzz)]pub struct Coords { pub x: i32, pub y: i32,}
let c = Coords::random();