From 560b3ab6d1111344a4fd01ae76e3a0eb4217afae Mon Sep 17 00:00:00 2001 From: Dallin Osmun Date: Thu, 31 Jul 2025 21:54:05 -0600 Subject: [PATCH 1/2] allow Mongo.Collection's to be defined inline --- lib/mongo/collection.ex | 60 +++++++++++++++++++++++++- test/collections/inlined_test.exs | 70 +++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 test/collections/inlined_test.exs diff --git a/lib/mongo/collection.ex b/lib/mongo/collection.ex index 490ddb19..92d8b475 100644 --- a/lib/mongo/collection.ex +++ b/lib/mongo/collection.ex @@ -879,8 +879,25 @@ defmodule Mongo.Collection do @doc """ Adds the struct to the `embeds_one` list. Calls `__embeds_one__` """ - defmacro embeds_one(name, mod, opts \\ []) do + defmacro embeds_one(name, mod, opts \\ []) + + defmacro embeds_one(name, mod, do: block) do + quote do + embeds_one(unquote(name), unquote(mod), [], do: unquote(block)) + end + end + + defmacro embeds_one(name, mod, opts) do + quote do + Collection.__embeds_one__(__MODULE__, unquote(name), unquote(mod), unquote(opts)) + end + end + + defmacro embeds_one(name, mod, opts, do: block) do + mod = expand_nested_module_alias(mod, __CALLER__) + quote do + Collection.__embeds_module__(__ENV__, unquote(mod), unquote(Macro.escape(block))) Collection.__embeds_one__(__MODULE__, unquote(name), unquote(mod), unquote(opts)) end end @@ -895,8 +912,26 @@ defmodule Mongo.Collection do @doc """ Adds the struct to the `embeds_many` list. Calls `__embeds_many__` """ - defmacro embeds_many(name, mod, opts \\ []) do + defmacro embeds_many(name, mod, opts \\ []) + + defmacro embeds_many(name, mod, do: block) do + quote do + embeds_many(unquote(name), unquote(mod), [], do: unquote(block)) + end + end + + defmacro embeds_many(name, mod, opts) do + quote do + type = unquote(Macro.escape({{:., [], [mod, :t]}, [], []})) + Collection.__embeds_many__(__MODULE__, unquote(name), unquote(mod), type, unquote(opts)) + end + end + + defmacro embeds_many(name, mod, opts, do: block) do + mod = expand_nested_module_alias(mod, __CALLER__) + quote do + Collection.__embeds_module__(__ENV__, unquote(mod), unquote(Macro.escape(block))) type = unquote(Macro.escape({{:., [], [mod, :t]}, [], []})) Collection.__embeds_many__(__MODULE__, unquote(name), unquote(mod), type, unquote(opts)) end @@ -910,6 +945,19 @@ defmodule Mongo.Collection do Module.put_attribute(mod, :embed_manys, {name, target, add_name(mod, opts, name)}) end + def __embeds_module__(env, mod, block) do + block = + quote do + use Collection + + document do + unquote(block) + end + end + + Module.create(mod, block, env) + end + @doc """ Adds the attribute to the attributes list. It call `__attribute__/4` function. """ @@ -1080,4 +1128,12 @@ defmodule Mongo.Collection do |> Map.new() end end + + defp expand_nested_module_alias({:__aliases__, _, [Elixir, _ | _] = alias}, _env), + do: Module.concat(alias) + + defp expand_nested_module_alias({:__aliases__, _, [h | t]}, env) when is_atom(h), + do: Module.concat([env.module, h | t]) + + defp expand_nested_module_alias(other, _env), do: other end diff --git a/test/collections/inlined_test.exs b/test/collections/inlined_test.exs new file mode 100644 index 00000000..e35ce7c3 --- /dev/null +++ b/test/collections/inlined_test.exs @@ -0,0 +1,70 @@ +defmodule Collections.InlinedTest do + use MongoTest.Case, async: true + + defmodule Person do + use Mongo.Collection + + collection "persons" do + attribute :name, String.t(), default: "new name" + + embeds_one :friend, Friend, default: &Person.Friend.new/0 do + attribute :name, String.t(), default: "new friend" + end + + embeds_one :hobby, Hobby do + attribute :name, String.t() + end + + embeds_many :pets, Pet, default: [] do + attribute :name, String.t(), default: "new pet" + end + + embeds_many :things, Thing do + attribute :name, String.t() + end + end + end + + test "created the appropriate modules", _c do + Code.ensure_compiled!(Person) + Code.ensure_compiled!(Person.Friend) + Code.ensure_compiled!(Person.Hobby) + Code.ensure_compiled!(Person.Pet) + Code.ensure_compiled!(Person.Thing) + end + + test "can create a document for an inlined collection", _c do + new_person = %{ + Person.new() + | friend: %{Person.Friend.new() | name: "new friend"}, + hobby: %{Person.Hobby.new() | name: "new hobby"}, + pets: [%{Person.Pet.new() | name: "new pet"}], + things: [%{Person.Thing.new() | name: "new thing"}] + } + + map_person = Person.dump(new_person) + struct_person = Person.load(map_person, false) + + assert %{ + "name" => "new name", + "friend" => %{"name" => "new friend"}, + "hobby" => %{"name" => "new hobby"}, + "pets" => [%{"name" => "new pet"}], + "things" => [%{"name" => "new thing"}] + } = map_person + + assert %Person{ + name: "new name", + hobby: %Person.Hobby{name: "new hobby"}, + friend: %Person.Friend{name: "new friend"}, + things: [%Person.Thing{name: "new thing"}], + pets: [%Person.Pet{name: "new pet"}] + } = struct_person + + new_person = Person.new() + map_person = Person.dump(new_person) + struct_person = Person.load(map_person, false) + assert %{"name" => "new name", "friend" => %{"name" => "new friend"}, "pets" => []} = map_person + assert %Person{name: "new name", friend: %Person.Friend{name: "new friend"}, pets: []} = struct_person + end +end From 65a579c50036e51108dd839521ce89faa28ce282 Mon Sep 17 00:00:00 2001 From: Dallin Osmun Date: Thu, 31 Jul 2025 22:05:56 -0600 Subject: [PATCH 2/2] add some docs --- lib/mongo/collection.ex | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/mongo/collection.ex b/lib/mongo/collection.ex index 92d8b475..b01fd562 100644 --- a/lib/mongo/collection.ex +++ b/lib/mongo/collection.ex @@ -440,6 +440,28 @@ defmodule Mongo.Collection do "modified" : ISODate("2020-05-19T15:15:14.374Z"), "title" : "Vega" } + + ## Example `Inlined Collections` + + You can define an embedded collection inline. In the following example, the `Person.Friend` + and `Person.Pet` modules are automatically defined for you. + + defmodule Person do + use Mongo.Collection + + collection "persons" do + attribute :name, String.t() + + embeds_one :friend, Friend do + attribute :name, String.t() + end + + embeds_many :pets, Pet, default: [] do + attribute :name, String.t() + end + end + end + ## Example `timestamps` defmodule Post do