Skip to content

Instantly share code, notes, and snippets.

@hl
Last active August 28, 2021 04:55
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save hl/c535b594b24e858d0e5ccfff29280c7a to your computer and use it in GitHub Desktop.
Save hl/c535b594b24e858d0e5ccfff29280c7a to your computer and use it in GitHub Desktop.
defmodule Context do
@moduledoc false
defmacro __using__(opts) do
repo = Keyword.fetch!(opts, :repo)
quote do
import Context, only: [context: 1, context: 2]
Module.put_attribute(__MODULE__, :__repo__, unquote(repo))
end
end
@doc false
defmacro context(schema, opts \\ []) do
name = Keyword.get(opts, :name, parse_name(schema, __CALLER__))
funs = build_functions(name, opts)
Enum.reduce(funs, [], fn {fun_name, arity}, acc ->
fun =
fun_name
|> Atom.to_string()
|> String.replace("_#{name}", "")
|> String.to_existing_atom()
[Kernel.apply(__MODULE__, :gen_fun, [{fun, arity}, name, schema]) | acc]
end)
end
@spec parse_name(atom, any) :: String.t()
defp parse_name(schema, caller) do
schema
|> Macro.expand_once(caller)
|> Atom.to_string()
|> String.split(".")
|> List.last()
|> Macro.underscore()
end
@spec build_functions(String.t(), keyword) :: [{atom, non_neg_integer}]
defp build_functions(name, opts) do
only = Keyword.get(opts, :only, default_functions(name))
except = Keyword.get(opts, :except, [])
only -- except
end
@spec default_functions(String.t()) :: [{atom, non_neg_integer}]
defp default_functions(name) do
[
{:"get_#{name}", 1},
{:"get_#{name}!", 1},
{:"get_#{name}_by", 1},
{:"get_#{name}_by!", 1},
{:"find_#{name}", 1},
{:"find_#{name}_by", 1},
{:"create_#{name}", 1},
{:"update_#{name}", 2},
{:"delete_#{name}", 1},
{:"create_#{name}!", 1},
{:"update_#{name}!", 2},
{:"delete_#{name}!", 1},
{:"change_#{name}", 2}
]
end
@doc false
@spec gen_fun(tuple, String.t(), atom) :: tuple
def gen_fun(fun_and_arity, name, schema)
def gen_fun({:get, 1}, name, schema) do
quote do
@doc """
Get a #{unquote(schema)} by ID
## Parameters
- id: Valid ID
## Examples
iex> #{__MODULE__}.get_#{unquote(name)}(1)
%#{unquote(schema)}{}
"""
@spec unquote(:"get_#{name}")(String.t()) :: unquote(schema).t() | nil
def unquote(:"get_#{name}")(id) when is_binary(id) do
@__repo__.get(unquote(schema), id)
end
defoverridable [{unquote(:"get_#{name}"), 1}]
end
end
def gen_fun({:get!, 1}, name, schema) do
quote do
@doc """
Find a #{unquote(schema)} by ID
## Parameters
- id: Valid ID
## Examples
iex> #{__MODULE__}.get_#{unquote(name)}!(1)
%#{unquote(schema)}{}
"""
@spec unquote(:"get_#{name}!")(String.t()) :: unquote(schema).t()
def unquote(:"get_#{name}!")(id) when is_binary(id) do
@__repo__.get!(unquote(schema), id)
end
defoverridable [{unquote(:"get_#{name}!"), 1}]
end
end
def gen_fun({:get_by, 1}, name, schema) do
quote do
@doc """
Find a #{unquote(schema)} by clauses
## Parameters
- clauses: keyword list
## Examples
iex> #{__MODULE__}.get_#{unquote(name)}_by(title: "title")
%#{unquote(schema)}{}
"""
@spec unquote(:"get_#{name}_by")(keyword) :: unquote(schema).t() | nil
def unquote(:"get_#{name}_by")(clauses) do
@__repo__.get_by(unquote(schema), clauses)
end
defoverridable [{unquote(:"get_#{name}_by"), 1}]
end
end
def gen_fun({:get_by!, 1}, name, schema) do
quote do
@doc """
Find a #{unquote(schema)} by clauses
## Parameters
- clauses: keyword list
## Examples
iex> #{__MODULE__}.get_#{unquote(name)}_by!(title: "title")
%#{unquote(schema)}{}
"""
@spec unquote(:"get_#{name}_by!")(keyword) :: unquote(schema).t()
def unquote(:"get_#{name}_by!")(clauses) do
@__repo__.get_by!(unquote(schema), clauses)
end
defoverridable [{unquote(:"get_#{name}_by!"), 1}]
end
end
def gen_fun({:find, 1}, name, schema) do
quote do
@doc """
Find a #{unquote(schema)} by ID
## Parameters
- id: Valid ID
## Examples
iex> #{__MODULE__}.find_#{unquote(name)}(1)
{:ok, %#{unquote(schema)}{}}
"""
@spec unquote(:"find_#{name}")(String.t()) ::
{:ok, unquote(schema).t()} | {:error, {unquote(schema), :not_found}}
def unquote(:"find_#{name}")(id) when is_binary(id) do
case @__repo__.get(unquote(schema), id) do
nil -> {:error, {unquote(schema), :not_found}}
term -> {:ok, term}
end
end
defoverridable [{unquote(:"find_#{name}"), 1}]
end
end
def gen_fun({:find_by, 1}, name, schema) do
quote do
@doc """
Find a #{unquote(schema)} by clauses
## Parameters
- clauses: keyword list
## Examples
iex> #{__MODULE__}.find_#{unquote(name)}_by(title: "title")
{:ok, %#{unquote(schema)}{}}
"""
@spec unquote(:"find_#{name}_by")(keyword) ::
{:ok, unquote(schema).t()} | {:error, {unquote(schema), :not_found}}
def unquote(:"find_#{name}_by")(clauses) do
case @__repo__.get_by(unquote(schema), clauses) do
nil -> {:error, {unquote(schema), :not_found}}
term -> {:ok, term}
end
end
defoverridable [{unquote(:"find_#{name}_by"), 1}]
end
end
def gen_fun({:create, 1}, name, schema) do
quote do
@doc """
Create a new #{unquote(schema)}
## Parameters
- args: map
## Examples
iex> #{__MODULE__}.create_#{unquote(name)}(%{})
{:ok, %#{unquote(schema)}{}}
"""
@spec unquote(:"create_#{name}")(map) ::
{:ok, unquote(schema).t()} | {:error, Ecto.Changeset.t()}
def unquote(:"create_#{name}")(args) when is_map(args) do
changeset = unquote(schema).changeset(%unquote(schema){}, args)
@__repo__.insert(changeset)
end
defoverridable [{unquote(:"create_#{name}"), 1}]
end
end
def gen_fun({:create!, 1}, name, schema) do
quote do
@doc """
Create a new #{unquote(schema)}
## Parameters
- args: map
## Examples
iex> #{__MODULE__}.create_#{unquote(name)}(%{})
%#{unquote(schema)}{}
"""
@spec unquote(:"create_#{name}!")(map) :: unquote(schema).t()
def unquote(:"create_#{name}!")(args) when is_map(args) do
changeset = unquote(schema).changeset(%unquote(schema){}, args)
@__repo__.insert!(changeset)
end
defoverridable [{unquote(:"create_#{name}!"), 1}]
end
end
def gen_fun({:update, 2}, name, schema) do
quote do
@doc """
Update an existing #{unquote(schema)}
## Parameters
- struct: %#{unquote(schema)}{}
- args: map
## Examples
iex> #{__MODULE__}.update_#{unquote(name)}!(%#{unquote(schema)}{}, %{})
{:ok, %#{unquote(schema)}{}}
"""
@spec unquote(:"update_#{name}")(unquote(schema).t(), map) ::
{:ok, unquote(schema).t()} | {:error, Ecto.Changeset.t()}
def unquote(:"update_#{name}")(%unquote(schema){} = struct, args) when is_map(args) do
struct =
case Keyword.has_key?(unquote(schema).__info__(:functions), :preload) do
true -> @__repo__.preload(struct, apply(unquote(schema), :preload, []))
false -> struct
end
changeset = unquote(schema).changeset(struct, args)
@__repo__.update(changeset)
end
defoverridable [{unquote(:"update_#{name}"), 2}]
end
end
def gen_fun({:update!, 2}, name, schema) do
quote do
@doc """
Update an existing #{unquote(schema)}
## Parameters
- struct: %#{unquote(schema)}{}
- args: map
## Examples
iex> #{__MODULE__}.update_#{unquote(name)}!(%#{unquote(schema)}{}, %{})
%#{unquote(schema)}{}
"""
@spec unquote(:"update_#{name}!")(unquote(schema).t(), map) :: unquote(schema).t()
def unquote(:"update_#{name}!")(%unquote(schema){} = struct, args) when is_map(args) do
struct =
case Keyword.has_key?(unquote(schema).__info__(:functions), :preload) do
true -> @__repo__.preload(struct, apply(unquote(schema), :preload, []))
false -> struct
end
changeset = unquote(schema).changeset(struct, args)
@__repo__.update!(changeset)
end
defoverridable [{unquote(:"update_#{name}!"), 2}]
end
end
def gen_fun({:delete, 1}, name, schema) do
quote do
@doc """
Delete an existing #{unquote(schema)}
## Parameters
- struct: %#{unquote(schema)}{}
## Examples
iex> #{__MODULE__}.delete_#{unquote(name)}(%#{unquote(name)}{})
{:ok, %#{unquote(schema)}{}}
"""
@spec unquote(:"delete_#{name}")(unquote(schema).t()) ::
{:ok, unquote(schema).t()} | {:error, Ecto.Changeset.t()}
def unquote(:"delete_#{name}")(%unquote(schema){} = struct) do
@__repo__.delete(struct)
end
defoverridable [{unquote(:"delete_#{name}"), 1}]
end
end
def gen_fun({:delete!, 1}, name, schema) do
quote do
@doc """
Delete an existing #{unquote(schema)}
## Parameters
- struct: %#{unquote(schema)}{}
## Examples
iex> #{__MODULE__}.delete_#{unquote(name)}(%#{unquote(name)}{})
{:ok, %#{unquote(schema)}{}}
"""
@spec unquote(:"delete_#{name}!")(unquote(schema).t()) :: unquote(schema).t()
def unquote(:"delete_#{name}!")(%unquote(schema){} = struct) do
@__repo__.delete(struct)
end
defoverridable [{unquote(:"delete_#{name}!"), 1}]
end
end
def gen_fun({:change, 2}, name, schema) do
quote do
@doc """
Returns an #{unquote(schema)} changeset
## Examples
iex> #{__MODULE__}.change_#{unquote(name)}(%#{__MODULE__}{}, %{})
%Ecto.Changeset{}
"""
@spec unquote(:"change_#{name}")(map) :: Ecto.Changeset.t()
def unquote(:"change_#{name}")(%unquote(schema){} = schema, args \\ %{}) do
unquote(schema).changeset(schema, args)
end
defoverridable [{unquote(:"change_#{name}"), 2}]
end
end
end
@kidbombay
Copy link

