defmodule Archeometer.Schema do
@moduledoc """
Specify data base table information in Elixir modules. The information must
be specified inside a `defschema/1` macro.
An schema has `field` properties. Foreign keys are specied with a `belongs_to`
property. And the opposite is spcified with a `has` property.
## Example
defmodule ModuleA do
use Archeometer.Schema
defschema do
field :first_module, primary_key: true
belongs_to ModuleB
has ModuleB, as: :other_b, key: :recurse_id
end
end
"""
defmacro __using__(_opts) do
quote do
import Archeometer.Schema
end
end
@doc """
Specify that the current schema has fields. The first argument is the name of
the field, and as an optional argument you can specify if the current field is
a primary key.
For example
defmodule A do
use Archeometer.Schema
defschema :a do
field :id, primary_key: true
field :name
end
end
"""
defmacro field(name, opts \\ []) do
Module.put_attribute(__CALLER__.module, :schema_fields, name)
maybe_register_primary_key(__CALLER__.module, name, opts)
end
defp maybe_register_primary_key(caller, name, opts) do
if {:primary_key, true} in opts do
Module.put_attribute(caller, :schema_primary_key, name)
end
end
defp default_key_atom(module) do
module
|> Archeometer.Util.Code.snakefy()
|> Archeometer.Util.Code.atom_concat(:_id)
end
defp register_assoc(module, name, keys, type) do
quote do
Module.put_attribute(
__MODULE__,
:schema_assocs,
%Archeometer.Schema.Assoc{
name: unquote(name),
module: unquote(module),
type: unquote(type),
keys: unquote(keys)
}
)
end
end
@doc """
This property denotes that there is a foreign key in other table that makes
reference to the current table. This is usually to specify the `one` entity
in a `many-to-one` relation.
Using the name of the reference table, a field will be generated. This
generated name will be a queryable field in the schema. The name can be
specified with the `as: name` optional parameter.
The name of the foreign key in the other table will be inferred from the name
of the current table. This can be overriden with the `key: key_name` optional
paramenter.
For example
defmodule A do
use Archeometer.Schema
defschema :schema do
field :id, primary_key: true
has B # with infererd name `:b` and foreign key `:a_id`
has B, as: :other_b, key: other_a
end
end
Where `B` is defined as follows
defmodule B do
use Archeometer.Schema
defschema :b do
field :id, primary_key: true
belongs_to A # with inferred name `:a` and key `:a_id`
belongs_to A, as: :other_a, key: :other_a_id
end
end
"""
defmacro has(module, opts \\ []) do
name = Keyword.get(opts, :as, Archeometer.Util.Code.snakefy(module))
key = Keyword.get(opts, :key, default_key_atom(__CALLER__.module))
register_assoc(module, name, key, :remote)
end
@doc """
This property marks a foreign key. This is usually to specify the `many`
entity in a `many-to-one` relation.
Using the name of the reference table, a field will be generated. This
generated name will be a queryable field in the schema. The name can be
specified with the `as: name` optional parameter.
The name of the foreign key will be inferred from the name of the referenced
table. This can be overriden with the `key: key_name` optional paramenter.
For example
defmodule B do
use Archeometer.Schema
defschema :b do
field :id, primary_key: true
belongs_to A # with inferred name `:a` and key `:a_id`
belongs_to A, as: :other_a, key: :other_a_id
end
end
Where `A` is defined as follows
defmodule A do
use Archeometer.Schema
defschema :schema do
field :id, primary_key: true
has B # with infererd name `:b` and foreign key `:a_id`
has B, as: :other_b, key: other_a
end
end
"""
defmacro belongs_to(module, opts \\ []) do
name = Keyword.get(opts, :as, Archeometer.Util.Code.snakefy(module))
key = Keyword.get(opts, :key, default_key_atom(name))
maybe_register_primary_key(__CALLER__.module, key, opts)
register_assoc(module, name, key, :local)
end
@doc """
Starts the definition of a new schema. This definition maps database columns
into Elixir data. The `field`, `has` and `belongs_to` macros are available to
specify how to map the data base table to Elixir terms.
See their individual documentation of each for the list of properties they
can specify.
"""
defmacro defschema(name, do: block) when is_atom(name) do
Module.register_attribute(__CALLER__.module, :schema_fields, accumulate: true)
Module.register_attribute(__CALLER__.module, :schema_assocs, accumulate: true)
Module.register_attribute(__CALLER__.module, :schema_primary_key, accumulate: true)
quote do
unquote(block)
def __archeometer_name__() do
unquote(name)
end
@schema_fields_set MapSet.new(@schema_fields)
def __archeometer_fields__(), do: @schema_fields_set
@schema_assocs_map Map.new(
@schema_assocs,
fn %Archeometer.Schema.Assoc{name: n} = a ->
{n, a}
end
)
def __archeometer_assocs__() do
@schema_assocs_map
end
case @schema_primary_key do
[] ->
raise ArgumentError, "Schema doesn't define a primary key"
[_] ->
@schema_single_key hd(@schema_primary_key)
def __archeometer_keys__() do
@schema_single_key
end
_ ->
def __archeometer_keys__() do
@schema_primary_key
end
end
defstruct @schema_fields ++ Enum.map(@schema_assocs, & &1.name)
end
end
defmodule Assoc do
@moduledoc false
defstruct [:name, :type, :module, :keys]
end
end