Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions lib/field.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ defmodule Strukt.Field do
:field,
:embeds_one,
:embeds_many,
:polymorphic_embeds_one,
:polymorphic_embeds_many,
:timestamps
]

Expand All @@ -41,6 +43,20 @@ defmodule Strukt.Field do
for {type, meta, args} <- fields, do: parse(type, meta, args)
end

defp parse(type, meta, [name, opts])
when type in [:polymorphic_embeds_one, :polymorphic_embeds_many] do
{validations, options} = Keyword.split(opts, @validation_opts)

%__MODULE__{
name: name,
type: type,
meta: meta,
value_type: PolymorphicEmbed,
options: options,
validations: validations
}
end

defp parse(type, meta, [name, value_type]) when type in @supported_field_types,
do: %__MODULE__{name: name, type: type, meta: meta, value_type: value_type}

Expand Down Expand Up @@ -93,6 +109,16 @@ defmodule Strukt.Field do
def to_ast(%__MODULE__{type: :timestamps, meta: meta, options: options}),
do: {:timestamps, meta, options}

def to_ast(%__MODULE__{
type: type,
name: name,
meta: meta,
options: options,
block: nil
})
when type in [:polymorphic_embeds_one, :polymorphic_embeds_many],
do: {type, meta, [name, options]}

def to_ast(%__MODULE__{
type: type,
name: name,
Expand Down
79 changes: 79 additions & 0 deletions lib/params.ex
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,90 @@ defmodule Strukt.Params do
cardinality: cardinality
)}

{:parameterized, {PolymorphicEmbed, _opts}} ->
{field,
transform_polymorphic(module, field, value, get_struct_field_value(struct, field))}

{:array, {:parameterized, {PolymorphicEmbed, _opts}}} ->
{field,
transform_polymorphic(module, field, value, get_struct_field_value(struct, field),
cardinality: :many
)}

_type ->
{field, value}
end
end

defp transform_polymorphic(module, field, params, struct, opts \\ [])

# Keep nil as-is so required validation and PolymorphicEmbed's own handling can run later.
defp transform_polymorphic(_module, _field, nil, _struct, _opts), do: nil

# Struct params have already been cast by the caller, so leave them untouched.
defp transform_polymorphic(_module, _field, %_{} = params, _struct, _opts), do: params

# For polymorphic_embeds_many, transform each map using the matching current embed by index.
# Non-map structs in the list are preserved by the clause above.
defp transform_polymorphic(module, field, params, struct, cardinality: :many)
when is_list(params) do
current = List.wrap(struct)

params
|> Enum.with_index()
|> Enum.map(fn
{%_{} = param, _index} ->
param

{param, index} ->
transform_polymorphic(module, field, param, Enum.at(current, index))
end)
end

# Map params need the selected polymorphic module before source-field mapping can be applied.
# If the type cannot be inferred, keep the params unchanged and let cast_polymorphic_embed/3
# produce the configured error/raise/nilify behavior.
defp transform_polymorphic(module, field, params, struct, _opts) when is_map(params) do
case PolymorphicEmbed.get_polymorphic_module(module, field, params) do
nil ->
params

embedded_module ->
type_field_name = polymorphic_type_field_name(module, field)
type = get_params_field_value(params, type_field_name, nil)
struct = polymorphic_struct_for_module(struct, embedded_module)

embedded_module
|> transform(params, struct)
|> maybe_put_polymorphic_type(type_field_name, type)
end
rescue
_ -> params
end

# Leave invalid shapes alone so the eventual cast can report the type error.
defp transform_polymorphic(_module, _field, params, _struct, _opts), do: params

defp polymorphic_struct_for_module(struct, module) when is_struct(struct, module), do: struct
defp polymorphic_struct_for_module(_struct, _module), do: nil

# The type marker must be restored after source-field mapping, otherwise PolymorphicEmbed
# cannot infer which embedded schema to cast.
defp polymorphic_type_field_name(module, field) do
case module.__schema__(:type, field) do
{:parameterized, {PolymorphicEmbed, opts}} ->
opts.type_field_name

{:array, {:parameterized, {PolymorphicEmbed, opts}}} ->
opts.type_field_name
end
end

defp maybe_put_polymorphic_type(params, _type_field_name, nil), do: params

