We are delighted to introduce v0.1.0 — a from-scratch rewrite of the macro core on top of Spark. Every existing 0.0.x public API is preserved. Bump the dep, run
mix deps.get, and existing tests stay green.
Tracking PR: #13
Features:
- Rewrite the 2,910-LOC
defmacrocore onSpark.Dsl.Extensionwith one:guardedstructsection, five entities (field,sub_field,conditional_field,virtual_field,dynamic_field), six transformers, three verifiers #13 - Add
Pattern-keyed maps—fieldwhose name is a regex declares a free-form map shape (closes #11) #13 - Add
virtual_field— validated through the full pipeline but excluded fromdefstruct(closes #5) #13 - Add
dynamic_field— free-form map with passthrough; atom-attack-safe (string keys stay strings) #13 - Add Erlang
Recordsupport viavalidate(record)andvalidate(record=tag)(closes #6) #13 - Add
GuardedStruct.Validatestandalone API —Validate.run/2,Validate.field/3,4,Validate.partial/2(closes #2) #13 - Add Spark-native custom derive DSL —
use GuardedStruct.Derive.Extension+derives do validator/2, sanitizer/2 endfor declarative custom ops #13 - Add per-module
derive_extensions:opt with:configsentinel for in-position merge with global registry #13 - Add compile-time shadow warning when a custom op-name collides with a built-in registered in
Derive.Registry#13 - Add
Splodeerror wrapping —GuardedStruct.Errors.from_tuple/1,traverse_errors/2,to_class/1, JSON-serializable shape (opt-in) #13 - Add
GuardedStruct.AshResourceextension — same DSL insideAsh.Resource; generates__guarded_change__/1,__guarded_information__/0,__guarded_fields__/0under the prefixed namespace #13 - Add
GuardedStruct.AshResource.Change— ready-madeAsh.Resource.Changemodule bridging__guarded_change__/1into the changeset pipeline #13 - Add
auto_wire: truesection option — Spark transformer injects the change into the resource'schangessection viaAsh.Resource.Builder.add_change/3; no manual wiring needed #13 - Add
batch_change/3on the Ash change —Ash.bulk_create/3andAsh.bulk_update/3(withstrategy: :stream) work end-to-end #13 - Add auto-map cascade for the Ash extension — every nested
sub_fieldreturns a plain map at every depth (matches Ash's:mapattribute type) #13 - Add Ash atomic-mode support —
Change.atomic/3runs the pipeline on plain literal inputs (from bothchangeset.attributesandchangeset.atomics) and returns{:atomic, sanitized_map}; updates stay atomic withoutrequire_atomic? false. Only theAsh.Changeset.atomic_update/3+Ash.Exprpath falls back to{:not_atomic, reason}#13 - Add
json: truesection option — auto-derivesJason.Encoder(if:jasonin deps) with fallback to built-inJSON.Encoderon Elixir 1.18+ #13 - Add
GuardedStruct.Info— full introspection API:describe/1,field_kind/2,enforce?/1,2,virtual?/2,dynamic?/2,sub_module/2,conditional_children/2, collection helpers, section-option shorthands #13 - Add
GuardedStruct.Diff—diff/2,apply/2,equal?/2for audit-log-friendly struct diffing #13 - Add
MyStruct.example/0— REPL helper returning a struct populated with defaults / type placeholders #13 Add telemetry events —
[:guarded_struct, :builder, :start | :stop | :exception]on every top-levelbuilder/1call #13- Add
@derivesdecorator attribute — alternative to inlinederives:for keeping fields short #13 - Add editor autocomplete inside
guardedstruct do … endvia Spark's ElixirSense plugin (closes #1) #13 - Add igniter installer —
mix igniter.install guarded_struct#13
Refactors:
- Move every static-string parse to compile time — derive op-strings,
from:/on:paths,domain:patterns are now parsed once during compilation; the runtime reads pre-built op-maps from__fields__/0#13 - Pre-evaluate
enum=Map[…]/enum=Tuple[…]/equal=Map::…operands at compile time — zeroCode.eval_stringon the runtime hot path #13 - Replace plain-macro
validator/2andsanitizer/2with proper Spark entities underderives do ... endblock — Spark.Formatter handles paren-stripping consistently with the rest of the DSL #13 - Surface compile-time warnings via the transformer's documented
{:warn, dsl_state, warnings}return shape instead ofIO.warn/2— shadow detection inDerive.Extension.Transformers.Codegenand legacy-derive:deprecation inTransformers.ParseDeriveboth flow through Spark, so warnings appear at the user's source line and the DSL state remains the transformer's only side effect #13 - Rename
__guarded_validate__/1→__guarded_change__/1on the Ash extension — name reflects that the function transforms (sanitize, auto-fill) as well as validates #13 - Rename
derive:option toderives:(plural) — aligns with the@derivesdecorator; legacyderive:still works but the transformer emits a compile-time deprecation warning through Spark's{:warn, ...}transformer return #13 - Rename
jason: truesection option tojson: true— option now derives whichever JSON encoder is available (Jason or built-in) #13 - Extract test fixtures (Ash resources + custom-derive modules) to top-level modules in
test/support/so Spark.Formatter applies paren-removal and section-ordering rules uniformly #13 - Normalize the error wire format — every
{:error, …}frombuilder/1,__guarded_change__/1,Validate.run/2,Validate.field/3,4,Validate.partial/2and the AshChangeis always{:error, [error_map, ...]}. Each error map follows the canonical shape%{field: atom, action: atom, message: String, [errors: [...]]}.:required_fieldsand:authorized_fieldsemit one error per affected field instead of one map with afields:list.:bad_parameterscarries:field => :__root__#13 - Flip
GuardedStruct.Derive.SanitizerDerive.sanitize/2to pipe-friendly(value, op)arg order — was(op, value). Applies project-wide:Extension.dispatch_sanitize/2,3, the generated__sanitize__/2callback on extension modules, and any user-supplied:sanitize_derivemodule'ssanitize/2function follow the same convention. Internal hot path now reduces withEnum.reduce(ops, value, fn op, acc -> sanitize(acc, op) end)#13 - Bake compile-time predicates on every guarded module —
__guarded_has_validator__/0,__guarded_has_main_validator__/0,__guarded_error_module__/0,__guarded_field_meta__/1(and Ash's__guarded_field_name_set__/0MapSet) — drops everyfunction_exported?/Code.ensure_loaded?call from the runtime hot path #13 - Cache
GuardedStruct.Derive.Extension.registered_extensions/0in:persistent_term, keyed by raw app config; auto-invalidates when config changes. Newclear_cache/0helper for test setup #13
Bugs:
- Fix nested
conditional_field— works to arbitrary depth viarecursive_as: :conditional_fields(closes #7, #8, #25) #13 - Restore i18n via
GuardedStruct.Messages.translated_message/1,2for orchestration-layer errors (authorized_fields,required_fields,:on/:domaincore keys, list-builder errors) — all 14 message callbacks reachable again #13 - Fix
__information__/0to populateconditional_keyswith actual conditional-field names (was always[]) #13 - Fix
MyStruct.Error.message/1to match master's format and usetranslated_message(:message_exception)for i18n #13 - Unblock the legacy
Parserraisesites that prevented nestedconditional_fieldfrom compiling #13 - Surface malformed
derives:strings asSpark.Error.DslErrorwith file:line — previously swallowed by arescue _ -> niland silently produced no validation #13 - Fix re-entrancy in the auto-map cascade — process-dict flag is saved+restored across nested
validate/3calls so a validator callback can recursively validate without clobbering outer state #13 - Fix
Logger.configure(level: :warning)global side-effect intest_helper.exs— replaced with@moduletag capture_log: trueon Ash test modules #13
Tests:
- Add 743+ tests (up from 146 in 0.0.4), including 6 property-based tests via
stream_data#13 - Add real Ash 3.x integration suite — ETS data layer, end-to-end
Ash.create/1,Ash.update/1,Ash.bulk_create/3,Ash.bulk_update/3#13 - Add
test/ash_integration_test.exsatomic-mode coverage — end-to-end create/update through Ash with sanitize/validate semantics intact under atomic SQL #13 - Add
test/info_test.exs— 38 tests covering every introspection helper includingdescribe/1consolidated dump #13 - Add
test/derive_extension_shadow_warning_test.exs— 9 tests for compile-time shadow detection #13 - Add
test/derive_extensions_per_module_test.exs— 19 tests for per-module opt resolution including the:configsentinel #13 - Add
test/jason_encoder_test.exs— Jason + built-in JSON encoder coverage with nested sub_field #13 - Add
test/telemetry_test.exs— start/stop/exception event coverage, including nested-build inheritance #13
Docs:
- Add full LiveBook walkthrough at
guidance/guarded-struct.livemdwith runnable end-to-end examples #13 - Add auto-generated DSL cheat sheets at
documentation/dsls/viamix spark.cheat_sheets#13 - Add
mix lintandmix cheataliases — wrapspark.formatter+formatandspark.cheat_sheets#13 - Add "Atom-attack safety" section to the
GuardedStructmodule @moduledoc covering the dynamic_field / pattern-keyed map threat model #13 - Add LLM agent context — root
usage-rules.mdplus topic-scoped sub-rules atusage-rules/dsl.md,derive.md,conditional.md,validators.md,core-keys.md,extensions.md,ash.md,api.md,errors.md(compatible with ash-project/usage_rules; consumers runmix usage_rules.syncand address sub-rules asguarded_struct:dsl,guarded_struct:ash, etc.) #13 - Add skills.sh-compatible
SKILL.mdfiles under.claude/skills/— one per subsystem (guarded-struct,-dsl,-derive,-conditional,-ash,-extensions,-api) with YAML frontmatter triggers so Claude Code / Cursor / Copilot auto-load the right context #13
Internals dropped:
- Remove
builder/4@doc falseform (with(actions, key, type, error)arity) — replaced by an internal runtime helper #13 - Remove
register_struct/4,__field__/6,__type__/2,delete_temporary_revaluation/1,create_builder/1,create_error_module/0#13 - Remove the 12
gs_*accumulator module attributes (gs_fields,gs_types,gs_enforce_keys, etc.) — replaced by Spark DSL state #13 - Remove
parser/3(the conditional variant ofParser.parser),elements_unification/2,find_node_tags/1,add_parent_tags/3,conds_list/2,find_conds_children_recursive/2#13 - Remove
Derive.pre_derives_check/3,get_derives_from_success_conditional_data/1,error_handler/2,halt_errors/1, the alternate-shapederive/1clauses #13 - Remove
Messages.unsupported_conditional_field/0andMessages.parser_field_value/0callbacks (dead code after the nested-conditional fix) #13
Dependencies:
- Add
{:spark, "~> 2.7"}(runtime — DSL framework) #13 - Add
{:splode, "~> 0.3"}(runtime — error class hierarchy for opt-in wrapper) #13 - Add
{:telemetry, "~> 1.0"}(runtime — builder events) #13 - Add
{:sourceror, "~> 1.7", only: [:dev, :test]}(required by Spark.Formatter) #13 - Add
{:igniter, "~> 0.8", only: [:dev, :test]}(installer mix task) #13 - Add
{:ash, "~> 3.0", only: [:dev, :test]}(real Ash integration suite — not a runtime dep) #13 - All optional deps unchanged (
html_sanitize_ex,email_checker,ex_url,ex_phone_number,sweet_xml) #13
Changelog for GuardedStruct 0.0.4
Bugs:
- Fix deprecated code from Elixir 1.18
Features:
- Support overridable messages for the
GuardedStructmodule with support for multiple languages
Changelog for GuardedStruct 0.0.3
Bugs:
- Fix deprecated code from Elixir 1.18.0-rc.0
Changelog for GuardedStruct 0.0.2
Bugs:
- Support charlists sigil warning and keep backward compatibility for charlist regex
Changelog for GuardedStruct 0.0.1
We are delighted to introduce our first standalone release of GuardedStruct — extracted from the Mishka developer tools library.
For more information please see: https://mishka.tools
Features:
- Detach from the Mishka developer tools library
Refactors:
- Remove optional libraries (must be enabled by the user)
- Improvements in some tests