Skip to content

Commit

Permalink
Introduce inline embeds (elixir-ecto#1668)
Browse files Browse the repository at this point in the history
  • Loading branch information
michalmuskala authored and josevalim committed Sep 7, 2016
1 parent f3ff08b commit 4a9181c
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 8 deletions.
6 changes: 5 additions & 1 deletion integration_test/support/schemas.exs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ defmodule Ecto.Integration.Comment do
belongs_to :author, Ecto.Integration.User
has_one :post_permalink, through: [:post, :permalink]
end

def changeset(schema, params) do
Ecto.Changeset.cast(schema, params, [:text])
end
end

defmodule Ecto.Integration.Permalink do
Expand Down Expand Up @@ -265,4 +269,4 @@ defmodule Ecto.Integration.PostUserCompositePk do
belongs_to :post, Ecto.Integration.Post, primary_key: true
timestamps()
end
end
end
12 changes: 11 additions & 1 deletion lib/ecto/changeset.ex
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,7 @@ defmodule Ecto.Changeset do
{changeset, false}
end

on_cast = opts[:with] || &related.changeset(&1, &2)
on_cast = Keyword.get_lazy(opts, :with, fn -> on_cast_default(type, related) end)
original = Map.get(data, key)
current = Relation.load!(data, original)

Expand All @@ -586,6 +586,16 @@ defmodule Ecto.Changeset do
end
end

defp on_cast_default(type, module) do
if function_exported?(module, :changeset, 2) do
&module.changeset/2
else
raise "the #{type} module does not define a changeset/2 function, which " <>
"is the default used by cast_#{type}/3. You need to either define " <>
"that function or provide a different one using the `:with` option"
end
end

defp expected_relation_type(%{cardinality: :one}), do: :map
defp expected_relation_type(%{cardinality: :many}), do: {:array, :map}

Expand Down
132 changes: 130 additions & 2 deletions lib/ecto/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1065,6 +1065,41 @@ defmodule Ecto.Schema do
# Update the order
changeset = Repo.update!(changeset)
## Inline embedded schema
The schema module can be defined inline in the parent schema in simple
cases:
defmodule Parent do
use Ecto.Schema
schema "parents" do
field :name, :string
embeds_one :child, Child do
field :name, :string
field :age, :integer
end
end
end
Defining embedded schema in such a way will define a `Parent.Child` module
with the appropriate struct. In order to properly cast the embedded schema.
When casting the inline-defined embedded schemas you need to use the `:with`
option of `cast_embed/3` to provide the proper function to do the casting.
For example:
def changeset(schema, params) do
schema
|> cast(params, [:name])
|> cast_embed(:child, with: &child_changeset/2)
end
defp child_changeset(schema, params) do
schema
|> cast(params, [:name, :age])
end
## Encoding and decoding
Because many databases do not support direct encoding and decoding
Expand All @@ -1078,13 +1113,35 @@ defmodule Ecto.Schema do
make sure all of your types can be JSON encoded/decoded correctly.
Ecto provides this guarantee for all built-in types.
"""
defmacro embeds_one(name, schema, opts \\ []) do
defmacro embeds_one(name, schema, opts \\ [])

defmacro embeds_one(name, schema, do: block) do
quote do
embeds_one(unquote(name), unquote(schema), [], do: unquote(block))
end
end

defmacro embeds_one(name, schema, opts) do
schema = expand_alias(schema, __CALLER__)
quote do
Ecto.Schema.__embeds_one__(__MODULE__, unquote(name), unquote(schema), unquote(opts))
end
end

@doc """
Indicates an embedding of a schema.
For options and examples see documentation of `embeds_one/3`.
"""
defmacro embeds_one(name, schema, opts, do: block) do
module = Ecto.Schema.__embed_module__(schema, block)
schema = quote(do: __MODULE__.unquote(schema))
quote do
unquote(module)
Ecto.Schema.__embeds_one__(__MODULE__, unquote(name), unquote(schema), unquote(opts))
end
end

@doc ~S"""
Indicates an embedding of many schemas.
Expand Down Expand Up @@ -1143,14 +1200,71 @@ defmodule Ecto.Schema do
# Update the order
changeset = Repo.update!(changeset)
## Inline embedded schema
The schema module can be defined inline in the parent schema in simple
cases:
defmodule Parent do
use Ecto.Schema
schema "parents" do
field :name, :string
embeds_one :children, Child do
field :name, :string
field :age, :integer
end
end
end
Defining embedded schema in such a way will define a `Parent.Child` module
with the appropriate struct. In order to properly cast the embedded schema.
When casting the inline-defined embedded schemas you need to use the `:with`
option of `cast_embed/3` to provide the proper function to do the casting.
For example:
def changeset(schema, params) do
schema
|> cast(params, [:name])
|> cast_embed(:children, with: &child_changeset/2)
end
defp child_changeset(schema, params) do
schema
|> cast(params, [:name, :age])
end
"""
defmacro embeds_many(name, schema, opts \\ []) do
defmacro embeds_many(name, schema, opts \\ [])

defmacro embeds_many(name, schema, do: block) do
quote do
embeds_many(unquote(name), unquote(schema), [], do: unquote(block))
end
end

defmacro embeds_many(name, schema, opts) do
schema = expand_alias(schema, __CALLER__)
quote do
Ecto.Schema.__embeds_many__(__MODULE__, unquote(name), unquote(schema), unquote(opts))
end
end

@doc """
Indicates an embedding of many schemas.
For options and examples see documentation of `embeds_many/3`.
"""
defmacro embeds_many(name, schema, opts, do: block) do
module = Ecto.Schema.__embed_module__(schema, block)
schema = quote(do: __MODULE__.unquote(schema))
quote do
unquote(module)
Ecto.Schema.__embeds_many__(__MODULE__, unquote(name), unquote(schema), unquote(opts))
end
end

## Callbacks

@doc false
Expand Down Expand Up @@ -1311,6 +1425,20 @@ defmodule Ecto.Schema do
embed(mod, :many, name, schema, opts)
end

@doc false
def __embed_module__(name, block) do
quote do
defmodule unquote(name) do
use Ecto.Schema

@primary_key {:id, :binary_id, autogenerate: true}
embedded_schema do
unquote(block)
end
end
end
end

## Quoted callbacks

@doc false
Expand Down
36 changes: 32 additions & 4 deletions test/ecto/changeset/embedded_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@ defmodule Ecto.Changeset.EmbeddedTest do
embeds_one :profile, Profile, on_replace: :delete
embeds_one :raise_profile, Profile, on_replace: :raise
embeds_one :invalid_profile, Profile, on_replace: :mark_as_invalid
embeds_one :inline_profile, Profile do
field :name, :string
end
embeds_many :posts, Post, on_replace: :delete
embeds_many :raise_posts, Post, on_replace: :raise
embeds_many :invalid_posts, Post, on_replace: :mark_as_invalid
embeds_many :inline_posts, Post do
field :title, :string
end
end
end

Expand All @@ -32,13 +38,13 @@ defmodule Ecto.Changeset.EmbeddedTest do
end

def changeset(schema, params) do
Changeset.cast(schema, params, ~w(title))
cast(schema, params, ~w(title))
|> validate_required(:title)
|> validate_length(:title, min: 3)
end

def optional_changeset(schema, params) do
Changeset.cast(schema, params, ~w(title))
cast(schema, params, ~w(title))
end

def set_action(schema, params) do
Expand All @@ -56,13 +62,13 @@ defmodule Ecto.Changeset.EmbeddedTest do
end

def changeset(schema, params) do
Changeset.cast(schema, params, ~w(name id))
cast(schema, params, ~w(name id))
|> validate_required(:name)
|> validate_length(:name, min: 3)
end

def optional_changeset(schema, params) do
Changeset.cast(schema, params, ~w(name))
cast(schema, params, ~w(name))
end

def set_action(schema, params) do
Expand Down Expand Up @@ -266,6 +272,17 @@ defmodule Ecto.Changeset.EmbeddedTest do
refute changeset.valid?
end

test "cast inline embeds_one with valid params" do
changeset = cast(%Author{}, %{"inline_profile" => %{"name" => "michal"}},
:inline_profile, with: &Profile.changeset/2)
profile = changeset.changes.inline_profile
assert profile.changes == %{name: "michal"}
assert profile.errors == []
assert profile.action == :insert
assert profile.valid?
assert changeset.valid?
end

## cast embeds many

test "cast embeds_many with only new schemas" do
Expand Down Expand Up @@ -407,6 +424,17 @@ defmodule Ecto.Changeset.EmbeddedTest do
refute changeset.valid?
end

test "cast inline embeds_many with valid params" do
changeset = cast(%Author{}, %{"inline_posts" => [%{"title" => "hello"}]},
:inline_posts, with: &Post.changeset/2)
[post] = changeset.changes.inline_posts
assert post.changes == %{title: "hello"}
assert post.errors == []
assert post.action == :insert
assert post.valid?
assert changeset.valid?
end

## Others

test "change embeds_one" do
Expand Down
22 changes: 22 additions & 0 deletions test/ecto/schema_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,28 @@ defmodule Ecto.SchemaTest do
assert CustomEmbeddedSchema.__schema__(:primary_key) == []
end

defmodule InlineEmbeddedSchema do
use Ecto.Schema

schema "inline_embedded_schema" do
embeds_one :one, One do
field :x
end
embeds_many :many, Many do
field :y
end
end
end

test "inline embedded schema" do
assert %Ecto.Embedded{related: InlineEmbeddedSchema.One} =
InlineEmbeddedSchema.__schema__(:embed, :one)
assert %Ecto.Embedded{related: InlineEmbeddedSchema.Many} =
InlineEmbeddedSchema.__schema__(:embed, :many)
assert InlineEmbeddedSchema.One.__schema__(:fields) == [:id, :x]
assert InlineEmbeddedSchema.Many.__schema__(:fields) == [:id, :y]
end

defmodule Timestamps do
use Ecto.Schema

Expand Down

0 comments on commit 4a9181c

Please sign in to comment.