defmodule Contexted.CRUD do
@moduledoc """
The `Contexted.CRUD` module generates common [CRUD](https://pl.wikipedia.org/wiki/CRUD) (Create, Read, Update, Delete) functions for a context, similar to what `mix phx gen context` task generates.
## Options
- `:repo` - The Ecto repository module used for database operations (required)
- `:schema` - The Ecto schema module representing the resource that these CRUD operations will be generated for (required)
- `:exclude` - A list of atoms representing the functions to be excluded from generation (optional)
- `:plural_resource_name` - A custom plural version of the resource name to be used in function names (optional). If not provided, singular version with 's' ending will be used to generate list function
## Usage
```elixir
defmodule MyApp.Accounts do
use Contexted.CRUD,
repo: MyApp.Repo,
schema: MyApp.Accounts.User,
exclude: [:delete],
plural_resource_name: "users"
end
```
This sample usage will generate all CRUD functions for `MyApp.Accounts.User` resource, excluding `delete_user/1`.
## Generated Functions
The following functions are generated by default. Any of them can be excluded by adding their correspoding atom to the `:exclude` option.
- `list_{plural resource name}` - Lists all resources in the schema.
- `get_{resource name}` - Retrieves a resource by its ID. Returns `nil` if not found.
- `get_{resource name}!` - Retrieves a resource by its ID. Raises an error if not found.
- `create_{resource name}` - Creates a new resource with the provided attributes. Returns an `:ok` tuple with the resource or an `:error` tuple with changeset.
- `create_{resource name}!` - Creates a new resource with the provided attributes. Raises an error if creation fails.
- `update_{resource name}` - Updates an existing resource with the provided attributes. Returns an `:ok` tuple with the resource or an `:error` tuple with changeset.
- `update_{resource name}!` - Updates an existing resource with the provided attributes. Raises an error if update fails.
- `delete_{resource name}` - Deletes an existing resource. Returns an `:ok` tuple with the resource or an `:error` tuple with changeset.
- `delete_{resource name}!` - Deletes an existing resource. Raises an error if delete fails.
- `change_{resource name}` - Returns changeset for given resource.
{resource name} and {plural resource name} will be replaced by the singular and plural forms of the resource name.
"""
defmacro __using__(opts) do
# Expanding opts
opts = Enum.map(opts, fn {key, val} -> {key, Macro.expand(val, __CALLER__)} end)
repo = Keyword.fetch!(opts, :repo)
schema = Keyword.fetch!(opts, :schema)
exclude = Keyword.get(opts, :exclude, [])
plural_resource_name = Keyword.get(opts, :resource_plural_name, nil)
resource_name = schema |> Module.split() |> List.last() |> Macro.underscore()
plural_resource_name =
if plural_resource_name, do: plural_resource_name, else: "#{resource_name}s"
# credo:disable-for-next-line Credo.Check.Refactor.LongQuoteBlocks
quote bind_quoted: [
repo: repo,
schema: schema,
exclude: exclude,
resource_name: resource_name,
plural_resource_name: plural_resource_name
] do
unless :list in exclude do
function_name = String.to_atom("list_#{plural_resource_name}")
@doc """
Returns a list of all #{plural_resource_name} from the database.
## Examples
iex> list_#{plural_resource_name}()
[%#{Macro.camelize(resource_name)}{}, ...]
"""
@spec unquote(function_name)() :: [%unquote(schema){}]
def unquote(function_name)() do
unquote(schema)
|> unquote(repo).all()
end
end
unless :get in exclude do
function_name = String.to_atom("get_#{resource_name}")
@doc """
Retrieves a single #{resource_name} by its ID from the database. Returns nil if the #{resource_name} is not found.
## Examples
iex> get_#{resource_name}(id)
%#{Macro.camelize(resource_name)}{} or nil
"""
@spec unquote(function_name)(integer() | String.t()) :: %unquote(schema){} | nil
def unquote(function_name)(id) do
unquote(schema)
|> unquote(repo).get(id)
end
function_name = String.to_atom("get_#{resource_name}!")
@doc """
Retrieves a single #{resource_name} by its ID from the database. Raises an error if the #{resource_name} is not found.
## Examples
iex> get_#{resource_name}!(id)
%#{Macro.camelize(resource_name)}{} or raises Ecto.NoResultsError
"""
@spec unquote(function_name)(integer() | String.t()) :: %unquote(schema){}
def unquote(function_name)(id) do
unquote(schema)
|> unquote(repo).get!(id)
end
end
unless :create in exclude do
function_name = String.to_atom("create_#{resource_name}")
@doc """
Creates a new #{resource_name} with the provided attributes.
Returns an `:ok` tuple with the #{resource_name} if successful, or an `:error` tuple with a changeset if not.
## Examples
iex> create_#{resource_name}(attrs)
{:ok, %#{Macro.camelize(resource_name)}{}} or {:error, Ecto.Changeset{}}
"""
@spec unquote(function_name)(map()) :: {:ok, %unquote(schema){}} | {:error, map()}
def unquote(function_name)(attrs \\ %{}) do
%unquote(schema){}
|> unquote(schema).changeset(attrs)
|> unquote(repo).insert()
end
function_name = String.to_atom("create_#{resource_name}!")
@doc """
Creates a new #{resource_name} with the provided attributes.
Returns the #{resource_name} if successful, or raises an error if not.
## Examples
iex> create_#{resource_name}!(attrs)
%#{Macro.camelize(resource_name)}{} or raises Ecto.StaleEntryError
"""
@spec unquote(function_name)(map()) :: %unquote(schema){}
def unquote(function_name)(attrs \\ %{}) do
%unquote(schema){}
|> unquote(schema).changeset(attrs)
|> unquote(repo).insert!()
end
end
unless :update in exclude do
function_name = String.to_atom("update_#{resource_name}")
@doc """
Updates an existing #{resource_name} with the provided attributes.
Returns an `:ok` tuple with the updated #{resource_name} if successful, or an `:error` tuple with a changeset if not.
## Examples
iex> update_#{resource_name}(#{resource_name}, attrs)
{:ok, %#{Macro.camelize(resource_name)}{}} or {:error, Ecto.Changeset{}}
"""
@spec unquote(function_name)(%unquote(schema){}, map()) ::
{:ok, %unquote(schema){}} | {:error, map()}
def unquote(function_name)(record, attrs \\ %{}) do
record
|> unquote(schema).changeset(attrs)
|> unquote(repo).update()
end
function_name = String.to_atom("update_#{resource_name}!")
@doc """
Updates an existing #{resource_name} with the provided attributes.
Returns the updated #{resource_name} if successful, or raises an error if not.
## Examples
iex> update_#{resource_name}!(#{resource_name}, attrs)
%#{Macro.camelize(resource_name)}{} or raises Ecto.StaleEntryError
"""
@spec unquote(function_name)(%unquote(schema){}, map()) :: %unquote(schema){}
def unquote(function_name)(record, attrs \\ %{}) do
record
|> unquote(schema).changeset(attrs)
|> unquote(repo).update!()
end
end
unless :delete in exclude do
function_name = String.to_atom("delete_#{resource_name}")
@doc """
Deletes an existing #{resource_name}.
Returns an `:ok` tuple with the deleted #{resource_name} if successful, or an `:error` tuple with a changeset if not.
## Examples
iex> delete_#{resource_name}(#{resource_name})
{:ok, %#{Macro.camelize(resource_name)}{}} or {:error, Ecto.Changeset{}}
"""
@spec unquote(function_name)(%unquote(schema){}) ::
{:ok, %unquote(schema){}} | {:error, map()}
def unquote(function_name)(record) do
record
|> unquote(repo).delete()
end
function_name = String.to_atom("delete_#{resource_name}!")
@spec unquote(function_name)(%unquote(schema){}) :: {:ok, %unquote(schema){}}
def unquote(function_name)(record) do
record
|> unquote(repo).delete!()
end
end
unless :change in exclude do
function_name = String.to_atom("change_#{resource_name}")
@doc """
Deletes an existing #{resource_name}.
Returns the deleted #{resource_name} if successful, or raises an error if not.
## Examples
iex> delete_#{resource_name}!(#{resource_name})
%#{Macro.camelize(resource_name)}{} or raises Ecto.StaleEntryError
"""
@spec unquote(function_name)(%unquote(schema){}, map()) :: map()
def unquote(function_name)(record, attrs \\ %{}) do
record
|> unquote(schema).changeset(attrs)
end
end
end
end
end