Skip to content

Commit

Permalink
5 query ancestors (#6)
Browse files Browse the repository at this point in the history
* Add ancestors queries

* Update the changelog
  • Loading branch information
iacobson authored Sep 28, 2023
1 parent f9922a9 commit 4f49f45
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 10 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
95 changes: 91 additions & 4 deletions lib/ecspanse/query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -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
Expand Down
87 changes: 82 additions & 5 deletions test/ecspanse/query_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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]}
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -592,15 +633,15 @@ 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!(
{Ecspanse.Entity,
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

Expand All @@ -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,
Expand All @@ -628,15 +669,15 @@ 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!(
{Ecspanse.Entity,
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

Expand Down Expand Up @@ -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 =
Expand Down

0 comments on commit 4f49f45

Please sign in to comment.