defmodule EctoCooler do
@moduledoc """
This module provides a DSL to easily generate the basic functions for a schema.
This allows the context to focus on interesting, atypical implementations rather
than the redundent, drifting CRUD functions.
"""
alias __MODULE__
alias EctoCooler.Helpers
alias EctoCooler.OptionParser
alias EctoCooler.ResourceFunctions
@doc """
Macro to import `EctoCooler.using_repo/2`
## Examples
use EctoCooler
"""
defmacro __using__(_) do
quote do
import EctoCooler, only: [using_repo: 2]
end
end
@doc """
Macro to define schema access within a given `Ecto.Repo`
## Examples
using_repo(Repo) do
resource(Schema)
end
"""
defmacro using_repo(repo, do: block) do
quote do
Module.register_attribute(__MODULE__, :repo, [])
Module.put_attribute(__MODULE__, :repo, unquote(repo))
Module.register_attribute(__MODULE__, :resources, accumulate: true)
import EctoCooler, only: [resource: 2, resource: 1]
unquote(block)
def __resource__(:resources), do: @resources
Module.delete_attribute(__MODULE__, :resources)
Module.delete_attribute(__MODULE__, :repo)
end
end
@doc """
Macro to define CRUD methods for the given `Ecto.Repo` in the using module.
## Examples
using(Repo) do
resource(Schema)
end
using(Repo) do
resource(Schema, suffix: false)
end
using(Repo) do
resource(Schema, only: [:get])
end
using(Repo) do
resource(Schema, except: [:delete])
end
using(Repo) do
resource(Schema, :read)
end
using(Repo) do
resource(Schema, :write)
end
using(Repo) do
resource(Schema, :delete)
end
"""
defmacro resource(schema, options \\ []) do
quote bind_quoted: [schema: schema, options: options] do
suffix = OptionParser.create_suffix(schema, options)
schema_name = Helpers.schema_name(schema)
resources = OptionParser.parse(suffix, options)
descriptions = Helpers.resource_descriptions(resources)
Module.put_attribute(__MODULE__, :resources, {@repo, schema, descriptions})
resources
|> Enum.each(fn {action, %{name: name}} ->
case action do
:all ->
@doc """
Fetches all #{schema_name} entries from the data store.
## Examples
#{name}()
[%#{schema_name}{id: 123}]
#{name}(preloads: [:relation])
[%#{schema_name}{id: 123, relation: %Relation{}}]
#{name}(order_by: [desc: :id])
[%#{schema_name}{id: 2}, %#{schema_name}{id: 1}]
#{name}(preloads: [:relation], order_by: [desc: :id])
[
%#{schema_name}{
id: 2,
relation: %Relation{}
},
%#{schema_name}{
id: 1,
relation: %Relation{}
}
]
"""
@spec unquote(name)(keyword(list())) :: list(Ecto.Schema.t())
def unquote(name)(options \\ []) do
ResourceFunctions.all(@repo, unquote(schema), options)
end
:change ->
@doc """
Creates a #{schema_name} changeset from an existing schema struct.
#{name}(%#{schema_name}{}, %{})
#Ecto.Changeset<
action: nil,
changes: %{},
errors: [],
data: ##{schema_name}<>,
valid?: true
>
#{name}(%#{schema_name}{}, [])
#Ecto.Changeset<
action: nil,
changes: %{},
errors: [],
data: ##{schema_name}<>,
valid?: true
>
"""
@spec unquote(name)(Ecto.Schema.t(), map() | Keyword.t()) :: Ecto.Changeset.t()
def unquote(name)(changeable, changes) do
ResourceFunctions.change(unquote(schema), changeable, changes)
end
:changeset ->
@doc """
Creates a blank changeset.
changeset()
#Ecto.Changeset<
action: nil,
changes: %{},
errors: [],
data: ##{schema_name}<>,
valid?: true
>
"""
@spec unquote(name)() :: Ecto.Changeset.t()
def unquote(name)() do
ResourceFunctions.changeset(unquote(schema))
end
:create ->
@doc """
Inserts a #{schema_name} with the given attributes in the data store.
## Examples
#{name}(%{})
{:ok, %#{schema_name}{}}
#{name}([])
{:ok, %#{schema_name}{}}
#{name}(%{invalid: "invalid"})
{:error, %Ecto.Changeset{}}
"""
@spec unquote(name)(map() | Keyword.t()) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
def unquote(name)(attributes) do
ResourceFunctions.create(@repo, unquote(schema), attributes)
end
:create! ->
@doc """
Same as create_#{suffix}/1 but returns the struct or raises if the changeset is invalid.
## Examples
#{name}(%{})
%#{schema_name}{}
#{name}([])
%#{schema_name}{}
#{name}(%{invalid: "invalid"})
** (Ecto.InvalidChangesetError)
"""
@spec unquote(name)(map() | Keyword.t()) ::
Ecto.Schema.t() | Ecto.InvalidChangesetError
def unquote(name)(attributes) do
ResourceFunctions.create!(@repo, unquote(schema), attributes)
end
:delete ->
@doc """
Deletes a given %#{schema_name}{} from the data store.
## Examples
#{name}(%#{schema_name}{id: 123})
{:ok, %#{schema_name}{id: 123}}
#{name}(%#{schema_name}{id: 456})
{:error, %Ecto.Changeset{}}
"""
@spec unquote(name)(Ecto.Schema.t()) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
def unquote(name)(struct) do
ResourceFunctions.delete(@repo, struct)
end
:delete! ->
@doc """
Same as delete_#{suffix}/1 but returns the struct or raises if the changeset is invalid.
## Examples
#{name}(%#{schema_name}{id: 123})
%#{schema_name}{id: 123}
#{name}(%#{schema_name}{id: 456})
** (Ecto.StaleEntryError)
"""
def unquote(name)(struct) do
ResourceFunctions.delete!(@repo, struct)
end
:get ->
@doc """
Fetches a single #{schema_name} from the data store where the primary key matches the given id.
## Examples
#{name}(123)
%#{schema_name}{id: 123}
#{name}(456)
nil
#{name}(123, preloads: [:relation])
%#{schema_name}{
id: 1,
relation: %Relation{}
}
"""
@spec unquote(name)(String.t() | integer(), keyword(list())) :: Ecto.Schema.t() | nil
def unquote(name)(id, options \\ []) do
ResourceFunctions.get(@repo, unquote(schema), id, options)
end
:get! ->
@doc """
Same as get_#{suffix}/2 but raises Ecto.NoResultsError if no record was found.
## Examples
#{name}(123)
%#{schema_name}{id: 123}
#{name}(456)
** (Ecto.NoResultsError)
#{name}(123, preloads: [:relation])
%#{schema_name}{
id: 1,
relation: %Relation{}
}
"""
def unquote(name)(id, options \\ []) do
ResourceFunctions.get!(@repo, unquote(schema), id, options)
end
:get_by ->
@doc """
Fetches a single result from the query.
Returns nil if no result was found. Raises if more than one entry.
## Examples
#{name}(name: "Some Name")
%#{schema_name}{name: "Some Name"}
#{name}(%{name: "Some Name"})
%#{schema_name}{name: "Some Name"}
#{name}(name: "Missing")
nil
"""
def unquote(name)(attributes, options \\ []) do
ResourceFunctions.get_by(@repo, unquote(schema), attributes, options)
end
:get_by! ->
@doc """
Similar to get_by/2 but raises Ecto.NoResultsError if no record was found.
Raises if more than one entry.
## Examples
#{name}(name: "Some Name")
%#{schema_name}{name: "Some Name"}
#{name}(%{name: "Some Name"})
%#{schema_name}{name: "Some Name"}
#{name}(name: "Missing")
** (Ecto.NoResultsError)
"""
def unquote(name)(attributes, options \\ []) do
ResourceFunctions.get_by!(@repo, unquote(schema), attributes, options)
end
:update ->
@doc """
Updates a %#{schema_name}{} with the given attributes.
## Examples
#{name}(%#{schema_name}{id: 123}, %{attribute: "updated attribute"})
{:ok, %#{schema_name}{id: 123, attribute: "updated attribute"}}
#{name}(%#{schema_name}{id: 123}, attribute: "updated attribute")
{:ok, %#{schema_name}{id: 123, attribute: "updated attribute"}}
#{name}(%#{schema_name}{id: 123}, %{}, force: true)
{:ok, %#{schema_name}{id: 123, attribute: "updated attribute"}}
#{name}(%#{schema_name}{id: 123}, %{}, prefix: "my_prefix")
{:ok, %#{schema_name}{id: 123, attribute: "updated attribute"}}
#{name}(%#{schema_name}{id: 123}, %{invalid: "invalid"})
{:error, %Ecto.Changeset{}}
"""
@spec unquote(name)(Ecto.Schema.t(), map() | Keyword.t()) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
def unquote(name)(struct, attributes) do
ResourceFunctions.update(@repo, unquote(schema), struct, attributes)
end
@doc """
Same as update_#{suffix}/2 returns a %#{schema_name}{} or raises if the changeset is invalid.
## Examples
#{name}(%#{schema_name}{id: 123}, %{attribute: "updated attribute"})
%#{schema_name}{id: 123, attribute: "updated attribute"}
#{name}(%#{schema_name}{id: 123}, %{}, force: true)
%#{schema_name}{id: 123, attribute: "updated attribute"}
#{name}(%#{schema_name}{id: 123}, %{}, prefix: "my_prefix")
%#{schema_name}{id: 123, attribute: "updated attribute"}
#{name}(%#{schema_name}{id: 123}, %{invalid: "invalid"})
** (Ecto.InvalidChangesetError)
"""
:update! ->
@spec unquote(name)(Ecto.Schema.t(), map() | Keyword.t()) ::
Ecto.Schema.t() | Ecto.InvalidChangesetError
def unquote(name)(struct, attributes) do
ResourceFunctions.update!(@repo, unquote(schema), struct, attributes)
end
_ ->
nil
end
end)
end
end
end