defmodule Manganese.EntityKit.Structs.EntitiesCollection do alias Manganese.EntityKit.Structs @type t :: %Structs.EntitiesCollection{ entities: %{ optional(atom) => %{ optional(integer) => term }} } defstruct [ entities: %{} ] # Utilities @doc """ Create an empty entities collection. """ @spec new :: t def new, do: %Structs.EntitiesCollection{} @doc """ """ @spec from_map(map, (map, atom -> term)) :: t def from_map(map, deserializer) do %Structs.EntitiesCollection{ entities: map["entities"] |> Map.new(fn { entity_type_string, serialized_entities_of_type } -> entity_type = String.to_atom entity_type_string { entity_type, serialized_entities_of_type |> Map.new(fn { id_string, serialized_entity } -> { String.to_integer(id_string), deserializer.(serialized_entity, entity_type) } end) } end) } end @doc """ Retrieve an entity from the collection. If no entity of the given type and ID pair exist, `nil` is returned instead. """ @spec get_entity(t, atom, pos_integer) :: term | nil def get_entity(entities_collection, entity_type, id) do case Map.get entities_collection.entities, entity_type do %{ ^id => entity } -> entity _ -> nil end end @doc """ Add an entity to the collection. If the entity of the given type and ID pair already exist, it will be overwritten with the new value. """ @spec put_entity(t, atom, term) :: t def put_entity(entities_collection, entity_type, entity) do %Structs.EntitiesCollection{ entities: entities_collection.entities |> Map.put( entity_type, (Map.get(entities_collection.entities, entity_type) || %{}) |> Map.put(entity.id, entity) ) } end @doc """ Add multiple entities of the same type to the collection. If an entity of the given type and ID pair already exist, it will be overwritten with the new value. """ @spec put_entities(t, atom, [ term ]) :: t def put_entities(entities_collection, entity_type, entities) do entities |> Enum.reduce(entities_collection, fn entity, entities_collection -> entities_collection |> put_entity(entity_type, entity) end) end @doc """ Combine two collections into one. If an entity of the same type and ID pair is present in both collections, the value in `entities_collection2` will take precedence. """ @spec merge(t, t) :: t def merge(entities_collection1, entities_collection2) do entities1 = (entities_collection1 || Structs.EntitiesCollection.new).entities entities2 = (entities_collection2 || Structs.EntitiesCollection.new).entities %Structs.EntitiesCollection{ entities: Map.merge(entities1, entities2, fn _, entities_of_type1, entities_of_type2 -> Map.merge(entities_of_type1 || %{}, entities_of_type2 || %{}, fn _, _entity1, entity2 -> entity2 end) end) } end @doc """ """ @spec map(t, (term -> term)) :: t def map(entities_collection, mapper) do %Structs.EntitiesCollection{ entities: entities_collection.entities |> Map.new(fn { entity_type, entities_of_type } -> { entity_type, entities_of_type |> Enum.map(fn { id, entity } -> { id, mapper.(entity_type, entity) } end) |> Enum.into(%{}) } end) |> Enum.into(%{}) } end end defimpl Manganese.EntityKit.Protocols.FilterableCollection, for: Manganese.EntityKit.Structs.EntitiesCollection do alias Manganese.EntityKit.Structs def filter(entities_collection, filter) do %Structs.EntitiesCollection{ entities: entities_collection.entities |> Map.new(fn { entity_type, entities_of_type } -> { entity_type, entities_of_type |> Enum.filter(fn { id, _entity } -> filter.(entity_type, id) end) |> Enum.into(%{}) } end) |> Enum.filter(fn { _, entities_of_type } -> !(is_nil(entities_of_type) || length(Map.keys entities_of_type) == 0) end) |> Enum.into(%{}) } end end defimpl Manganese.EntityKit.Protocols.SerializableCollection, for: Manganese.EntityKit.Structs.EntitiesCollection do alias Manganese.EntityKit.Behaviors def serialize(entities_collection, serializer) do %{ "entities" => entities_collection.entities |> Map.new(fn { entity_type, entities_of_type } -> { Atom.to_string(entity_type), entities_of_type |> Map.new(fn { id, entity } -> serialized_entity = entity |> Behaviors.Entity.serialize(entity_type, serializer) { id, serialized_entity } end) } end) |> Enum.filter(fn { _, entities_of_type } -> !is_nil(entities_of_type) end) |> Enum.into(%{}) } end end defimpl Manganese.EntityKit.Protocols.ReloadableCollection, for: Manganese.EntityKit.Structs.EntitiesCollection do alias Manganese.EntityKit.Structs alias Manganese.EntityKit.Behaviors def reload(entities_collection, repo) do reloaded_entities_collection = entities_collection |> Structs.EntitiesCollection.map(fn entity_type, entity -> { :ok, reloaded_entity } = entity |> Behaviors.Entity.reload(entity_type, repo) reloaded_entity end) { :ok, reloaded_entities_collection } end def reload!(entities_collection, repo) do { :ok, reloaded_entities_collection } = reload entities_collection, repo reloaded_entities_collection end end