Localize.Unit.CustomRegistry (Localize v1.0.0-rc.5)

Copy Markdown View Source

Runtime registry for user-defined units backed by :persistent_term.

Custom units are stored in a single map keyed by unit name. The registry is checked by Localize.Unit.Data, Localize.Unit.BaseUnit, Localize.Unit.Conversion, and the unit formatter to overlay runtime definitions on top of the compile-time CLDR data.

Definition structure

Each definition is a map with the following keys:

  • :base_unit (required) — the CLDR unit this custom unit converts to (e.g., "meter", "kilogram", "second"). It need not be a fundamental base unit: a derived unit such as "day" is accepted and folded down to its fundamental base ("second") at registration, with the factor and offset adjusted accordingly, so the custom unit stays convertible against every CLDR unit in the same category.

  • :factor (required) — the conversion factor: 1 custom_unit = factor * base_unit.

  • :offset (optional) — additive offset for the conversion. Defaults to 0.0.

  • :category (required) — the unit category (e.g., "length", "mass").

  • :display (optional) — locale-specific display patterns. A nested map of locale => style => plural_patterns.

Summary

Functions

Returns all registered custom units as a map of name to definition.

Removes all custom unit registrations. Primarily for testing.

Returns the definition for a custom unit, or nil if not registered.

Loads custom unit definitions from an .exs file.

Registers a custom unit definition.

Registers multiple custom units in a single persistent_term update.

Returns whether a unit name is registered in the custom registry.

Functions

all()

@spec all() :: %{required(String.t()) => map()}

Returns all registered custom units as a map of name to definition.

Returns

  • A map of unit name to definition map. Empty when no custom units are registered.

Examples

iex> :ok = Localize.Unit.CustomRegistry.register("smoot", %{base_unit: "meter", factor: 1.7018, category: "length"})
iex> Localize.Unit.CustomRegistry.all()["smoot"].factor
1.7018

clear()

@spec clear() :: :ok

Removes all custom unit registrations. Primarily for testing.

Returns

  • :ok.

Examples

iex> :ok = Localize.Unit.CustomRegistry.register("smoot", %{base_unit: "meter", factor: 1.7018, category: "length"})
iex> Localize.Unit.CustomRegistry.clear()
:ok
iex> Localize.Unit.CustomRegistry.registered?("smoot")
false

get(unit_name)

@spec get(String.t()) :: map() | nil

Returns the definition for a custom unit, or nil if not registered.

Arguments

  • unit_name — the unit identifier string.

Returns

  • A definition map or nil.

Examples

iex> :ok = Localize.Unit.CustomRegistry.register("smoot", %{base_unit: "meter", factor: 1.7018, category: "length"})
iex> Localize.Unit.CustomRegistry.get("smoot").base_unit
"meter"

iex> Localize.Unit.CustomRegistry.get("no-such-unit")
nil

load_file(path)

@spec load_file(String.t()) :: {:ok, non_neg_integer()} | {:error, String.t()}

Loads custom unit definitions from an .exs file.

The file must evaluate to a list of maps, each with a :unit key and the standard definition fields.

Security

This function uses Code.eval_file/1 to evaluate the given file, which executes arbitrary Elixir code. Only load files from trusted sources. Never call this function with unsanitised user input or paths derived from external data.

In a :prod Mix environment this function additionally requires the :localize, :allow_runtime_unit_files config flag to be explicitly set to true. The flag exists so that an unintended feature switch in a production deployment cannot accidentally surface arbitrary code execution. Set it in config/runtime.exs (not config/config.exs) so the decision is visible at the same layer as other deployment-time policy.

Arguments

  • path — path to the .exs file.

Returns

  • {:ok, count} with the number of units loaded.

  • {:error, reason} on failure, including a refusal in :prod when the flag is not set.

Examples

iex> {:error, message} = Localize.Unit.CustomRegistry.load_file("no/such/file.exs")
iex> message =~ "file not found"
true

register(name, definition)

@spec register(String.t(), map()) :: :ok | {:error, String.t()}

Registers a custom unit definition.

Arguments

  • name — the unit identifier string.

  • definition — a map with :base_unit, :factor, and :category keys.

Returns

  • :ok on success.

  • {:error, reason} if validation fails.

Examples

iex> Localize.Unit.CustomRegistry.register("smoot", %{base_unit: "meter", factor: 1.7018, category: "length"})
:ok

iex> {:error, message} = Localize.Unit.CustomRegistry.register("123bad", %{base_unit: "meter", factor: 1.0, category: "length"})
iex> message =~ "invalid unit name"
true

register_batch(definitions)

@spec register_batch(%{required(String.t()) => map()}) :: {:ok, non_neg_integer()}

Registers multiple custom units in a single persistent_term update.

This is significantly more memory-efficient than calling register/2 in a loop, because it avoids creating intermediate persistent_term snapshots for each unit. Each snapshot is stored in the BEAM's literal area and is not freed until a global garbage collection sweep, so bulk registration via register/2 can exhaust literal memory.

Arguments

  • definitions — a map of %{name => definition} where each definition has :base_unit, :factor, and :category keys.

Returns

  • {:ok, count} with the number of units registered.

  • {:error, reason} if any validation fails. No units are registered on error (the operation is atomic).

Examples

iex> definitions = %{
...>   "smoot" => %{base_unit: "meter", factor: 1.7018, category: "length"},
...>   "sheppey" => %{base_unit: "meter", factor: 1400.0, category: "length"}
...> }
iex> Localize.Unit.CustomRegistry.register_batch(definitions)
{:ok, 2}

registered?(unit_name)

@spec registered?(String.t()) :: boolean()

Returns whether a unit name is registered in the custom registry.

Arguments

  • unit_name — the unit identifier string.

Returns

  • true if the unit is registered, otherwise false.

Examples

iex> Localize.Unit.CustomRegistry.registered?("no-such-unit")
false

iex> :ok = Localize.Unit.CustomRegistry.register("smoot", %{base_unit: "meter", factor: 1.7018, category: "length"})
iex> Localize.Unit.CustomRegistry.registered?("smoot")
true