diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..d422fff --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +elixir 1.17.3 +erlang 26.2.5.6 diff --git a/README.md b/README.md index 537eae7..8b73946 100644 --- a/README.md +++ b/README.md @@ -130,20 +130,31 @@ AsNestedSet.roots(Taxon, %{taxonomy_id: 1}) |> AsNestedSet.execute(TestRepo) # find all children of target AsNestedSet.children(target) |> AsNestedSet.execute(TestRepo) +# find children of a target filtered by an Ecto query +import Ecto.Query, only: [from: 2] +... +from(r in Records, where: r.public == true) |> AsNestedSet.children(target) |> AsNestedSet.execute(TestRepo) + # find all the leaves for given scope AsNestedSet.leaves(Taxon, %{taxonomy_id: 1}) |> AsNestedSet.execute(TestRepo) # find all descendants AsNestedSet.descendants(target) |> AsNestedSet.execute(TestRepo) + +# filtered descendants +from(r in Records, where: r.public == true) |> AsNestedSet.descendants(target) |> AsNestedSet.execute(TestRepo) + # include self AsNestedSet.self_and_descendants(target) |> AsNestedSet.execute(TestRepo) # find all ancestors AsNestedSet.ancestors(target) |> AsNestedSet.execute(TestRepo) -#find all siblings (self included) +# find all siblings (self included) AsNestedSet.self_and_siblings(target) |> AsNestedSet.execute(TestRepo) +# find siblings filtered by a query (self included) +from(r in Records, where: r.public == true) |> AsNestedSet.self_and_siblings(target) |> AsNestedSet.execute(TestRepo) ``` Traverse the tree diff --git a/config/config.exs b/config/config.exs index 5112600..707c016 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,6 +1,6 @@ # This file is responsible for configuring your application # and its dependencies with the aid of the Mix.Config module. -use Mix.Config +import Config # This configuration is loaded before any dependency and is restricted # to this project. If another project depends on this project, this @@ -28,6 +28,6 @@ use Mix.Config # here (which is why it is important to import them last). # -if Mix.env == :test do +if Mix.env() == :test do import_config "test.exs" end diff --git a/config/test.exs b/config/test.exs index 36259c8..76d3f40 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,4 +1,6 @@ -use Mix.Config +import Config + +config :as_nested_set, ecto_repos: [AsNestedSet.TestRepo] config :as_nested_set, AsNestedSet.TestRepo, hostname: "localhost", diff --git a/config/test.exs.travis b/config/test.exs.travis index 9e9d42e..2ea48ab 100644 --- a/config/test.exs.travis +++ b/config/test.exs.travis @@ -1,4 +1,4 @@ -use Mix.Config +import Config config :as_nested_set, AsNestedSet.TestRepo, hostname: "localhost", diff --git a/lib/as_nested_set.ex b/lib/as_nested_set.ex index 3c9e9ad..fc9b264 100644 --- a/lib/as_nested_set.ex +++ b/lib/as_nested_set.ex @@ -1,6 +1,7 @@ defmodule AsNestedSet do defmacro __using__(args) do scope = Keyword.get(args, :scope, []) + quote do use AsNestedSet.Model use AsNestedSet.Scoped, scope: unquote(scope) @@ -9,7 +10,7 @@ defmodule AsNestedSet do @type t :: struct - @type executable :: (Ecto.Repo.t -> any) + @type executable :: (Ecto.Repo.t() -> any) @spec defined?(struct) :: boolean def defined?(struct) when is_atom(struct) do @@ -21,12 +22,14 @@ defmodule AsNestedSet do false end end + def defined?(%{__struct__: struct}) do defined?(struct) end + def defined?(_), do: false - @spec execute(executable, Ecto.Repo.t) :: any + @spec execute(executable, Ecto.Repo.t()) :: any def execute(call, repo) do call.(repo) end @@ -39,13 +42,17 @@ defmodule AsNestedSet do defdelegate move(model, position), to: AsNestedSet.Modifiable defdelegate self_and_siblings(target), to: AsNestedSet.Queriable + defdelegate self_and_siblings(query, target), to: AsNestedSet.Queriable defdelegate ancestors(target), to: AsNestedSet.Queriable defdelegate self_and_descendants(target), to: AsNestedSet.Queriable + defdelegate self_and_descendants(query, target), to: AsNestedSet.Queriable defdelegate root(module, scope), to: AsNestedSet.Queriable defdelegate roots(module, scope), to: AsNestedSet.Queriable defdelegate descendants(target), to: AsNestedSet.Queriable + defdelegate descendants(query, target), to: AsNestedSet.Queriable defdelegate leaves(module, scope), to: AsNestedSet.Queriable defdelegate children(target), to: AsNestedSet.Queriable + defdelegate children(query, target), to: AsNestedSet.Queriable defdelegate dump(module, scope), to: AsNestedSet.Queriable defdelegate dump(module, scope, parent_id), to: AsNestedSet.Queriable defdelegate dump_one(module, scope), to: AsNestedSet.Queriable diff --git a/lib/as_nested_set/helper.ex b/lib/as_nested_set/helper.ex index 66ceb6b..d3b4e22 100644 --- a/lib/as_nested_set/helper.ex +++ b/lib/as_nested_set/helper.ex @@ -1,5 +1,4 @@ defmodule AsNestedSet.Helper do - def get_column_name(%{__struct__: struct}, field) do get_column_name(struct, field) end @@ -19,6 +18,4 @@ defmodule AsNestedSet.Helper do def fields(module) when is_atom(module) do module.__as_nested_set_fields__() end - - -end \ No newline at end of file +end diff --git a/lib/as_nested_set/model.ex b/lib/as_nested_set/model.ex index ab4f425..613ca60 100644 --- a/lib/as_nested_set/model.ex +++ b/lib/as_nested_set/model.ex @@ -1,5 +1,4 @@ defmodule AsNestedSet.Model do - defmacro __using__(_) do quote do @node_id_column :id @@ -18,36 +17,43 @@ defmodule AsNestedSet.Model do end defp define_accessors(names, env) do - fields = Enum.map(names, fn - name -> - attribute_name = String.to_atom("#{name}_column") - column_name = Module.get_attribute(env.module, attribute_name) - {name, column_name} - end) |> Enum.into(%{}) + fields = + Enum.map(names, fn + name -> + attribute_name = String.to_atom("#{name}_column") + column_name = Module.get_attribute(env.module, attribute_name) + {name, column_name} + end) + |> Enum.into(%{}) Enum.map(fields, fn - {name, column_name}-> + {name, column_name} -> quote do def __as_nested_set_column_name__(unquote(name)) do unquote(column_name) end + def __as_nested_set_get_field__(model, unquote(name)) do Map.get(model, unquote(column_name)) end + def __as_nested_set_set_field__(model, unquote(name), value) do Map.put(model, unquote(column_name), value) end end - end) ++ [ - quote do - def __as_nested_set_field__(field) do - raise ArgumentError, "Unknown AsNestedSet field #{inspect field} for #{inspect __MODULE__}" + end) ++ + [ + quote do + def __as_nested_set_field__(field) do + raise ArgumentError, + "Unknown AsNestedSet field #{inspect(field)} for #{inspect(__MODULE__)}" + end + + def __as_nested_set_get_field__(model, _), do: nil + def __as_nested_set_set_field__(model, _, _), do: model + def __as_nested_set_fields__(), do: unquote(fields |> Macro.escape()) end - def __as_nested_set_get_field__(model, _), do: nil - def __as_nested_set_set_field__(model, _, _), do: model - def __as_nested_set_fields__(), do: unquote(fields |> Macro.escape) - end - ] + ] end defp define_queriers(_env) do diff --git a/lib/as_nested_set/modifiable.ex b/lib/as_nested_set/modifiable.ex index 5415390..873e634 100644 --- a/lib/as_nested_set/modifiable.ex +++ b/lib/as_nested_set/modifiable.ex @@ -1,12 +1,11 @@ defmodule AsNestedSet.Modifiable do - @type position :: :left | :right | :child | :parent import Ecto.Query import AsNestedSet.Helper - @spec create(AsNestedSet.t, AsNestedSet.t, position) :: AsNestedSet.executable - @spec create(AsNestedSet.t, nil, :root) :: AsNestedSet.executable + @spec create(AsNestedSet.t(), AsNestedSet.t(), position) :: AsNestedSet.executable() + @spec create(AsNestedSet.t(), nil, :root) :: AsNestedSet.executable() def create(new_model, target \\ nil, position) when is_atom(position) do fn repo -> case validate_create(new_model, target, position) do @@ -16,7 +15,7 @@ defmodule AsNestedSet.Modifiable do end end - @spec reload(AsNestedSet.t) :: AsNestedSet.executable + @spec reload(AsNestedSet.t()) :: AsNestedSet.executable() def reload(model) do fn repo -> do_reload(repo, model) @@ -25,9 +24,14 @@ defmodule AsNestedSet.Modifiable do defp validate_create(new_model, parent, position) do cond do - parent == nil && position != :root -> {:error, :target_is_required} - position != :root && !AsNestedSet.Scoped.same_scope?(parent, new_model) -> {:error, :not_the_same_scope} - true -> :ok + parent == nil && position != :root -> + {:error, :target_is_required} + + position != :root && !AsNestedSet.Scoped.same_scope?(parent, new_model) -> + {:error, :not_the_same_scope} + + true -> + :ok end end @@ -50,16 +54,17 @@ defmodule AsNestedSet.Modifiable do |> AsNestedSet.Scoped.scoped_query(target) |> repo.update_all([]) - parent_id_column = get_column_name(target, :parent_id) parent_id = get_field(target, :parent_id) # insert the new model new_model - |> struct.changeset(Map.new([ + |> struct.changeset( + Map.new([ {left_column, left}, {right_column, left + 1}, {parent_id_column, parent_id} - ])) + ]) + ) |> repo.insert! end @@ -75,6 +80,7 @@ defmodule AsNestedSet.Modifiable do |> repo.update_all([]) right_column = get_column_name(target, :right) + from(q in struct, where: field(q, ^right_column) > ^right, update: [inc: ^[{right_column, 2}]] @@ -86,19 +92,21 @@ defmodule AsNestedSet.Modifiable do parent_id = get_field(target, :parent_id) # insert new model new_model - |> struct.changeset(Map.new([ + |> struct.changeset( + Map.new([ {left_column, right + 1}, {right_column, right + 2}, {parent_id_column, parent_id} - ])) + ]) + ) |> repo.insert! end defp do_safe_create(repo, %{__struct__: struct} = new_model, target, :child) do - left_column = get_column_name(target, :left) right = get_field(target, :right) + from(q in struct, where: field(q, ^left_column) > ^right, update: [inc: ^[{left_column, 2}]] @@ -107,6 +115,7 @@ defmodule AsNestedSet.Modifiable do |> repo.update_all([]) right_column = get_column_name(target, :right) + from(q in struct, where: field(q, ^right_column) >= ^right, update: [inc: ^[{right_column, 2}]] @@ -114,26 +123,29 @@ defmodule AsNestedSet.Modifiable do |> AsNestedSet.Scoped.scoped_query(target) |> repo.update_all([]) - parent_id_column = get_column_name(target, :parent_id) node_id = get_field(target, :node_id) + new_model - |> struct.changeset(Map.new([ + |> struct.changeset( + Map.new([ {left_column, right}, {right_column, right + 1}, {parent_id_column, node_id} - ])) + ]) + ) |> repo.insert! end defp do_safe_create(repo, %{__struct__: struct} = new_model, _target, :root) do right_most = AsNestedSet.Queriable.right_most(struct, new_model).(repo) || -1 - new_model = new_model - |> set_field(:left, right_most + 1) - |> set_field(:right, right_most + 2) - |> set_field(:parent_id, nil) - |> repo.insert! + new_model = + new_model + |> set_field(:left, right_most + 1) + |> set_field(:right, right_most + 2) + |> set_field(:parent_id, nil) + |> repo.insert! new_model end @@ -143,6 +155,7 @@ defmodule AsNestedSet.Modifiable do left = get_field(target, :left) right_column = get_column_name(target, :right) + from(q in struct, where: field(q, ^right_column) > ^right, update: [inc: ^[{right_column, 2}]] @@ -151,6 +164,7 @@ defmodule AsNestedSet.Modifiable do |> repo.update_all([]) left_column = get_column_name(target, :left) + from(q in struct, where: field(q, ^left_column) > ^right, update: [inc: ^[{left_column, 2}]] @@ -166,11 +180,13 @@ defmodule AsNestedSet.Modifiable do |> repo.update_all([]) parent_id = get_field(target, :parent_id) - new_model = new_model - |> set_field(:left, left) - |> set_field(:right, right + 2) - |> set_field(:parent_id, parent_id) - |> repo.insert! + + new_model = + new_model + |> set_field(:left, left) + |> set_field(:right, right + 2) + |> set_field(:parent_id, parent_id) + |> repo.insert! node_id = get_field(target, :node_id) node_id_column = get_column_name(target, :node_id) @@ -193,6 +209,7 @@ defmodule AsNestedSet.Modifiable do defp do_reload(repo, %{__struct__: struct} = target) do node_id = get_field(target, :node_id) node_id_column = get_column_name(target, :node_id) + from(q in struct, where: field(q, ^node_id_column) == ^node_id, limit: 1 @@ -201,7 +218,7 @@ defmodule AsNestedSet.Modifiable do |> repo.one end - @spec delete(AsNestedSet.t) :: AsNestedSet.executable + @spec delete(AsNestedSet.t()) :: AsNestedSet.executable() def delete(%{__struct__: struct} = model) do fn repo -> left = get_field(model, :left) @@ -233,11 +250,12 @@ defmodule AsNestedSet.Modifiable do end end - @spec move(AsNestedSet.t, AsNestedSet.t, position) :: AsNestedSet.executable - @spec move(AsNestedSet.t, nil, :root) :: AsNestedSet.executable + @spec move(AsNestedSet.t(), AsNestedSet.t(), position) :: AsNestedSet.executable() + @spec move(AsNestedSet.t(), nil, :root) :: AsNestedSet.executable() def move(%{__struct__: _} = model, target \\ nil, position) when is_atom(position) do fn repo -> model = do_reload(repo, model) + case validate_move(model, target, position) do :ok -> do_safe_move(repo, model, do_reload(repo, target), position) error -> error @@ -247,11 +265,21 @@ defmodule AsNestedSet.Modifiable do defp validate_move(model, target, position) do cond do - target == nil && position != :root -> {:error, :target_is_required} - position == :parent -> {:error, :cannot_move_to_parent} - target != nil && get_field(model, :left) <= get_field(target, :left) && get_field(model, :right) >= get_field(target, :right) -> {:error, :within_the_same_tree} - position != :root && !AsNestedSet.Scoped.same_scope?(target, model) -> {:error, :not_the_same_scope} - true -> :ok + target == nil && position != :root -> + {:error, :target_is_required} + + position == :parent -> + {:error, :cannot_move_to_parent} + + target != nil && get_field(model, :left) <= get_field(target, :left) && + get_field(model, :right) >= get_field(target, :right) -> + {:error, :within_the_same_tree} + + position != :root && !AsNestedSet.Scoped.same_scope?(target, model) -> + {:error, :not_the_same_scope} + + true -> + :ok end end @@ -262,9 +290,16 @@ defmodule AsNestedSet.Modifiable do target_bound = target_bound(repo, model, target, position) left = get_field(model, :left) right = get_field(model, :right) + case get_bounaries(model, target_bound) do {bound, other_bound} -> - do_switch(repo, model, {left, right, bound, other_bound}, new_parent_id(target, position)) + do_switch( + repo, + model, + {left, right, bound, other_bound}, + new_parent_id(target, position) + ) + :no_operation -> model end @@ -283,11 +318,14 @@ defmodule AsNestedSet.Modifiable do def get_bounaries(model, target_bound) do left = get_field(model, :left) right = get_field(model, :right) + cond do target_bound - 1 >= right + 1 -> {right + 1, target_bound - 1} + target_bound <= left - 1 -> {target_bound, left - 1} + true -> :no_operation end @@ -304,7 +342,7 @@ defmodule AsNestedSet.Modifiable do defp do_switch(repo, %{__struct__: struct} = model, boundaries, new_parent_id) do # As we checked the boundaries, the two interval is non-overlapping - [a, b, c, d]= boundaries |> Tuple.to_list |> Enum.sort + [a, b, c, d] = boundaries |> Tuple.to_list() |> Enum.sort() node_id = get_field(model, :node_id) node_id_column = get_column_name(model, :node_id) parent_id_column = get_column_name(model, :parent_id) @@ -312,26 +350,31 @@ defmodule AsNestedSet.Modifiable do do_shift(repo, model, {a, b}, -b - 1) do_shift(repo, model, {c, d}, a - c) do_shift(repo, model, {a - b - 1, -1}, d + 1) - from(n in struct, where: field(n, ^node_id_column) == ^node_id, update: [set: ^[{parent_id_column, new_parent_id}]]) + + from(n in struct, + where: field(n, ^node_id_column) == ^node_id, + update: [set: ^[{parent_id_column, new_parent_id}]] + ) |> AsNestedSet.Scoped.scoped_query(model) |> repo.update_all([]) + do_reload(repo, model) end defp do_shift(repo, %{__struct__: struct} = model, {left, right}, delta) do left_column = get_column_name(model, :left) right_column = get_column_name(model, :right) + from(struct) |> where([n], field(n, ^left_column) >= ^left and field(n, ^left_column) <= ^right) - |> update([n], [inc: ^[{left_column, delta}]]) + |> update([n], inc: ^[{left_column, delta}]) |> AsNestedSet.Scoped.scoped_query(model) |> repo.update_all([]) from(struct) |> where([n], field(n, ^right_column) >= ^left and field(n, ^right_column) <= ^right) - |> update([n], [inc: ^[{right_column, delta}]]) + |> update([n], inc: ^[{right_column, delta}]) |> AsNestedSet.Scoped.scoped_query(model) |> repo.update_all([]) - end end diff --git a/lib/as_nested_set/queriable.ex b/lib/as_nested_set/queriable.ex index d619a9f..047f965 100644 --- a/lib/as_nested_set/queriable.ex +++ b/lib/as_nested_set/queriable.ex @@ -1,14 +1,18 @@ defmodule AsNestedSet.Queriable do - import Ecto.Query import AsNestedSet.Helper def self_and_siblings(%{__struct__: struct} = target) do + self_and_siblings(struct, target) + end + + def self_and_siblings(query, %{__struct__: _} = target) do fn repo -> parent_id_column = get_column_name(target, :parent_id) left_column = get_column_name(target, :left) parent_id = get_field(target, :parent_id) - from(q in struct, + + from(q in query, where: field(q, ^parent_id_column) == ^parent_id, order_by: ^[left_column] ) @@ -23,6 +27,7 @@ defmodule AsNestedSet.Queriable do right = get_field(target, :right) left_column = get_column_name(target, :left) right_column = get_column_name(target, :right) + from(q in struct, where: field(q, ^left_column) < ^left and field(q, ^right_column) > ^right, order_by: ^[left_column] @@ -33,12 +38,17 @@ defmodule AsNestedSet.Queriable do end def self_and_descendants(%{__struct__: struct} = target) do + self_and_descendants(struct, target) + end + + def self_and_descendants(query, %{__struct__: _} = target) do fn repo -> left = get_field(target, :left) right = get_field(target, :right) left_column = get_column_name(target, :left) right_column = get_column_name(target, :right) - from(q in struct, + + from(q in query, where: field(q, ^left_column) >= ^left and field(q, ^right_column) <= ^right, order_by: ^[left_column] ) @@ -50,6 +60,7 @@ defmodule AsNestedSet.Queriable do def root(module, scope) when is_atom(module) do fn repo -> parent_id_column = get_column_name(module, :parent_id) + from(q in module, where: is_nil(field(q, ^parent_id_column)), limit: 1 @@ -63,6 +74,7 @@ defmodule AsNestedSet.Queriable do fn repo -> parent_id_column = get_column_name(module, :parent_id) left_column = get_column_name(module, :left) + from(q in module, where: is_nil(field(q, ^parent_id_column)), order_by: ^[left_column] @@ -73,12 +85,17 @@ defmodule AsNestedSet.Queriable do end def descendants(%{__struct__: struct} = target) do + descendants(struct, target) + end + + def descendants(query, %{__struct__: _} = target) do fn repo -> left = get_field(target, :left) right = get_field(target, :right) left_column = get_column_name(target, :left) right_column = get_column_name(target, :right) - from(q in struct, + + from(q in query, where: field(q, ^left_column) > ^left and field(q, ^right_column) < ^right, order_by: ^[left_column] ) @@ -91,6 +108,7 @@ defmodule AsNestedSet.Queriable do fn repo -> left_column = get_column_name(module, :left) right_column = get_column_name(module, :right) + from(q in module, where: fragment("? - ?", field(q, ^right_column), field(q, ^left_column)) == 1, order_by: ^[left_column] @@ -101,11 +119,16 @@ defmodule AsNestedSet.Queriable do end def children(%{__struct__: struct} = target) do + children(struct, target) + end + + def children(query, %{__struct__: _} = target) do fn repo -> parent_id_column = get_column_name(target, :parent_id) left_column = get_column_name(target, :left) node_id = get_field(target, :node_id) - from(q in struct, + + from(q in query, where: field(q, ^parent_id_column) == ^node_id, order_by: ^[left_column] ) @@ -119,21 +142,22 @@ defmodule AsNestedSet.Queriable do parent_id_column = get_column_name(module, :parent_id) left_column = get_column_name(module, :left) - children = if parent_id do - from(q in module, - where: field(q, ^parent_id_column) == ^parent_id, - order_by: ^[left_column] - ) - else - from(q in module, - where: is_nil(field(q, ^parent_id_column)), - order_by: ^[left_column] - ) - end - |> AsNestedSet.Scoped.scoped_query(scope) - |> repo.all + children = + if parent_id do + from(q in module, + where: field(q, ^parent_id_column) == ^parent_id, + order_by: ^[left_column] + ) + else + from(q in module, + where: is_nil(field(q, ^parent_id_column)), + order_by: ^[left_column] + ) + end + |> AsNestedSet.Scoped.scoped_query(scope) + |> repo.all - Enum.map(children, fn(child) -> + Enum.map(children, fn child -> node_id = get_field(child, :node_id) {child, dump(module, scope, node_id).(repo)} end) @@ -143,7 +167,7 @@ defmodule AsNestedSet.Queriable do def dump_one(module, scope) do fn repo -> case dump(module, scope).(repo) do - [dump|_] -> dump + [dump | _] -> dump error -> error end end @@ -152,6 +176,7 @@ defmodule AsNestedSet.Queriable do def right_most(module, scope) when is_atom(module) do fn repo -> right_column = get_column_name(module, :right) + from(q in module, select: max(field(q, ^right_column)) ) @@ -161,6 +186,7 @@ defmodule AsNestedSet.Queriable do end def right_most(nil), do: fn _ -> -1 end + def right_most(%{__struct__: struct} = target) do fn repo -> right_most(struct, target).(repo) diff --git a/lib/as_nested_set/scoped.ex b/lib/as_nested_set/scoped.ex index a2c72dc..d97cf26 100644 --- a/lib/as_nested_set/scoped.ex +++ b/lib/as_nested_set/scoped.ex @@ -1,5 +1,4 @@ defmodule AsNestedSet.Scoped do - import Ecto.Query @type scope :: [atom] @@ -13,18 +12,19 @@ defmodule AsNestedSet.Scoped do defmacro __before_compile__(env) do scope = Module.get_attribute(env.module, :scope) + quote do def __as_nested_set_scope__(), do: unquote(scope) end end - @spec same_scope?(AsNestedSet.t, AsNestedSet.t) :: boolean + @spec same_scope?(AsNestedSet.t(), AsNestedSet.t()) :: boolean def same_scope?(source, target) do - source.__struct__ == target.__struct__ - && do_same_scope?(source, target) + source.__struct__ == target.__struct__ && + do_same_scope?(source, target) end - @spec scoped_query(Ecto.Query.t, map) :: Ecto.Query.t + @spec scoped_query(Ecto.Query.t(), map) :: Ecto.Query.t() def scoped_query(query, scope) do %Ecto.Query.FromExpr{source: {_, module}} = query.from do_scoped_query(query, scope, module.__as_nested_set_scope__()) @@ -32,29 +32,32 @@ defmodule AsNestedSet.Scoped do @spec assign_scope_from(any, any) :: any def assign_scope_from(%{__struct__: struct} = target, %{__struct__: struct} = source) do - scope = struct.__as_nested_set_scope__ - Enum.reduce(scope, target, fn(scope, acc) -> + scope = struct.__as_nested_set_scope__() + + Enum.reduce(scope, target, fn scope, acc -> Map.put(acc, scope, Map.fetch!(source, scope)) end) end @spec scope(any) :: map def scope(%{__struct__: struct} = target) do - scope = struct.__as_nested_set_scope__ + scope = struct.__as_nested_set_scope__() + Enum.reduce(scope, %{}, fn scope, acc -> Map.put(acc, scope, Map.fetch!(target, scope)) end) end def scope(module) when is_atom(module) do - module.__as_nested_set_scope__ + module.__as_nested_set_scope__() end defp do_scoped_query(query, scope, scope_fields) do - Enum.reduce(scope_fields, query, fn(scope_field, acc) -> + Enum.reduce(scope_fields, query, fn scope_field, acc -> case Map.fetch!(scope, scope_field) do nil -> acc |> where([p], is_nil(field(p, ^scope_field))) + v -> acc |> where([p], field(p, ^scope_field) == ^v) end @@ -63,6 +66,7 @@ defmodule AsNestedSet.Scoped do defp do_same_scope?(%{__struct__: struct} = source, target) do scope = struct.__as_nested_set_scope__() + Enum.all?(scope, fn field -> Map.get(source, field) == Map.get(target, field) end) diff --git a/lib/as_nested_set/traversable.ex b/lib/as_nested_set/traversable.ex index bfab623..6746893 100644 --- a/lib/as_nested_set/traversable.ex +++ b/lib/as_nested_set/traversable.ex @@ -1,9 +1,11 @@ defmodule AsNestedSet.Traversable do + @type pre_fun :: (AsNestedSet.t(), any -> {AsNestedSet.t(), any}) + @type post_fun :: + (AsNestedSet.t(), [AsNestedSet.t()], any -> {AsNestedSet.t(), any}) + | (AsNestedSet.t(), any -> {AsNestedSet.t(), any}) - @type pre_fun :: (AsNestedSet.t, any -> {AsNestedSet.t, any}) - @type post_fun :: (AsNestedSet.t, [AsNestedSet.t], any -> {AsNestedSet.t, any}) | (AsNestedSet.t, any -> {AsNestedSet.t, any}) - - @spec traverse(AsNestedSet.t, any, pre_fun, post_fun) :: (Ecto.Repo.t -> {AsNestedSet.t, any}) + @spec traverse(AsNestedSet.t(), any, pre_fun, post_fun) :: + (Ecto.Repo.t() -> {AsNestedSet.t(), any}) def traverse(%{__struct__: _} = node, context, pre, post) do fn repo -> node = do_reload(node, repo) @@ -12,7 +14,8 @@ defmodule AsNestedSet.Traversable do end end - @spec traverse(module, AsNestedSet.Scoped.scope, any, pre_fun, post_fun) :: (Ecto.Repo.t -> {[AsNestedSet.t], any}) + @spec traverse(module, AsNestedSet.Scoped.scope(), any, pre_fun, post_fun) :: + (Ecto.Repo.t() -> {[AsNestedSet.t()], any}) def traverse(module, scope, context, pre, post) do fn repo -> AsNestedSet.Queriable.roots(module, scope).(repo) @@ -21,20 +24,25 @@ defmodule AsNestedSet.Traversable do end defp do_traverse(node, context, pre, post, repo) do - {children, context} = AsNestedSet.Queriable.children(node).(repo) - |> do_traverse_children(context, pre, post, repo) + {children, context} = + AsNestedSet.Queriable.children(node).(repo) + |> do_traverse_children(context, pre, post, repo) + call_post(post, do_reload(node, repo), do_reload(children, repo), context) end defp do_traverse_children([], context, _pre, _post, _repo), do: {[], context} + defp do_traverse_children(children, context, pre, post, repo) do - {children, context} = Enum.reduce(children, {[], context}, fn - child, {acc, context} -> - {child, context} = call_pre(pre, do_reload(child, repo), context) - {child, context} = do_traverse(child, context, pre, post, repo) - {[child|acc], context} - end) - {children |> Enum.reverse, context} + {children, context} = + Enum.reduce(children, {[], context}, fn + child, {acc, context} -> + {child, context} = call_pre(pre, do_reload(child, repo), context) + {child, context} = do_traverse(child, context, pre, post, repo) + {[child | acc], context} + end) + + {children |> Enum.reverse(), context} end defp do_reload(nodes, repo) when is_list(nodes) do @@ -49,7 +57,7 @@ defmodule AsNestedSet.Traversable do pre.(node, context) |> check_callback_response(:pre) end - defp call_post(post, node, children, context) when is_function(post, 3)do + defp call_post(post, node, children, context) when is_function(post, 3) do post.(node, children, context) |> check_callback_response(:post) end @@ -58,5 +66,11 @@ defmodule AsNestedSet.Traversable do end defp check_callback_response({_node, _context} = ok, _), do: ok - defp check_callback_response(error, callback), do: raise ArgumentError, "Expect #{inspect callback} to return {AsNestedSet.t, context} but got #{inspect error}" + + defp check_callback_response(error, callback), + do: + raise( + ArgumentError, + "Expect #{inspect(callback)} to return {AsNestedSet.t, context} but got #{inspect(error)}" + ) end diff --git a/mix.exs b/mix.exs index a80926d..2832da1 100644 --- a/mix.exs +++ b/mix.exs @@ -5,7 +5,7 @@ defmodule AsNestedSet.Mixfile do [ app: :as_nested_set, version: "3.4.1", - elixir: "~> 1.2", + elixir: "~> 1.16", elixirc_paths: elixirc_paths(Mix.env()), description: description(), package: package(), @@ -43,16 +43,16 @@ defmodule AsNestedSet.Mixfile do [applications: app_list(Mix.env())] end - def app_list(:test), do: app_list() ++ [:ecto, :postgrex, :ex_machina] + def app_list(:test), do: app_list() ++ [:postgrex, :ex_machina] def app_list(_), do: app_list() - def app_list, do: [:logger] + def app_list, do: [:ecto, :logger] defp deps do [ - {:ecto, "~> 3.4"}, - {:ecto_sql, "~> 3.4"}, + {:ecto, "~> 3.10"}, + {:ecto_sql, "~> 3.10"}, {:ex_doc, ">= 0.18.3", only: :dev}, - {:postgrex, "~> 0.15.0", only: :test}, + {:postgrex, "~> 0.16.0", only: :test}, {:ex_machina, "~> 2.2", only: [:test]}, {:excoveralls, "~> 0.12", only: [:test]} ] diff --git a/mix.lock b/mix.lock index 1c040d8..80ebbd4 100644 --- a/mix.lock +++ b/mix.lock @@ -1,25 +1,27 @@ %{ "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"}, - "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, - "db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"}, - "decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"}, + "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, + "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, + "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, - "ecto": {:hex, :ecto, "3.4.5", "2bcd262f57b2c888b0bd7f7a28c8a48aa11dc1a2c6a858e45dd8f8426d504265", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8c6d1d4d524559e9b7a062f0498e2c206122552d63eacff0a6567ffe7a8e8691"}, - "ecto_sql": {:hex, :ecto_sql, "3.4.4", "d28bac2d420f708993baed522054870086fd45016a9d09bb2cd521b9c48d32ea", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "edb49af715dd72f213b66adfd0f668a43c17ed510b5d9ac7528569b23af57fe8"}, - "ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f1155337ae17ff7a1255217b4c1ceefcd1860b7ceb1a1874031e7a861b052e39"}, - "ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "b84f6af156264530b312a8ab98ac6088f6b77ae5fe2058305c81434aa01fbaf9"}, - "excoveralls": {:hex, :excoveralls, "0.12.1", "a553c59f6850d0aff3770e4729515762ba7c8e41eedde03208182a8dc9d0ce07", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "5c1f717066a299b1b732249e736c5da96bb4120d1e55dc2e6f442d251e18a812"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"}, + "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, + "ecto_sql": {:hex, :ecto_sql, "3.11.3", "4eb7348ff8101fbc4e6bbc5a4404a24fecbe73a3372d16569526b0cf34ebc195", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e5f36e3d736b99c7fee3e631333b8394ade4bafe9d96d35669fca2d81c2be928"}, + "ex_doc": {:hex, :ex_doc, "0.37.0", "970f92b39e62c460aa8a367508e938f5e4da6e2ff3eaed3f8530b25870f45471", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "b0ee7f17373948e0cf471e59c3a0ee42f3bd1171c67d91eb3626456ef9c6202c"}, + "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, + "excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"}, "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"}, "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"}, - "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"}, - "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, + "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, - "postgrex": {:hex, :postgrex, "0.15.5", "aec40306a622d459b01bff890fa42f1430dac61593b122754144ad9033a2152f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ed90c81e1525f65a2ba2279dbcebf030d6d13328daa2f8088b9661eb9143af7f"}, + "postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"}, - "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"}, } diff --git a/test/as_nested_set/helper_test.exs b/test/as_nested_set/helper_test.exs index cc245b3..6a6a4f0 100644 --- a/test/as_nested_set/helper_test.exs +++ b/test/as_nested_set/helper_test.exs @@ -1,19 +1,16 @@ defmodule AsNestedSet.HelperTest do - use AsNestedSet.EctoCase - + import AsNestedSet.Helper defmodule Sample do use AsNestedSet defstruct id: "id", lft: "left", rgt: "right", parent_id: "parent_id" end - + describe "fields/1" do - test "should return fields configurations for specified module" do assert %{left: :lft, node_id: :id, parent_id: :parent_id, right: :rgt} = fields(Sample) end end - end diff --git a/test/as_nested_set/modifiable_test.exs b/test/as_nested_set/modifiable_test.exs index 0b3f441..f04edb6 100644 --- a/test/as_nested_set/modifiable_test.exs +++ b/test/as_nested_set/modifiable_test.exs @@ -1,5 +1,4 @@ defmodule AsNestedSet.ModifiableTest do - use AsNestedSet.EctoCase import AsNestedSet.Factory, only: [insert: 2] @@ -22,15 +21,22 @@ defmodule AsNestedSet.ModifiableTest do n0 = insert(:taxon, name: "n0", lft: 0, rgt: 9, taxonomy_id: taxonomy_id) n00 = insert(:taxon, name: "n00", lft: 1, rgt: 2, parent_id: n0.id, taxonomy_id: taxonomy_id) n01 = insert(:taxon, name: "n01", lft: 3, rgt: 8, parent_id: n0.id, taxonomy_id: taxonomy_id) - n010 = insert(:taxon, name: "n010", lft: 4, rgt: 5, parent_id: n01.id, taxonomy_id: taxonomy_id) - n011 = insert(:taxon, name: "n011", lft: 6, rgt: 7, parent_id: n01.id, taxonomy_id: taxonomy_id) - {n0, [ - {n00, []}, - {n01, [ - {n010, []}, - {n011, []} - ]} - ]} + + n010 = + insert(:taxon, name: "n010", lft: 4, rgt: 5, parent_id: n01.id, taxonomy_id: taxonomy_id) + + n011 = + insert(:taxon, name: "n011", lft: 6, rgt: 7, parent_id: n01.id, taxonomy_id: taxonomy_id) + + {n0, + [ + {n00, []}, + {n01, + [ + {n010, []}, + {n011, []} + ]} + ]} end def execute(executable) do @@ -43,7 +49,9 @@ defmodule AsNestedSet.ModifiableTest do test "create/3 should return {:error, :not_the_same_scope} for creating node from another scope" do node = insert(:taxon, lft: 0, rgt: 1, taxonomy_id: 0) - assert create(%Taxon{taxonomy_id: 1}, node , :child) |> execute == {:error, :not_the_same_scope} + + assert create(%Taxon{taxonomy_id: 1}, node, :child) |> execute == + {:error, :not_the_same_scope} end test "create/3 should return {:error, :target_is_required} for creating without passing a target" do @@ -52,246 +60,309 @@ defmodule AsNestedSet.ModifiableTest do end test "create/3 should create left node" do - {_, [{target, _}|_]} = create_tree(1) + {_, [{target, _} | _]} = create_tree(1) %Taxon{name: "left", taxonomy_id: 1} |> create(target, :left) |> execute - assert match(dump_one(Taxon, %{taxonomy_id: 1}) |> execute, - {%{name: "n0", lft: 0, rgt: 11, taxonomy_id: 1}, [ - {%{ name: "left", lft: 1, rgt: 2, taxonomy_id: 1}, []}, - {%{ name: "n00", lft: 3, rgt: 4, taxonomy_id: 1}, []}, - {%{ name: "n01", lft: 5, rgt: 10, taxonomy_id: 1}, [ - {%{ name: "n010", lft: 6, rgt: 7, taxonomy_id: 1}, []}, - {%{ name: "n011", lft: 8, rgt: 9, taxonomy_id: 1}, []} - ]} - ]} - ) + assert match( + dump_one(Taxon, %{taxonomy_id: 1}) |> execute, + {%{name: "n0", lft: 0, rgt: 11, taxonomy_id: 1}, + [ + {%{name: "left", lft: 1, rgt: 2, taxonomy_id: 1}, []}, + {%{name: "n00", lft: 3, rgt: 4, taxonomy_id: 1}, []}, + {%{name: "n01", lft: 5, rgt: 10, taxonomy_id: 1}, + [ + {%{name: "n010", lft: 6, rgt: 7, taxonomy_id: 1}, []}, + {%{name: "n011", lft: 8, rgt: 9, taxonomy_id: 1}, []} + ]} + ]} + ) end test "create/3 should create right node" do - {_, [{target, _}|_]} = create_tree(1) + {_, [{target, _} | _]} = create_tree(1) %Taxon{name: "right", taxonomy_id: 1} |> create(target, :right) |> execute - assert match(dump_one(Taxon, %{taxonomy_id: 1}) |> execute, - {%{name: "n0", lft: 0, rgt: 11, taxonomy_id: 1}, [ - {%{ name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, []}, - {%{ name: "right", lft: 3, rgt: 4, taxonomy_id: 1}, []}, - {%{ name: "n01", lft: 5, rgt: 10, taxonomy_id: 1}, [ - {%{ name: "n010", lft: 6, rgt: 7, taxonomy_id: 1}, []}, - {%{ name: "n011", lft: 8, rgt: 9, taxonomy_id: 1}, []} - ]} - ]} - ) + + assert match( + dump_one(Taxon, %{taxonomy_id: 1}) |> execute, + {%{name: "n0", lft: 0, rgt: 11, taxonomy_id: 1}, + [ + {%{name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, []}, + {%{name: "right", lft: 3, rgt: 4, taxonomy_id: 1}, []}, + {%{name: "n01", lft: 5, rgt: 10, taxonomy_id: 1}, + [ + {%{name: "n010", lft: 6, rgt: 7, taxonomy_id: 1}, []}, + {%{name: "n011", lft: 8, rgt: 9, taxonomy_id: 1}, []} + ]} + ]} + ) end test "create/3 should create child node" do - {_, [{target, _}|_]} = create_tree(1) + {_, [{target, _} | _]} = create_tree(1) %Taxon{name: "child", taxonomy_id: 1} |> create(target, :child) |> execute - assert match(dump_one(Taxon, %{taxonomy_id: 1}) |> execute, - {%{name: "n0", lft: 0, rgt: 11, taxonomy_id: 1}, [ - {%{ name: "n00", lft: 1, rgt: 4, taxonomy_id: 1}, [ - {%{ name: "child", lft: 2, rgt: 3, taxonomy_id: 1}, []} - ]}, - {%{ name: "n01", lft: 5, rgt: 10, taxonomy_id: 1}, [ - {%{ name: "n010", lft: 6, rgt: 7, taxonomy_id: 1}, []}, - {%{ name: "n011", lft: 8, rgt: 9, taxonomy_id: 1}, []} - ]} - ]} - ) + assert match( + dump_one(Taxon, %{taxonomy_id: 1}) |> execute, + {%{name: "n0", lft: 0, rgt: 11, taxonomy_id: 1}, + [ + {%{name: "n00", lft: 1, rgt: 4, taxonomy_id: 1}, + [ + {%{name: "child", lft: 2, rgt: 3, taxonomy_id: 1}, []} + ]}, + {%{name: "n01", lft: 5, rgt: 10, taxonomy_id: 1}, + [ + {%{name: "n010", lft: 6, rgt: 7, taxonomy_id: 1}, []}, + {%{name: "n011", lft: 8, rgt: 9, taxonomy_id: 1}, []} + ]} + ]} + ) end test "create/2 should create root node for empty tree" do %Taxon{name: "root", taxonomy_id: 1} |> create(:root) |> execute - assert match(dump_one(Taxon, %{taxonomy_id: 1}) |> execute, - {%{name: "root", lft: 0, rgt: 1, taxonomy_id: 1}, []} - ) + + assert match( + dump_one(Taxon, %{taxonomy_id: 1}) |> execute, + {%{name: "root", lft: 0, rgt: 1, taxonomy_id: 1}, []} + ) end test "create/2 should create root node" do create_tree(1) %Taxon{name: "root", taxonomy_id: 1} |> create(:root) |> execute + assert match(dump(Taxon, %{taxonomy_id: 1}) |> execute, [ - {%{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1}, [ - {%{ name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, []}, - {%{ name: "n01", lft: 3, rgt: 8, taxonomy_id: 1}, [ - {%{ name: "n010", lft: 4, rgt: 5, taxonomy_id: 1}, []}, - {%{ name: "n011", lft: 6, rgt: 7, taxonomy_id: 1}, []} - ]} - ]}, - {%{name: "root", lft: 10, rgt: 11, taxonomy_id: 1}, []} - ]) + {%{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1}, + [ + {%{name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, []}, + {%{name: "n01", lft: 3, rgt: 8, taxonomy_id: 1}, + [ + {%{name: "n010", lft: 4, rgt: 5, taxonomy_id: 1}, []}, + {%{name: "n011", lft: 6, rgt: 7, taxonomy_id: 1}, []} + ]} + ]}, + {%{name: "root", lft: 10, rgt: 11, taxonomy_id: 1}, []} + ]) end test "create/2 should create parent node" do - {_, [{target, _}|_]} = create_tree(1) + {_, [{target, _} | _]} = create_tree(1) %Taxon{name: "parent", taxonomy_id: 1} |> create(target, :parent) |> execute + assert match(dump_one(Taxon, %{taxonomy_id: 1}) |> execute, { - %{name: "n0", lft: 0, rgt: 11, taxonomy_id: 1}, [ - {%{name: "parent", lft: 1, rgt: 4, taxonomy_id: 1}, [ - {%{ name: "n00", lft: 2, rgt: 3, taxonomy_id: 1}, []}, - ]}, - {%{ name: "n01", lft: 5, rgt: 10, taxonomy_id: 1}, [ - {%{ name: "n010", lft: 6, rgt: 7, taxonomy_id: 1}, []}, - {%{ name: "n011", lft: 8, rgt: 9, taxonomy_id: 1}, []} - ]} - ] - }) + %{name: "n0", lft: 0, rgt: 11, taxonomy_id: 1}, + [ + {%{name: "parent", lft: 1, rgt: 4, taxonomy_id: 1}, + [ + {%{name: "n00", lft: 2, rgt: 3, taxonomy_id: 1}, []} + ]}, + {%{name: "n01", lft: 5, rgt: 10, taxonomy_id: 1}, + [ + {%{name: "n010", lft: 6, rgt: 7, taxonomy_id: 1}, []}, + {%{name: "n011", lft: 8, rgt: 9, taxonomy_id: 1}, []} + ]} + ] + }) end test "create/3 should not affect other tree" do create_tree(1) create_tree(2) %Taxon{name: "root", taxonomy_id: 1} |> create(:root) |> execute - assert match(dump_one(Taxon, %{taxonomy_id: 2}) |> execute, - {%{name: "n0", lft: 0, rgt: 9, taxonomy_id: 2}, [ - {%{ name: "n00", lft: 1, rgt: 2, taxonomy_id: 2}, []}, - {%{ name: "n01", lft: 3, rgt: 8, taxonomy_id: 2}, [ - {%{ name: "n010", lft: 4, rgt: 5, taxonomy_id: 2}, []}, - {%{ name: "n011", lft: 6, rgt: 7, taxonomy_id: 2}, []} - ]} - ]} - ) + + assert match( + dump_one(Taxon, %{taxonomy_id: 2}) |> execute, + {%{name: "n0", lft: 0, rgt: 9, taxonomy_id: 2}, + [ + {%{name: "n00", lft: 1, rgt: 2, taxonomy_id: 2}, []}, + {%{name: "n01", lft: 3, rgt: 8, taxonomy_id: 2}, + [ + {%{name: "n010", lft: 4, rgt: 5, taxonomy_id: 2}, []}, + {%{name: "n011", lft: 6, rgt: 7, taxonomy_id: 2}, []} + ]} + ]} + ) end test "delete/1 should delete a node and all its descendants" do - {_, [_,{target, _}]} = create_tree(1) + {_, [_, {target, _}]} = create_tree(1) delete(target) |> execute - assert match(dump_one(Taxon, %{taxonomy_id: 1}) |> execute, - {%{name: "n0", lft: 0, rgt: 3, taxonomy_id: 1}, [ - {%{ name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, []} - ]} - ) + + assert match( + dump_one(Taxon, %{taxonomy_id: 1}) |> execute, + {%{name: "n0", lft: 0, rgt: 3, taxonomy_id: 1}, + [ + {%{name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, []} + ]} + ) end test "create/3 should create consecutive children" do root = %Taxon{name: "root", taxonomy_id: 1} |> create(:root) |> execute %Taxon{name: "child0", taxonomy_id: 1} |> create(root, :child) |> execute %Taxon{name: "child1", taxonomy_id: 1} |> create(root, :child) |> execute - assert match(dump_one(Taxon, %{taxonomy_id: 1}) |> execute, - {%{name: "root", lft: 0, rgt: 5, taxonomy_id: 1}, [ - {%{name: "child0", lft: 1, rgt: 2, taxonomy_id: 1}, []}, - {%{name: "child1", lft: 3, rgt: 4, taxonomy_id: 1}, []} - ]} - ) + + assert match( + dump_one(Taxon, %{taxonomy_id: 1}) |> execute, + {%{name: "root", lft: 0, rgt: 5, taxonomy_id: 1}, + [ + {%{name: "child0", lft: 1, rgt: 2, taxonomy_id: 1}, []}, + {%{name: "child1", lft: 3, rgt: 4, taxonomy_id: 1}, []} + ]} + ) end test "move(node, :root) should move node to root" do {_, [_, {_, [{n010, _} | _]}]} = create_tree(1) move(n010, :root) |> execute - assert match(dump(Taxon, %{taxonomy_id: 1}) |> execute, - [ - {%{name: "n0", lft: 0, rgt: 7, taxonomy_id: 1}, [ - {%{ name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, []}, - {%{ name: "n01", lft: 3, rgt: 6, taxonomy_id: 1}, [ - {%{ name: "n011", lft: 4, rgt: 5, taxonomy_id: 1}, []} - ]} - ]}, - {%{ name: "n010", lft: 8, rgt: 9, taxonomy_id: 1}, []} - ] - ) + + assert match( + dump(Taxon, %{taxonomy_id: 1}) |> execute, + [ + {%{name: "n0", lft: 0, rgt: 7, taxonomy_id: 1}, + [ + {%{name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, []}, + {%{name: "n01", lft: 3, rgt: 6, taxonomy_id: 1}, + [ + {%{name: "n011", lft: 4, rgt: 5, taxonomy_id: 1}, []} + ]} + ]}, + {%{name: "n010", lft: 8, rgt: 9, taxonomy_id: 1}, []} + ] + ) end test "move(node, target, :child) should move given node to the child of target" do {_, [{n00, _}, {n01, _}]} = create_tree(1) move(n01, n00, :child) |> execute - assert match(dump_one(Taxon, %{taxonomy_id: 1}) |> execute, - {%{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1}, [ - {%{ name: "n00", lft: 1, rgt: 8, taxonomy_id: 1}, [ - {%{ name: "n01", lft: 2, rgt: 7, taxonomy_id: 1}, [ - {%{ name: "n010", lft: 3, rgt: 4, taxonomy_id: 1}, []}, - {%{ name: "n011", lft: 5, rgt: 6, taxonomy_id: 1}, []} - ]} - ]} - ]} - ) + + assert match( + dump_one(Taxon, %{taxonomy_id: 1}) |> execute, + {%{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1}, + [ + {%{name: "n00", lft: 1, rgt: 8, taxonomy_id: 1}, + [ + {%{name: "n01", lft: 2, rgt: 7, taxonomy_id: 1}, + [ + {%{name: "n010", lft: 3, rgt: 4, taxonomy_id: 1}, []}, + {%{name: "n011", lft: 5, rgt: 6, taxonomy_id: 1}, []} + ]} + ]} + ]} + ) end test "move(node, target, :left) should move given node to the left of target" do {_, [{n00, _}, {n01, _}]} = create_tree(1) move(n01, n00, :left) |> execute - assert match(dump_one(Taxon, %{taxonomy_id: 1}) |> execute, - {%{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1}, [ - {%{ name: "n01", lft: 1, rgt: 6, taxonomy_id: 1}, [ - {%{ name: "n010", lft: 2, rgt: 3, taxonomy_id: 1}, []}, - {%{ name: "n011", lft: 4, rgt: 5, taxonomy_id: 1}, []} - ]}, - {%{ name: "n00", lft: 7, rgt: 8, taxonomy_id: 1}, []} - ]} - ) + + assert match( + dump_one(Taxon, %{taxonomy_id: 1}) |> execute, + {%{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1}, + [ + {%{name: "n01", lft: 1, rgt: 6, taxonomy_id: 1}, + [ + {%{name: "n010", lft: 2, rgt: 3, taxonomy_id: 1}, []}, + {%{name: "n011", lft: 4, rgt: 5, taxonomy_id: 1}, []} + ]}, + {%{name: "n00", lft: 7, rgt: 8, taxonomy_id: 1}, []} + ]} + ) end test "move(node, target, :right) should move given node to the right of target" do {_, [{n00, _}, {n01, _}]} = create_tree(1) move(n00, n01, :right) |> execute - assert match(dump_one(Taxon, %{taxonomy_id: 1}) |> execute, - {%{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1}, [ - {%{ name: "n01", lft: 1, rgt: 6, taxonomy_id: 1}, [ - {%{ name: "n010", lft: 2, rgt: 3, taxonomy_id: 1}, []}, - {%{ name: "n011", lft: 4, rgt: 5, taxonomy_id: 1}, []} - ]}, - {%{ name: "n00", lft: 7, rgt: 8, taxonomy_id: 1}, []} - ]} - ) + + assert match( + dump_one(Taxon, %{taxonomy_id: 1}) |> execute, + {%{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1}, + [ + {%{name: "n01", lft: 1, rgt: 6, taxonomy_id: 1}, + [ + {%{name: "n010", lft: 2, rgt: 3, taxonomy_id: 1}, []}, + {%{name: "n011", lft: 4, rgt: 5, taxonomy_id: 1}, []} + ]}, + {%{name: "n00", lft: 7, rgt: 8, taxonomy_id: 1}, []} + ]} + ) end test "move(root_node, :root) should execute without error" do {n0, _} = create_tree(1) move(n0, :root) |> execute - assert match(dump_one(Taxon, %{taxonomy_id: 1}) |> execute, - {%{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1}, [ - {%{ name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, []}, - {%{ name: "n01", lft: 3, rgt: 8, taxonomy_id: 1}, [ - {%{ name: "n010", lft: 4, rgt: 5, taxonomy_id: 1}, []}, - {%{ name: "n011", lft: 6, rgt: 7, taxonomy_id: 1}, []} - ]} - ]} - ) + + assert match( + dump_one(Taxon, %{taxonomy_id: 1}) |> execute, + {%{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1}, + [ + {%{name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, []}, + {%{name: "n01", lft: 3, rgt: 8, taxonomy_id: 1}, + [ + {%{name: "n010", lft: 4, rgt: 5, taxonomy_id: 1}, []}, + {%{name: "n011", lft: 6, rgt: 7, taxonomy_id: 1}, []} + ]} + ]} + ) end test "move(child_node, parent_node, :child) should move given child to the end of children of parent" do {n0, [{n00, _}, _]} = create_tree(1) move(n00, n0, :child) |> execute - assert match(dump_one(Taxon, %{taxonomy_id: 1}) |> execute, - {%{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1}, [ - {%{ name: "n01", lft: 1, rgt: 6, taxonomy_id: 1}, [ - {%{ name: "n010", lft: 2, rgt: 3, taxonomy_id: 1}, []}, - {%{ name: "n011", lft: 4, rgt: 5, taxonomy_id: 1}, []} - ]}, - {%{ name: "n00", lft: 7, rgt: 8, taxonomy_id: 1}, []} - ]} - ) + + assert match( + dump_one(Taxon, %{taxonomy_id: 1}) |> execute, + {%{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1}, + [ + {%{name: "n01", lft: 1, rgt: 6, taxonomy_id: 1}, + [ + {%{name: "n010", lft: 2, rgt: 3, taxonomy_id: 1}, []}, + {%{name: "n011", lft: 4, rgt: 5, taxonomy_id: 1}, []} + ]}, + {%{name: "n00", lft: 7, rgt: 8, taxonomy_id: 1}, []} + ]} + ) end test "move(last_child_node, parent_node, :child) should do nothing" do {n0, [_, {n01, _}]} = create_tree(1) move(n01, n0, :child) |> execute - assert match(dump_one(Taxon, %{taxonomy_id: 1}) |> execute, - {%{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1}, [ - {%{ name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, []}, - {%{ name: "n01", lft: 3, rgt: 8, taxonomy_id: 1}, [ - {%{ name: "n010", lft: 4, rgt: 5, taxonomy_id: 1}, []}, - {%{ name: "n011", lft: 6, rgt: 7, taxonomy_id: 1}, []} - ]} - ]} - ) + + assert match( + dump_one(Taxon, %{taxonomy_id: 1}) |> execute, + {%{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1}, + [ + {%{name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, []}, + {%{name: "n01", lft: 3, rgt: 8, taxonomy_id: 1}, + [ + {%{name: "n010", lft: 4, rgt: 5, taxonomy_id: 1}, []}, + {%{name: "n011", lft: 6, rgt: 7, taxonomy_id: 1}, []} + ]} + ]} + ) end - + test "move to the same node should do nothing" do {n0, _} = create_tree(1) move(n0, n0, :child) |> execute - assert match(dump_one(Taxon, %{taxonomy_id: 1}) |> execute, - {%{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1}, [ - {%{ name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, []}, - {%{ name: "n01", lft: 3, rgt: 8, taxonomy_id: 1}, [ - {%{ name: "n010", lft: 4, rgt: 5, taxonomy_id: 1}, []}, - {%{ name: "n011", lft: 6, rgt: 7, taxonomy_id: 1}, []} - ]} - ]} - ) + + assert match( + dump_one(Taxon, %{taxonomy_id: 1}) |> execute, + {%{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1}, + [ + {%{name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, []}, + {%{name: "n01", lft: 3, rgt: 8, taxonomy_id: 1}, + [ + {%{name: "n010", lft: 4, rgt: 5, taxonomy_id: 1}, []}, + {%{name: "n011", lft: 6, rgt: 7, taxonomy_id: 1}, []} + ]} + ]} + ) end - + test "move node across different scope should be refused" do {_, [_, {n01, _}]} = create_tree(1) {_, [_, {m01, _}]} = create_tree(2) diff --git a/test/as_nested_set/queriable_test.exs b/test/as_nested_set/queriable_test.exs index c9c3af3..43cf0aa 100644 --- a/test/as_nested_set/queriable_test.exs +++ b/test/as_nested_set/queriable_test.exs @@ -3,13 +3,13 @@ defmodule AsNestedSet.QueriableTest do import AsNestedSet.Factory import AsNestedSet.Matcher + import Ecto.Query, only: [from: 2] alias AsNestedSet.Taxon alias AsNestedSet.TestRepo, as: Repo import AsNestedSet.Queriable - @doc """ Create a nested set tree +--------------------+ @@ -22,26 +22,34 @@ defmodule AsNestedSet.QueriableTest do n0 = insert(:taxon, name: "n0", lft: 0, rgt: 9, taxonomy_id: taxonomy_id) n00 = insert(:taxon, name: "n00", lft: 1, rgt: 2, parent_id: n0.id, taxonomy_id: taxonomy_id) n01 = insert(:taxon, name: "n01", lft: 3, rgt: 8, parent_id: n0.id, taxonomy_id: taxonomy_id) - n010 = insert(:taxon, name: "n010", lft: 4, rgt: 5, parent_id: n01.id, taxonomy_id: taxonomy_id) - n011 = insert(:taxon, name: "n011", lft: 6, rgt: 7, parent_id: n01.id, taxonomy_id: taxonomy_id) - {n0, [ - {n00, []}, - {n01, [ - {n010, []}, - {n011, []} - ]} - ]} + + n010 = + insert(:taxon, name: "n010", lft: 4, rgt: 5, parent_id: n01.id, taxonomy_id: taxonomy_id) + + n011 = + insert(:taxon, name: "n011", lft: 6, rgt: 7, parent_id: n01.id, taxonomy_id: taxonomy_id) + + {n0, + [ + {n00, []}, + {n01, + [ + {n010, []}, + {n011, []} + ]} + ]} end def create_forest(taxonomy_id) do n0 = insert(:taxon, name: "n0", lft: 0, rgt: 1, taxonomy_id: taxonomy_id) n1 = insert(:taxon, name: "n1", lft: 2, rgt: 3, taxonomy_id: taxonomy_id) + [ {n0, []}, {n1, []} ] end - + def execute(executable) do AsNestedSet.execute(executable, Repo) end @@ -58,34 +66,61 @@ defmodule AsNestedSet.QueriableTest do test "children/1 should get all children in the right sequence" do {root, [{no_child, []}, _]} = create_tree(1) - assert match(children(root) |> execute(),[ - %{name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, - %{name: "n01", lft: 3, rgt: 8, taxonomy_id: 1}, - ]) + + assert match(children(root) |> execute(), [ + %{name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, + %{name: "n01", lft: 3, rgt: 8, taxonomy_id: 1} + ]) + + assert match(children(no_child) |> execute(), []) + end + + test "children/2 should get children filtered by the query" do + {root, [{no_child, []}, _]} = create_tree(1) + + assert match( + from(t in Taxon, where: t.name != "n01") + |> children(root) + |> execute(), + [%{name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}] + ) + assert match(children(no_child) |> execute(), []) end test "leaves/2 should return all leaves" do create_tree(1) + assert match(leaves(Taxon, %{taxonomy_id: 1}) |> execute(), [ - %{name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, - %{name: "n010", lft: 4, rgt: 5, taxonomy_id: 1}, - %{name: "n011", lft: 6, rgt: 7, taxonomy_id: 1}, - ]) + %{name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, + %{name: "n010", lft: 4, rgt: 5, taxonomy_id: 1}, + %{name: "n011", lft: 6, rgt: 7, taxonomy_id: 1} + ]) end - test "descendants/1 should return all decendants" do + test "descendants/1 returns all decendants" do {root, _} = create_tree(1) + assert match(descendants(root) |> execute(), [ - %{name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, - %{name: "n01", lft: 3, rgt: 8, taxonomy_id: 1}, - %{name: "n010", lft: 4, rgt: 5, taxonomy_id: 1}, - %{name: "n011", lft: 6, rgt: 7, taxonomy_id: 1} - ]) + %{name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, + %{name: "n01", lft: 3, rgt: 8, taxonomy_id: 1}, + %{name: "n010", lft: 4, rgt: 5, taxonomy_id: 1}, + %{name: "n011", lft: 6, rgt: 7, taxonomy_id: 1} + ]) + end + + test "descendants/2 returns the decendants filtered by the query" do + {root, _} = create_tree(1) + + assert match(from(t in Taxon, where: t.name != "n010") |> descendants(root) |> execute(), [ + %{name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, + %{name: "n01", lft: 3, rgt: 8, taxonomy_id: 1}, + %{name: "n011", lft: 6, rgt: 7, taxonomy_id: 1} + ]) end test "descendants/1 returns [] for node without child" do - {_, [{no_child, []}|_]} = create_tree(1) + {_, [{no_child, []} | _]} = create_tree(1) assert descendants(no_child) |> execute() == [] end @@ -95,42 +130,72 @@ defmodule AsNestedSet.QueriableTest do test "root/2 returns the root of the tree" do create_tree(1) - assert match(root(Taxon, %{taxonomy_id: 1}) |> execute(), - %{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1} - ) + + assert match( + root(Taxon, %{taxonomy_id: 1}) |> execute(), + %{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1} + ) end test "roots/2 returns the roots of the forest" do create_forest(1) + assert match(roots(Taxon, %{taxonomy_id: 1}) |> execute(), [ - %{name: "n0", lft: 0, rgt: 1, taxonomy_id: 1}, - %{name: "n1", lft: 2, rgt: 3, taxonomy_id: 1} - ]) + %{name: "n0", lft: 0, rgt: 1, taxonomy_id: 1}, + %{name: "n1", lft: 2, rgt: 3, taxonomy_id: 1} + ]) end - test "self_and_descendants/1 should returns self and all descendants" do - {_, [_, {target, _}|_]} = create_tree(1) + test "self_and_descendants/1 returns self and all descendants" do + {_, [_, {target, _} | _]} = create_tree(1) + assert match(self_and_descendants(target) |> execute(), [ - %{name: "n01", lft: 3, rgt: 8, taxonomy_id: 1}, - %{name: "n010", lft: 4, rgt: 5, taxonomy_id: 1}, - %{name: "n011", lft: 6, rgt: 7, taxonomy_id: 1} - ]) + %{name: "n01", lft: 3, rgt: 8, taxonomy_id: 1}, + %{name: "n010", lft: 4, rgt: 5, taxonomy_id: 1}, + %{name: "n011", lft: 6, rgt: 7, taxonomy_id: 1} + ]) + end + + test "self_and_descendants/2 returns self and all descendants filtered by the query" do + {_, [_, {target, _} | _]} = create_tree(1) + + assert match( + from(t in Taxon, where: t.name != "n010") + |> self_and_descendants(target) + |> execute(), + [ + %{name: "n01", lft: 3, rgt: 8, taxonomy_id: 1}, + %{name: "n011", lft: 6, rgt: 7, taxonomy_id: 1} + ] + ) end test "ancestors/1 should return all its ancestors" do - {_, [_, {_, [{target, []}|_]}|_]} = create_tree(1) + {_, [_, {_, [{target, []} | _]} | _]} = create_tree(1) + assert match(ancestors(target) |> execute(), [ - %{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1}, - %{name: "n01", lft: 3, rgt: 8, taxonomy_id: 1} - ]) + %{name: "n0", lft: 0, rgt: 9, taxonomy_id: 1}, + %{name: "n01", lft: 3, rgt: 8, taxonomy_id: 1} + ]) end - test "self_and_siblings/1 should return self and all its sliblings" do - {_, [{target, _}|_]} = create_tree(1) + test "self_and_siblings/1 should return self and all its siblings" do + {_, [{target, _} | _]} = create_tree(1) + assert match(self_and_siblings(target) |> execute(), [ - %{name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, - %{name: "n01", lft: 3, rgt: 8, taxonomy_id: 1} - ]) + %{name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}, + %{name: "n01", lft: 3, rgt: 8, taxonomy_id: 1} + ]) end + test "self_and_siblings/2 should return self and siblings filtered by the query" do + {_, [{target, _} | _]} = create_tree(1) + + assert match( + from(t in Taxon, where: t.name != "n01") + |> self_and_siblings(target) + |> execute(), + [%{name: "n00", lft: 1, rgt: 2, taxonomy_id: 1}] + ) + end end diff --git a/test/as_nested_set/scoped_test.exs b/test/as_nested_set/scoped_test.exs index 82d6e70..1d3b1be 100644 --- a/test/as_nested_set/scoped_test.exs +++ b/test/as_nested_set/scoped_test.exs @@ -12,11 +12,16 @@ defmodule AsNestedSet.ScopedTest do end test "same_scope/2 should return true for models with the same scope field value" do - assert AsNestedSet.Scoped.same_scope?(%Sample{scope_field: "same", non_scope_field: "diff0"}, %Sample{scope_field: "same", non_scope_field: "diff1"}) + assert AsNestedSet.Scoped.same_scope?( + %Sample{scope_field: "same", non_scope_field: "diff0"}, + %Sample{scope_field: "same", non_scope_field: "diff1"} + ) end test "same_scope/2 should return false for models with different scope field value" do - assert !AsNestedSet.Scoped.same_scope?(%Sample{scope_field: "diff0"}, %Sample{scope_field: "diff1"}) + assert !AsNestedSet.Scoped.same_scope?(%Sample{scope_field: "diff0"}, %Sample{ + scope_field: "diff1" + }) end describe "scope/1" do diff --git a/test/as_nested_set_test.exs b/test/as_nested_set_test.exs index 41a50c9..0ae29d8 100644 --- a/test/as_nested_set_test.exs +++ b/test/as_nested_set_test.exs @@ -1,5 +1,4 @@ defmodule AsNestedSetTest do - use ExUnit.Case defmodule Sample do @@ -15,7 +14,7 @@ defmodule AsNestedSetTest do @right_column :right defstruct node_id: "node_id", left: "left", right: "right", pid: "parent_id" end - + defmodule Undefined do defstruct id: "id" end @@ -41,20 +40,40 @@ defmodule AsNestedSetTest do test "should define __as_nested_set_set_field__(model, field, value)" do sample = %Sample{} redefined = %Redefined{} - sample = @fields |> Enum.reduce(sample, fn field, sample -> - Sample.__as_nested_set_set_field__(sample, field, "test_value") - end) - assert %Sample{id: "test_value", lft: "test_value", rgt: "test_value", parent_id: "test_value"} == sample - - redefined = @fields |> Enum.reduce(redefined, fn field, redefined -> - Redefined.__as_nested_set_set_field__(redefined, field, "test_value") - end) - assert %Redefined{node_id: "test_value", left: "test_value", right: "test_value", pid: "test_value"} == redefined + + sample = + @fields + |> Enum.reduce(sample, fn field, sample -> + Sample.__as_nested_set_set_field__(sample, field, "test_value") + end) + + assert %Sample{ + id: "test_value", + lft: "test_value", + rgt: "test_value", + parent_id: "test_value" + } == sample + + redefined = + @fields + |> Enum.reduce(redefined, fn field, redefined -> + Redefined.__as_nested_set_set_field__(redefined, field, "test_value") + end) + + assert %Redefined{ + node_id: "test_value", + left: "test_value", + right: "test_value", + pid: "test_value" + } == redefined end test "should define __as_nested_set_fields__" do - assert %{left: :lft, node_id: :id, parent_id: :parent_id, right: :rgt} == Sample.__as_nested_set_fields__ - assert %{left: :left, node_id: :node_id, parent_id: :pid, right: :right} == Redefined.__as_nested_set_fields__ + assert %{left: :lft, node_id: :id, parent_id: :parent_id, right: :rgt} == + Sample.__as_nested_set_fields__() + + assert %{left: :left, node_id: :node_id, parent_id: :pid, right: :right} == + Redefined.__as_nested_set_fields__() end test "should define child?(model)" do @@ -64,12 +83,13 @@ defmodule AsNestedSetTest do assert Redefined.child?(%Redefined{pid: "parent_id"}) refute Redefined.child?(%Redefined{pid: nil}) end - + describe "AsNestedSet.defined?/1" do test "should return true for a struct defined AsNestedSet" do assert AsNestedSet.defined?(Sample) assert AsNestedSet.defined?(%Sample{}) end + test "should return false for a module defined AsNestedSet" do refute AsNestedSet.defined?(Undefined) refute AsNestedSet.defined?(%Undefined{}) diff --git a/test/support/matcher.ex b/test/support/matcher.ex index 26966d1..6659dc3 100644 --- a/test/support/matcher.ex +++ b/test/support/matcher.ex @@ -1,6 +1,5 @@ defmodule AsNestedSet.Matcher do - - def match([source|source_tail], [target|target_tail]) do + def match([source | source_tail], [target | target_tail]) do match(source, target) and match(source_tail, target_tail) end @@ -17,13 +16,15 @@ defmodule AsNestedSet.Matcher do lft = source.lft rgt = source.rgt taxonomy_id = source.taxonomy_id + try do %{:name => ^name, :lft => ^lft, :rgt => ^rgt, :taxonomy_id => ^taxonomy_id} = target rescue _x in [MatchError] -> expected = %{:name => name, :lft => lft, :rgt => rgt, :taxonomy_id => taxonomy_id} - raise "Expected #{inspect expected} but got #{inspect target}" + raise "Expected #{inspect(expected)} but got #{inspect(target)}" end + true end end diff --git a/test/support/models/taxon.ex b/test/support/models/taxon.ex index 8c7b162..713830b 100644 --- a/test/support/models/taxon.ex +++ b/test/support/models/taxon.ex @@ -4,12 +4,11 @@ defmodule AsNestedSet.Taxon do use AsNestedSet, scope: [:taxonomy_id] schema "taxons" do - - field :name, :string - field :taxonomy_id, :integer - field :parent_id, :integer - field :lft, :integer - field :rgt, :integer + field(:name, :string) + field(:taxonomy_id, :integer) + field(:parent_id, :integer) + field(:lft, :integer) + field(:rgt, :integer) timestamps() end @@ -17,7 +16,6 @@ defmodule AsNestedSet.Taxon do @required_fields ~w(name taxonomy_id lft rgt)a @optional_fields ~w(parent_id)a - @doc """ Creates a changeset based on the `model` and `params`. diff --git a/test/test_helper.exs b/test/test_helper.exs index 17aa746..094e5fd 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,8 +1,8 @@ -Mix.Task.run "ecto.drop", ["quiet", "-r", "AsNestedSet.TestRepo"] -Mix.Task.run "ecto.create", ["quiet", "-r", "AsNestedSet.TestRepo"] -Mix.Task.run "ecto.migrate", ["-r", "AsNestedSet.TestRepo"] +Mix.Task.run("ecto.drop", ["quiet", "-r", "AsNestedSet.TestRepo"]) +Mix.Task.run("ecto.create", ["quiet", "-r", "AsNestedSet.TestRepo"]) +Mix.Task.run("ecto.migrate", ["-r", "AsNestedSet.TestRepo"]) -AsNestedSet.TestRepo.start_link +AsNestedSet.TestRepo.start_link() ExUnit.start() Ecto.Adapters.SQL.Sandbox.mode(AsNestedSet.TestRepo, :manual)