Current section
Files
Jump to
Current section
Files
lib/model.ex
defmodule Exddb.Model do
@moduledoc ~S"""
`Exddb.Model` lets you define how `:exddb` should access a given DynamoDB table.
For example:
defmodule MyShopApp.ReceiptModel do
use Exddb.Model
@table_name "receipts"
@key :receipt_id
# define our schema
model do
field :receipt_id, :string
field :client_id, :string, null: false
field :total, :float
field :items, :integer
field :processed, :boolean
field :raw, :binary
end
end
Here each `field` is assumed to allow null values, unless you specify `null: false`.
Nulls will not be stored in DynamoDB or kept in the model data.
"""
@type t :: module
@callback schema(t :: term) :: no_return
@callback dump(model :: Exddb.Model.t) :: []
@callback parse(t :: term) :: Exddb.Model.t
defmacro __using__(_) do
quote do
import Exddb.Model, only: [model: 1]
@key :hash
@table_name Atom.to_string(Mix.env) <> "_" <> inspect(__MODULE__)
end
end
defmacro model([do: block]) do
quote do
Module.register_attribute(__MODULE__, :model_keys, accumulate: true)
Module.register_attribute(__MODULE__, :struct_fields, accumulate: true)
Module.register_attribute(__MODULE__, :model_fields, accumulate: true)
Module.register_attribute(__MODULE__, :allow_null, accumulate: true)
@behaviour Exddb.Model
try do
import Exddb.Model
unquote(block)
after
:ok
end
struct_fields = @struct_fields |> Enum.reverse
model_fields = @model_fields |> Enum.reverse
# Generate code for the module
Module.eval_quoted __MODULE__, [
Exddb.Model.generate_setters,
Exddb.Model.generate_struct(struct_fields),
Exddb.Model.generate_field_schemas(model_fields),
Exddb.Model.generate_key_schema(@key),
Exddb.Model.generate_new,
Exddb.Model.generate_table_name(@table_name),
Exddb.Model.generate_schema_null_checks(@allow_null, @key),
Exddb.Model.generate_type_conversions,
Exddb.Model.generate_validation
]
end
end
@doc ~S"""
Define a field. For example `field :receipt_id, :string`
"""
defmacro field(name, type \\ :string, opts \\ []) do
quote do
Exddb.Model.define_field(__MODULE__, unquote(name), unquote(type), unquote(opts))
end
end
def define_field(module, name, :integer, []) do
define_field(module, name, :integer, [default: 0])
end
def define_field(module, name, :boolean, []) do
define_field(module, name, :boolean, [default: false])
end
def define_field(module, name, :float, []) do
define_field(module, name, :float, [default: 0.0])
end
def define_field(module, name, type, opts) do
Module.put_attribute(module, :struct_fields, {name, opts[:default]})
Module.put_attribute(module, :model_fields, {name, type})
Module.put_attribute(module, :allow_null, {name, Keyword.get(opts, :null, true)})
end
def generate_struct(struct_fields) do
quote do
defstruct unquote(Macro.escape(struct_fields))
end
end
def generate_new do
quote do
def new(attributes \\ []) do
defaults = __struct__
attributes = Enum.into(attributes, Map.new)
for k <- Map.keys(attributes) do
if not Map.has_key?(defaults, k) do
raise ArgumentError, "Key #{inspect k} not defined in schema #{__MODULE__}"
end
end
Map.merge(defaults, attributes)
end
end
end
def generate_setters do
quote do
def set(item, attributes \\ []) do
defaults = __struct__
attributes = Enum.into(attributes, Map.new)
for k <- Map.keys(attributes) do
if not Map.has_key?(defaults, k) do
raise ArgumentError, "Key #{inspect k} not defined in schema #{__MODULE__}"
end
end
Map.merge(item, attributes)
end
end
end
def generate_key_schema(name) do
quote do
def schema(:key), do: unquote(name)
end
end
def generate_schema_null_checks(fields, key) do
Enum.map(fields, fn {name, allow_null} ->
if name == key do
quote do
def schema(:null, unquote(name)), do: false
end
else
quote do
def schema(:null, unquote(name)), do: unquote(allow_null)
end
end
end)
end
def generate_field_schemas(fields) do
quoted = Enum.map(fields, fn {name, type} ->
quote do
def schema(:field, unquote(name)), do: unquote(type)
end
end)
field_names = Enum.map(fields, &elem(&1, 0))
quoted ++ [quote do
def schema(:field, _), do: nil
def schema(:fields), do: unquote(field_names)
end]
end
def generate_type_conversions do
quote do
def parse(record), do: Exddb.Type.parse(record, new)
def dump(record), do: Exddb.Type.dump(record)
end
end
def generate_table_name(table_name) do
quote do
def schema(:table_name), do: unquote(table_name)
end
end
def generate_validation do
quote do
def __validate__(%{:__struct__ => module} = record) do
if module != __MODULE__, do: raise(ArgumentError, "Cannot validate items of type #{module} with #{__MODULE__}")
__validate__(Map.keys(record), record)
end
def __validate__([:__struct__|rest], record), do: __validate__(rest, record)
def __validate__([key|rest], record) do
value = Map.get(record, key)
if value == nil and not schema(:null, key) do
{:error, "#{key} cannot be null!"}
else
__validate__(rest, record)
end
end
def __validate__([], record), do: :ok
end
end
end