Skip to content

Commit

Permalink
Fix inheritance of fields
Browse files Browse the repository at this point in the history
  • Loading branch information
cichacz committed Jan 13, 2025
1 parent d1d1842 commit 1b992b4
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 49 deletions.
21 changes: 13 additions & 8 deletions lib/ecto_discriminator/discriminator_changeset.ex
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,21 @@ defimpl EctoDiscriminator.DiscriminatorChangeset, for: Ecto.Changeset do
Ecto.Changeset.get_field(changeset, discriminator) ||
struct

data = EctoDiscriminator.Schema.to_base(data, diverged_schema)
if struct != diverged_schema do
data = EctoDiscriminator.Schema.to_base(data, diverged_schema)

# just call changeset from the derived schema and hope it calls cast_base to pull fields from the base schema
diverged_changeset = diverged_schema.changeset(data, params)
# just call changeset from the derived schema and hope it calls cast_base to pull fields from the base schema
diverged_changeset = diverged_schema.changeset(data, params)

changeset
# replace data & types with ones from diverged changeset to be able to continue in original changeset
|> Map.put(:data, diverged_changeset.data)
|> Map.put(:types, diverged_changeset.types)
|> Ecto.Changeset.merge(diverged_changeset)
changeset
# replace data & types with ones from diverged changeset to be able to continue in original changeset
|> Map.put(:data, diverged_changeset.data)
|> Map.put(:types, diverged_changeset.types)
|> Ecto.Changeset.merge(diverged_changeset)
else
# we can just safely run the changeset
diverged_schema.changeset(changeset, params)
end
end

def base_changeset(%{data: data} = changeset, params) do
Expand Down
54 changes: 48 additions & 6 deletions lib/ecto_discriminator/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ defmodule EctoDiscriminator.Schema do
quote do
# nothing to do, we're already in base
def to_base(%__MODULE__{} = struct), do: struct
def __schema__(:unique_fields), do: []
end

# helpers must come last to use __MODULE__
Expand Down Expand Up @@ -170,22 +171,38 @@ defmodule EctoDiscriminator.Schema do
end

# transforms `struct` to `destination`
def to_base(%_{} = struct, destination) do
def to_base(%source{} = struct, destination) do
data =
struct
|> Map.from_struct()
# have to update __meta__ because it comes from different schema
|> put_in([Access.key(:__meta__), Access.key(:schema)], destination)
# take only items that hold some value to avoid differences in relationship owners
|> Enum.reject(fn {k, value} ->
|> maybe_update_meta(destination)
# take only unique items that hold some value
|> Enum.reject(fn {key, value} ->
match?(%Ecto.Association.NotLoaded{}, value) ||
(is_nil(value) && !destination.__schema__(:association, k))
key in source.__schema__(:unique_fields)
end)
# we have to recursively map things to their base variants (if available)
|> Enum.map(fn {key, value} ->
case destination.__schema__(:embed, key) do
%{related: related} when is_struct(value) ->
{key, to_base(value, related)}

_ ->
{key, value}
end
end)
|> Enum.into(%{})

struct(destination, data)
end

defp maybe_update_meta(%{__meta__: %{schema: _}} = map, destination) do
# have to update __meta__ because it comes from different schema
put_in(map, [Access.key(:__meta__), Access.key(:schema)], destination)
end

defp maybe_update_meta(map, _destination), do: map

defp set_up_schema() do
quote do
use Ecto.Schema
Expand Down Expand Up @@ -292,6 +309,31 @@ defmodule EctoDiscriminator.Schema do
rest = merge_rest_options(rest, default: caller_module)
{:field, meta, [name, alias | rest]}

# resolve embeds aliases on the source module level
# this applies only when embed is defined inline (rest is not empty)
{:embeds_one, meta,
[
field,
{:__aliases__, _, alias}
| rest
]}
when rest != [] ->
{:embeds_one, meta, [field, {:__aliases__, meta, [Elixir, source_module | alias]}]}

# we have to do some extra rewritting for relationships
{:has_one, meta, [field, type | rest]} ->
rest =
with %{related_key: key} <- source_module.__schema__(:association, field) do
case rest do
[] -> [[foreign_key: key]]
[opts] -> [Keyword.put(opts, :foreign_key, key)]
end
else
_ -> rest
end

{:has_one, meta, [field, type | rest]}

other ->
other
end)
Expand Down
42 changes: 38 additions & 4 deletions test/ecto_discriminator/discriminator_changeset_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ defmodule EctoDiscriminator.DiscriminatorChangesetTest do
end

test "uses defaults from diverged schema" do
changeset =
SomeTablePk.diverged_changeset(%SomeTablePk{}, %{type: SomeTable.FooPk, source: "asdf"})
changeset = SomeTablePk.diverged_changeset(%SomeTable.FooPk{}, %{source: "asdf"})

assert %SomeTable.FooPk{title: :b} = changeset.data
end
Expand All @@ -84,11 +83,45 @@ defmodule EctoDiscriminator.DiscriminatorChangesetTest do
})

assert %SomeTable.FooPk{title: nil} = Ecto.Changeset.apply_action!(changeset, :insert)

# make sure defaults aren't used when value is provided by base schema (we may want to nil-ify value)
changeset =
SomeTablePk.diverged_changeset(%SomeTablePk{title: nil}, %{
type: SomeTable.FooPk,
source: "asdf"
})

assert %SomeTable.FooPk{title: nil} = Ecto.Changeset.apply_action!(changeset, :insert)
end

@tag :only
test "properly handles overriden embeds" do
changeset =
SomeTable.diverged_changeset(%SomeTable.Grault{}, %{
source: "source",
title: "abc",
is_special: true,
content: %{grault_text: "grault"}
})

