Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
316 changes: 172 additions & 144 deletions lib/contexted/crud.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule Contexted.CRUD do
- `: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
- `:read_repo` - `&MyApp.Repo.replica/0` function for read operations (`list_*`, `get_*`). Writes still use `:repo`.

## Usage

Expand Down Expand Up @@ -42,12 +43,13 @@ defmodule Contexted.CRUD do
"""

defmacro __using__(opts) do
# Expanding opts
opts = Enum.map(opts, fn {key, val} -> {key, Macro.expand(val, __CALLER__)} end)
caller = __CALLER__
{read_repo_fun, opts} = Keyword.pop(opts, :read_repo)

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, :plural_resource_name, nil)

Expand All @@ -56,203 +58,229 @@ defmodule Contexted.CRUD do
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()
read_repo_body = read_repo_body(read_repo_fun, repo, caller)

read_repo_def =
quote do
defp crud_read_repo do
unquote(read_repo_body)
end
end

unless :get in exclude do
function_name = String.to_atom("get_#{resource_name}")
# credo:disable-for-lines:3 Credo.Check.Refactor.LongQuoteBlocks
main =
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)
|> crud_read_repo().all()
end
end

@doc """
Retrieves a single #{resource_name} by its ID from the database. Returns nil if the #{resource_name} is not found.
unless :get in exclude do
function_name = String.to_atom("get_#{resource_name}")

## Examples
@doc """
Retrieves a single #{resource_name} by its ID from the database. Returns nil if the #{resource_name} is not found.

iex> get_#{resource_name}(id)
%#{Macro.camelize(resource_name)}{} or nil
"""
## Examples

@spec unquote(function_name)(integer() | String.t()) :: %unquote(schema){} | nil
def unquote(function_name)(id) do
unquote(schema)
|> unquote(repo).get(id)
end
iex> get_#{resource_name}(id)
%#{Macro.camelize(resource_name)}{} or nil
"""

function_name = String.to_atom("get_#{resource_name}!")
@spec unquote(function_name)(integer() | String.t()) :: %unquote(schema){} | nil
def unquote(function_name)(id) do
unquote(schema)
|> crud_read_repo().get(id)
end

@doc """
Retrieves a single #{resource_name} by its ID from the database. Raises an error if the #{resource_name} is not found.
function_name = String.to_atom("get_#{resource_name}!")

## Examples
@doc """
Retrieves a single #{resource_name} by its ID from the database. Raises an error if the #{resource_name} is not found.

iex> get_#{resource_name}!(id)
%#{Macro.camelize(resource_name)}{} or raises Ecto.NoResultsError
"""
## Examples

@spec unquote(function_name)(integer() | String.t()) :: %unquote(schema){}
def unquote(function_name)(id) do
unquote(schema)
|> unquote(repo).get!(id)
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)
|> crud_read_repo().get!(id)
end
end
end

unless :create in exclude do
function_name = String.to_atom("create_#{resource_name}")
unless :create in exclude do
function_name = String.to_atom("create_#{resource_name}")

@doc """
Creates a new #{resource_name} with the provided attributes.
@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.
Returns an `:ok` tuple with the #{resource_name} if successful, or an `:error` tuple with a changeset if not.

## Examples
## Examples

iex> create_#{resource_name}(attrs)
{:ok, %#{Macro.camelize(resource_name)}{}} or {:error, Ecto.Changeset{}}
"""
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
@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}!")
function_name = String.to_atom("create_#{resource_name}!")

@doc """
Creates a new #{resource_name} with the provided attributes.
@doc """
Creates a new #{resource_name} with the provided attributes.

Returns the #{resource_name} if successful, or raises an error if not.
Returns the #{resource_name} if successful, or raises an error if not.

## Examples
## Examples

iex> create_#{resource_name}!(attrs)
%#{Macro.camelize(resource_name)}{} or raises Ecto.StaleEntryError
"""
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!()
@spec unquote(function_name)(map()) :: %unquote(schema){}
def unquote(function_name)(attrs \\ %{}) do
%unquote(schema){}
|> unquote(schema).changeset(attrs)
|> unquote(repo).insert!()
end
end
end

unless :update in exclude do
function_name = String.to_atom("update_#{resource_name}")
unless :update in exclude do
function_name = String.to_atom("update_#{resource_name}")

@doc """
Updates an existing #{resource_name} with the provided attributes.
@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.
Returns an `:ok` tuple with the updated #{resource_name} if successful, or an `:error` tuple with a changeset if not.

## Examples
## Examples

iex> update_#{resource_name}(#{resource_name}, attrs)
{:ok, %#{Macro.camelize(resource_name)}{}} or {:error, Ecto.Changeset{}}
"""
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
@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}!")
function_name = String.to_atom("update_#{resource_name}!")

@doc """
Updates an existing #{resource_name} with the provided attributes.
@doc """
Updates an existing #{resource_name} with the provided attributes.

Returns the updated #{resource_name} if successful, or raises an error if not.
Returns the updated #{resource_name} if successful, or raises an error if not.

## Examples
## Examples

iex> update_#{resource_name}!(#{resource_name}, attrs)
%#{Macro.camelize(resource_name)}{} or raises Ecto.StaleEntryError
"""
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!()
@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
end

unless :delete in exclude do
function_name = String.to_atom("delete_#{resource_name}")
unless :delete in exclude do
function_name = String.to_atom("delete_#{resource_name}")

@doc """
Deletes an existing #{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.
Returns an `:ok` tuple with the deleted #{resource_name} if successful, or an `:error` tuple with a changeset if not.

## Examples
## Examples

iex> delete_#{resource_name}(#{resource_name})
{:ok, %#{Macro.camelize(resource_name)}{}} or {:error, Ecto.Changeset{}}
"""
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
@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}!")
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!()
@spec unquote(function_name)(%unquote(schema){}) :: {:ok, %unquote(schema){}}
def unquote(function_name)(record) do
record
|> unquote(repo).delete!()
end
end
end

unless :change in exclude do
function_name = String.to_atom("change_#{resource_name}")
unless :change in exclude do
function_name = String.to_atom("change_#{resource_name}")

@doc """
Deletes an existing #{resource_name}.
@doc """
Deletes an existing #{resource_name}.

Returns the deleted #{resource_name} if successful, or raises an error if not.
Returns the deleted #{resource_name} if successful, or raises an error if not.

## Examples
## Examples

iex> delete_#{resource_name}!(#{resource_name})
%#{Macro.camelize(resource_name)}{} or raises Ecto.StaleEntryError
"""
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)
@spec unquote(function_name)(%unquote(schema){}, map()) :: map()
def unquote(function_name)(record, attrs \\ %{}) do
record
|> unquote(schema).changeset(attrs)
end
end
end

[read_repo_def, main]
end

defp read_repo_body(nil, repo, _caller), do: repo

defp read_repo_body({:&, _, _} = read_repo_fun_ast, _repo, caller) do
read_repo_fun = Macro.expand(read_repo_fun_ast, caller)

quote do
unquote(read_repo_fun).()
end
end

defp read_repo_body(_read_repo_fun, _repo, _caller) do
raise ArgumentError, ":read_repo must be a zero-arity function, e.g. &MyApp.Repo.replica/0"
end
end
Loading