erli18n_http_apply (erli18n v0.7.0)

Copy Markdown View Source

Internal effectful runner shared by the optional web adapters.

This module ties the pure negotiation core (erli18n_http:negotiate_locale/3) to the per-request side effects (erli18n:setlocale/1 plus optional logger process metadata), parameterized by a per-source candidate-extraction callback each adapter supplies. It is the shared body of erli18n_cowboy:execute/2 and erli18n_elli:preprocess/2.

It makes zero framework calls — it never touches cowboy_req / elli_request. The only external module it invokes is the extraction fun the caller passes, and that fun is where the framework seam (and its scoped suppressions) lives, in the adapter. So this module needs no suppressions of any kind: every call it makes (erli18n:loaded_locales/0, erli18n:default_locale/0, erli18n:setlocale/1, erli18n_http:negotiate_locale/3, logger:update_process_metadata/1) resolves to an in-tree or kernel/stdlib module that xref, dialyzer, and eqwalizer all see.

Purity contrast: erli18n_http stays pure (negotiation only — no setlocale, no logger, no I/O); erli18n_http_apply is its effectful sibling (it does setlocale and logger) but remains framework-agnostic.

Source extraction itself is lazy and short-circuiting: run/2 drives erli18n_http:negotiate_locale_lazy/4, which calls the per-source Extract callback only when a source is reached and stops at the first source that yields a supported locale, so a higher-precedence winner means the later sources are never extracted. The available and default option defaults are likewise resolved lazily via thunks: erli18n:loaded_locales/0 is forced only once a source yields a non-empty value and erli18n:default_locale/0 only on a total miss, so an explicitly-supplied available/default is truly zero-cost.

Summary

Types

A per-source extraction callback supplied by an adapter: given a source tag and the untrusted per-request option map (raw_options/0, exactly as the operator configured it), it returns the raw candidate value (or undefined). This fun is the ONLY thing that touches the framework request, so the seam (and its scoped suppressions) stays in the adapter, never here. The callback reads only the non-value-typed keys it needs (query_param, cookie_name, cowboy's path_binding) with its own per-read guards, so it tolerates a malformed value fail-soft. run/2 adapts it to the arity-1 erli18n_http:extract_fun/0 consumed by the lazy engine by closing over the raw Opts.

Framework-agnostic per-request options for run/2, owned by this core module and shared by the adapters (erli18n_cowboy extends it with path_binding; erli18n_elli references it directly). Every key is optional; an omitted key falls back to the documented default, and available/default are resolved lazily so an explicitly-supplied value is zero-cost.

The untrusted per-request option map exactly as it arrives from the operator's configuration (the erli18n key of Cowboy's env, or an Elli middleware Args map). Unlike options/0 — which is the well-typed shape the runner relies on internally — every value here may be arbitrary: an operator may have written a non-binary default or a non-list available. run/2 accepts this wider type and validate_options/1 narrows it to options/0 at the request boundary, dropping any malformed value (fail-soft-and-observable). Modelled as an arbitrary map (any key, any value) — the unconstrained shape an operator map actually has at the boundary — so the malformed-value clauses of the validators are reachable and statically checked, and a generic map() from the adapter (e.g. Cowboy's env value) assigns to it without a narrowing cast.

Functions

Resolves the request locale and applies it as side effects.

Types

extract()

-type extract() :: fun((erli18n_http:source(), raw_options()) -> binary() | undefined).

A per-source extraction callback supplied by an adapter: given a source tag and the untrusted per-request option map (raw_options/0, exactly as the operator configured it), it returns the raw candidate value (or undefined). This fun is the ONLY thing that touches the framework request, so the seam (and its scoped suppressions) stays in the adapter, never here. The callback reads only the non-value-typed keys it needs (query_param, cookie_name, cowboy's path_binding) with its own per-read guards, so it tolerates a malformed value fail-soft. run/2 adapts it to the arity-1 erli18n_http:extract_fun/0 consumed by the lazy engine by closing over the raw Opts.

options()

-type options() ::
          #{sources => [erli18n_http:source()],
            query_param => binary(),
            cookie_name => binary(),
            available => [erli18n_negotiate:locale()],
            default => erli18n_negotiate:locale(),
            set_logger_metadata => boolean()}.

Framework-agnostic per-request options for run/2, owned by this core module and shared by the adapters (erli18n_cowboy extends it with path_binding; erli18n_elli references it directly). Every key is optional; an omitted key falls back to the documented default, and available/default are resolved lazily so an explicitly-supplied value is zero-cost.

  • sources — precedence order, highest first (default [query, cookie, header]).
  • query_param — query-string parameter carrying a locale (adapter default <<"locale">>).
  • cookie_name — cookie carrying a locale (adapter default <<"locale">>).
  • available — the authoritative supported-locale set (default erli18n:loaded_locales/0).
  • default — fallback when nothing matches (default erli18n:default_locale/0).
  • set_logger_metadata — also set logger process metadata #{locale => L} (default true).

This is the precise set of keys the core runner itself reads; every key is optional. An adapter that needs an extra key (e.g. cowboy's path_binding) declares its own options/0 extending this shape and reads that key in its own extraction callback — the core never reads it, so it stays out of this type. run/2 reads each key with a default, so a user map that omits some (or carries extra, adapter-specific) keys is consumed safely. Beyond omitted keys, malformed option values are consumed safely too: run/2 validates the available and default values at the request boundary (see validate_options/1); a non-binary default or a non-list / bad-element available is dropped — so the documented default (erli18n:default_locale/0 / erli18n:loaded_locales/0) applies — and the misconfiguration is reported once via logger:warning/2. So operator misconfiguration is fail-soft-and-observable, never request-fatal: no user-supplied option value can crash the request process.

raw_options()

-type raw_options() :: #{term() => term()}.

The untrusted per-request option map exactly as it arrives from the operator's configuration (the erli18n key of Cowboy's env, or an Elli middleware Args map). Unlike options/0 — which is the well-typed shape the runner relies on internally — every value here may be arbitrary: an operator may have written a non-binary default or a non-list available. run/2 accepts this wider type and validate_options/1 narrows it to options/0 at the request boundary, dropping any malformed value (fail-soft-and-observable). Modelled as an arbitrary map (any key, any value) — the unconstrained shape an operator map actually has at the boundary — so the malformed-value clauses of the validators are reachable and statically checked, and a generic map() from the adapter (e.g. Cowboy's env value) assigns to it without a narrowing cast.

Functions

run(Extract, Opts0)

Resolves the request locale and applies it as side effects.

Given an adapter's per-source Extract callback and its Opts, this delegates to erli18n_http:negotiate_locale_lazy/4, which extracts each source on demand in sources order and stops at the first source that yields a supported locale, then sets the locale on the calling process via erli18n:setlocale/1, optionally updates logger process metadata, and returns the chosen locale.

The sources, available, and default defaults are resolved lazily from Opts: available (default erli18n:loaded_locales/0) is forced only when a source actually yields a non-empty value, default (default erli18n:default_locale/0) only on a total miss, and an explicitly-supplied available/default is never recomputed (truly zero-cost). set_logger_metadata defaults to true.

Opts is first passed through validate_options/1, which drops any malformed available/default value (falling back to the documented default and emitting one logger:warning/2), so an operator misconfiguration is fail-soft-and-observable rather than request-fatal.