diff --git a/lib/contexted/crud.ex b/lib/contexted/crud.ex index 0fed69e..65df500 100644 --- a/lib/contexted/crud.ex +++ b/lib/contexted/crud.ex @@ -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 @@ -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) @@ -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 - if :list not 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 - if :get not 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 + if :list not 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. + if :get not 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 - if :create not in exclude do - function_name = String.to_atom("create_#{resource_name}") + if :create not 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 - if :update not in exclude do - function_name = String.to_atom("update_#{resource_name}") + if :update not 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 - if :delete not in exclude do - function_name = String.to_atom("delete_#{resource_name}") + if :delete not 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 - if :change not in exclude do - function_name = String.to_atom("change_#{resource_name}") + if :change not 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