GuardedStruct.Derive.Extension (GuardedStruct v0.1.0-beta.1)

Copy Markdown View Source

Define custom derive validators / sanitizers via a Spark DSL.

Usage

defmodule MyApp.Derives do
  use GuardedStruct.Derive.Extension

  derives do
    validator :slug, fn input ->
      is_binary(input) and Regex.match?(~r/^[a-z0-9-]+$/, input)
    end

    sanitizer :slugify, fn input when is_binary(input) ->
      input
      |> String.downcase()
      |> String.replace(~r/[^a-z0-9-]+/u, "-")
    end
  end
end

Register globally in config/config.exs:

config :guarded_struct, derive_extensions: [MyApp.Derives]

Then any GuardedStruct module can use the new ops:

defmodule Post do
  use GuardedStruct

  guardedstruct do
    field :slug, String.t(), derives: "sanitize(slugify) validate(slug)"
  end
end

Validator return shape

Validator functions return:

  • true — input passes
  • false — input fails (default error message generated)
  • {:error, field, action, message} — explicit error tuple
  • any other value — used as the validated value (for coercing validators)

Introspection

The derives do ... end block is introspectable via Spark.Dsl.Extension.get_entities/2. Verifiers and transformers can plug in at the standard Spark extension points.

Options

  • :extensions (list of module that adopts Spark.Dsl.Extension) - A list of DSL extensions to add to the Spark.Dsl

  • :otp_app (atom/0) - The otp_app to use for any application configurable options

  • :fragments (list of module/0) - Fragments to include in the Spark.Dsl. See the fragments guide for more.

Summary

Functions

All sanitizer op atoms known across every extension visible from module.

All validator op atoms known across every extension visible from the given module.

Clear the cached extension list. Call from test setup if you mutate :guarded_struct, :derive_extensions and need the change visible before the next registered_extensions/0 call.

The user module currently being built. Set by Runtime.with_telemetry/2 around every top-level builder/1 call; nested sub_field builds inherit via the process dictionary.

Try the current module's extensions for a sanitizer op.

Try a specific list of extensions for a sanitizer op.

Try the current module's extensions for a validator op.

Try a specific list of extensions for a validator op.

Resolve the effective extension list for a specific user module.

Resolved list of registered extension modules from :guarded_struct, :derive_extensions application config.

Resolve a per-module derive_extensions: opt — the raw list user wrote in use GuardedStruct, derive_extensions: [...] — into a flat list of extension modules with :config expanded to the current global config at the position it appears.

Validate a derive_extensions: opt list at compile time. Raises ArgumentError if entries are not modules / :config, or if :config appears more than once.

Functions

all_extension_sanitizers(module \\ nil)

All sanitizer op atoms known across every extension visible from module.

all_extension_validators(module \\ nil)

All validator op atoms known across every extension visible from the given module.

clear_cache()

Clear the cached extension list. Call from test setup if you mutate :guarded_struct, :derive_extensions and need the change visible before the next registered_extensions/0 call.

current_module()

The user module currently being built. Set by Runtime.with_telemetry/2 around every top-level builder/1 call; nested sub_field builds inherit via the process dictionary.

dispatch_sanitize(input, op)

Try the current module's extensions for a sanitizer op.

dispatch_sanitize(input, op, extensions)

Try a specific list of extensions for a sanitizer op.

dispatch_validate(op, input, field)

Try the current module's extensions for a validator op.

dispatch_validate(op, input, field, extensions)

Try a specific list of extensions for a validator op.

extensions_for(module)

@spec extensions_for(module() | nil) :: [module()]

Resolve the effective extension list for a specific user module.

registered_extensions()

Resolved list of registered extension modules from :guarded_struct, :derive_extensions application config.

Cached in :persistent_term and invalidated automatically when the underlying config changes (e.g. between test cases that call Application.put_env/3).

resolve_opt(list)

@spec resolve_opt(list() | nil) :: [module()]

Resolve a per-module derive_extensions: opt — the raw list user wrote in use GuardedStruct, derive_extensions: [...] — into a flat list of extension modules with :config expanded to the current global config at the position it appears.

Resolution rules

  • nil → fall back to the global Application config (no per-module opt set)
  • [] → no extensions at all (intentional opt-out, ignores global)
  • [A, B] (no :config) → these only; global is ignored
  • [:config, A] → global ++ [A] (global wins on op-name collisions)
  • [A, :config] → [A] ++ global (A wins on op-name collisions)
  • [A, :config, B] → [A] ++ global ++ [B]

validate_opt!(list)

Validate a derive_extensions: opt list at compile time. Raises ArgumentError if entries are not modules / :config, or if :config appears more than once.