diff --git a/lib/ecto.ex b/lib/ecto.ex index c84ed3f820..a574864a34 100644 --- a/lib/ecto.ex +++ b/lib/ecto.ex @@ -601,4 +601,39 @@ defmodule Ecto do true end end + + @doc """ + Loads previously dumped `data` in the given `format` into a schema. + + The first argument can be a an embedded schema module, or a map (of types) and + determines the return value: a struct or a map, respectively. + + The second argument `data` specifies fields and values that are to be loaded. + It can be a map, a keyword list, or a `{fields, values}` tuple. Fields can be + atoms or strings. + + The third argument `format` is the format the data has been dumped as. For + example, databases may dump embedded to `:json`, this function allows such + dumped data to be put back into the schemas. + + Fields that are not present in the schema (or `types` map) are ignored. + If any of the values has invalid type, an error is raised. + + Note that if you want to load data into a non-embedded schema that was + directly persisted into a given repository, then use `c:Ecto.Repo.load/3`. + + ## Examples + + iex> result = Ecto.Adapters.SQL.query!(MyRepo, "SELECT users.settings FROM users", []) + iex> Enum.map(result.rows, fn [settings] -> Ecto.embedded_load(Setting, Jason.decode!(settings), :json) end) + [%Setting{...}, ...] + """ + @spec embedded_load( + module_or_map :: module | map(), + data :: map() | Keyword.t(), + format :: atom() + ) :: Ecto.Schema.t() | map() + def embedded_load(schema_or_types, data, format) do + Ecto.Repo.Schema.embedded_load(schema_or_types, data, format) + end end diff --git a/lib/ecto/repo.ex b/lib/ecto/repo.ex index bcb02c3146..dc4e7e8669 100644 --- a/lib/ecto/repo.ex +++ b/lib/ecto/repo.ex @@ -450,6 +450,8 @@ defmodule Ecto.Repo do Fields that are not present in the schema (or `types` map) are ignored. If any of the values has invalid type, an error is raised. + To load data from non-database sources, use `Ecto.embedded_load/3`. + ## Examples iex> MyRepo.load(User, %{name: "Alice", age: 25}) diff --git a/lib/ecto/repo/schema.ex b/lib/ecto/repo/schema.ex index a2bb837b5b..b71a550d06 100644 --- a/lib/ecto/repo/schema.ex +++ b/lib/ecto/repo/schema.ex @@ -454,6 +454,10 @@ defmodule Ecto.Repo.Schema do do_load(schema_or_types, data, &Ecto.Type.adapter_load(adapter, &1, &2)) end + def embedded_load(schema_or_types, data, format) do + do_load(schema_or_types, data, &Ecto.Type.embedded_load(&1, &2, format)) + end + defp do_load(schema, data, loader) when is_list(data), do: do_load(schema, Map.new(data), loader) defp do_load(schema, {fields, values}, loader) when is_list(fields) and is_list(values), diff --git a/test/ecto/embedded_test.exs b/test/ecto/embedded_test.exs index 8fd0889f0b..0007d8ef49 100644 --- a/test/ecto/embedded_test.exs +++ b/test/ecto/embedded_test.exs @@ -15,6 +15,14 @@ defmodule Ecto.EmbeddedTest do end end + defmodule MySchemaWithUuid do + use Ecto.Schema + + schema "my_schema" do + field :uuid, Ecto.UUID + end + end + test "__schema__" do assert Author.__schema__(:embeds) == [:profile, :post, :posts] @@ -25,4 +33,23 @@ defmodule Ecto.EmbeddedTest do assert Author.__schema__(:embed, :posts) == %Embedded{field: :posts, cardinality: :many, owner: Author, on_replace: :delete, related: Post} end + + test "embedded_load/3" do + uuid = Ecto.UUID.generate() + + assert %MySchemaWithUuid{uuid: ^uuid} = + Ecto.embedded_load(MySchemaWithUuid, %{"uuid" => uuid}, :json) + + assert %MySchemaWithUuid{uuid: ^uuid} = + Ecto.embedded_load(MySchemaWithUuid, %{uuid: uuid}, :json) + + assert %MySchemaWithUuid{uuid: nil} = + Ecto.embedded_load(MySchemaWithUuid, %{"uuid" => nil}, :json) + + assert_raise ArgumentError, + ~s[cannot load `"ABC"` as type Ecto.UUID for field `uuid` in schema Ecto.EmbeddedTest.MySchemaWithUuid], + fn -> + Ecto.embedded_load(MySchemaWithUuid, %{"uuid" => "ABC"}, :json) + end + end end