How to load direct macro instead of input it into a function to get parameters

Hello, I have a CRUD macro that I use everywhere I need to create a CRUD query, but I am always forced to load it in the middle of a function to be able to get parameters like this:

def create(attrs, allowed_fields) do
  crud_add(attrs, allowed_fields)
end

is there a way to load it like this

defmodule User do
  crud_add(attrs, allowed_fields)
  ....
end

but I have to use it like this: User.crud_add(%{name: "test", [:name])

My CRUD macro:

Thanks

You can define the function in a macro so if I’ve understood correctly, a small change to your macros will suffice (not tested):

defmacro crud_add(allowed_fields)  do
  quote do
    def create(attrs) do
      module_selected = Keyword.get(@interface_module, :module)
      error_atom =  Keyword.get(@interface_module, :error_atom)
      repo =  Keyword.get(@interface_module, :repo)

      add = module_selected.__struct__
      |> module_selected.changeset(Map.take(attrs, unquote(allowed_fields)))
      |> repo.insert()
      case add do
        {:ok, data}             -> {:ok, :add, error_atom, data}
        {:error, changeset}     -> {:error, :add, error_atom, changeset}
      end
    end
  end
end

The in a module:

defmodule MyModule do
  use MishkaDatabase.CRUD,
                        module: YOURschemaMODULE,
                        error_atom: :your_error_tag,
                        repo: Your.Repo

  crud_add(attrs)
end

The idea is that the allowed_attrs is “baked in” to the generated function. But attrs is supplied at runtime and therefore doesn’t look like it should be a parameter to the macro.

3 Likes

In general, I would decompose this further in the interests of clarity and better runtime stack traces. Macros should delegate to functions where possible. So:

defmodule MishkaDatabase.CRUD do
  defmacro crud_add(allowed_fields)  do
    quote do
      def create(attrs) do
        module_selected = Keyword.get(@interface_module, :module)
        error_atom =  Keyword.get(@interface_module, :error_atom)
        repo =  Keyword.get(@interface_module, :repo)
      
        MishkaDatabase.CRUD.do_create(module_selected, repo, error_atom, attrs, unquote(allowed_fields))
      end
    end
  end

  def do_create(module_selected, repo, error_atom, attrs, allowed_fields) do
    add = module_selected.__struct__
    |> module_selected.changeset(Map.take(attrs, allowed_fields))
    |> repo.insert()
    case add do
      {:ok, data}             -> {:ok, :add, error_atom, data}
      {:error, changeset}     -> {:error, :add, error_atom, changeset}
    end
  end
end
2 Likes

Hello, @kip
First, thank you for improving my code, and thank you again for your suggestion :heart_eyes:

I wanted to separate map dropper out of my macro, but I had to decide to create a full tools or forced the developers to drop parameters of a map which do not want themselves, in my test it works :face_with_head_bandage: as I understand your suggestion!!

I just have one error, I add this:

use MishkaDatabase.CRUD

but when I call in tereminal it shows me you must require MishkaDatabase.CRUD before invoking the macro MishkaDatabase.CRUD.crud_add/1

In your __using__/1 macro, which you didn’t show, you need to have require MishkaDatabase.CRUD added.

Something like:

defmodule MishkaDatabase.CRUD do
  def __using__(_opts) do
    module = __MODULE__
    
    quote do
      import unquote(module)
      # Other stuff here ....
    end
  end

  ...
end
2 Likes

This is my bad, I should stop server and compile again. Thanks