diff --git a/CHANGELOG.md b/CHANGELOG.md index 5aef989..b8ad806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## v0.5.0 (2023-09-28) + +### Features + +- introducing ancestors queries to query for parents of an entity, the parents of parents, and so on: + - `Ecspanse.Query.select/2` new option: `:for_ancestors_of` + - `Ecspanse.Query.list_ancestors/1` + - `Ecspanse.Query.list_tagged_components_for_ancestors/2` + ## v0.4.0 (2023-09-17) ### Breaking diff --git a/lib/ecspanse/query.ex b/lib/ecspanse/query.ex index e7ebbd4..e37e011 100644 --- a/lib/ecspanse/query.ex +++ b/lib/ecspanse/query.ex @@ -30,7 +30,8 @@ defmodule Ecspanse.Query do not_for_entities: list(Ecspanse.Entity.t()), for_children_of: list(Ecspanse.Entity.t()), for_descendants_of: list(Ecspanse.Entity.t()), - for_parents_of: list(Ecspanse.Entity.t()) + for_parents_of: list(Ecspanse.Entity.t()), + for_ancestors_of: list(Ecspanse.Entity.t()) } @enforce_keys [:select] @@ -43,7 +44,8 @@ defmodule Ecspanse.Query do :not_for_entities, :for_children_of, :for_descendants_of, - :for_parents_of + :for_parents_of, + :for_ancestors_of ] defmodule Error do @@ -108,9 +110,10 @@ defmodule Ecspanse.Query do - `:for_children_of` - a list of `t:Ecspanse.Entity.t/0`. The components will be returned only for the children of those entities. - `:for_descendants_of` - a list of `t:Ecspanse.Entity.t/0`. The components will be returned only for all descendants of those entities. - `:for_parents_of` - a list of `t:Ecspanse.Entity.t/0`. The components will be returned only for the parents of those entities. + - `:for_ancestors_of` - a list of `t:Ecspanse.Entity.t/0`. The components will be returned only for all ancestors of those entities. > #### Info {: .error} - > Combining the following filters is not supported: `:for, :not_for, :for_children_of, :for_descendants_of, :for_parents_of`. + > Combining the following filters is not supported: `:for, :not_for, :for_children_of, :for_descendants_of, :for_parents_of, :for_ancestors_of`. > Only one of them can be used in a query. Otherwise it will rise an error. ## Examples @@ -177,6 +180,7 @@ defmodule Ecspanse.Query do for_children_of = Keyword.get(filters, :for_children_of, []) |> Enum.uniq() for_descendants_of = Keyword.get(filters, :for_descendants_of, []) |> Enum.uniq() for_parents_of = Keyword.get(filters, :for_parents_of, []) |> Enum.uniq() + for_ancestors_of = Keyword.get(filters, :for_ancestors_of, []) |> Enum.uniq() :ok = validate_entities(for_entities) @@ -189,7 +193,8 @@ defmodule Ecspanse.Query do not_for_entities: not_for_entities, for_children_of: for_children_of, for_descendants_of: for_descendants_of, - for_parents_of: for_parents_of + for_parents_of: for_parents_of, + for_ancestors_of: for_ancestors_of } end @@ -350,6 +355,27 @@ defmodule Ecspanse.Query do end end + @doc """ + Returns the list of ancestor entities for the given entity. + That means the parents of the entity and their parents and so on. + + ## Examples + + ```elixir + [hero_entity, level_entity] = Ecspanse.Query.list_ancestors(compass_entity) + ``` + """ + @doc group: :relationships + @spec list_ancestors(Ecspanse.Entity.t()) :: list(Ecspanse.Entity.t()) + def list_ancestors(%Entity{} = entity) do + memo_list_ancestors(entity) + end + + @doc false + defmemo memo_list_ancestors(%Entity{} = entity), max_waiter: 1000, waiter_sleep_ms: 0 do + list_ancestors_entities([entity], []) + end + @doc """ Fetches an entity's component by a list of tags. Raises if more than one entry is found. @@ -397,6 +423,7 @@ defmodule Ecspanse.Query do @doc """ Returns a list of components tagged with a list of tags for all entities. + The components need to be tagged with all the given tags to return. ## Examples @@ -441,6 +468,8 @@ defmodule Ecspanse.Query do @doc """ Returns a list of components tagged with a list of tags for a given entity. + The components need to be tagged with all the given tags to return. + ## Examples ```elixir @@ -467,6 +496,8 @@ defmodule Ecspanse.Query do Returns a list of components tagged with a list of tags for a given list of entities. The components are not grouped by entity, but returned as a flat list. + The components need to be tagged with all the given tags to return. + ## Examples ```elixir @@ -502,6 +533,8 @@ defmodule Ecspanse.Query do @doc """ Returns a list of components tagged with a list of tags for the children of a given entity. + The components need to be tagged with all the given tags to return. + ## Examples ```elixir @@ -522,6 +555,8 @@ defmodule Ecspanse.Query do @doc """ Returns a list of components tagged with a list of tags for the descendants of a given entity. + The components need to be tagged with all the given tags to return. + ## Examples ```elixir @@ -542,6 +577,8 @@ defmodule Ecspanse.Query do @doc """ Returns a list of components tagged with a list of tags for the parents of a given entity. + The components need to be tagged with all the given tags to return. + ## Examples ```elixir @@ -559,6 +596,28 @@ defmodule Ecspanse.Query do end end + @doc """ + Returns a list of components tagged with a list of tags for the ancestors of a given entity. + + The components need to be tagged with all the given tags to return. + + ## Examples + + ```elixir + [dungeon_component] = Ecspanse.Query.list_tagged_components_for_ancestors(hero_entity, [:dungeon]) + ``` + """ + @doc group: :tags + @spec list_tagged_components_for_ancestors(Ecspanse.Entity.t(), list(tag :: atom())) :: + list(components_state :: struct()) + def list_tagged_components_for_ancestors(entity, tags) do + case list_ancestors(entity) do + [] -> [] + [ancestor] -> list_tagged_components_for_entity(ancestor, tags) + ancestors -> list_tagged_components_for_entities(ancestors, tags) + end + end + @doc """ Fetches the component by its module for a given entity. @@ -853,6 +912,9 @@ defmodule Ecspanse.Query do not Enum.empty?(query.for_parents_of) -> entities_with_components_stream_for_parents(query) + not Enum.empty?(query.for_ancestors_of) -> + entities_with_components_stream_for_ancestors(query) + true -> filter_for_entities([]) end @@ -879,6 +941,13 @@ defmodule Ecspanse.Query do end end + defp entities_with_components_stream_for_ancestors(query) do + case list_ancestors_entities(query.for_ancestors_of, []) do + [] -> [] + entities -> filter_for_entities(entities) + end + end + defp list_children_entities(entities) do select({Component.Children}, for: entities) |> stream() @@ -911,6 +980,24 @@ defmodule Ecspanse.Query do |> Stream.concat() end + defp list_ancestors_entities([], acc) do + acc + end + + defp list_ancestors_entities(entities, acc) do + parents = + select({Component.Parents}, for: entities) + |> stream() + |> Stream.map(fn {%Component.Parents{entities: parents}} -> parents end) + |> Enum.concat() + + # avoid circular dependencies + parents = Enum.uniq(parents -- acc) + acc = Enum.uniq(acc ++ parents) + + list_ancestors_entities(parents, acc) + end + defp filter_for_entities([]) do Ecspanse.Util.list_entities_components() |> Stream.map(fn {k, v} -> {k, v} end) diff --git a/mix.exs b/mix.exs index 16e63c6..8c4923b 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Ecspanse.MixProject do use Mix.Project - @version "0.4.0" + @version "0.5.0" @source_url "https://github.com/iacobson/ecspanse" def project do diff --git a/test/ecspanse/query_test.exs b/test/ecspanse/query_test.exs index 4b9d5c4..f80763d 100644 --- a/test/ecspanse/query_test.exs +++ b/test/ecspanse/query_test.exs @@ -355,6 +355,35 @@ defmodule Ecspanse.QueryTest do assert length(components) == 1 end + test "can query just ancestors of entities" do + entity_1 = + Ecspanse.Command.spawn_entity!({Ecspanse.Entity, components: [TestComponent1]}) + + entity_2 = + Ecspanse.Command.spawn_entity!( + {Ecspanse.Entity, components: [TestComponent1], parents: [entity_1]} + ) + + entity_3 = + Ecspanse.Command.spawn_entity!( + {Ecspanse.Entity, components: [TestComponent1], parents: [entity_2]} + ) + + assert result = + Ecspanse.Query.select({Ecspanse.Entity, TestComponent1}, + for_ancestors_of: [entity_3] + ) + |> Ecspanse.Query.stream() + |> Enum.to_list() + + assert length(result) == 2 + + for {entity, component} <- result do + assert entity.id in [entity_1.id, entity_2.id] + assert %TestComponent1{} = component + end + end + test "can return only one result and not a stream" do Ecspanse.Command.spawn_entity!( {Ecspanse.Entity, components: [TestComponent1, TestComponent2, TestComponent3]} @@ -426,6 +455,18 @@ defmodule Ecspanse.QueryTest do end end + describe "list_ancestors/1" do + test "returns the ancestors of an entity" do + entity_1 = Ecspanse.Command.spawn_entity!({Ecspanse.Entity, components: [TestComponent1]}) + + entity_2 = Ecspanse.Command.spawn_entity!({Ecspanse.Entity, children: [entity_1]}) + + entity_3 = Ecspanse.Command.spawn_entity!({Ecspanse.Entity, children: [entity_2]}) + + assert [entity_2, entity_3] == Ecspanse.Query.list_ancestors(entity_1) + end + end + describe "fetch_tagged_component" do test "fetches one entity's component by its tags" do entity = @@ -592,7 +633,7 @@ defmodule Ecspanse.QueryTest do entity_3 = Ecspanse.Command.spawn_entity!( - {Ecspanse.Entity, components: [{TestComponent4, [], [:alpha]}], parents: [entity_1]} + {Ecspanse.Entity, components: [{TestComponent4, [], [:alpha]}], parents: [entity_2]} ) Ecspanse.Command.spawn_entity!( @@ -600,7 +641,7 @@ defmodule Ecspanse.QueryTest do components: [TestComponent3, {TestComponent4, [], [:alpha]}, TestComponent5]} ) - components = Ecspanse.Query.list_tagged_components_for_descendants(entity_2, [:bar, :alpha]) + components = Ecspanse.Query.list_tagged_components_for_children(entity_2, [:bar, :alpha]) assert length(components) == 2 @@ -612,7 +653,7 @@ defmodule Ecspanse.QueryTest do end describe "list_tagged_components_for_descendants/2" do - test "returns the components for a list of tags for the children of a given entity" do + test "returns the components for a list of tags for the descendants of a given entity" do entity_1 = Ecspanse.Command.spawn_entity!( {Ecspanse.Entity, @@ -628,7 +669,7 @@ defmodule Ecspanse.QueryTest do entity_3 = Ecspanse.Command.spawn_entity!( - {Ecspanse.Entity, components: [{TestComponent4, [], [:alpha]}], parents: [entity_2]} + {Ecspanse.Entity, components: [{TestComponent4, [], [:alpha]}], parents: [entity_1]} ) Ecspanse.Command.spawn_entity!( @@ -636,7 +677,7 @@ defmodule Ecspanse.QueryTest do components: [TestComponent3, {TestComponent4, [], [:alpha]}, TestComponent5]} ) - components = Ecspanse.Query.list_tagged_components_for_children(entity_2, [:bar, :alpha]) + components = Ecspanse.Query.list_tagged_components_for_descendants(entity_2, [:bar, :alpha]) assert length(components) == 2 @@ -683,6 +724,42 @@ defmodule Ecspanse.QueryTest do end end + describe "list_tagged_components_for_ancestors/2" do + test "returns the components for a list of tags for the ancestors of a given entity" do + entity_1 = + Ecspanse.Command.spawn_entity!( + {Ecspanse.Entity, + components: [ + TestComponent1, + TestComponent2, + {TestComponent4, [], [:alpha]}, + TestComponent5 + ]} + ) + + entity_2 = Ecspanse.Command.spawn_entity!({Ecspanse.Entity, parents: [entity_1]}) + + entity_3 = + Ecspanse.Command.spawn_entity!( + {Ecspanse.Entity, components: [{TestComponent4, [], [:alpha]}], children: [entity_1]} + ) + + Ecspanse.Command.spawn_entity!( + {Ecspanse.Entity, + components: [TestComponent3, {TestComponent4, [], [:alpha]}, TestComponent5]} + ) + + components = Ecspanse.Query.list_tagged_components_for_ancestors(entity_2, [:bar, :alpha]) + + assert length(components) == 2 + + assert [%TestComponent4{} = comp_1, %TestComponent4{} = comp_2] = components + e1 = Ecspanse.Query.get_component_entity(comp_1) + e2 = Ecspanse.Query.get_component_entity(comp_2) + assert Enum.all?([e1, e2], fn e -> e.id in [entity_1.id, entity_3.id] end) + end + end + describe "fetch_component/2" do test "returns a component for a given entity" do entity_1 =