Changelog
View SourceAll notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.2.0 - 2026-09-24
Changed
- The generated functions assemble the struct as a single literal that names
every field, where they used to call
struct/2,Map.update!/3andEnum.reduce/3. Those take a field name as a runtime value and hand back a barestruct(), so dialyzer could see neither the module nor any field type through them, and every generated@specwas a promise it took on faith rather than one it checked. The assembled struct is also carried throughvalidate/1, which is specced to taket(), so a field copied from the struct in hand keeps its declared type instead of reading asany(). validate/1is generated as@spec validate(t()) :: :okfor a struct that declares no validators, since there is nothing it could report. On such a structput/2,put!/2,update/2andupdate!/2are typed as{:ok, t()}and have nothing to raise, andnew/1keeps its error half for as long as a key can be missing from the attributes it is given. Matching on{:error, reasons}from any of them is then a clause the compiler reports as unreachable.update/2applies its functions in field declaration order rather than the order the updates map happens to iterate in. Each function still receives the value its own field held before the update, so this is only visible to functions with side effects.- Together these cut the work per call considerably: on an eight-field struct
with two validators,
new/1went from 268ns to 58ns given every field, and from 87ns to 45ns given only the two enforced ones. One struct is allocated per call rather than one per field written, and a passingvalidate/1now allocates nothing.
Removed
The runtime rejection of a key the struct does not declare. Such a key is not in the generated argument types, so dialyzer reports it at the line that writes it, and paying for the same answer again on every call is work the types already did. A key that reaches a generated function anyway is ignored, where it used to come back as
{:error, ["unknown key(s): ..."]}.This is the one change here that can break a caller: code matching on that error, or relying on an undeclared key being refused at runtime, no longer gets it. A missing enforced key is still reported as before.
0.1.0 - 2026-09-22
Initial release.
plugin TypedStructBuilderValidatorsgeneratesnew/1,new!/1,validate/1,put/2,put!/2,update/2andupdate!/2on atypedstructblock, each with a@specbuilt from the declared field types.validator/1andvalidator/2declare predicates on the completed struct, inlined intovalidate/1, with the predicate's source as the default failure message.:onlynarrows the generated set; passing a default name as an option renames that function.:fields_type_name,:changes_type_nameand:updates_type_namedeclare named types for the generated argument maps.