Current section
Files
Jump to
Current section
Files
lib/ex_open_api_utils/mapper.ex
defprotocol ExOpenApiUtils.Mapper do
@fallback_to_any true
@spec to_map(Ecto.Schema.t() | OpenApiSpex.Schema.t()) :: map() | list() | atom()
def to_map(struct)
end
defimpl ExOpenApiUtils.Mapper, for: Any do
defmacro __deriving__(module, _struct, opts) do
property_attrs = Keyword.fetch!(opts, :property_attrs)
map_direction = Keyword.fetch!(opts, :map_direction)
polymorphic_variants = Keyword.get(opts, :polymorphic_variants, %{})
# GH-34 self-stamping — at parent-contextual sibling derive time, the
# parent passes its own discriminator atom and wire value so the sibling's
# generated walker can stamp :__type__ on its own result map from
# compile-time constants, with no dependency on the outer walker's
# polymorphic_variants map knowing about nested polymorphic fields.
self_stamp_atom = Keyword.get(opts, :self_stamp_atom)
self_stamp_wire = Keyword.get(opts, :self_stamp_wire)
self_stamp_ast = build_self_stamp_ast(self_stamp_atom, self_stamp_wire)
quote do
alias ExOpenApiUtils.Property
defimpl ExOpenApiUtils.Mapper, for: unquote(module) do
def to_map(arg) do
result =
case unquote(map_direction) do
:from_ecto ->
Enum.reduce(unquote(Macro.escape(property_attrs)), %{}, fn %Property{} = property,
acc ->
source = property.source || property.key
val =
if is_list(source) do
Enum.reduce(source, arg, fn key, acc ->
if acc, do: Map.get(acc, key)
end)
else
Map.get(arg, source)
end
raw_val = val
val = val |> ExOpenApiUtils.Mapper.to_map()
val =
ExOpenApiUtils.Mapper.Polymorphic.inject(
val,
raw_val,
property.key,
unquote(Macro.escape(polymorphic_variants)),
:from_ecto
)
ExOpenApiUtils.Mapper.Utils.nil_aware_put(
acc,
Atom.to_string(property.key),
val,
property.schema
)
end)
:from_open_api ->
Enum.reduce(unquote(Macro.escape(property_attrs)), %{}, fn %Property{} = property,
acc ->
destination = property.source || property.key
raw_val = Map.get(arg, property.key)
val = raw_val |> ExOpenApiUtils.Mapper.to_map()
val =
ExOpenApiUtils.Mapper.Polymorphic.inject(
val,
raw_val,
property.key,
unquote(Macro.escape(polymorphic_variants)),
:from_open_api
)
if is_list(destination) do
[root | rest] = destination
exploded_map = ExOpenApiUtils.Mapper.Utils.explode_map(rest, val)
ExOpenApiUtils.Mapper.Utils.nil_aware_merge(
acc,
root,
exploded_map,
val,
property.schema
)
else
ExOpenApiUtils.Mapper.Utils.nil_aware_put(
acc,
destination,
val,
property.schema
)
end
end)
end
unquote(self_stamp_ast)
end
end
end
end
def to_map(val) do
val
end
# GH-34 — compile-time helper that builds the optional `Map.put(result,
# atom, wire)` splice appended to the tail of each derived sibling's
# `to_map/1` body. When a parent-contextual sibling's derive passes
# `self_stamp_atom` + `self_stamp_wire`, the sibling stamps its own
# discriminator on its own result map; otherwise the splice is a no-op
# that just returns `result`.
defp build_self_stamp_ast(atom, wire)
when is_atom(atom) and not is_nil(atom) and is_binary(wire) do
quote do
Map.put(result, unquote(atom), unquote(wire))
end
end
defp build_self_stamp_ast(_atom, _wire), do: quote(do: result)
end
defimpl ExOpenApiUtils.Mapper, for: [Map] do
def to_map(val) do
Enum.reduce(val, %{}, fn {k, v}, acc ->
Map.put(acc, k, ExOpenApiUtils.Mapper.to_map(v))
end)
end
end
defimpl ExOpenApiUtils.Mapper, for: [List] do
def to_map(list) do
Enum.map(list, &ExOpenApiUtils.Mapper.to_map/1)
end
end
defimpl ExOpenApiUtils.Mapper, for: Ecto.Association.NotLoaded do
def to_map(_val) do
:not_loaded
end
end
defmodule ExOpenApiUtils.Mapper.Utils do
def explode_map(key, val) when is_atom(key) do
%{key => val}
end
def explode_map([root | rest], val) do
%{root => explode_map(rest, val)}
end
def explode_map([], val) do
val
end
# GH-38 — nil-aware put that respects OpenAPI nullable semantics.
# Pattern-matched on val so non-nil values skip the nullable check entirely.
def nil_aware_put(acc, key, nil, %{nullable: true}), do: Map.put(acc, key, nil)
def nil_aware_put(acc, _key, nil, _schema), do: acc
def nil_aware_put(acc, key, val, _schema), do: Map.put(acc, key, val)
# GH-38 — nil-aware merge for list-destination properties (source: [:a, :b]).
# Same nullable semantics as nil_aware_put, but uses Map.update for nested paths.
def nil_aware_merge(acc, root, exploded, nil, %{nullable: true}) do
Map.update(acc, root, %{}, fn existing -> Map.merge(existing, exploded) end)
end
def nil_aware_merge(acc, _root, _exploded, nil, _schema), do: acc
def nil_aware_merge(acc, root, exploded, _val, _schema) do
Map.update(acc, root, %{}, fn existing -> Map.merge(existing, exploded) end)
end
end
defmodule ExOpenApiUtils.Mapper.Polymorphic do
@moduledoc false
# Injects a discriminator key into `val` when `property_key` is a polymorphic
# field. Looks up the raw struct's module in the variant_map; if found, picks
# the direction-appropriate injection key (string for :from_ecto, atom for
# :from_open_api) and puts the wire value on the map.
#
# When the property is not polymorphic, `val` is returned unchanged.
def inject(val, raw_val, property_key, polymorphic_variants, direction)
when is_map(polymorphic_variants) and map_size(polymorphic_variants) > 0 do
case Map.fetch(polymorphic_variants, property_key) do
:error ->
val
{:ok,
%{variant_map: variant_map, discriminator_string: disc_str, type_field_atom: type_atom}} ->
with %struct_mod{} <- raw_val,
{:ok, wire_value} <- Map.fetch(variant_map, struct_mod),
true <- is_map(val) do
case direction do
:from_ecto -> Map.put(val, disc_str, wire_value)
:from_open_api -> Map.put(val, type_atom, wire_value)
end
else
_ -> val
end
end
end
def inject(val, _raw_val, _property_key, _polymorphic_variants, _direction), do: val
end