1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
//! `formars-derive`: opt-in `#[derive(FormSchema)]` macro generating
//! `formars-core` companion schemas.
//!
//! Snapshot regen protocol: trybuild `.stderr` snapshots regenerate ONLY on
//! the pinned gate toolchain 1.98.0 (see `.github/workflows/ci.yml`) via
//! `TRYBUILD=overwrite cargo test -p formars-derive --test ui`, diffs
//! hand-reviewed; never per-toolchain.
//!
//! `formars-core` is builder-first and macro-free; this crate is the optional
//! ergonomic layer that removes the hand-composition step. Deriving
//! `FormSchema` on a plain named-field struct emits a companion schema type
//! whose single composed representation is a real `::formars_core::ObjectSchema`
//! assembled through the public builders — the walk kernel, joined paths,
//! fail-fast descent and dual-view parity are all inherited from core, never
//! reimplemented.
//!
//! # Example
//!
//! Typed parse, erased validation and nesting, all from one derive:
//!
//! ```
//! use formars_core::prelude::*;
//! use formars_core::value::{Object, Value};
//! use formars_derive::FormSchema;
//!
//! #[derive(FormSchema)]
//! struct Signup {
//! #[form(label = "Full name")]
//! name: String,
//! age: u32, // HTML-form strings coerce into numeric fields
//! address: Address,
//! }
//!
//! #[derive(FormSchema)]
//! struct Address {
//! city: String,
//! }
//!
//! // ONE composed representation serves both views (SC-9).
//! let schema = <Signup as FormSchema>::form_schema();
//!
//! // Typed parse: struct in, validated struct out.
//! let parsed = schema.parse(&Signup {
//! name: "Ada".into(),
//! age: 36,
//! address: Address { city: "London".into() },
//! });
//! assert!(parsed.is_ok());
//!
//! // Erased view over a raw Value tree (what formars-signals/formars-ui consume).
//! let mut addr = Object::new();
//! addr.insert("city", Value::from("London"));
//! let mut input = Object::new();
//! input.insert("name", Value::from("Ada"));
//! input.insert("age", Value::from("abc")); // not coercible into u32
//! input.insert("address", Value::Object(addr));
//!
//! let issues = schema.validate_value(&Value::Object(input));
//! assert_eq!(issues.len(), 1);
//! assert_eq!(issues[0].code, IssueCode::Coerce);
//! assert_eq!(issues[0].path.to_string(), "age");
//! ```
//!
//! # Attribute reference (v0 closed grammar)
//!
//! Exactly six keys are recognized inside `#[form(..)]`; anything else is a
//! hard compile error listing the valid keys.
//!
//! | Attribute | Example | Effect |
//! |-----------|---------|--------|
//! | `schema = <expr>` | `#[form(schema = string().trim().email())]` | Full override: `expr` replaces the default mapping at this field's position |
//! | `rename = "wire"` | `#[form(rename = "user_email")]` | Remaps the object key everywhere (lookup, reconstruction, `shape()`, `field_meta`); the Rust identifier is untouched |
//! | `skip` | `#[form(skip)]` | Field excluded from schema and bridging entirely; passed through untouched on typed parse |
//! | `label = ".."` | `#[form(label = "Email")]` | Populates `FieldMeta::label`, reachable via `metadata()` / `field_meta` |
//! | `description = ".."` | `#[form(description = "..")]` | Populates `FieldMeta::description` |
//! | `placeholder = ".."` | `#[form(placeholder = "..")]` | Populates `FieldMeta::placeholder` |
//!
//! Rules: one attribute list per field; duplicate keys error; `skip` cannot be
//! combined with any other key; container-level attributes do not exist in v0
//! and are rejected as unrecognized.
//!
//! # Supported field types (default mapping)
//!
//! | Type(s) | Mapping |
//! |---------|---------|
//! | `String` | `string()` |
//! | `bool` | `bool()` |
//! | `i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `f32`, `f64` | `coerced::<T>()` (HTML forms deliver strings) |
//! | Any type implementing `FormSchema` | Nested composition via `Nested::new(<T as FormSchema>::form_schema())` |
//! | Anything else (aliases included) | Compile error naming the field, suggesting `#[form(schema = ..)]` |
//!
//! Mapping is strictly NAME-based: only a single-segment path whose final
//! segment is exactly an identifier from the table above resolves to its
//! builder. Type aliases (`type Age = u32;`) are NOT resolved by trait
//! bounds: they fall through to nested composition and compile only if the
//! aliased type independently implements `FormSchema` + `FormBridge`.
//! Fully-qualified primitive paths (`::std::string::String`,
//! `::core::primitive::u32`) are not mapped by name either; the derive
//! rejects them with a targeted compile error suggesting the bare name or
//! `#[form(schema = ..)]`.
//!
//! # Generated contract
//!
//! For `struct X { .. }` the derive emits, in the same module:
//!
//! - `struct XSchema` taking the STRUCT'S visibility (a private struct yields
//! a private companion; `pub` yields `pub`) owning its composed
//! `::formars_core::ObjectSchema`, with an `XSchema::new()` constructor;
//! - `impl Schema for XSchema` (`Input = Output = X`) — bridges `X` to an
//! object via `FormBridge`, validates, reconstructs;
//! - `impl DynSchema for XSchema` — pure pass-through to the composed object;
//! - `impl AsRef<::formars_core::ObjectSchema> for XSchema`;
//! - `impl FormSchema for X` and `impl FormBridge for X`.
//!
//! # Companion naming (reserved suffix)
//!
//! `struct Foo` generates its companion `FooSchema` **in the same module**.
//! If a pre-existing item named `FooSchema` is already in scope there, the
//! generated definition collides with it and rustc reports E0428 ("the name
//! `FooSchema` is defined multiple times") pointing INTO generated code,
//! with no hint about the derive as the cause. Convention: never name items
//! `{Struct}Schema` — that suffix is reserved for generated companions.
//!
//! Nested fields carry a **dual bound**: their type must implement BOTH
//! `FormSchema` (composition) AND `FormBridge` (typed-parse bridging). A
//! hand-written `FormSchema` impl without `FormBridge` fails compilation.
//!
//! Skipped fields must implement `Clone`: typed parse copies them verbatim
//! from the input. Because such a struct declines erased reconstruction in
//! v0, it must NOT be used as a nested child — the parent compiles but typed
//! parse fails at runtime with `TypeMismatch` ("validated output is missing
//! this field").
//!
//! Deviation from the spec's soft zero-sized-companion preference: `XSchema`
//! stores its composed `ObjectSchema` instead of being a ZST. This buys the
//! natural `AsRef` used by nested composition and keeps `shape()`
//! memoization; construction cost equals hand-building once.
//!
//! # Package-name coupling
//!
//! All generated code references core items via absolute `::formars_core::..`
//! paths (proc-macros have no `$crate` escape hatch). Renaming or shadowing
//! the `formars-core` package is therefore a breaking change for derive users.
use TokenStream;
use parse_macro_input;
/// Derives a `formars` form schema companion for the annotated struct.
///
/// See the crate-level docs for the supported shapes and attribute grammar.