Skip to content

Commit

Permalink
Refactor Ecto.Strategy to use Ecto.Schema reflections (#248)
Browse files Browse the repository at this point in the history
Summary
=======

`Ecto.Schema` has a `__schema__/1` function that allows us to
instrospect which fields are non-virtual, which are embeds, and which
are associations.

We already make use of this `__schema__` function for determining
associations and embeds, but the code seemed somewhat complicated in
that when we cast fields, we had to know to skip associations and
embeds by checking each fields type. By using `__schema__(:fields)` and `__schema__(:embeds)` we are able to cast fields first, then embeds, and finally associations.

This has the advantage of allowing us to untangle associations and
embeds code which, although is somewhat similar, does not overlap
completely (there are more conditions to check when casting
associations).
  • Loading branch information
germsvel authored Oct 13, 2017
1 parent 313e924 commit 3776a51
Showing 1 changed file with 60 additions and 38 deletions.
98 changes: 60 additions & 38 deletions lib/ex_machina/ecto_strategy.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,36 +29,25 @@ defmodule ExMachina.EctoStrategy do
defp cast(record) do
record
|> cast_all_fields
|> cast_all_embeds
|> cast_all_assocs
end

defp cast_all_fields(struct) do
struct
|> ExMachina.Ecto.drop_ecto_fields
|> Map.keys
|> cast_all_fields(struct)
end
defp cast_all_fields(%{__struct__: schema} = struct) do
schema
|> schema_fields()
|> Enum.reduce(struct, fn(field_key, struct) ->
casted_value = cast_field(field_key, struct)

defp cast_all_fields(fields, struct) do
Enum.reduce(fields, struct, fn(field, struct) ->
casted_value = cast_field(field, struct)
Map.put(struct, field, casted_value)
Map.put(struct, field_key, casted_value)
end)
end

defp cast_field(field, %{__struct__: schema} = struct) do
field_type = schema.__schema__(:type, field)
virtual_field? = !field_type
embed_type = schema.__schema__(:embed, field)
embedded_field? = !!embed_type

value = Map.get(struct, field)
defp cast_field(field_key, %{__struct__: schema} = struct) do
field_type = schema.__schema__(:type, field_key)
value = Map.get(struct, field_key)

if virtual_field? || embedded_field? do
value
else
cast_value(field_type, value, struct)
end
cast_value(field_type, value, struct)
end

defp cast_value(field_type, value, struct) do
Expand All @@ -70,40 +59,73 @@ defmodule ExMachina.EctoStrategy do
end
end

defp cast_all_embeds(%{__struct__: schema} = struct) do
schema
|> schema_embeds()
|> Enum.reduce(struct, fn(embed_key, struct) ->
casted_value = struct |> Map.get(embed_key) |> cast_embed(embed_key, struct)

Map.put(struct, embed_key, casted_value)
end)
end

defp cast_embed(embeds_many, embed_key, struct) when is_list(embeds_many) do
Enum.map(embeds_many, &(cast_embed(&1, embed_key, struct)))
end
defp cast_embed(embed, embed_key, %{__struct__: schema}) do
if embed do
embedding_reflection = schema.__schema__(:embed, embed_key)
embed_type = embedding_reflection.related
embed_type |> struct() |> Map.merge(embed) |> cast()
end
end

defp cast_all_assocs(%{__struct__: schema} = struct) do
assocs = get_schema_assocs(schema)
assoc_keys = schema_associations(schema)

Enum.reduce(assocs, struct, fn(assoc, struct) ->
casted_value = struct |> Map.get(assoc) |> cast_assoc(assoc, struct)
Enum.reduce(assoc_keys, struct, fn(assoc_key, struct) ->
casted_value = struct |> Map.get(assoc_key) |> cast_assoc(assoc_key, struct)

Map.put(struct, assoc, casted_value)
Map.put(struct, assoc_key, casted_value)
end)
end

defp cast_assoc(original_assoc, assoc_key, %{__struct__: schema} = struct) do
case original_assoc do
has_or_embeds_many when is_list(has_or_embeds_many) ->
Enum.map(has_or_embeds_many, &(cast_assoc(&1, assoc_key, struct)))

defp cast_assoc(has_many_assoc, assoc_key, struct) when is_list(has_many_assoc) do
Enum.map(has_many_assoc, &(cast_assoc(&1, assoc_key, struct)))
end
defp cast_assoc(assoc, assoc_key, %{__struct__: schema}) do
case assoc do
%{__meta__: %{__struct__: Ecto.Schema.Metadata, state: :built}} ->
cast(original_assoc)
cast(assoc)

%{__struct__: Ecto.Association.NotLoaded} ->
original_assoc
assoc

%{__struct__: _} ->
cast(original_assoc)
cast(assoc)

%{} ->
assoc_reflection = schema.__schema__(:association, assoc_key) || schema.__schema__(:embed, assoc_key)
assoc_reflection = schema.__schema__(:association, assoc_key)
assoc_type = assoc_reflection.related
assoc_type |> struct() |> Map.merge(original_assoc) |> cast()
assoc_type |> struct() |> Map.merge(assoc) |> cast()

nil -> nil
end
end

defp get_schema_assocs(schema) do
schema.__schema__(:associations) ++ schema.__schema__(:embeds)
defp schema_fields(schema) do
schema_non_virtual_fields(schema) -- schema_embeds(schema)
end

defp schema_non_virtual_fields(schema) do
schema.__schema__(:fields)
end

defp schema_embeds(schema) do
schema.__schema__(:embeds)
end

defp schema_associations(schema) do
schema.__schema__(:associations)
end
end

0 comments on commit 3776a51

Please sign in to comment.