inserted = Ecto.Changeset.apply_action!(changeset, :insert)

assert %SomeTable.Grault{
title: "abc",
source: "source",
is_special: true,
content: %SomeTable.Grault.Content{grault_text: "grault"}
} == inserted

changeset =
SomeTable.diverged_changeset(inserted, %{content: %{grault_text: "grault_updated"}})

assert %SomeTable.Grault{content: %SomeTable.Grault.Content{grault_text: "grault_updated"}} =
Ecto.Changeset.apply_action!(changeset, :insert)
end
end

describe "base_changeset/2" do
@tag :only
test "returns itself on base schema" do
changeset =
DiscriminatorChangeset.base_changeset(
Expand Down Expand Up @@ -149,7 +182,8 @@ defmodule EctoDiscriminator.DiscriminatorChangesetTest do
inserted_at: foo.inserted_at,
updated_at: foo.updated_at,
title: foo.title,
content: foo.content,
# content is overridden in Foo so base table won't have it populated
content: nil,
parent: %Ecto.Association.NotLoaded{
__cardinality__: :one,
__field__: :parent,
Expand Down
15 changes: 13 additions & 2 deletions test/ecto_discriminator/schema_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ defmodule EctoDiscriminator.SchemaTest do
SomeTable.Quux.changeset(%SomeTable.Quux{}, %{
title: :a,
source: "qux",
content: %{quux_text: "abc"},
content: %{text: "abc"},
is_special: false,
is_last: true
})
Expand Down Expand Up @@ -468,8 +468,19 @@ defmodule EctoDiscriminator.SchemaTest do

test "can override schema protocol" do
assert_raise RuntimeError, fn ->
EctoDiscriminator.DiscriminatorChangeset.diverged_changeset(%SomeTable.FooPk{})
EctoDiscriminator.DiscriminatorChangeset.diverged_changeset(%SomeTable.Baz{})
end
end

test "can inherit relationships" do
bar =
%SomeTable.Corge{}
|> SomeTable.Corge.changeset(%{is_special: true, content: %{name: "def"}})
|> Repo.insert!()

[row] = SomeTable.Corge |> preload([:content, :sibling]) |> Repo.all()

assert row == bar
end
end
end
16 changes: 16 additions & 0 deletions test/support/some_table/baz.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,20 @@ defmodule EctoDiscriminator.SomeTable.Baz do
|> cast_assoc(:content)
|> validate_required([:is_special])
end

defimpl EctoDiscriminator.DiscriminatorChangeset do
def diverged_changeset(_, _), do: raise("There is no diverged schema for #{@for}")

def base_changeset(data, params) do
data
|> change()
|> EctoDiscriminator.DiscriminatorChangeset.base_changeset(params)
end

def cast_base(data, params) do
data
|> change()
|> EctoDiscriminator.DiscriminatorChangeset.cast_base(params)
end
end
end
17 changes: 17 additions & 0 deletions test/support/some_table/corge.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule EctoDiscriminator.SomeTable.Corge do
use EctoDiscriminator.Schema

import Ecto.Changeset

schema EctoDiscriminator.SomeTable.Bar do
field :is_special, :boolean
# content is inherited from Bar
end

def changeset(struct, params \\ %{}) do
struct
|> cast_base(params)
|> cast(params, [:is_special])
|> validate_required([:is_special])
end
end
18 changes: 0 additions & 18 deletions test/support/some_table/foo_pk.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
defmodule EctoDiscriminator.SomeTable.FooPk do
use EctoDiscriminator.Schema

import Ecto.Changeset

schema EctoDiscriminator.SomeTablePk do
field :source, :string
field :title, Ecto.Enum, default: :b
Expand All @@ -12,20 +10,4 @@ defmodule EctoDiscriminator.SomeTable.FooPk do
struct
|> cast_base(params)
end

defimpl EctoDiscriminator.DiscriminatorChangeset do
def diverged_changeset(_, _), do: raise("There is no diverged schema for #{@for}")

def base_changeset(data, params) do
data
|> change()
|> EctoDiscriminator.DiscriminatorChangeset.base_changeset(params)
end

def cast_base(data, params) do
data
|> change()
|> EctoDiscriminator.DiscriminatorChangeset.cast_base(params)
end
end
end
23 changes: 23 additions & 0 deletions test/support/some_table/grault.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule EctoDiscriminator.SomeTable.Grault do
use EctoDiscriminator.Schema

import Ecto.Changeset

schema EctoDiscriminator.SomeTable.Qux do
embeds_one :content, Content, primary_key: false, on_replace: :update do
field :grault_text, :string
end
end

def changeset(struct, params \\ %{}) do
struct
|> cast_base(params)
|> cast_embed(:content, required: true, with: &content_changeset/2)
end

def content_changeset(struct, params \\ %{}) do
struct
|> cast(params, [:grault_text])
|> validate_required([:grault_text])
end
end
12 changes: 1 addition & 11 deletions test/support/some_table/quux.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,12 @@ defmodule EctoDiscriminator.SomeTable.Quux do
# make sure field types can contain calls on module attributes
field :title, Ecto.Enum, values: Keyword.keys(@values)
field :is_last, :boolean, virtual: true

embeds_one :content, Content, primary_key: false, on_replace: :update do
field :quux_text, :string
end
# content is inherited from Qux
end

def changeset(struct, params \\ %{}) do
struct
|> cast_base(params)
|> cast(params, [:title])
|> cast_embed(:content, required: true, with: &content_changeset/2)
end

def content_changeset(struct, params \\ %{}) do
struct
|> cast(params, [:quux_text])
|> validate_required([:quux_text])
end
end

0 comments on commit 1b992b4

Please sign in to comment.