defp maybe_put_polymorphic_type(params, type_field_name, type),
do: Map.put(params, type_field_name, type)

defp get_params_field_value(nil, _field, _struct), do: nil

defp get_params_field_value(params, field, struct) when is_list(params) do
Expand Down
79 changes: 72 additions & 7 deletions lib/strukt.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ defmodule Strukt do
:field,
:embeds_one,
:embeds_many,
:polymorphic_embeds_one,
:polymorphic_embeds_many,
:belongs_to,
:has_many,
:has_one,
Expand Down Expand Up @@ -283,7 +285,12 @@ defmodule Strukt do
{node, meta, elements} ->
kvs =
Keyword.merge(
[type: t, value_type: f.value_type, default: f.options[:default]],
[
type: t,
value_type: f.value_type,
default: f.options[:default],
types: f.options[:types]
],
f.validations
)

Expand All @@ -297,6 +304,12 @@ defmodule Strukt do
# Get a list of embeds valid for `cast_embed/3`
cast_embed_fields = for %{type: t} = f <- fields, t in [:embeds_one, :embeds_many], do: f.name

# Get a list of polymorphic embeds valid for `PolymorphicEmbed.cast_polymorphic_embed/3`
cast_polymorphic_embed_fields =
for %{type: t} = f <- fields, t in [:polymorphic_embeds_one, :polymorphic_embeds_many] do
f.name
end

# Expand fields back to their final AST form
fields_ast =
fields
Expand All @@ -321,6 +334,7 @@ defmodule Strukt do

use Ecto.Schema
import Ecto.Changeset, except: [change: 2]
import PolymorphicEmbed, only: [polymorphic_embeds_one: 2, polymorphic_embeds_many: 2]

@behaviour unquote(__MODULE__)
@before_compile unquote(__MODULE__)
Expand Down Expand Up @@ -358,6 +372,7 @@ defmodule Strukt do
@schema_name Macro.underscore(__MODULE__)
@validated_fields unquote(validated_fields)
@cast_embed_fields unquote(Macro.escape(cast_embed_fields))
@cast_polymorphic_embed_fields unquote(Macro.escape(cast_polymorphic_embed_fields))

# Ensure primary key can be cast, if applicable
case Module.get_attribute(__MODULE__, :primary_key) do
Expand Down Expand Up @@ -444,12 +459,13 @@ defmodule Strukt do
def change(entity_or_changeset, params) do
case entity_or_changeset do
%Ecto.Changeset{} = cs ->
cs
|> Ecto.Changeset.change(params)
|> validate()
entity = Ecto.Changeset.apply_changes(cs)
formed_params = Strukt.Params.transform(__MODULE__, params, entity)
changeset(cs, formed_params, cs.action)

%__MODULE__{} = entity ->
changeset(entity, params, :update)
formed_params = Strukt.Params.transform(__MODULE__, params, entity)
changeset(entity, formed_params, :update)
end
end

Expand Down Expand Up @@ -493,7 +509,7 @@ defmodule Strukt do
caller: __MODULE__,
info: @validated_fields,
fields: @cast_fields,
embeds: @cast_embed_fields
embeds: @cast_embed_fields ++ @cast_polymorphic_embed_fields
})

Code.eval_quoted(typespec_ast, [], __ENV__)
Expand Down Expand Up @@ -535,6 +551,28 @@ defmodule Strukt do
cast(entity, params, @cast_fields)
|> Map.put(:action, action)
|> __cast_embeds__(@cast_embed_fields)
|> __cast_polymorphic_embeds__(@cast_polymorphic_embed_fields)
|> validate()
end

def changeset(%Ecto.Changeset{data: %__MODULE__{}} = changeset, params, action)
when action in [:insert, :update, :delete, nil] do
params =
case params do
%__MODULE__{} ->
Map.from_struct(params)

m when is_map(m) ->
m

other ->
Enum.into(other, %{})
end

cast(changeset, params, @cast_fields)
|> Map.put(:action, action)
|> __cast_embeds__(@cast_embed_fields)
|> __cast_polymorphic_embeds__(@cast_polymorphic_embed_fields)
|> validate()
end

Expand Down Expand Up @@ -574,6 +612,33 @@ defmodule Strukt do
end
end

