Dowser.Client.Codec.Builder (Dowser.Client v0.1.1)

View Source

Builds a load/2 + dump/2 dispatcher from a list of Dowser.Client.Field mappings.

use Dowser.Client.Codec.Builder turns a module into a Dowser.Client.Field- shaped dispatcher: it exposes a cast/2 macro to declare, for a pattern matched against the second argument (typically field metadata), which Dowser.Client.Field module handles it. At compile time this expands into plain load/2/dump/2 function clauses — one pattern-matched clause per cast/2, dispatching straight to the field module, with no indirection at runtime.

A module built this way implements Dowser.Client.Field's per-value contract (load/2/dump/2) — it is not a Dowser.Client.Codec and can't be set directly as :codec_adapter, which casts a whole body (encode/2/decode/2). See the bridging example at the bottom of this moduledoc, and Dowser.Client.Codec for the full :codec_adapter contract.

defmodule Dowser.Elasticsearch.Fields.Date do
  @behaviour Dowser.Client.Field

  @impl true
  def load(value, %{"format" => "strict_date"}) when is_binary(value) do
    case Date.from_iso8601(value) do
      {:ok, date} -> date
      {:error, _reason} -> value
    end
  end

  def load(value, _field), do: value

  @impl true
  def dump(%Date{} = date, %{"format" => "strict_date"}) do
    Date.to_iso8601(date)
  end

  def dump(value, _field), do: value
end

defmodule Dowser.Elasticsearch.FieldCodec do
  use Dowser.Client.Codec.Builder

  cast %{"type" => "date"}, Dowser.Elasticsearch.Fields.Date
end

compiles down to (roughly):

defmodule Dowser.Elasticsearch.FieldCodec do
  def load(nil, _field), do: nil

  def load(value, %{"type" => "date"} = field) do
    Dowser.Elasticsearch.Fields.Date.load(value, field)
  end

  def load(value, _field), do: value

  def dump(nil, _field), do: nil

  def dump(value, %{"type" => "date"} = field) do
    Dowser.Elasticsearch.Fields.Date.dump(value, field)
  end

  def dump(value, _field), do: value
end

From field codec to :codec_adapter

Bridging FieldCodec.load/2/dump/2 into something usable as :codec_adapter means walking a document alongside its mapping/schema — backend-specific knowledge dowser_client doesn't have, so it can't provide that walk generically. A backend package writes its own thin Dowser.Client.Codec:

defmodule Dowser.Elasticsearch.Codec do
  @behaviour Dowser.Client.Codec

  alias Dowser.CoreExt.Keyable
  alias Dowser.Elasticsearch.FieldCodec

  @impl true
  def decode(body, opts) do
    key_fn = Keyword.fetch!(opts, :key_fn)
    # `walk_mapping/2` is the backend-specific part: recurse through
    # `body` alongside its index mapping, calling
    # `FieldCodec.load/2` with each value's own field metadata.
    {:ok, body |> Keyable.transform_keys(key_fn) |> walk_mapping(&FieldCodec.load/2)}
  end

  @impl true
  def encode(body, _opts) do
    {:ok, walk_mapping(body, &FieldCodec.dump/2)}
  end
end

See Dowser.Client.Codec for the full :codec_adapter contract, including opts[:key_fn].

Options

  • :inherit — a module built with Dowser.Client.Codec.Builder whose cast/2 declarations are inserted ahead of this module's own. A pattern declared by the inherited module wins over one declared later for an overlapping match, since clauses are tried top to bottom. Default nil (no inheritance).
  • :fallbacktrue (default) adds a catch-all load/2/dump/2 clause returning the value as-is when nothing else matched. false omits it, so an unmatched value raises FunctionClauseError.
  • :niltrue (default) adds a load(nil, _)/dump(nil, _) clause returning nil ahead of every other clause, so fields never have to guard against nil themselves. false omits it.

Ordering

Generated clauses are tried in this order: the nil guard (if enabled), then the inherited module's casts (if any), then this module's own casts in declaration order, then the fallback (if enabled). load/2 clauses are emitted before dump/2 clauses.

Summary

Functions

Declares that a value whose field metadata matches pattern is handled by field_module — a module implementing Dowser.Client.Field.

Functions

cast(pattern, field_module)

(macro)

Declares that a value whose field metadata matches pattern is handled by field_module — a module implementing Dowser.Client.Field.

pattern is matched against the second argument of load/2/dump/2 (conventionally the field's own metadata); the matched value is bound to field and passed through to field_module.load/2 or field_module.dump/2 alongside the original value.