In that case I guess you can compile the regular expression in compile time, then inspect it to get a string with sigil and finally convert a string to the AST.
Here is a complete script I have prepared:
Mix.install([:ecto])
defmodule MyLib.Schema do
@doc """
Generates the changeset function.
The `name` is a changeset function name. Default to `:changeset`.
The `properties` is an `Elixir` map with `atom` keys.
The `func` is an optional funtion that could be used by the developer to add other validations.
"""
defmacro changeset(name \\ :changeset, properties, func \\ nil) do
quote bind_quoted: [func: func, module: __MODULE__, name: name, properties: properties] do
pipe =
properties
|> module.from_properties(__MODULE__)
|> module.pipe_func(func)
# pipe
# |> Macro.to_string()
# |> Code.format_string!()
# =>
# struct
# |> cast(params, [:four_digit_code])
# |> validate_format(:four_digit_code, ~r/[0-9]{4}/)
struct = Macro.var(:struct, __MODULE__)
params = Macro.var(:params, __MODULE__)
def unquote(name)(unquote(struct), unquote(params)) do
unquote(pipe)
end
end
end
@doc false
def from_properties(properties, module) do
fields = Map.keys(properties)
struct = Macro.var(:struct, module)
params = Macro.var(:params, module)
cast =
quote do
cast(unquote(params), unquote(fields))
end
pipe = ast_pipe(struct, cast)
Enum.reduce(properties, pipe, &from_field_properties/2)
end
@supported_validators ~w[pattern]a
defp from_field_properties({field, properties}, acc) do
properties
|> Map.take(@supported_validators)
|> Enum.reduce(acc, fn {key, value}, acc ->
ast_pipe(acc, validator(field, key, value))
end)
end
defp validator(field, :pattern, value) do
quote do
validate_format(unquote(field), unquote(quoted_regex_sigil(value)))
end
end
defp quoted_regex_sigil(source) do
source
# creates regular expression from source
|> Regex.compile!()
# inspect returns a string with a sigil
|> inspect()
# escaped AST form
|> Code.string_to_quoted!()
end
def pipe_func(left, nil), do: left
def pipe_func(left, func) do
ast_pipe(
left,
quote do
then(unquote(func))
end
)
end
defp ast_pipe(left, right) do
quote do
unquote(left) |> unquote(right)
end
end
end
defmodule MyApp.Schema do
use Ecto.Schema
import Ecto.Changeset
import MyLib.Schema
embedded_schema do
field(:four_digit_code, :string)
end
changeset(%{four_digit_code: %{pattern: "[0-9]{4}"}})
end
defmodule Example do
def sample do
MyApp.Schema.changeset(%MyApp.Schema{}, %{four_digit_code: "0007"})
# => %Ecto.Changeset{valid?: true}
end
end
You should be able to easily adapt the example code to your needs.






