Is the change function correct? I couldn't get it to work and had to modify to

      def unquote(:"change_#{name}")(%unquote(schema){} = schema, args \\ %{}) do
        unquote(schema).changeset(schema, args)
      end

@hl
Copy link
Author

hl commented Jun 24, 2019

Is the change function correct? I couldn't get it to work and had to modify to

      def unquote(:"change_#{name}")(%unquote(schema){} = schema, args \\ %{}) do
        unquote(schema).changeset(schema, args)
      end

The function is correct for my use-case but I do see that yours is more like Phoenix generates it.

@hl
Copy link
Author

hl commented Jun 24, 2019

I've updated the gist to reflect the change function changes

@kidbombay
Copy link

defoverridable [{unquote(:"change_#{name}"), 1}]

should defoverridable [{unquote(:"change_#{name}"), 2}] no?

@hl
Copy link
Author

hl commented Jun 24, 2019

defoverridable [{unquote(:"change_#{name}"), 1}]

should defoverridable [{unquote(:"change_#{name}"), 2}] no?

Yes, fixed and thanks!

@kidbombay
Copy link

Thanks this is very handy!

@programisti
Copy link

programisti commented Jun 25, 2019

this is for list_posts
add this in mix.exs

      {:inflex, "~> 2.0.0"},

put this after line 50

    {:"list_#{name}", 0},

put this after line 66

  def gen_fun({:list, 0}, name, schema) do
    quote do
      @doc """
      Get all #{unquote(schema)}

      ## Examples

        iex> #{__MODULE__}.list_#{unquote(Inflex.pluralize(name))}(1)
        [%#{unquote(schema)}{}]
      """
      # @spec unquote(:"list_#{Inflex.pluralize(name)}") :: [unquote(schema).t()]
      def unquote(:"list_#{Inflex.pluralize(name)}")() do
        @__repo__.all(unquote(schema))
      end

      defoverridable [{unquote(:"list_#{Inflex.pluralize(name)}"), 0}]
    end
  end

Only thing Im missing is valid @SPEC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment