Macro before compile/1 is undefined or private, how to use macros in other files

I have no clue why I exactly get this error or more like how I exactly use the typedex_type macro in my example.

here is my example code:

defmodule UserSchema do
  use Tipx

  @typedex_type [
    name: :string,
    age: :integer,
    email: :string
  ]
end

defmodule UserProcessor do
  def process_user_data(data) do
    validated_data = validate_user(data)
    # Process the validated data
    IO.inspect(validated_data, label: "Validated User Data")
  end

  defp validate_user(data) do
    validate_type(data, UserSchema)
  end
end

user_data = %{
  name: "John Doe",
  age: 30,
  email: "john@example.com"
}

UserProcessor.process_user_data(user_data)

error stack:

== Compilation error in file lib/user_schema.ex ==
** (UndefinedFunctionError) function Tipx.__before_compile__/1 is undefined or private
    Tipx.__before_compile__(#Macro.Env<aliases: [], context: nil, context_modules: [UserSchema], file: "c:/tipx/lib/user_schema.ex", function: nil, functions: [{Kernel, [!=: 2, !==: 2, *: 2, **: 2, +: 1, +: 2, ++: 2, -: 1, -: 2, --: 2, /: 2, <: 2, <=: 2, ==: 2, ===: 2, =~: 2, >: 2, >=: 2, abs: 1, apply: 2, apply: 3, binary_part: 3, binary_slice: 2, binary_slice: 3, bit_size: 1, byte_size: 1, ceil: 1, div: 2, elem: 2, exit: 1, floor: 1, function_exported?: 3, get_and_update_in: 3, get_in: 2, hd: 1, inspect: 1, inspect: 2, is_atom: 1, is_binary: 1, is_bitstring: 1, is_boolean: 1, ...]}], lexical_tracker: #PID<0.117.0>, line: 1, macro_aliases: [], macros: [{Kernel, [!: 1, &&: 2, ..: 0, ..: 2, ..//: 3, <>: 2, @: 1, alias!: 1, and: 2, binding: 0, binding: 1, dbg: 0, dbg: 1, dbg: 2, def: 1, def: 2, defdelegate: 2, defexception: 1, defguard: 1, defguardp: 1, defimpl: 2, defimpl: 3, defmacro: 1, defmacro: 2, defmacrop: 1, defmacrop: 2, defmodule: 2, defoverridable: 1, defp: 1, defp: 2, defprotocol: 2, defstruct: 1, destructure: 2, get_and_update_in: 2, if: 2, in: 2, is_exception: 1, ...]}], module: UserSchema, requires: [Application, Kernel, Kernel.Typespec, Tipx], ...>)
    (stdlib 5.0.2) lists.erl:1594: :lists.foldl/3
    lib/user_schema.ex:1: (file)

whereas here is my main module code:

defmodule Tipx do
  defmacro typedex_type(schema) when is_list(schema) do
    quote do
      @typedex_annotation unquote(schema)
    end
  end

  defmacro __using__(_) do
    quote do
      @before_compile unquote(__MODULE__)
    end
  end

  defmacro validate_type(data, schema) do
    quote do
      case Tipx.validate(unquote(data), unquote(schema)) do
        {:ok, validated_data} -> validated_data
        {:error, errors} -> raise RuntimeError, "Type validation error: #{inspect(errors)}"
      end
    end
  end

  defmacro generate(schema) do
    quote do
      Tipx.generate(unquote(schema))
    end
  end

  def validate(data, schema) when is_map(data) and is_map(schema),
    do: validate_map(data, schema, [])

  defp validate_map(data, schema, path) when is_list(schema) do
    do_validate_map(data, schema, path)
  end

  defp validate_map(_, _, _), do: {:error, [{"root", "invalid schema"}]}

  defp do_validate_map(data, schema, path) do
    Enum.reduce(schema, {:ok, %{}}, fn {key, type}, {:ok, acc} ->
      case Map.get(data, key) do
        nil ->
          {:error, [{path ++ [key], "missing"}]}

        value ->
          case Tipx.validate(value, type) do
            {:ok, validated_value} -> {:ok, Map.put(acc, key, validated_value)}
            {:error, errors} -> {:error, merge_errors(errors, path ++ [key])}
          end
      end
    end)
  end

  defp merge_errors(errors, path) do
    Enum.map(errors, fn {sub_path, error} -> {path ++ sub_path, error} end)
  end
end

@before_compile just calls __before_compile__ on the module you pass it and you must define this macro yourself. It’s useful if you want to hook into the module’s compilation from another module. As you have it, you’re actually calling it on the Tipx module itself—unquote(__MODULE__) in your case will expand to Tipx which means it will be looking for a __before_compile__ in this module. That’s not really useful though and I think you just want import unquote(__MODULE__) here? If that’s the case you can avoid the __using__ altogether and just do import Tipx in UserSchema.

1 Like

Yea I tried that but then I got some problems with the arguments (function clause didnt match)

$ iex -S mix
Compiling 2 files (.ex)

== Compilation error in file lib/user_schema.ex ==
** (FunctionClauseError) no function clause matching in Tipx.validate/2    

    The following arguments were given to Tipx.validate/2:

        # 1
        %{name: "John Doe", age: 30, email: "john@example.com"}

        # 2
        UserSchema

    lib/tipx.ex:29: Tipx.validate/2
    lib/user_schema.ex:20: UserProcessor.validate_user/1
    lib/user_schema.ex:15: UserProcessor.process_user_data/1
    lib/user_schema.ex:30: (file)

That’s because your validate/2 function has an is_map(schema) guard and you are passing it an atom: UserSchema. You want to pass %UserSchema{} for is_map to work or change is_map to is_atom.

Im missing something as it seems like because when I try:

defp validate_user(data) do
    validate_type(data, %UserSchema{})
end

I receive

error: UserSchema.__struct__/1 is undefined, cannot expand struct UserSchema. Make sure the struct name is correct. If the struct name exists and is correct but it still cannot be found, you likely have cyclic module usage in your code
  lib/user_schema.ex:20: UserProcessor.validate_user/1

with is_map I receive the custom error I defined below it

 def validate(data, schema) when is_map(data) and is_atom(schema),
    do: validate_map(data, schema, [])
== Compilation error in file lib/user_schema.ex ==
** (RuntimeError) Type validation error: [{"root", "invalid schema"}]
    lib/user_schema.ex:20: UserProcessor.validate_user/1
    lib/user_schema.ex:15: UserProcessor.process_user_data/1
    lib/user_schema.ex:30: (file)

Because you haven’t defined a struct for your UserSchema module. You need to do:

defmodule UserSchema do
  defstruct [:name, :age, :email]
end

for %UserSchema{} to work. I assume you might want your library to metaprogram this in, though? Unless this is an extrension for Ecto, in which case you’ll need a mock for Ecto.Schema if you want to test it.

Its just J4F, playing around a little bit. But actually I tried that with a struct above but it gave me the same error as at the beginning with the function clause match (I post it here:)

== Compilation error in file lib/user_schema.ex ==
** (FunctionClauseError) no function clause matching in Tipx.validate/2    

    The following arguments were given to Tipx.validate/2:

        # 1
        %{name: "John Doe", age: 30, email: "john@example.com"}

        # 2
        %UserSchema{name: nil, age: nil, email: nil}

    lib/tipx.ex:29: Tipx.validate/2
    lib/user_schema.ex:22: UserProcessor.validate_user/1
    lib/user_schema.ex:17: UserProcessor.process_user_data/1
    lib/user_schema.ex:32: (file)

corresponding code for that was

defmodule UserSchema do
  import Tipx

  defstruct [:name, :age, :email]

  typedex_type [
    name: :string,
    age: :integer,
    email: :string
  ]
end

That’s odd… did you change the definition of validate/2 since? Those both look like maps to me.

For that error it looks like this:

def validate(data, schema) when is_map(data) and is_atom(schema),
    do: validate_map(data, schema, [])
``´
if I change both to is_map, then I get

== Compilation error in file lib/user_schema.ex ==
** (RuntimeError) Type validation error: [{“root”, “invalid schema”}]
lib/user_schema.ex:22: UserProcessor.validate_user/1
lib/user_schema.ex:17: UserProcessor.process_user_data/1
lib/user_schema.ex:32: (file)

:/

Ha, ya %User{} is the map (struct) and User is the atom.

Now your error is being thrown by your business logic so I’m gonna leave that as an exercise for you to figure out! I have to get back to work—I’ve been procrastinating like crazy this morning as my task is to learn a JS lib I’m not too jazzed about. Though I’m pretty sure you want to revert to where you first where when you asked the question and just change that one is_map to is_atom (ie, pass UserSchema).