defmodule Exto do
@moduledoc """
Configuration-driven Ecto Schemata.
"""
@doc """
Adds additional associations dynamically from app config.
Reads config for the given OTP application, under the name of the
current module. Each key maps to an Ecto.Schema function:
* `belongs_to`
* `field`
* `has_many`
* `has_one`
* `many_to_many`
Each of these keys should map to a keyword list where the key is the
name of the field or association and the value is one of:
* A type
* A tuple of type and options (keyword list)
Example Schema:
```
defmodule My.Schema do
use Ecto.Schema
import Exto, only: [flex_schema: 1]
schema "my_table" do
field :name, :string # just normal schema things
flex_schema(:my_app) # boom! give me the stuff
end
end
```
Example configuration:
```
config :my_app, My.Schema,
belongs_to: [
foo: Foo, # belongs_to :foo, Foo
bar: {Bar, type: :integer}, # belongs_to :bar, Bar, type: :integer
],
field: [
foo: :string, # field :foo, :string
bar: {:integer, default: 4}, # field :foo, :integer, default: 4
],
has_one: [
foo: Foo, # has_one :foo, Foo
bar: {Bar, foreign_key: :the_bar_id}, # has_one :bar, Bar, foreign_key: :the_bar_id
]
has_many: [
foo: Foo, # has_many :foo, Foo
bar: {Bar, foreign_key: :the_bar_id}, # has_many :bar, Bar, foreign_key: :the_bar_id
]
many_to_many: [
foo: Foo, # many_to_many :foo, Foo
bar: {Bar, join_through: FooBar}, # many_to_many :bar, Bar, :join_through: FooBar
]
```
This one won't work very well because we define `foo` and `bar` 5
times each, but I think you get the point.
Reading of configuration is done during compile time. The relations
will be baked in during compilation, thus:
* Do not expect this to work in runtime config.
* You will need to rebuild all dependencies which use this macro
when you change their configuration.
"""
defmacro flex_schema(otp_app) when is_atom(otp_app) do
module = __CALLER__.module
config = Application.get_env(otp_app, module, [])
code = Enum.flat_map(config, &flex_category/1)
quote do
(unquote_splicing(code))
end
end
# flex_schema impl
@cats [:belongs_to, :field, :has_one, :has_many, :many_to_many]
defp flex_category({:code, code}), do: [code]
defp flex_category({cat, items}) when cat in @cats and is_list(items),
do: Enum.map(items, &flex_association(cat, &1))
# skip over anything else, they might use it!
defp flex_category(_), do: []
defp flex_association(rel, {name, type})
when is_atom(name) and is_atom(type),
do: flex_association(rel, name, type, [])
defp flex_association(rel, {name, opts})
when is_atom(name) and is_list(opts),
do: flex_association(rel, name, opts)
defp flex_association(rel, {name, {type, opts}})
when is_atom(name) and is_atom(type) and is_list(opts),
do: flex_association(rel, name, type, opts)
defp flex_association(rel, {name, {opts}})
when is_atom(name) and is_list(opts),
do: flex_association(rel, name, opts)
defp flex_association(rel, name, opts) do
quote do
unquote(rel)(unquote(name), unquote(opts))
end
end
defp flex_association(rel, name, type, opts) do
quote do
unquote(rel)(unquote(name), unquote(type), unquote(opts))
end
end
end