Ootempl.DataAccess (ootempl v0.3.0)

Provides data access for nested Elixir data structures with case-insensitive key matching.

This module enables retrieval of values from nested maps and lists using dot notation paths, with automatic case-insensitive key matching and type conversion to strings.

Examples

iex> data = %{"name" => "John", "age" => 30}
iex> Ootempl.DataAccess.get_value(data, ["name"])
{:ok, "John"}

iex> data = %{"customer" => %{"name" => "Jane"}}
iex> Ootempl.DataAccess.get_value(data, ["customer", "name"])
{:ok, "Jane"}

iex> data = %{"count" => 42}
iex> Ootempl.DataAccess.get_value(data, ["count"])
{:ok, "42"}

iex> data = %{"items" => [%{"price" => 99.99}]}
iex> Ootempl.DataAccess.get_value(data, ["items", "0", "price"])
{:ok, "99.99"}

Case-Insensitive Matching

The module matches keys case-insensitively, so {{Name}}, {{name}}, and {{NAME}} all match the same data key:

iex> data = %{"name" => "John"}
iex> Ootempl.DataAccess.get_value(data, ["Name"])
{:ok, "John"}

If multiple case variants exist, an error is returned:

iex> data = %{"name" => "John", "Name" => "Jane"}
iex> Ootempl.DataAccess.get_value(data, ["name"])
{:error, {:ambiguous_key, "name", ["Name", "name"]}}

Summary

Functions

Retrieves the raw (un-stringified) value from nested data using a path.

Retrieves a value from nested data using a path with case-insensitive key matching.

Converts a resolved value to its string representation for the document.

Types

data()

@type data() :: map() | list()

error_reason()

@type error_reason() ::
  {:path_not_found, path()}
  | {:ambiguous_key, String.t(), [String.t() | atom()]}
  | {:conflicting_key_types, String.t(), atom(), String.t()}
  | {:invalid_index, String.t()}
  | {:index_out_of_bounds, non_neg_integer(), non_neg_integer()}
  | {:not_a_list, term()}
  | :nil_value
  | :unsupported_type

path()

@type path() :: [String.t()]

Functions

get_raw_value(data, path)

@spec get_raw_value(data(), path()) :: {:ok, term()} | {:error, error_reason()}

Retrieves the raw (un-stringified) value from nested data using a path.

Unlike get_value/2, this returns the value with its original Elixir type intact (e.g. a %Date{} stays a %Date{}, a number stays a number, nil is returned as {:ok, nil}). This is used by the filter pipeline so that filters can operate on real types before the value is converted to a string.

Parameters

  • data - The data structure to traverse (map or list)
  • path - List of path segments to navigate

Returns

  • {:ok, term} - The raw value
  • {:error, reason} - Error with details about what went wrong

Examples

iex> Ootempl.DataAccess.get_raw_value(%{"count" => 5}, ["count"])
{:ok, 5}

iex> Ootempl.DataAccess.get_raw_value(%{"name" => nil}, ["name"])
{:ok, nil}

iex> Ootempl.DataAccess.get_raw_value(%{}, ["missing"])
{:error, {:path_not_found, ["missing"]}}

get_value(data, path)

@spec get_value(data(), path()) :: {:ok, String.t()} | {:error, error_reason()}

Retrieves a value from nested data using a path with case-insensitive key matching.

Returns {:ok, value} where the value is converted to a string, or {:error, reason} if the path cannot be resolved.

Parameters

  • data - The data structure to traverse (map or list)
  • path - List of path segments to navigate (e.g., ["customer", "name"])

Returns

  • {:ok, string} - The value converted to a string
  • {:error, reason} - Error with details about what went wrong

Examples

iex> Ootempl.DataAccess.get_value(%{"name" => "John"}, ["name"])
{:ok, "John"}

iex> Ootempl.DataAccess.get_value(%{"count" => 5}, ["count"])
{:ok, "5"}

iex> Ootempl.DataAccess.get_value(%{"active" => true}, ["active"])
{:ok, "true"}

iex> Ootempl.DataAccess.get_value(%{}, ["missing"])
{:error, {:path_not_found, ["missing"]}}

to_string_value(value)

@spec to_string_value(term()) ::
  {:ok, String.t()} | {:error, :nil_value | :unsupported_type}

Converts a resolved value to its string representation for the document.

Every value type we support has a sensible default rendering, so a value used without a formatting filter still produces output:

  • binaries are used as-is
  • numbers and booleans are stringified
  • Date, Time, NaiveDateTime, and DateTime use ISO-style defaults (matching the date/time/datetime filters)
  • any other struct implementing String.Chars (e.g. Decimal) uses it

Returns {:error, :nil_value} for nil, and {:error, :unsupported_type} for values with no string representation (maps, lists, structs that don't implement String.Chars) — these usually indicate a path that stopped short of a leaf value.