defp __cast_polymorphic_embeds__(changeset, []), do: changeset

if length(@cast_polymorphic_embed_fields) > 0 do
defp __cast_polymorphic_embeds__(%Ecto.Changeset{params: params} = changeset, [
field | fields
]) do
f = to_string(field)

changeset =
case Map.get(params, f) do
nil ->
changeset

%_{} = entity ->
Ecto.Changeset.put_change(changeset, field, entity)

[%_{} | _] = entities ->
Ecto.Changeset.put_change(changeset, field, entities)

_other ->
PolymorphicEmbed.cast_polymorphic_embed(changeset, field)
end

__cast_polymorphic_embeds__(changeset, fields)
end
end

@doc """
Applies the changes in the changset if the changeset is valid, returning the
updated data. The action must be one of `:insert`, `:update`, or `:delete` and
Expand All @@ -592,7 +657,7 @@ defmodule Strukt do
@doc "Deserialize this type from a JSON string or iodata"
@spec from_json(binary | iodata) :: {:ok, t} | {:error, reason :: term}
def from_json(input) do
with {:ok, map} <- Jason.decode(input, keys: :atoms!, strings: :copy) do
with {:ok, map} <- Jason.decode(input, strings: :copy) do
{:ok, Ecto.embedded_load(__MODULE__, map, :json)}
end
end
Expand Down
31 changes: 31 additions & 0 deletions lib/typespec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,19 @@ defmodule Strukt.Typespec do

{name, %{type: :embeds_many, value_type: type}} ->
{name, List.wrap(compose_call(type, :t, []))}

{name, %{type: :polymorphic_embeds_one, types: types} = meta} ->
required? = Map.get(meta, :required) == true
type_name = polymorphic_type_name(types)

if required? do
{name, type_name}
else
{name, nilable(type_name)}
end

{name, %{type: :polymorphic_embeds_many, types: types}} ->
{name, List.wrap(polymorphic_type_name(types))}
end)

# Join all fields together
Expand All @@ -104,6 +117,24 @@ defmodule Strukt.Typespec do

defp nilable(type_name), do: {:|, [], [type_name, nil]}

defp polymorphic_type_name(types) when is_list(types) do
types
|> Enum.map(fn
{_type, type_opts} when is_list(type_opts) ->
Keyword.fetch!(type_opts, :module)

{_type, module} ->
module
end)
|> Enum.map(&compose_call(&1, :t, []))
|> union()
end

defp polymorphic_type_name(_types), do: primitive(:any)

defp union([type]), do: type
defp union([type | types]), do: {:|, [], [type, union(types)]}

defp type_to_type_name(:id), do: primitive(:non_neg_integer)
defp type_to_type_name(:binary_id), do: primitive(:binary)
defp type_to_type_name(:integer), do: primitive(:integer)
Expand Down
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ defmodule Strukt.MixProject do
defp deps do
[
{:ecto, "~> 3.12"},
{:polymorphic_embed, "~> 5.0"},
{:jason, "> 0.0.0", optional: true},
{:ex_doc, "> 0.0.0", only: [:docs], runtime: false}
]
Expand Down
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
%{
"attrs": {:hex, :attrs, "0.6.0", "25d738b47829f964a786ef73897d2550b66f3e7d1d7c49a83bc8fd81c71bed93", [:mix], [], "hexpm", "9c30ac15255c2ba8399263db55ba32c2f4e5ec267b654ce23df99168b405c82e"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"},
"ecto": {:hex, :ecto, "3.13.4", "27834b45d58075d4a414833d9581e8b7bb18a8d9f264a21e42f653d500dbeeb5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5ad7d1505685dfa7aaf86b133d54f5ad6c42df0b4553741a1ff48796736e88b2"},
Expand All @@ -8,5 +9,6 @@
"makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
"polymorphic_embed": {:hex, :polymorphic_embed, "5.0.6", "58d8c5fe3df3c0cf1e3b8990318cf93489360fbaea555d9520f0b50577ea7e5b", [:mix], [{:attrs, "~> 0.6", [hex: :attrs, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}], "hexpm", "99c7a7aed3d4b00bd08ce91c9b880deaa47b649904fca8b745f3a50b9b3b3835"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
}
Loading
Loading