# `MishkaGervaz.Helpers`
[🔗](https://github.com/mishka-group/mishka_gervaz/blob/v0.0.1-alpha.3/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` with non-master user → not accessible
- `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?(%{visible: false}, %{})
    false

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

# `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

# `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_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.

## 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`)

Avoids `String.to_existing_atom/1` and rescue blocks for safe user input validation.

## 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()
```

# `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.

# `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,
matching the conservative default Phoenix uses for `<input>` shapes.

Recognises both the bare atom forms (`:uuid`, `:integer`, `:string`)
and the `Ash.Type.*` modules. Future `Ash.Type.UUIDv7` / `UUID7` /
`UUID` / `Integer` modules are matched by name string so a Spark
release that adds new vendor variants (`Ash.Type.UUIDv7Foo`) still
routes correctly.

# `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(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.

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_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.

This enables i18n support in DSL labels by allowing users to pass
`fn -> gettext("...") end` which defers execution 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()
```

# `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_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 or
zero-arity functions (for i18n support).

## 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

# `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

# `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*
