Skip to content

Commit 7c1aebb

Browse files
docs: update and expand validation module documentation
1 parent b01a89f commit 7c1aebb

1 file changed

Lines changed: 74 additions & 74 deletions

File tree

docs/src/validation.md

Lines changed: 74 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Validation module
22

3+
## Contents
4+
```@contents
5+
Pages = ["validation.md"]
6+
Depth = 3
7+
```
8+
9+
---
10+
311
This section documents the validation framework used by component constructors. The design is deterministic, non‑magical, and trait‑driven. The flow is:
412

513
```julia
@@ -10,15 +18,15 @@ The **typed cores** accept **numbers only**. All proxy handling happens in the *
1018

1119
---
1220

13-
## 1. Architecture
21+
## Architecture
1422

15-
### 1.1 Pipeline
23+
### Pipeline
1624

1725
* **`sanitize(::Type{T}, args::Tuple, kwargs::NamedTuple)`**
1826

19-
* Rejects wrong arities and enforces presence using `required_fields(T)`.
20-
* Maps optional keywords using `keyword_fields(T)`.
21-
* For `has_radii(T) == true`, checks admissibility of raw radius inputs with `is_radius_input(T, x)` (numbers by default; types may extend to allow proxies).
27+
* Rejects wrong arities: exactly `length(required_fields(T))` positionals are expected; optionals must be passed as keywords listed in `keyword_fields(T)`.
28+
* Maps positionals to names using `required_fields(T)`; merges keyword arguments; rejects unknown keywords.
29+
* If `has_radii(T) == true`, checks admissibility of raw radius inputs with `is_radius_input(T, Val(:radius_in), x)` and `is_radius_input(T, Val(:radius_ext), x)`. The default accepts only real, non‑complex numbers; types may extend to allow proxies.
2230
* Returns a **raw** `NamedTuple`.
2331

2432
* **`parse(::Type{T}, nt)`**
@@ -34,38 +42,38 @@ The **typed cores** accept **numbers only**. All proxy handling happens in the *
3442

3543
* **`validate!(::Type{T}, args...; kwargs...)`**
3644

37-
* Orchestrates the pipeline.
38-
* Converts `Base.Pairs``NamedTuple` once; do not pass `Pairs` to user extensions.
45+
* Orchestrates the pipeline. Use this in all convenience constructors.
3946

40-
### 1.2 Traits (configuration surface)
47+
### Traits (configuration surface)
4148

4249
* `has_radii(::Type{T})::Bool` — enables the radii rule bundle and raw acceptance checks.
4350
* `has_temperature(::Type{T})::Bool` — enables finiteness check on `:temperature`.
4451
* `required_fields(::Type{T})::NTuple` — positional keys.
4552
* `keyword_fields(::Type{T})::NTuple` — keyword argument keys.
46-
* `is_radius_input(::Type{T}, x)::Bool` — raw admissibility predicate for radii inputs; extend to allow proxies.
53+
* `coercive_fields(::Type{T})::NTuple` — values that participate in type promotion and will be coerced (default: `required_fields ∪ keyword_fields`).
54+
* `is_radius_input(::Type{T}, Val(:field), x)::Bool` — raw admissibility predicate for radii inputs; extend to allow proxies by field.
4755
* `extra_rules(::Type{T})::NTuple{K,Rule}` — additional constraints appended to the generated bundle.
4856

4957
**Import before extending**:
5058

5159
```julia
5260
import ..Validation: has_radii, has_temperature, required_fields, keyword_fields,
53-
is_radius_input, parse, extra_rules
61+
coercive_fields, is_radius_input, parse, extra_rules
5462
```
5563

5664
Failing to import will create shadow functions in your module; the engine will not see your methods.
5765

5866
---
5967

60-
## 2. Rules
68+
## Rules
6169

6270
Rules are small value types `struct <: Rule` with an `_apply(::Rule, nt, ::Type{T})` method. All rule methods must:
6371

6472
* Read data from the **normalized** `NamedTuple` `nt`.
65-
* Throw `ArgumentError` for logical violations; `DomainError` for numerical domain violations (e.g., non‑finite).
73+
* Throw `ArgumentError` for logical violations; `DomainError` for numerical domain violations (non‑finite).
6674
* Avoid allocations; use `@inline` where appropriate.
6775

68-
### 2.1 Standard rules
76+
### Standard rules
6977

7078
* `Normalized(:field)` — field must be numeric post‑parse.
7179
* `Finite(:field)``isfinite` must hold.
@@ -75,11 +83,12 @@ Rules are small value types `struct <: Rule` with an `_apply(::Rule, nt, ::Type{
7583
* `Less(:a,:b)` — strict ordering `a < b`.
7684
* `LessEq(:a,:b)` — non‑strict ordering `a ≤ b`.
7785
* `IsA{M}(:field)` — type membership check.
86+
* `OneOf(:field, set)` — membership in a finite set.
7887

79-
### 2.2 Extending with custom rules (pattern)
88+
### Custom rule pattern
8089

8190
```julia
82-
struct InRange{T} <: Rule
91+
struct InRange{T} <: Validation.Rule
8392
name::Symbol; lo::T; hi::T
8493
end
8594

@@ -94,7 +103,7 @@ Attach via `extra_rules(::Type{X}) = (InRange(:alpha, 0.0, 1.0), ...)`.
94103

95104
---
96105

97-
## 3. Reverse tutorial — [`LineCableModels.DataModel.Tubular`](@ref)
106+
## Example implementation — `DataModel.Tubular`
98107

99108
**Typed core** (numbers only):
100109

@@ -127,42 +136,33 @@ Validation.keyword_fields(::Type{Tubular}) = _OPT_TUBULAR
127136
```julia
128137
Validation.is_radius_input(::Type{Tubular}, ::Val{:radius_in}, x::AbstractCablePart) = true
129138
Validation.is_radius_input(::Type{Tubular}, ::Val{:radius_ext}, x::Thickness) = true
139+
Validation.is_radius_input(::Type{Tubular}, ::Val{:radius_ext}, x::Diameter) = true
130140
```
131141

132-
Rationale: convenience constructors may accept layer objects or thickness/diameter wrappers; they are *raw* inputs and must be transformed.
142+
The inner radius may take an existing cable part (its `radius_ext`); the outer radius may take a `Thickness` or `Diameter` wrapper.
133143

134144
**Extra rules**:
135145

136146
```julia
137-
Validation.extra_rules(::Type{Tubular}) = (
138-
IsA{Material}(:material_props),)
147+
Validation.extra_rules(::Type{Tubular}) = (IsA{Material}(:material_props),)
139148
```
140149

141-
* `IsA{Material}` enforces the material argument type.
142-
143150
**Parsing**:
144151

145152
```julia
146153
Validation.parse(::Type{Tubular}, nt) = begin
147154
rin, rex = _normalize_radii(Tubular, nt.radius_in, nt.radius_ext)
148-
(; nt..., radius_in = rin, radius_ext = rex)
155+
(; nt..., radius_in=rin, radius_ext=rex)
149156
end
150157
```
151158

152-
Rationale: centralizes radius proxy resolution and uncertainty semantics in one place. The output is numeric radii. After this step, the `Normalized` rules guarantee that rule checks run on numbers.
153-
154159
**Convenience constructor**:
155160

156161
```julia
157-
function Tubular(radius_in, radius_ext, material_props; temperature = _DEFS_TUBULAR[1])
158-
ntv = validate!(Tubular, radius_in, radius_ext, material_props; temperature)
159-
T = resolve_T(ntv.radius_in, ntv.radius_ext, material_props, ntv.temperature)
160-
return Tubular(coerce_to_T(ntv.radius_in, T), coerce_to_T(ntv.radius_ext, T),
161-
coerce_to_T(material_props, T), coerce_to_T(ntv.temperature, T))
162-
end
162+
@_ctor Tubular _REQ_TUBULAR _OPT_TUBULAR _DEFS_TUBULAR
163163
```
164164

165-
Rationale: forces all user entry through `validate!`, then hands a coherent, type‑promoted set of values to the numeric core.
165+
This expands to a weakly‑typed method that calls `validate!`, promotes using `_promotion_T`, coerces via `_coerced_args`, and delegates to the numeric core.
166166

167167
**Failure modes intentionally trapped**:
168168

@@ -173,98 +173,98 @@ Rationale: forces all user entry through `validate!`, then hands a coherent, typ
173173

174174
---
175175

176-
## 4. Template for a New Component
177-
178-
Use the following checklist as a copy/paste starting point. Replace `NewPart` and fields accordingly.
176+
## Template for a new component
179177

180178
```julia
181179
# 1) Numeric core (numbers only)
182180
function NewPart(a::T, b::T, material::Material{T}, temperature::T) where {T<:REALSCALAR}
183-
# compute derived quantities, then construct
181+
# compute derived, then construct
184182
end
185183

186184
# 2) Trait config
187-
Validation.has_radii(::Type{NewPart}) = true # if the type has radii
188-
Validation.has_temperature(::Type{NewPart}) = true # if temperature is used
189-
Validation.field_order(::Type{NewPart}) = (:a, :b, :material)
185+
Validation.has_radii(::Type{NewPart}) = true
186+
Validation.has_temperature(::Type{NewPart}) = true
190187
Validation.required_fields(::Type{NewPart}) = (:a, :b, :material)
188+
Validation.keyword_fields(::Type{NewPart}) = (:temperature,)
191189

192190
# 3) Raw acceptance (extend only what you intend to parse)
193-
Validation.is_radius_input(::Type{NewPart}, x::AbstractCablePart) = true
194-
Validation.is_radius_input(::Type{NewPart}, x::Thickness) = true
195-
Validation.is_radius_input(::Type{NewPart}, x::Diameter) = true
191+
Validation.is_radius_input(::Type{NewPart}, ::Val{:radius_in}, x::AbstractCablePart) = true
192+
Validation.is_radius_input(::Type{NewPart}, ::Val{:radius_ext}, x::Thickness) = true
196193

197-
# 4) Extra rules (append per‑type constraints)
194+
# 4) Extra rules
198195
Validation.extra_rules(::Type{NewPart}) = (
199196
IsA{Material}(:material),
200-
# InRange(:alpha, 0.0, 1.0), IntegerField(:num_wires), etc.
201197
)
202198

203199
# 5) Parsing (proxy → numeric)
204200
Validation.parse(::Type{NewPart}, nt) = begin
205201
a′, b′ = _normalize_radii(NewPart, nt.a, nt.b)
206-
(; nt..., a = a′, b = b′)
202+
(; nt..., a=a′, b=b′)
207203
end
208204

209-
# 6) Convenience constructor — call validate!, then delegate to numeric core
210-
function NewPart(a, b, material; temperature = T₀)
211-
ntv = validate!(NewPart, a, b, material; temperature)
212-
T = resolve_T(ntv.a, ntv.b, material, ntv.temperature)
213-
return NewPart(coerce_to_T(ntv.a, T), coerce_to_T(ntv.b, T),
214-
coerce_to_T(material, T), coerce_to_T(ntv.temperature, T))
215-
end
205+
# 6) Convenience constructor — generated
206+
@_ctor NewPart (:a, :b, :material) (:temperature,) (T₀,)
216207
```
217208

218209
---
219210

220-
## 5. Extending Traits
211+
## Extending traits
221212

222213
Traits are just methods. Add traits only when behavior toggles; avoid proliferation.
223214

224-
### 5.1 New feature flags
215+
### New feature flags
225216

226-
Example: a shielding flag that enables a rule bundle for `:shield_thickness`.
217+
Example: a shielding flag with its own bundle.
227218

228219
```julia
229-
# Trait
230220
Validation.has_shield(::Type) = false
231221
Validation.has_shield(::Type{SomeType}) = true
232-
233-
# Generated rule splice (in Validation, not per‑type)
234-
# Extend `_rules` to inject `(Nonneg(:shield_thickness), Finite(:shield_thickness))` when `has_shield(T)`.
235222
```
236223

237-
### 5.2 New admissibility predicate
224+
Extend `_rules` inside `Validation` to splice the corresponding checks when `has_shield(T)` is true.
225+
226+
### Field‑specific admissibility
238227

239-
Example: accept `Symbol` values for a categorical option.
228+
If only `:radius_in` should accept proxies, extend the field‑tagged predicate:
240229

241230
```julia
242-
Validation.is_option(::Type{T}, x) where {T} = false # default
243-
Validation.is_option(::Type{X}, ::Symbol) where {X} = true # for type X
231+
Validation.is_radius_input(::Type{X}, ::Val{:radius_in}, p::AbstractCablePart) = true
232+
Validation.is_radius_input(::Type{X}, ::Val{:radius_ext}, ::AbstractCablePart) = false
244233
```
245234

246-
Use in `sanitize` for key `:option` prior to parsing.
247-
248235
---
249236

250-
## 6. Testing guidelines
237+
## Testing guidelines
251238

252239
* Test arity: `()`, `(1)`, `(1,2)``ArgumentError`.
253240
* Test raw type rejections: strings, complex numbers.
254-
* Test proxy acceptance: prior layer objects, `Thickness`, `Diameter`.
255-
* Test parse correctness: outputs are numeric and respect uncertainty rules.
256-
* Test rule violations: negative radii, inverted radii, non‑finite values.
241+
* Test proxy acceptance: prior layer objects, `Thickness`, `Diameter` when allowed.
242+
* Test parse correctness: outputs numeric and respect uncertainty rules.
243+
* Test rule violations: negative radii, inverted radii, non‑finite values, invalid sets.
257244
* Test constructor round‑trip: convenience path and numeric core produce equivalent instances after coercion.
258245

246+
## Usage notes
247+
248+
* Keep all proxy handling in `parse`. Do not call normalizers in constructors.
249+
* Error messages must be terse and contextualized with the component type name.
250+
* Prefer tuple returns and `NamedTuple` updates to avoid allocations.
251+
* When adding rules, benchmark `_apply` implementations.
252+
259253
---
260254

261-
## 7. Operational notes
255+
## API reference
262256

263-
* Keep all proxy handling in `parse`. Do not call normalizers in constructors.
264-
* Keep error messages terse and contextualized with the component type name.
265-
* Prefer tuple returns and `NamedTuple` updates for zero‑allocation pipelines.
266-
* When adding rules, benchmark `_apply` methods; avoid dynamic dispatch inside rules.
257+
```@autodocs
258+
Modules = [LineCableModels.Validation]
259+
Order = [:module, :constant, :type, :function, :macro]
260+
Public = true
261+
Private = true
262+
```
267263

268264
---
269265

270-
This framework is designed to be extended in small, explicit increments. If behavior changes, change the trait and the minimal corresponding code; the rest of the pipeline remains stable.
266+
## Index
267+
```@index
268+
Pages = ["validation.md"]
269+
Order = [:module, :constant, :type, :function, :macro]
270+
```

0 commit comments

Comments
 (0)