# `MishkaGervaz.Helpers`
[🔗](https://github.com/mishka-group/mishka_gervaz/blob/v0.0.2/lib/mishka_gervaz/_helpers.ex#L1)

Shared helper functions for MishkaGervaz.

# `accessible?`

```elixir
@spec accessible?(map(), map()) :: boolean()
```

Checks if an entity (filter, action, etc.) is accessible based on visibility and restrictions.

Handles:
- `restricted: true` → accessible only when `state.master_user?` is `true`
- `visible: false` → not accessible
- `visible: fn state -> boolean end` → calls function

## Examples

    iex> MishkaGervaz.Helpers.accessible?(%{restricted: true}, %{master_user?: false})
    false

    iex> MishkaGervaz.Helpers.accessible?(%{restricted: true}, %{master_user?: true})
    true

    iex> MishkaGervaz.Helpers.accessible?(%{restricted: true}, %{})
    false

    iex> MishkaGervaz.Helpers.accessible?(%{visible: false}, %{})
    false

    iex> MishkaGervaz.Helpers.accessible?(%{visible: true}, %{})
    true

# `action_label`

```elixir
@spec action_label(map()) :: String.t()
```

A row action's button label: its `ui` label, else its humanized name, translated in the caller's
locale.

## Examples

    iex> MishkaGervaz.Helpers.action_label(%{name: :mark_done, ui: %{label: "Finish"}})
    "Finish"

    iex> MishkaGervaz.Helpers.action_label(%{name: :mark_done, ui: nil})
    "Mark Done"

# `compact_to_nil`

```elixir
@spec compact_to_nil(map() | nil) :: map() | nil
```

Drops nil values from a map and returns `nil` if the result is empty,
otherwise returns the cleaned map.

Used by transformers that build optional sub-config blocks where
"no overrides set" should compile down to `nil` rather than an empty
map. Idempotent on `nil` input.

# `dynamic_component`

```elixir
@spec dynamic_component(map()) :: Phoenix.LiveView.Rendered.t()
```

Dynamic component wrapper using apply/3 for proper Phoenix lifecycle.
See: https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#module-dynamic-component-rendering

# `exports?`

```elixir
@spec exports?(module() | nil, atom(), arity()) :: boolean()
```

Whether `module` defines `fun/arity`, loading the module first.

Prefer this over a bare `function_exported?/3`, which only sees modules already in memory.

Returns `false` for `nil`, for a module that does not exist, and for anything that is not an
atom.

    iex> MishkaGervaz.Helpers.exports?(Enum, :map, 2)
    true

    iex> MishkaGervaz.Helpers.exports?(nil, :map, 2)
    false

# `extract_preload_source`

```elixir
@spec extract_preload_source(atom() | {atom(), atom()}) :: atom()
```

Unwrap a preload entry to its source atom. Preloads can be plain atoms or
`{source, alias}` tuples — this returns the atom either way.

# `extract_singleton_entity`

```elixir
@spec extract_singleton_entity(map(), atom()) :: map()
```

Unwraps a Spark singleton sub-entity wrapper into the entity itself.

Spark stores DSL singleton entities under `key` as a single-element
list during parse. After the parent entity's `transform/1` runs, the
list is collapsed to the entity struct (or `nil` if absent). Used by
every entity that owns one or more singleton sub-entities (e.g. `:ui`,
`:preload`, plus `submit`'s `:create` / `:update` / `:cancel`).

Idempotent: if the value is already a struct (transform already ran)
or absent, the map is returned unchanged.

# `find_all_dependents`

```elixir
@spec find_all_dependents(MapSet.t(), list()) :: MapSet.t()
```

The names of every entry that `depends_on` one of `changed_parents`, directly or down the chain.

`entries` are filters or form fields: maps with `:name` and `:depends_on`. The parents themselves
are left out unless another parent depends on them.

## Examples

    iex> fields = [
    ...>   %{name: :site_id, depends_on: nil},
    ...>   %{name: :workspace, depends_on: :site_id},
    ...>   %{name: :version_id, depends_on: :workspace},
    ...>   %{name: :title, depends_on: nil}
    ...> ]
    iex> MishkaGervaz.Helpers.find_all_dependents(MapSet.new([:site_id]), fields)
    MapSet.new([:workspace, :version_id])

# `find_by_name`

```elixir
@spec find_by_name(list() | nil, atom()) :: map() | nil
```

Finds an entity in a list by its name field.

## Examples

    iex> MishkaGervaz.Helpers.find_by_name([%{name: :foo}, %{name: :bar}], :bar)
    %{name: :bar}

    iex> MishkaGervaz.Helpers.find_by_name([%{name: :foo}], :missing)
    nil

    iex> MishkaGervaz.Helpers.find_by_name(nil, :foo)
    nil

# `format_filesize`

```elixir
@spec format_filesize(integer() | nil) :: String.t()
```

Formats a file size in bytes to a human-readable string.

## Examples

    iex> MishkaGervaz.Helpers.format_filesize(500)
    "500 B"

    iex> MishkaGervaz.Helpers.format_filesize(1024)
    "1.0 KB"

    iex> MishkaGervaz.Helpers.format_filesize(1048576)
    "1.0 MB"

    iex> MishkaGervaz.Helpers.format_filesize(nil)
    "-"

# `get_domain`

```elixir
@spec get_domain(module()) :: {:ok, module()} | :error
```

Look up the Ash domain for a resource.

# `get_domain_defaults`

```elixir
@spec get_domain_defaults(module(), atom()) :: map()
```

Fetch the domain-side defaults map for a given section (`:form` or `:table`).
Returns `%{}` when the resource has no domain or the domain has no
`mishka_gervaz` block.

# `get_resource_attributes`

```elixir
@spec get_resource_attributes(module()) :: map()
```

Returns the resource's Ash attributes as a `%{name => attribute_struct}` map.

Used by builders that need O(1) attribute lookup by name (FieldBuilder,
ColumnBuilder).

# `get_ui_label`

```elixir
@spec get_ui_label(map() | struct()) :: String.t()
```

Extracts and resolves a label from a UI structure, with fallback to humanized name.

Similar to `resolve_ui_label/1` but falls back to humanizing the `:name` field
when no UI label is found. The humanized name is translated in the caller's locale.

## Examples

    iex> MishkaGervaz.Helpers.get_ui_label(%{ui: %{label: "Custom"}, name: :field})
    "Custom"

    iex> MishkaGervaz.Helpers.get_ui_label(%{name: :user_name})
    "User Name"

    iex> MishkaGervaz.Helpers.get_ui_label(%{ui: nil, name: :created_at})
    "Created At"

# `get_visible_columns`

```elixir
@spec get_visible_columns([map()], map()) :: [map()]
```

Filters columns based on their visibility setting.

Evaluates the `visible` field of each column:
- Function with arity 1: calls with state and uses the result
- Boolean: uses the value directly
- Any other value: defaults to true (visible)

## Examples

    iex> columns = [%{name: :id, visible: true}, %{name: :secret, visible: false}]
    iex> MishkaGervaz.Helpers.get_visible_columns(columns, %{})
    [%{name: :id, visible: true}]

    iex> columns = [%{name: :admin_only, visible: fn state -> state.master_user? end}]
    iex> MishkaGervaz.Helpers.get_visible_columns(columns, %{master_user?: true})
    [%{name: :admin_only, visible: _}]

    iex> MishkaGervaz.Helpers.get_visible_columns(columns, %{master_user?: false})
    []

# `has_value?`

```elixir
@spec has_value?(any()) :: boolean()
```

Checks if a value is present (not nil, empty string, or empty list).

## Examples

    iex> MishkaGervaz.Helpers.has_value?(nil)
    false

    iex> MishkaGervaz.Helpers.has_value?("")
    false

    iex> MishkaGervaz.Helpers.has_value?([])
    false

    iex> MishkaGervaz.Helpers.has_value?("test")
    true

    iex> MishkaGervaz.Helpers.has_value?(["a", "b"])
    true

# `humanize`

```elixir
@spec humanize(atom() | String.t()) :: String.t()
```

Converts an atom or string to a human-readable label.

## Examples

    iex> MishkaGervaz.Helpers.humanize(:first_name)
    "First Name"

    iex> MishkaGervaz.Helpers.humanize(:user_id)
    "User Id"

    iex> MishkaGervaz.Helpers.humanize("already_formatted")
    "already_formatted"

# `inject_preload_aliases`

```elixir
@spec inject_preload_aliases(struct() | [struct()], map() | nil) ::
  struct() | [struct()]
```

Injects preload alias values into a record or list of records.

When using master/tenant preload patterns (e.g., `master_media_category` aliased to
`media_category`), this function copies the loaded relationship data from the source
field to the alias field.

## Examples

    iex> record = %{id: 1, master_media_category: %{name: "Photos"}}
    iex> MishkaGervaz.Helpers.inject_preload_aliases(record, %{media_category: :master_media_category})
    %{id: 1, master_media_category: %{name: "Photos"}, media_category: %{name: "Photos"}}

    iex> MishkaGervaz.Helpers.inject_preload_aliases([%{id: 1, source: "val"}], %{alias: :source})
    [%{id: 1, source: "val", alias: "val"}]

    iex> MishkaGervaz.Helpers.inject_preload_aliases(%{id: 1}, nil)
    %{id: 1}

    iex> MishkaGervaz.Helpers.inject_preload_aliases(%{id: 1}, %{})
    %{id: 1}

# `invalidate_dependents`

```elixir
@spec invalidate_dependents(map(), map(), map()) :: {map(), map()}
```

Invalidates dependent filter values and relation_filter_state when parent filters change.

Compares old vs new filter values to find changed parents, then recursively finds
all children whose `depends_on` points to a changed filter. Removes those children
from both `filter_values` and `relation_filter_state`.

Returns `{cleaned_filter_values, cleaned_relation_filter_state}`.

## Examples

    iex> filters = [%{name: :region, depends_on: nil}, %{name: :city, depends_on: :region}]
    iex> old = %{region: "us", city: "ny"}
    iex> new = %{region: "eu", city: "ny"}
    iex> state = %{static: %{filters: filters}, relation_filter_state: %{}}
    iex> {cleaned_fv, cleaned_rfs} = MishkaGervaz.Helpers.invalidate_dependents(new, old, state)
    iex> cleaned_fv
    %{region: "eu"}

# `known_name?`

```elixir
@spec known_name?(String.t(), map()) :: boolean()
```

Checks if a string name is a known entity name in the given state.

Pattern matches on the state structure to auto-detect the context:
- Form state (`static.fields`) — checks field names
- Table state (`static.columns`) — checks column names
- Table state (`static.filters`) — checks filter names (with explicit `:filters`)
- Form state (`static.steps`) — checks step names (with explicit `:steps`)
- Form state (`static.uploads`) — checks upload names (with explicit `:uploads`)

## Examples

    iex> state = %{static: %{fields: [%{name: :title}, %{name: :tags}]}}
    iex> MishkaGervaz.Helpers.known_name?("tags", state)
    true

    iex> MishkaGervaz.Helpers.known_name?("unknown", state)
    false

    iex> state = %{static: %{columns: [%{name: :id}, %{name: :status}]}}
    iex> MishkaGervaz.Helpers.known_name?("status", state)
    true

# `known_name?`

```elixir
@spec known_name?(String.t(), map(), :filters | :steps | :uploads) :: boolean()
```

Whether `name` is a declared filter, step or upload on this state.

The guard event handlers use before acting on a name that arrived from the client, so a
crafted `phx-value` cannot reach an entity the resource never declared.

    known_name?("status", state, :filters)

# `map_get`

```elixir
@spec map_get(map() | nil, atom(), term()) :: term()
```

Fetch a key from a possibly-nil map, returning `default` when the key is
missing or the value is nil.

Used by the `Resource.Info.{Form,Table}` and `Domain.Info.{Form,Table}`
introspection modules to absorb the repeating
`case map do %{key: v} -> v; _ -> default end` shape.

# `map_put_if_set`

```elixir
@spec map_put_if_set(map(), atom(), {:ok, any()} | {:error, any()} | :error | any()) ::
  map()
```

Puts a key-value pair into a map only if the value is present and valid.

Designed for building configuration maps where optional values should only
be included when explicitly set. Supports multiple input formats commonly
used with Spark DSL introspection functions.

## Supported Formats

- `{:ok, value}` - Spark/Ash introspection result (value must not be nil)
- `{:error, _}` - Ignored, returns original map
- `:error` - Ignored, returns original map
- Direct value - Added if not nil

## Examples

    iex> %{} |> MishkaGervaz.Helpers.map_put_if_set(:name, {:ok, "John"})
    %{name: "John"}

    iex> %{} |> MishkaGervaz.Helpers.map_put_if_set(:name, {:ok, nil})
    %{}

    iex> %{} |> MishkaGervaz.Helpers.map_put_if_set(:name, :error)
    %{}

    iex> %{} |> MishkaGervaz.Helpers.map_put_if_set(:name, {:error, :not_found})
    %{}

    iex> %{} |> MishkaGervaz.Helpers.map_put_if_set(:name, "John")
    %{name: "John"}

    iex> %{} |> MishkaGervaz.Helpers.map_put_if_set(:name, nil)
    %{}

    iex> %{a: 1} |> MishkaGervaz.Helpers.map_put_if_set(:b, {:ok, 2}) |> MishkaGervaz.Helpers.map_put_if_set(:c, {:ok, 3})
    %{a: 1, b: 2, c: 3}

# `master_user?`

```elixir
@spec master_user?(map() | struct() | nil) :: boolean()
```

Returns `true` when the user has no tenant — i.e. is a master user.

Canonical default for `master_user?/1` across the framework. The tenant
attribute is currently `:site_id`.

# `maybe_assign`

```elixir
@spec maybe_assign(map(), atom(), any()) :: map()
```

Conditionally assigns a key-value pair to assigns only if the value is not nil.

This is useful when building assigns for components where you want to allow
upstream defaults (via `assign_new`) to take effect when no value is provided.

## Examples

    iex> %{} |> Phoenix.Component.assign(:foo, "bar") |> MishkaGervaz.Helpers.maybe_assign(:icon, nil)
    %{foo: "bar"}

    iex> %{} |> Phoenix.Component.assign(:foo, "bar") |> MishkaGervaz.Helpers.maybe_assign(:icon, "hero-trash")
    %{foo: "bar", icon: "hero-trash"}

# `merge_relation_field_values`

```elixir
@spec merge_relation_field_values(map(), map()) :: map()
```

Merges form-state relation field values into a params map.

For each relation field with a value in `state.field_values`, places the
value at the string-keyed slot in `params`. The sentinel `"__nil__"` is
rewritten to a real `nil` so AshPhoenix can clear the relation. Empty
string and `nil` values are ignored.

Used by validation/submit handlers to ensure relation selections survive
param re-merging.

# `missing_actions`

```elixir
@spec missing_actions(
  module() | map(),
  atom(),
  atom() | {atom(), atom()} | nil,
  atom()
) :: [map()]
```

The actions `value` names that `resource` does not have as a `type` action.

`value` is an action name, a `{master_action, tenant_action}` tuple or `nil`. `resource` is a
resource module or a resource's DSL state. Each entry is a map with `:key`, `:value`, `:name`,
`:type` and `:found` — `nil` when the resource has no action with that name, otherwise the type of
the action that has it.

    missing_actions(MyApp.Post, :update, {:master_update, :update}, :update)
    #=> [%{key: :update, value: {:master_update, :update}, name: :master_update,
    #      type: :update, found: nil}]

# `missing_actions_message`

```elixir
@spec missing_actions_message(module(), String.t(), [map()], String.t()) :: String.t()
```

The compile error for the entries `missing_actions/4` returned, each with an `:origin` of
`:resource`, `{:domain, domain}` or `:default`.

`what` names the DSL block (`"form"`, `"table"`) and `fix` is the closing paragraph telling the
reader where to name the actions instead.

# `module_to_snake`

```elixir
@spec module_to_snake(module(), String.t()) :: String.t()
```

Converts a module name to its snake_case short name.

Takes the last part of a module name and converts it to snake_case.
Optionally appends a suffix.

## Examples

    iex> MishkaGervaz.Helpers.module_to_snake(MyApp.Users.User)
    "user"

    iex> MishkaGervaz.Helpers.module_to_snake(MyApp.BlogPost)
    "blog_post"

    iex> MishkaGervaz.Helpers.module_to_snake(MyApp.Users.User, "_stream")
    "user_stream"

    iex> MishkaGervaz.Helpers.module_to_snake(MyApp.BlogPost, "_table")
    "blog_post_table"

# `normalize_id_type`

```elixir
@spec normalize_id_type(any()) :: :uuid | :uuid_v7 | :integer | :string
```

Normalizes an Ash primary-key type to one of `:uuid`, `:uuid_v7`,
`:integer`, or `:string`. Falls back to `:uuid` for anything else.

Recognises the bare atom forms (`:uuid`, `:integer`, `:string`), the
`Ash.Type.*` modules, and name variants such as `Ash.Type.UUIDv7`,
which are matched by name string.

# `normalize_options`

```elixir
@spec normalize_options(list() | nil) :: [{String.t(), String.t()}]
```

Normalizes a list of options for HTML select elements.

Converts various option formats to `{label, value}` tuples with string values,
ensuring compatibility with Phoenix HTML attributes.

## Examples

    iex> MishkaGervaz.Helpers.normalize_options([{"API Only", :api_only}, {"Hybrid", :hybrid}])
    [{"API Only", "api_only"}, {"Hybrid", "hybrid"}]

    iex> MishkaGervaz.Helpers.normalize_options([:active, :inactive])
    [{"Active", "active"}, {"Inactive", "inactive"}]

    iex> MishkaGervaz.Helpers.normalize_options(["foo", "bar"])
    [{"foo", "foo"}, {"bar", "bar"}]

    iex> MishkaGervaz.Helpers.normalize_options([[label: "Admin", value: "admin"]])
    [{"Admin", "admin"}]

    iex> MishkaGervaz.Helpers.normalize_options(nil)
    []

# `normalize_selected_values`

```elixir
@spec normalize_selected_values(list() | any() | nil) :: [String.t()]
```

Normalizes selected values for multi-select components.

Converts various input formats to a list of non-empty string values,
filtering out empty strings and "nil" string representations.

## Examples

    iex> MishkaGervaz.Helpers.normalize_selected_values(nil)
    []

    iex> MishkaGervaz.Helpers.normalize_selected_values(["a", "b", "c"])
    ["a", "b", "c"]

    iex> MishkaGervaz.Helpers.normalize_selected_values([:foo, :bar])
    ["foo", "bar"]

    iex> MishkaGervaz.Helpers.normalize_selected_values(["valid", "", nil, "nil"])
    ["valid"]

    iex> MishkaGervaz.Helpers.normalize_selected_values("single")
    ["single"]

# `primary_key_type`

```elixir
@spec primary_key_type(module()) :: :uuid | :uuid_v7 | :integer | :string
```

Returns the normalized type of `resource`'s primary key as one of
`:uuid`, `:uuid_v7`, `:integer`, or `:string`. Defaults to `:uuid`
when introspection fails or the resource has no primary key.

# `relation_id_type`

```elixir
@spec relation_id_type(map(), module() | nil) ::
  :uuid | :uuid_v7 | :integer | :string | nil
```

Returns the normalized id-type for a `:relation` entity, defaulting
to `:uuid` when the related resource cannot be found. A `:relation`
entity with no related resource and its own `options` list is
`:string` — its values are the list's, not a resource's keys.

Combines `relation_target_resource/2` and `primary_key_type/1`. For
non-relation entities returns `nil`.

# `relation_target_resource`

```elixir
@spec relation_target_resource(map(), module() | nil) :: module() | nil
```

Resolves the destination resource of a relation field/filter.

Accepts any map with `:name` / `:source` / `:resource` keys (the
shape Form.Field and Table.Filter both have):

  * If `:resource` is set on the entity, returns it directly.
  * Otherwise looks up `parent_resource`'s Ash relationships for the
    one whose `source_attribute` matches `entity.source || entity.name`
    and returns its `:destination`.
  * Returns `nil` when no match is found, when `parent_resource` is
    `nil`, or on any introspection failure.

# `resolve_bulk_confirm`

```elixir
@spec resolve_bulk_confirm(boolean() | String.t() | nil, non_neg_integer() | nil) ::
  boolean() | String.t() | nil
```

Resolves a bulk action's `confirm`.

A string is translated in the caller's locale (`translate_text/1`), then every `{count}` in it
becomes `count`. With no `count`, the text keeps `{count}` as written. Anything else is returned
as it is.

## Examples

    iex> MishkaGervaz.Helpers.resolve_bulk_confirm("Delete {count} posts?", 3)
    "Delete 3 posts?"

    iex> MishkaGervaz.Helpers.resolve_bulk_confirm("Delete {count} posts?", nil)
    "Delete {count} posts?"

    iex> MishkaGervaz.Helpers.resolve_bulk_confirm("Sure?", 3)
    "Sure?"

    iex> MishkaGervaz.Helpers.resolve_bulk_confirm(nil, 3)
    nil

# `resolve_confirm`

```elixir
@spec resolve_confirm(String.t() | (map() -&gt; String.t()) | nil, map()) ::
  String.t() | nil
```

Resolves an action's `confirm` — the sibling of `resolve_label/1` for a message that needs the
record.

`confirm` is declared as `String.t() | (map() -> String.t()) | nil`; the function form lets a
prompt name what it is about to act on ("Delete the draft “Autosave”?"). A string is translated
in the caller's locale (`translate_text/1`); a function's result is returned as it is.

## Examples

    iex> MishkaGervaz.Helpers.resolve_confirm("Sure?", %{id: 1})
    "Sure?"

    iex> MishkaGervaz.Helpers.resolve_confirm(fn r -> "Delete #{r.name}?" end, %{name: "Cat"})
    "Delete Cat?"

    iex> MishkaGervaz.Helpers.resolve_confirm(nil, %{id: 1})
    nil

# `resolve_dynamic`

```elixir
@spec resolve_dynamic(term(), map()) :: term()
```

Resolves a header, footer or notice text: a string (translated), `fn -> text end`, or
`fn state -> text end`.

## Examples

    iex> MishkaGervaz.Helpers.resolve_dynamic("A heading", %{})
    "A heading"

    iex> MishkaGervaz.Helpers.resolve_dynamic(fn state -> state.title end, %{title: "From state"})
    "From state"

    iex> MishkaGervaz.Helpers.resolve_dynamic(nil, %{})
    nil

# `resolve_label`

```elixir
@spec resolve_label(String.t() | (-&gt; String.t()) | nil) :: String.t() | nil
```

Resolves a label that may be a string or a zero-arity function.

A string is translated in the caller's locale (`translate_text/1`). A function is called and its
result returned as it is, so `fn -> gettext("...") end` defers the lookup to runtime.

## Examples

    iex> MishkaGervaz.Helpers.resolve_label("Static Label")
    "Static Label"

    iex> MishkaGervaz.Helpers.resolve_label(fn -> "Dynamic Label" end)
    "Dynamic Label"

    iex> MishkaGervaz.Helpers.resolve_label(nil)
    nil

# `resolve_label`

```elixir
@spec resolve_label(
  struct(),
  atom() | (struct() -&gt; String.t()) | (struct(), map() -&gt; String.t())
) ::
  String.t()
```

Resolves a display value from a record using either an atom field or a function.

Supports:
- 2-arity: `resolve_label(record, display_field)` - for atom or 1-arity function
- 3-arity: `resolve_label(record, display_field, state)` - for 2-arity function

## Examples

    iex> MishkaGervaz.Helpers.resolve_label(%{name: "Test"}, :name)
    "Test"

    iex> MishkaGervaz.Helpers.resolve_label(%{name: "Cat", site: %{name: "Site1"}}, fn r -> "#{r.name} - #{r.site.name}" end)
    "Cat - Site1"

    iex> MishkaGervaz.Helpers.resolve_label(%{id: 123, name: nil}, :name)
    "123"

# `resolve_label`

```elixir
@spec resolve_label(struct(), (struct(), map() -&gt; String.t()), map()) :: String.t()
```

A relation option's label, with the live state available to the `display_field`.

Accepts every shape `display_field` may take — an attribute name, `fn record -> …`, or
`fn record, state -> …` for a label that differs by role.

    resolve_label(site, :name, state)

    resolve_label(cat, fn cat, state ->
      if state.master_user?, do: cat.name <> " — " <> cat.site.name, else: cat.name
    end, state)

# `resolve_options`

```elixir
@spec resolve_options(list() | (-&gt; list()) | nil) :: list()
```

Resolves options that may be a list or a zero-arity function returning a list.

This enables dynamic options (e.g., from a database query) in DSL fields by
allowing users to pass `fn -> query_options() end` which defers execution to runtime.

## Examples

    iex> MishkaGervaz.Helpers.resolve_options([{"A", "a"}, {"B", "b"}])
    [{"A", "a"}, {"B", "b"}]

    iex> MishkaGervaz.Helpers.resolve_options(fn -> [{"X", "x"}] end)
    [{"X", "x"}]

    iex> MishkaGervaz.Helpers.resolve_options(nil)
    []

# `resolve_translated_options`

```elixir
@spec resolve_translated_options(list() | (-&gt; list()) | nil) :: list()
```

`resolve_options/1` for the options a resource declared: the labels of a list are translated in
the caller's locale (`translate_options/1`), the result of a function is returned as it is.

## Examples

    iex> MishkaGervaz.Helpers.resolve_translated_options([{"A", "a"}])
    [{"A", "a"}]

    iex> MishkaGervaz.Helpers.resolve_translated_options(fn -> [{"X", "x"}] end)
    [{"X", "x"}]

    iex> MishkaGervaz.Helpers.resolve_translated_options(nil)
    []

# `resolve_ui_label`

```elixir
@spec resolve_ui_label(map() | struct() | nil) :: String.t() | nil
```

Resolves a label from a nested UI structure.

Extracts the label from entities that have a `ui` field containing a `label`.
Supports both map and struct formats, with labels that can be strings (translated in the
caller's locale) or zero-arity functions (called, their result returned as it is).

## Examples

    iex> MishkaGervaz.Helpers.resolve_ui_label(%{ui: %{label: "Name"}})
    "Name"

    iex> MishkaGervaz.Helpers.resolve_ui_label(%{ui: %{label: fn -> "Dynamic" end}})
    "Dynamic"

    iex> MishkaGervaz.Helpers.resolve_ui_label(%{ui: nil})
    nil

    iex> MishkaGervaz.Helpers.resolve_ui_label(%{other: "field"})
    nil

    iex> MishkaGervaz.Helpers.resolve_ui_label(nil)
    nil

# `strip_tags`

```elixir
@spec strip_tags(term()) :: term()
```

`value` with every HTML tag taken out, by `HtmlSanitizeEx.strip_tags/1`, and its text as typed:
`&`, `<` and `>` are not escaped, and an entity typed into it, such as `&amp;`, is kept as typed.
Tags are taken out until none is left, so a tag hidden inside a tag goes too. Anything but a
string is returned as it is.

## Examples

    iex> MishkaGervaz.Helpers.strip_tags(~s(Tom & Jerry's <b>"cat"</b>))
    ~s(Tom & Jerry's "cat")

    iex> MishkaGervaz.Helpers.strip_tags("<<b>b>")
    ""

    iex> MishkaGervaz.Helpers.strip_tags(42)
    42

# `to_boolean`

```elixir
@spec to_boolean(String.t() | boolean() | nil) :: boolean() | nil
```

Converts string boolean representations to actual booleans.

Safely handles values coming from URL params, form inputs, or other
string-based sources. Follows Phoenix's `normalize_value/2` pattern.

## Examples

    iex> MishkaGervaz.Helpers.to_boolean("true")
    true

    iex> MishkaGervaz.Helpers.to_boolean("false")
    false

    iex> MishkaGervaz.Helpers.to_boolean(true)
    true

    iex> MishkaGervaz.Helpers.to_boolean(nil)
    nil

    iex> MishkaGervaz.Helpers.to_boolean("")
    nil

# `translate_options`

```elixir
@spec translate_options(list()) :: list()
```

Translates the label of every option in the caller's locale (`translate_text/1`).

Takes the shapes a select accepts: `{label, value}`, `[label: label, value: value]`, a bare
value that is its own label, and `{group_label, options}` for a group, whose label and options
are translated. A bare value or keyword option becomes `{label, value}`. The value of an option
is never translated, and a shape this does not know is returned as it is.

## Examples

    iex> MishkaGervaz.Helpers.translate_options([{"Open", :open}, :on_hold, [label: "Gone", value: "gone"]])
    [{"Open", :open}, {"On Hold", "on_hold"}, {"Gone", "gone"}]

    iex> MishkaGervaz.Helpers.translate_options([{"Group", [{"A", "a"}]}])
    [{"Group", [{"A", "a"}]}]

# `translate_text`

```elixir
@spec translate_text(term()) :: term()
```

Translates a string a resource declared in its DSL, in the caller's locale.

See `MishkaGervaz.Messages.translate_text/1`.

## Examples

    iex> MishkaGervaz.Helpers.translate_text("Static Label")
    "Static Label"

    iex> MishkaGervaz.Helpers.translate_text(nil)
    nil

# `user_tenant`

```elixir
@spec user_tenant(map() | struct() | nil) :: any()
```

Extracts the tenant value from a user map/struct (currently `:site_id`).

Returns `nil` for nil users or users without the tenant attribute.

# `validate_field_errors`

```elixir
@spec validate_field_errors(AshPhoenix.Form.t(), map(), map() | nil) :: {map(), map()}
```

Validates a form and returns per-field errors for the currently-changing field only.

Designed for use in `on_validate` hooks to provide live inline errors without
showing unrelated required-field errors for untouched fields.

Extracts `_target` from `params` to identify the current field, validates via
`AshPhoenix.Form.validate/3`, then filters errors to only that field.

Returns `{params, errors}` ready to return directly from an `on_validate` hook.

## Examples

    hooks do
      on_validate fn params, state ->
        MishkaGervaz.Helpers.validate_field_errors(state.form.source, params)
      end
    end

    # With param mutation before validation:
    hooks do
      on_validate fn params, state ->
        updated = put_in(params, ["form", "slug"], slugify(params["form"]["title"]))
        MishkaGervaz.Helpers.validate_field_errors(state.form.source, updated)
      end
    end

---

*Consult [api-reference.md](api-reference.md) for complete listing*
