Weave objects together by their external ids
If available in Hex, the package can be installed as:
- Add
weaver
to your list of dependencies inmix.exs
:
```elixir
def deps do
[{:weaver, "~> 0.1.0"}]
end
```
- Ensure
weaver
is started before your application:
```elixir
def application do
[applications: [:weaver]]
end
```
For an invariable language like Elixir, composing objects together can always be a pain. Consider export an object with all the images it has, and all the images that its associated objects have, like this:
Listing:
cover: Image
houses: [House]
House:
cover: Image
owner: User
User:
avatar: Image
Getting all the images directly or indirectly associated with the Listing
object is painful. Not to mention, we have to cram them back to the appropriate position.
The Weaver
comes to help, to
- Do topological sorting bases on the dependencies between objects, to determine the most efficient way to fetch objects with different types (at compile time, Woo! Yeah! no performance penalty!)
- Walk through the tree to collect corresponding ids
- Batch query objects of the same type
- Cram the fetched objects back to appropriate position
Basically, the weaver system composes by two parts:
- Providers, which accept several ids and return a Map mapping from the id to corresponding object(s)
- Weavers, which define how to weave objects provided by providers into the targeting object
Here is a quick example:
# Assuming we have a Book module, and every book has a cover image specified by the image_id attribute
defmodule ImageProvider do
def find(ids) do
Enum.reduce(ids, Map.new, fn id, acc ->
Map.put(acc, id, %Image{id: id})
end)
end
end
defmodule BookWeaver do
use Weaver.BuilderV2
# weave_one target, by: which_provider, though: which_external_id
weave_one :cover, by: ImageProvider, through: [:image_id]
end
ImageProvider
provides Image by giving image_ids, and the BookWeaver
defines how Image
get weaved into the Book
object. After defined the provider, and weaver, we can:
- Weave a single object:
assert %Book{id: "1", image_id: "book_1"} |> BookWeaver.weave
== %Book{id: "1", image_id: "book_1", image: %Image{id: "book_1"}}
- Weave a collections of objects:
# weave a list of objects
assert [%Book{id: "1", image_id: "book_1"}] |> BookWeaver.weave
== [%Book{id: "1", image_id: "book_1", image: %Image{id: "book_1"}}]
# weave a map of objects
assert %{first: %Book{id: "1", image_id: "book_1"}, second: %Book{id: "2", image_id: "book_2"}} |> BookWeaver.weave
== %{first: %Book{id: "1", image_id: "book_1", image: %Image{id: "book_1"}}, second: %Book{id: "2", image_id: "book_2", image: %Image{id: "book_2"}}}
# weave a tuple of objects
assert {%Book{id: "1", image_id: "book_1"}, %Book{id: "2", image_id: "book_2"}} |> BookWeaver.weave
== {%Book{id: "1", image_id: "book_1", image: %Image{id: "book_1"}}, %Book{id: "2", image_id: "book_2", image: %Image{id: "book_2"}}}
As simple as it should be~
Provider is just a behaviour defining a function named find
with the type ([id] -> %{id => any()})
A typical Provider defines like this (use ecto as database wrapper)
defmodule UserProvider do
import Ecto.Query
def find(ids) do
from(o in User)
|> where([o], o.id in ^ids)
|> Flatie.Repo.all
|> Enum.reduce(Map.new, fn user, map->
Map.put(map, user.id, user)
end)
end
end
Weaver is just another behaviour defining a function named weave
with the type (any() -> any())
.
So without our builder, you can simply define a dump Weaver:
defmodule DumpWeaver do
def weave(any), do: any
end
To do the complicate things you can use our builder Weaver.BuilderV2
(We have shifted to the version 2).
To use it, just include use Weaver.BuilderV2
in you weaver ( as we have seen in the quick example). After that you can have two very useful directives weave_one
and weave_many
at you hand. As their names implied
weave_one
: used to handle the one-one mappingweave_many
: used to handle the one-many mapping
The two directives, can be used in three ways
- The normal way
weave_(one|many) target, by: your_provider, though: the_id_path
- The compositional way
weave_(one|many) target, with: another_weaver
- The mix. The weaver will work in the normal way first, then is the compositional way
weave_(one|many) target, by: your_provider, though: the_id_path, with: another_weaver
target
(atom()) the target field you want to weave into
your_provider
: the provider that provides the ids referenced objects
the_id_field
([atom()]): the relative path to find the id
another_weaver
: just another weaver, use the same weaver will form a hazardous loop, so don't try
To make things easier (avoid defining too many un-reusable Weavers), we introduced a syntax to define inline weaver
weave_many :books do
weave_one :image, by: ImageProvider, through: [:image_id]
weave_many :taxons, by: TaxonProvider, through: [:taxon_ids]
end
Here's a full example from our project
defmodule ListingWeaver do
use Weaver.BuilderV2
weave_one :community, by: CommunityProvider, through: [:community_id], with: Flatie.CommunityWeaver
weave_one :offering, by: OfferingProvider, through: [:offering_id]
weave_many :listing_properties, by: ListingPropertyProvider, through: [:data, :properties], with: ListingPropertyWeaver
weave_one :cover_image, by: ImageProvider, through: [:data, :cover_image_id]
weave_many :images, by: ListingImageProvider, through: [:data, :images] do
weave_one :image, by: ImageProvider, through: [:id]
weave_many :taxons, by: TaxonProvider, through: [:taxon_ids]
end
end