From b299638a1fb2ef0ba377b7f7640b1d5b36cd868a Mon Sep 17 00:00:00 2001 From: Jan Fornoff Date: Wed, 20 Mar 2019 13:01:42 +0100 Subject: [PATCH 1/3] Validator lookup precompilation This adds a `precompile_validator_lookup` flag (defaulting to false) to `use Vex.Struct`. This changes behavior as follows: * a module attribute (@precompiled_validator_lookup) is initialized for the struct that holds a lookup data structure * calls to `validates :attribute, ` proactively look up the validator AT COMPILE TIME and add them to the lookup data structure * At runtime, the expensive validator lookup is then skipped in favor of just performing a Map fetch from the precompiled validator lookup datastructure. --- README.md | 14 ++++++++++++-- lib/vex.ex | 2 +- lib/vex/extract.ex | 9 +++++++++ lib/vex/struct.ex | 25 ++++++++++++++++++++++++- lib/vex/validator/lookup.ex | 21 +++++++++++++++++++++ lib/vex/validator/source.ex | 6 ++++++ 6 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 lib/vex/validator/lookup.ex 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..1a47686 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,19 @@ 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), + @precompiled_validator_lookup, + fn {validator_name, _validator_opts}, lookup -> + Map.put_new_lazy( + lookup, + validator_name, + fn -> Vex.validator(validator_name) 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 From 13ee888ec4cde946df241e86f4192dc7acc8f352 Mon Sep 17 00:00:00 2001 From: Jan Fornoff Date: Thu, 21 Mar 2019 12:51:05 +0100 Subject: [PATCH 2/3] Add exception for function validatiors --- lib/vex/struct.ex | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/vex/struct.ex b/lib/vex/struct.ex index 1a47686..6d783e3 100644 --- a/lib/vex/struct.ex +++ b/lib/vex/struct.ex @@ -36,12 +36,18 @@ defmodule Vex.Struct do @precompiled_validator_lookup Enum.reduce( unquote(validations), @precompiled_validator_lookup, - fn {validator_name, _validator_opts}, lookup -> - Map.put_new_lazy( - lookup, - validator_name, - fn -> Vex.validator(validator_name) end - ) + fn + {validator_name, _validator_opts}, lookup -> + Map.put_new_lazy( + lookup, + validator_name, + fn -> Vex.validator(validator_name) end + ) + + # Functions are directly stored in @vex_validations, no need + # to look up and cache a validator. + fun, lookup when is_function(fun) -> + lookup end ) end From 486c255e5cd4f385f14c04f96d438b1e805d5f70 Mon Sep 17 00:00:00 2001 From: Jan Fornoff Date: Thu, 21 Mar 2019 13:27:58 +0100 Subject: [PATCH 3/3] Vex.Struct: Fix precompilation of plain function validators It is allowed to state `validates :attribute, &myfunction/1`. This special case is expanded to `validates :attribute, by: &myfunction/1` during the validation at runtime. Therefore, we need to cache the `:by` validator if we encounter a function. --- lib/vex/struct.ex | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/vex/struct.ex b/lib/vex/struct.ex index 6d783e3..4b729cf 100644 --- a/lib/vex/struct.ex +++ b/lib/vex/struct.ex @@ -34,7 +34,7 @@ defmodule Vex.Struct do @vex_validations Map.put(@vex_validations, unquote(name), unquote(validations)) if @precompile_validator_lookup do @precompiled_validator_lookup Enum.reduce( - unquote(validations), + unquote(validations) |> List.wrap(), @precompiled_validator_lookup, fn {validator_name, _validator_opts}, lookup -> @@ -44,10 +44,12 @@ defmodule Vex.Struct do fn -> Vex.validator(validator_name) end ) - # Functions are directly stored in @vex_validations, no need - # to look up and cache a validator. fun, lookup when is_function(fun) -> - lookup + Map.put_new_lazy( + lookup, + :by, + fn -> Vex.validator(:by) end + ) end ) end