Skip to main content

aphyn_serialization_macros/
lib.rs

1//! # aphyn_serialization_macros
2//!
3//! Provides the `#[derive(Serializable)]` macro that writes the boilerplate `Serializable`
4//! impl for a struct or enum, so types opt into the engine's serialization framework without
5//! hand-writing field-by-field (de)serialization.
6//!
7//! This crate is the procedural-macro half of `aphyn_serialization`; the macro is
8//! re-exported from there, so users derive it through `aphyn_serialization` rather than
9//! depending on this crate directly.
10//!
11//! # How to use
12//!
13//! Annotate a type with `#[derive(Serializable)]`. The type must also implement `Default`
14//! (deserialization starts from it) and be `'static`. The derive generates code that names
15//! `Serializable`, `SerializerCtx`, `SerializerCtxState`, and `SerializedValue` unqualified,
16//! so those names must be in scope at the derive site — `use aphyn_serialization::*;` brings
17//! them in. Registering the type and round-tripping it is covered in `aphyn_serialization`.
18//!
19//! #### Derive on a struct
20//!
21//! Each named field becomes a map entry keyed by the field name:
22//!
23//! ```ignore
24//! use aphyn_serialization::*;
25//!
26//! #[derive(Serializable, Default)]
27//! struct Player {
28//!     name: String,
29//!     score: u32,
30//! }
31//! ```
32//!
33//! Tuple structs are supported too — their fields are keyed by index (`"0"`, `"1"`, …) — as
34//! are unit structs, which serialize as an empty map.
35//!
36//! #### Derive on an enum
37//!
38//! Every variant is supported (unit, tuple, and struct variants). The serialized value records
39//! which variant is active along with the full list of variant names. Because the type needs a
40//! `Default`, mark a default variant:
41//!
42//! ```ignore
43//! use aphyn_serialization::*;
44//!
45//! #[derive(Serializable, Default)]
46//! enum Shape {
47//!     #[default]
48//!     Empty,
49//!     Circle(f32),
50//!     Rect { w: f32, h: f32 },
51//! }
52//! ```
53//!
54//! #### Derive on a generic type
55//!
56//! Generic parameters, including lifetimes and const generics, are carried through to the
57//! generated impl, and any `where` clause on the type is preserved:
58//!
59//! ```ignore
60//! use aphyn_serialization::*;
61//!
62//! #[derive(Serializable, Default)]
63//! struct Wrapper<T> {
64//!     value: T,
65//! }
66//! ```
67//!
68//! # How to maintain
69//!
70//! #### Code generation by string-building
71//!
72//! The entry point [`derive_serializable`] forwards to `impl_serializable::derive`. Unlike most
73//! derives, it does not emit a token stream with `quote!`. It appends Rust *source text* to a
74//! `String` and parses it back at the end. The two methods are produced separately by
75//! `serialize_impl` and `deserialize_impl`, which each `match` on `input.data` and write the
76//! method body for the struct/enum shape at hand. When changing the generated code, remember
77//! you are writing strings: every brace in a `write!` format string that should reach the
78//! output must be escaped (`{{` / `}}`). If the generated text fails to parse, the macro emits
79//! it as a `const fail: &str = r##"…"##;` item instead (so the source can be inspected) and
80//! panics only if even that fails to parse.
81//!
82//! #### Generics and the emitted header
83//!
84//! The impl is generic over an extra context-state parameter, `SerializerCtxStateTy`, bounded
85//! by `Copy` plus `SerializerCtxState<FieldTy>` for every distinct field type (collected as
86//! token strings from all struct fields or all enum-variant fields). The type's own generic
87//! params are prepended to it, and `type_generics` is rebuilt by iterating the params and
88//! emitting the lifetime, type, or const ident for each (trailing comma included). The header
89//! is `impl<{params}, SerializerCtxStateTy: …> Serializable<SerializerCtxStateTy> for
90//! {type_name}<{type_generics}> where Self: 'static + Default`, with the type's own `where`
91//! predicates appended after a comma when present.
92//!
93//! #### How each shape maps to a `SerializedValue`
94//!
95//! Serialization always builds a *rigid* composite through `ctx.serialize_map(path)` and
96//! `finish_as_map(true)` / `finish_as_enum(true, …)`, so the path handling of the map builder
97//! applies. Named struct fields are keyed by field name; tuple fields are keyed by their index
98//! rendered as a string; unit structs produce an empty map. Enums emit a map tagged via
99//! `finish_as_enum` with the active variant name and a `variants` vector of every variant name
100//! (built once, before the `match`). Deserialization is in place: structs go through
101//! `ctx.deserialize_map(self, path, value)` followed by `.with_field("…", &mut self.…)` for each
102//! field, and enums go through `ctx.deserialize_enum(path, value)` with one
103//! `.with_variant("Name", || Self::Name { …: Default::default() })` per variant, then
104//! `.finish(self, …)` whose closure feeds the chosen variant's fields to `with_field`.
105//!
106//! #### Variant binding names
107//!
108//! When generating the `match` arm for an enum variant, fields are bound to `f_<field>` for
109//! struct variants and `f_<index>` for tuple variants, keeping the generated bindings from
110//! colliding with the field/index names used as map keys.
111//!
112//! #### Unions are rejected
113//!
114//! `syn::Data::Union` is unsupported: collecting field types hits a `todo!()` for it, and both
115//! `serialize_impl` and `deserialize_impl` also panic on it, surfacing as a compile error at
116//! the derive site.
117
118use proc_macro::TokenStream;
119
120mod impl_serializable;
121
122#[proc_macro_derive(Serializable)]
123pub fn derive_serializable(stream: TokenStream) -> TokenStream {
124    impl_serializable::derive(stream)
125}