diff --git a/README.md b/README.md index f7d0930..099374e 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ Ensure a value is a number is within a given range: Vex.valid? value, number: [greater_than_or_equal_to: 0, less_than: 10] ``` -This validation can be skipped for `nil` or blank values by including +This validation can be skipped for `nil` or blank values by including `allow_nil: true` or `allow_blank: true` respectively in the options. See the documentation on `Vex.Validators.Number` for details @@ -155,7 +155,7 @@ Ensure a value is a valid UUID string in a given format: Vex.valid? value, uuid: [format: :hex] ``` -This validation can be skipped for `nil` or blank values by including +This validation can be skipped for `nil` or blank values by including `allow_nil: true` or `allow_blank: true` respectively in the options. See the documentation on `Vex.Validators.Uuid` for details @@ -349,6 +349,16 @@ You can also use `valid?` directly from the Module: user |> User.valid? ``` +**Performance optimization tip:** + +You can get a significant performance boost on validation when using: +```elixir +use Vex.Struct, precompile_validator_lookup: true +``` +This comes at the cost of precomputing the validator lookup at compile time. +Runtime modification of the validator lookup from updating the Vex config (i.e., `config :vex, sources: [..]`) +will not have an effect if you are using this method. + ### In Keyword Lists In your list, just include a `:_vex` entry and use `Vex.valid?/1`: diff --git a/lib/vex.ex b/lib/vex.ex index 1257fde..9976f32 100644 --- a/lib/vex.ex +++ b/lib/vex.ex @@ -58,7 +58,7 @@ defmodule Vex do end defp result(data, attribute, name, options) do - v = validator(name) + v = Vex.Validator.Lookup.lookup(data, name) if Validator.validate?(data, options) do result = data |> extract(attribute, name) |> v.validate(data, options) diff --git a/lib/vex/extract.ex b/lib/vex/extract.ex index 7f3a61c..42b2ba5 100644 --- a/lib/vex/extract.ex +++ b/lib/vex/extract.ex @@ -38,6 +38,15 @@ defmodule Vex.Extract.Struct do def blank?(struct), do: struct |> Map.from_struct() |> map_size == 0 end + defimpl Vex.Validator.Lookup, for: __MODULE__ do + def lookup(%{__struct__: module}, name) do + case(module.__vex_validator__(name)) do + {:error, :not_enabled} -> Vex.validator(name) + {:ok, validator} -> validator + end + end + end + defimpl Vex.Extract, for: __MODULE__ do def settings(%{__struct__: module}) do module.__vex_validations__ diff --git a/lib/vex/struct.ex b/lib/vex/struct.ex index ff85fd9..4b729cf 100644 --- a/lib/vex/struct.ex +++ b/lib/vex/struct.ex @@ -1,9 +1,11 @@ defmodule Vex.Struct do @moduledoc false - defmacro __using__(_) do + defmacro __using__(opts) do quote do @vex_validations %{} + @precompile_validator_lookup unquote(Keyword.get(opts, :precompile_validator_lookup, false)) + @precompiled_validator_lookup %{} @before_compile unquote(__MODULE__) import unquote(__MODULE__) def valid?(self), do: Vex.valid?(self) @@ -14,6 +16,14 @@ defmodule Vex.Struct do quote do def __vex_validations__(), do: @vex_validations + if @precompile_validator_lookup do + def __vex_validator__(name) do + {:ok, Map.fetch!(@precompiled_validator_lookup, name)} + end + else + def __vex_validator__(_name), do: {:error, :not_enabled} + end + require Vex.Extract.Struct Vex.Extract.Struct.for_struct() end @@ -22,6 +32,27 @@ defmodule Vex.Struct do defmacro validates(name, validations \\ []) do quote do @vex_validations Map.put(@vex_validations, unquote(name), unquote(validations)) + if @precompile_validator_lookup do + @precompiled_validator_lookup Enum.reduce( + unquote(validations) |> List.wrap(), + @precompiled_validator_lookup, + fn + {validator_name, _validator_opts}, lookup -> + Map.put_new_lazy( + lookup, + validator_name, + fn -> Vex.validator(validator_name) end + ) + + fun, lookup when is_function(fun) -> + Map.put_new_lazy( + lookup, + :by, + fn -> Vex.validator(:by) end + ) + end + ) + end end end end diff --git a/lib/vex/validator/lookup.ex b/lib/vex/validator/lookup.ex new file mode 100644 index 0000000..1761c24 --- /dev/null +++ b/lib/vex/validator/lookup.ex @@ -0,0 +1,21 @@ +defprotocol Vex.Validator.Lookup do + @doc """ + Determines the lookup method of validator modules based on the datastructure at hand. + Defaults to `Vex.validator/1`. + + `Vex.Struct` types can leverage a more optimized mechanism when initialized by using + ``` + use Vex.Struct, precompile_validator_lookup: true + ``` + This performs the validator lookup once, and pushes a lookup data structure into the module at compile time. + """ + + @fallback_to_any true + def lookup(to_validate, name) +end + +defimpl Vex.Validator.Lookup, for: Any do + def lookup(_to_validate, name) do + Vex.validator(name) + end +end diff --git a/lib/vex/validator/source.ex b/lib/vex/validator/source.ex index 1e8d967..2b384de 100644 --- a/lib/vex/validator/source.ex +++ b/lib/vex/validator/source.ex @@ -35,3 +35,9 @@ defimpl Vex.Validator.Source, for: List do Keyword.get(list, name) end end + +defimpl Vex.Validator.Source, for: Map do + def lookup(map, name) do + Map.get(map, name) + end +end