Packages

A pure-Elixir implementation of the Open vSwitch Database Management Protocol (OVSDB), per RFC 7047. Provides protocol primitives, operation and transaction builders, schema parsing, client/server session handling over TCP/TLS, and an in-memory IDL replica.

Current section

Files

Jump to
ovsdb_ex lib ovsdb map.ex
Raw

lib/ovsdb/map.ex

defmodule OVSDB.Map do
@moduledoc """
An OVSDB map — a collection of key/value pairs, both of atomic type.
## Wire form
Per [RFC 7047 §5.1][1], a map is always encoded on the wire as a
2-element tagged JSON array:
["map", [[k1, v1], [k2, v2], ...]]
Unlike `OVSDB.Set`, there is no bare-value optimization for
maps — even a 1-element map is fully tagged. Decoding is therefore
unambiguous.
[1]: https://www.rfc-editor.org/rfc/rfc7047#section-5.1
## Name collision
This module shadows Elixir's built-in `Map`. Inside this file we
refer to the standard library as `Elixir.Map` when needed. Callers
who `alias OVSDB.Map` should either use the full module
name or alias as `alias OVSDB.Map, as: OvsdbMap`.
## Ordering
OVSDB maps are logically unordered. We store entries as a list of
2-tuples `[{k, v}, ...]` rather than as a native Elixir `%{}` for
three reasons:
1. It preserves the wire's encoding structure for lossless
roundtrips.
2. It supports non-string atomic keys (integers, booleans) which
Elixir maps handle but JSON wire form constrains to atomic
types only.
3. It avoids the mental tax of "is this an OVSDB map or an
Elixir map" at the struct level.
Use `equal?/2` for logical (order-independent) equality, and
`to_elixir_map/1` when you want a native `%{}` for ergonomic access.
"""
@enforce_keys [:entries]
defstruct [:entries]
@type t :: %__MODULE__{entries: [{term(), term()}]}
@typedoc """
A map that is statically known to be empty. Narrower than `t()` so
that Dialyzer can verify `empty/0`'s return type exactly.
"""
@type empty :: %__MODULE__{entries: []}
@doc """
Creates a map from a list of `{key, value}` tuples or from a native
Elixir map.
iex> OVSDB.Map.new([{"a", 1}, {"b", 2}])
%OVSDB.Map{entries: [{"a", 1}, {"b", 2}]}
iex> OVSDB.Map.new(%{"a" => 1, "b" => 2}) |> OVSDB.Map.equal?(
...> OVSDB.Map.new([{"a", 1}, {"b", 2}])
...> )
true
"""
@spec new([{term(), term()}] | %{optional(term()) => term()}) :: t()
def new(entries) when is_list(entries) do
Enum.each(entries, fn
{_k, _v} ->
:ok
other ->
raise ArgumentError, "map entries must be {key, value} tuples, got: #{inspect(other)}"
end)
%__MODULE__{entries: entries}
end
def new(%{} = map) do
%__MODULE__{entries: Elixir.Map.to_list(map)}
end
@doc """
Returns the empty map.
"""
@spec empty() :: empty()
def empty, do: %__MODULE__{entries: []}
@doc """
Returns the number of entries in the map.
"""
@spec size(t()) :: non_neg_integer()
def size(%__MODULE__{entries: entries}), do: length(entries)
@doc """
Returns the value associated with `key`, or `default` if not present.
O(n) — maps are stored as lists, not hash tables.
iex> m = OVSDB.Map.new([{"a", 1}, {"b", 2}])
iex> OVSDB.Map.get(m, "a")
1
iex> OVSDB.Map.get(m, "missing", :not_found)
:not_found
"""
@spec get(t(), term(), term()) :: term()
def get(%__MODULE__{entries: entries}, key, default \\ nil) do
case List.keyfind(entries, key, 0) do
{^key, value} -> value
nil -> default
end
end
@doc """
Returns `true` if the two maps contain the same entries regardless
of order.
iex> a = OVSDB.Map.new([{"a", 1}, {"b", 2}])
iex> b = OVSDB.Map.new([{"b", 2}, {"a", 1}])
iex> OVSDB.Map.equal?(a, b)
true
"""
@spec equal?(t(), t()) :: boolean()
def equal?(%__MODULE__{entries: a}, %__MODULE__{entries: b}) do
Enum.sort(a) == Enum.sort(b)
end
@doc """
Converts to a native Elixir map. Loses original entry ordering.
Raises on duplicate keys (which shouldn't occur in valid OVSDB
maps — the server enforces key uniqueness).
iex> m = OVSDB.Map.new([{"a", 1}, {"b", 2}])
iex> OVSDB.Map.to_elixir_map(m)
%{"a" => 1, "b" => 2}
"""
@spec to_elixir_map(t()) :: %{optional(term()) => term()}
def to_elixir_map(%__MODULE__{entries: entries}) do
Elixir.Map.new(entries)
end
@doc """
Encodes a map to its RFC 7047 wire form.
As with `Set`, nested wrapped types (UUID, NamedUUID) must already
be encoded by the caller — this function does not recurse.
iex> m = OVSDB.Map.new([{"k1", "v1"}, {"k2", "v2"}])
iex> OVSDB.Map.encode(m)
["map", [["k1", "v1"], ["k2", "v2"]]]
iex> OVSDB.Map.encode(OVSDB.Map.empty())
["map", []]
"""
@typedoc """
The wire form of a map — always a tagged `["map", [[k, v], ...]]`
array, regardless of cardinality.
"""
@type wire :: nonempty_list()
@spec encode(t()) :: wire()
def encode(%__MODULE__{entries: entries}) do
["map", Enum.map(entries, fn {k, v} -> [k, v] end)]
end
@doc """
Decodes the `["map", [...]]` wire form.
iex> OVSDB.Map.decode(["map", [["k1", "v1"], ["k2", "v2"]]])
{:ok, %OVSDB.Map{entries: [{"k1", "v1"}, {"k2", "v2"}]}}
iex> OVSDB.Map.decode(["map", []])
{:ok, %OVSDB.Map{entries: []}}
iex> OVSDB.Map.decode(["not-map", []])
{:error, :malformed}
"""
@spec decode(term()) :: {:ok, t()} | {:error, :malformed}
def decode(["map", entries]) when is_list(entries) do
with {:ok, tuples} <- decode_entries(entries, []) do
{:ok, %__MODULE__{entries: tuples}}
end
end
def decode(_), do: {:error, :malformed}
@doc """
Decodes the wire form with per-key and per-value decoder callbacks.
Used by the IDL layer to resolve nested UUIDs in ref-typed columns.
iex> uuid_wire = ["uuid", "550e8400-e29b-41d4-a716-446655440000"]
iex> wire = ["map", [["k", uuid_wire]]]
iex> value_decoder = fn w -> {:ok, u} = OVSDB.UUID.decode(w); u end
iex> OVSDB.Map.decode_with(wire, &Function.identity/1, value_decoder)
{:ok, %OVSDB.Map{entries: [{"k", %OVSDB.UUID{value: "550e8400-e29b-41d4-a716-446655440000"}}]}}
"""
@spec decode_with(term(), (term() -> term()), (term() -> term())) ::
{:ok, t()} | {:error, :malformed}
def decode_with(["map", entries], key_decoder, value_decoder) when is_list(entries) do
decoded =
Enum.map(entries, fn
[k, v] -> {key_decoder.(k), value_decoder.(v)}
_ -> throw(:malformed_entry)
end)
{:ok, %__MODULE__{entries: decoded}}
catch
:malformed_entry -> {:error, :malformed}
end
def decode_with(_, _, _), do: {:error, :malformed}
# --- private ---
defp decode_entries([], acc), do: {:ok, Enum.reverse(acc)}
defp decode_entries([[k, v] | rest], acc) do
decode_entries(rest, [{k, v} | acc])
end
defp decode_entries(_, _), do: {:error, :malformed}
end