├── .travis.yml ├── test ├── predicates_test.exs ├── notation │ ├── subtype │ │ ├── direct_mod_access_test.exs │ │ ├── direct_fun_test.exs │ │ ├── module_fun_test.exs │ │ └── direct_pred_test.exs │ ├── refine_test.exs │ ├── struct_test.exs │ └── typespec_test.exs ├── types │ ├── optional_test.exs │ ├── struct_test.exs │ ├── list_test.exs │ ├── map_test.exs │ ├── one_of_test.exs │ ├── one_struct_of_test.exs │ └── basic_types_test.exs ├── test_helper.exs ├── env_predicate_config_test.exs ├── error_test.exs ├── predicate_library_test.exs └── exchema_test.exs ├── lib ├── exchema │ ├── types │ │ ├── float.ex │ │ ├── integer.ex │ │ ├── number.ex │ │ ├── atom.ex │ │ ├── boolean.ex │ │ ├── date.ex │ │ ├── time.ex │ │ ├── string.ex │ │ ├── datetime.ex │ │ ├── naive_datetime.ex │ │ ├── tuple.ex │ │ ├── list.ex │ │ ├── optional.ex │ │ ├── map.ex │ │ ├── one_struct_of.ex │ │ ├── one_of.ex │ │ └── struct.ex │ ├── notation │ │ ├── struct.ex │ │ ├── subtype.ex │ │ └── typespec.ex │ ├── type.ex │ ├── macros │ │ └── numeric_type.ex │ ├── notation.ex │ ├── errors.ex │ └── predicates.ex └── exchema.ex ├── guides ├── predicates.md ├── types.md ├── introduction.md └── checking_types.md ├── .gitignore ├── LICENSE ├── mix.lock ├── CHANGELOG.md ├── config ├── config.exs └── .credo.exs ├── mix.exs └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | 3 | elixir: 4 | - 1.6.6 5 | - 1.7.2 6 | 7 | otp_release: 8 | - 20.3 9 | - 21.0 -------------------------------------------------------------------------------- /test/predicates_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exchema.PredicatesTest do 2 | use ExUnit.Case 3 | doctest Exchema.Predicates 4 | end 5 | -------------------------------------------------------------------------------- /lib/exchema/types/float.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.Float do 2 | @moduledoc "Represents a float" 3 | use Exchema.Macros.NumericType, type: :float 4 | end 5 | -------------------------------------------------------------------------------- /lib/exchema/types/integer.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.Integer do 2 | @moduledoc "Represents an integer" 3 | use Exchema.Macros.NumericType, type: :integer 4 | end 5 | -------------------------------------------------------------------------------- /lib/exchema/types/number.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.Number do 2 | @moduledoc "Represents a number, either a float or an integer" 3 | use Exchema.Macros.NumericType, type: :number 4 | end 5 | -------------------------------------------------------------------------------- /lib/exchema/types/atom.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.Atom do 2 | @moduledoc "Represents an atom" 3 | alias Exchema.Predicates 4 | 5 | @doc false 6 | def __type__({}) do 7 | {:ref, :any, [{{Predicates, :is}, :atom}]} 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/exchema/types/boolean.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.Boolean do 2 | @moduledoc "Represents a boolean" 3 | alias Exchema.Predicates 4 | 5 | @doc false 6 | def __type__({}) do 7 | {:ref, :any, [{{Predicates, :is}, :boolean}]} 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/exchema/types/date.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.Date do 2 | @moduledoc "Represents Date struct" 3 | alias Exchema.Predicates 4 | 5 | @doc false 6 | def __type__({}) do 7 | {:ref, :any, [{{Predicates, :is_struct}, Date}]} 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/exchema/types/time.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.Time do 2 | @moduledoc "Represents Time struct" 3 | alias Exchema.Predicates 4 | 5 | @doc false 6 | def __type__({}) do 7 | {:ref, :any, [{{Predicates, :is_struct}, Time}]} 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/exchema/types/string.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.String do 2 | @moduledoc "Represents any string/binary" 3 | alias Exchema.Predicates 4 | 5 | @doc false 6 | def __type__({}) do 7 | {:ref, :any, [{{Predicates, :is}, :binary}]} 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/exchema/types/datetime.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.DateTime do 2 | @moduledoc "Represents DateTime struct" 3 | alias Exchema.Predicates 4 | 5 | @doc false 6 | def __type__({}) do 7 | {:ref, :any, [{{Predicates, :is_struct}, DateTime}]} 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/exchema/types/naive_datetime.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.NaiveDateTime do 2 | @moduledoc "Represents NaiveDateTime struct" 3 | alias Exchema.Predicates 4 | 5 | @doc false 6 | def __type__({}) do 7 | {:ref, :any, [{{Predicates, :is_struct}, NaiveDateTime}]} 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/exchema/types/tuple.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.Tuple do 2 | @moduledoc "Represents a tuple of any size" 3 | alias Exchema.Predicates 4 | 5 | @type t :: tuple() 6 | 7 | @doc false 8 | def __type__({}) do 9 | {:ref, :any, [{{Predicates, :is}, :tuple}]} 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/exchema/notation/struct.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Notation.Struct do 2 | @moduledoc false 3 | def __struct(fields) do 4 | quote do 5 | defstruct unquote(__field_keys(fields)) 6 | Exchema.Notation.subtype({Exchema.Types.Struct, {__MODULE__, unquote(fields)}}, []) 7 | end 8 | end 9 | 10 | defp __field_keys(fields) do 11 | fields 12 | |> Enum.map(fn {k,_} -> k end) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /guides/predicates.md: -------------------------------------------------------------------------------- 1 | # Predicates 2 | 3 | Predicate is how we can refine types. It narrows the possible input 4 | values down to those your type support. Let's see how `DateTime` type 5 | is implemented: 6 | 7 | ``` elixir 8 | defmodule Exchema.Types.DateTime do 9 | alias Exchema.Predicates 10 | 11 | def __type__({}) do 12 | {:ref, :any, [{{Predicates, :is_struct}, DateTime}]} 13 | end 14 | end 15 | ``` 16 | 17 | -------------------------------------------------------------------------------- /test/notation/subtype/direct_mod_access_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Notation.Subtype.DirectModAccessTest do 2 | use ExUnit.Case 3 | import Exchema.Notation 4 | alias Exchema.Types, as: T 5 | 6 | subtype Type1, T.Integer, [inclusion: (1..10)] do 7 | def foo, do: :foo 8 | end 9 | 10 | subtype Type2, T.Integer, &(&1 > 2) do 11 | def foo, do: :foo 12 | end 13 | 14 | test "it executes in the module context" do 15 | assert :foo = Type1.foo 16 | assert :foo = Type2.foo 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/notation/subtype/direct_fun_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Notation.Subtype.DirectFunTest do 2 | use ExUnit.Case 3 | import Exchema.Notation 4 | alias Exchema.Types, as: T 5 | 6 | subtype Type, T.Integer, &(&1 < 5) 7 | 8 | test "it generates an exchema type" do 9 | assert :erlang.function_exported(Type, :__type__, 1) 10 | end 11 | 12 | test "it executes the function to check the type" do 13 | assert Exchema.is?(1, Type) 14 | refute Exchema.is?(:a, Type) 15 | refute Exchema.is?(10, Type) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/notation/subtype/module_fun_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Notation.Subtype.ModuleFunTest do 2 | use ExUnit.Case 3 | 4 | defmodule Type do 5 | import Exchema.Notation 6 | alias Exchema.Types, as: T 7 | subtype T.Integer, &(&1 < 5) 8 | end 9 | 10 | test "it generates an exchema type" do 11 | assert :erlang.function_exported(Type, :__type__, 1) 12 | end 13 | 14 | test "it executes the function to check the type" do 15 | assert Exchema.is?(1, Type) 16 | refute Exchema.is?(:a, Type) 17 | refute Exchema.is?(10, Type) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/types/optional_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Types.OptionalTest do 2 | use ExUnit.Case 3 | 4 | alias Exchema.Types, as: T 5 | import Exchema, only: [is?: 2, errors: 2] 6 | 7 | test "but can have a specific inner type" do 8 | refute is?("something", {T.Optional, T.Integer}) 9 | end 10 | 11 | test "the error messages are propagated" do 12 | assert [{_, _, [{_, _, :not_an_integer}]}] = errors("1", {T.Optional, T.Integer}) 13 | end 14 | 15 | test "but still can be nil even with specific inner type" do 16 | assert is?(nil, {T.Optional, T.Integer}) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Bernardo Amorim 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /test/notation/refine_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Notation.RefineTest do 2 | use ExUnit.Case 3 | import Exchema.Notation 4 | alias Exchema.Types, as: T 5 | 6 | subtype Type, T.Integer, [] do 7 | refine [inclusion: (0..100)] 8 | refine &(&1 < 5) 9 | end 10 | 11 | test "it generates an exchema type" do 12 | assert :erlang.function_exported(Type, :__type__, 1) 13 | end 14 | 15 | test "it executes the function to check the type" do 16 | refute Exchema.is?(:a, Type) 17 | refute Exchema.is?(11, Type) 18 | refute Exchema.is?(-3, Type) 19 | assert Exchema.is?(3, Type) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/notation/subtype/direct_pred_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Notation.Subtype.DirectPredTest do 2 | defmodule Notation.Type.DirectPredTest do 3 | use ExUnit.Case 4 | import Exchema.Notation 5 | alias Exchema.Types, as: T 6 | 7 | subtype Type, T.Integer, [inclusion: (1..10)] 8 | 9 | test "it generates an exchema type" do 10 | assert :erlang.function_exported(Type, :__type__, 1) 11 | end 12 | 13 | test "it executes the function to check the type" do 14 | assert Exchema.is?(1, Type) 15 | refute Exchema.is?(:a, Type) 16 | refute Exchema.is?(11, Type) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/exchema/types/list.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.List do 2 | @moduledoc """ 3 | Represent a List and its element types. 4 | 5 | If you want a list of integers, then you want to 6 | use `{Exchema.Types.List, Exchema.Types.Integer}` 7 | as your type. 8 | 9 | If you don't care about list element types, it will 10 | default to `:any`, so `Exchema.Types.List` is the same 11 | as `{Exchema.Types.List, :any}`. 12 | """ 13 | alias Exchema.Predicates 14 | 15 | @doc false 16 | def __type__({}), do: __type__({:any}) 17 | def __type__({type}) do 18 | {:ref, :any, [{{Predicates, :list}, type}]} 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/types/struct_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.StructTest do 2 | use ExUnit.Case 3 | alias Exchema.Types, as: T 4 | defmodule Struct do 5 | defstruct [a: nil] 6 | end 7 | 8 | test "it allows only Struct with the right values" do 9 | r nil 10 | r "" 11 | r 1 12 | r %{} 13 | end 14 | 15 | test "it can check the element type" do 16 | a %Struct{a: 1} 17 | r %Struct{a: nil} 18 | end 19 | 20 | def a(val) do 21 | assert Exchema.is?(val, {T.Struct, {Struct, [a: T.Integer]}}) 22 | end 23 | def r(val) do 24 | refute Exchema.is?(val, {T.Struct, {Struct, [a: T.Integer]}}) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/types/list_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.ListTest do 2 | use ExUnit.Case 3 | 4 | alias Exchema.Types, as: T 5 | 6 | test "it allows only lists" do 7 | a [] 8 | a [1] 9 | a [1,2] 10 | r nil 11 | r "" 12 | r 1 13 | end 14 | 15 | test "it can check the element type" do 16 | a [1] 17 | a [] 18 | r ["1"] 19 | end 20 | 21 | test "allow list without inner type" do 22 | assert Exchema.is?([1, "2"], T.List) 23 | end 24 | 25 | def a(val) do 26 | assert Exchema.is?(val, {T.List, T.Integer}) 27 | end 28 | def r(val) do 29 | refute Exchema.is?(val, {T.List, T.Integer}) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | defmodule Predicates do 2 | def is_integer(value, _) when is_integer(value), do: :ok 3 | def is_integer(_,_), do: {:error, :not_an_integer} 4 | 5 | def min(value, min) when value >= min, do: :ok 6 | def min(_, _), do: {:error, :should_be_bigger} 7 | 8 | def err_list(_, _), do: [{:error, 1}, {:error, 2}] 9 | def err_false(_, _), do: false 10 | def ok_list(_, _), do: [] 11 | def ok_true(_, _), do: true 12 | 13 | defmodule Overrides do 14 | def is(_, _), do: {:error, :custom_error} 15 | end 16 | 17 | defmodule Overrides2 do 18 | def is(_, _), do: {:error, :custom_error_2} 19 | end 20 | end 21 | 22 | ExUnit.start() 23 | -------------------------------------------------------------------------------- /test/types/map_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.MapTest do 2 | use ExUnit.Case 3 | 4 | alias Exchema.Types, as: T 5 | 6 | test "it allows only maps" do 7 | a %{} 8 | a %{1 => 2} 9 | r nil 10 | r "" 11 | r 1 12 | end 13 | 14 | test "it can check the element type" do 15 | a %{1 => 2} 16 | a %{1 => 2, 3 => 4} 17 | r %{1 => "1"} 18 | r %{"1" => 1} 19 | r %{"1" => "1"} 20 | end 21 | 22 | test "allow map without inner type" do 23 | assert Exchema.is?(%{"1" => :a}, T.Map) 24 | end 25 | 26 | def a(val) do 27 | assert Exchema.is?(val, {T.Map, {T.Integer, T.Integer}}) 28 | end 29 | def r(val) do 30 | refute Exchema.is?(val, {T.Map, {T.Integer, T.Integer}}) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 2 | "credo": {:hex, :credo, "0.8.10", "261862bb7363247762e1063713bb85df2bbd84af8d8610d1272cd9c1943bba63", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, 3 | "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, 4 | "earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [], [], "hexpm"}, 5 | "ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}} 6 | -------------------------------------------------------------------------------- /lib/exchema/types/optional.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.Optional do 2 | @moduledoc """ 3 | Represents a value which can be nil 4 | 5 | You can specify the type when it is not nil, so if you want an 6 | integer that can be nil you can represent it with 7 | `{Exchema.Types.Optional, Exchema.Types.Integer}` 8 | 9 | With that, either `nil` and `1` are valid values, however 10 | `"this"` is not a valid one. 11 | """ 12 | 13 | @doc false 14 | def __type__({type}) do 15 | {:ref, :any, [{{__MODULE__, :pred}, type}]} 16 | end 17 | 18 | @doc false 19 | def pred(nil, _), do: :ok 20 | def pred(val, type) do 21 | case Exchema.errors(val, type) do 22 | [] -> 23 | :ok 24 | errors -> 25 | {:error, errors} 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/exchema/types/map.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.Map do 2 | @moduledoc """ 3 | Represents a Map with given key and value types. 4 | 5 | If you want a map of integers to atoms, then you want to 6 | use `{Exchema.Types.Map, {Exchema.Types.Integer, Exchema.Types.Atom}}` 7 | as your type. 8 | 9 | If you don't care about map key and value types, it will 10 | default to `:any`, so `Exchema.Types.Map` is the same 11 | as `{Exchema.Types.Map, {:any, :any}}`. 12 | """ 13 | 14 | @doc false 15 | def __type__({}), do: __type__({:any, :any}) 16 | def __type__({key_type, value_type}) do 17 | { 18 | :ref, 19 | :any, 20 | [ 21 | {{Exchema.Predicates, :key_type}, key_type}, 22 | {{Exchema.Predicates, :value_type}, value_type}, 23 | ] 24 | } 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/env_predicate_config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EnvPredicateConfigTest do 2 | use ExUnit.Case, async: false 3 | 4 | setup do 5 | Application.put_env(:exchema, :predicates, [Predicates.Overrides, Predicates]) 6 | on_exit fn -> 7 | Application.put_env(:exchema, :predicates, []) 8 | end 9 | end 10 | 11 | test "we can pass predicates by config" do 12 | type1 = {:ref, :any, is: :integer} 13 | type2 = {:ref, :any, is_integer: nil} 14 | assert [{_, _, :custom_error}] = Exchema.errors("1", type1) 15 | assert [{_, _, :not_an_integer}] = Exchema.errors("1", type2) 16 | end 17 | 18 | test "predicate library passed as params are matched first" do 19 | type = {:ref, :any, is: :integer} 20 | assert [{_, _, :custom_error_2}] = Exchema.errors("1", type, predicates: [Predicates.Overrides2]) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/exchema/types/one_struct_of.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.OneStructOf do 2 | @moduledoc """ 3 | This is a specification of the `Exchema.Types.OneOf` type. 4 | 5 | This optmizes the type discobery by checking the data against the 6 | module `.__struct__` directly. 7 | 8 | This also has better error reporting, because it returns the errors of 9 | the given type. 10 | """ 11 | 12 | @doc false 13 | def __type__({types}) when is_list(types) do 14 | {:ref, :any, [{{__MODULE__, :predicate}, types}]} 15 | end 16 | 17 | @doc false 18 | def predicate(%struct{} = value, structs) do 19 | if struct in structs do 20 | Exchema.errors(value, struct) 21 | |> Enum.map(fn {_, _, error} -> 22 | {:error, error} 23 | end) 24 | else 25 | {:error, :invalid_struct} 26 | end 27 | end 28 | def predicate(_, _), do: {:error, :invalid_struct} 29 | end 30 | -------------------------------------------------------------------------------- /test/notation/struct_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Notation.StructTest do 2 | use ExUnit.Case 3 | 4 | defmodule Struct do 5 | import Exchema.Notation 6 | structure [ 7 | foo: Exchema.Types.Integer.Negative, 8 | bar: Exchema.Types.Integer.Positive 9 | ] 10 | end 11 | 12 | test "it generates a struct" do 13 | assert :erlang.function_exported(Struct, :__struct__, 1) 14 | end 15 | 16 | test "it have all the fields" do 17 | s = %Struct{} 18 | assert Map.has_key?(s, :foo) 19 | assert Map.has_key?(s, :bar) 20 | end 21 | 22 | test "it generates an exchema type" do 23 | assert :erlang.function_exported(Struct, :__type__, 1) 24 | end 25 | 26 | test "it tests all the fields" do 27 | valid = %Struct{foo: -1, bar: 1} 28 | invalid = %Struct{foo: 1, bar: -1} 29 | assert Exchema.is?(valid, Struct) 30 | refute Exchema.is?(invalid, Struct) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/types/one_of_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.OneOfTest do 2 | use ExUnit.Case 3 | 4 | alias Exchema.Types, as: T 5 | 6 | @extype {T.OneOf, [T.Integer, T.String, T.Date]} 7 | 8 | test "it allows all the specified types" do 9 | assert Exchema.is?(1, @extype) 10 | assert Exchema.is?("s", @extype) 11 | assert Exchema.is?(Date.utc_today(), @extype) 12 | refute Exchema.is?(1.0, @extype) 13 | refute Exchema.is?(DateTime.utc_now(), @extype) 14 | end 15 | 16 | test "it returns a simple error" do 17 | assert [ 18 | {_, _, 19 | {:nested_errors, 20 | [ 21 | {Exchema.Types.Integer, [{_, _, :not_an_integer}]}, 22 | {Exchema.Types.String, [{_, _, :not_a_binary}]}, 23 | {Exchema.Types.Date, [{_, _, :not_a_struct}]} 24 | ]}} 25 | ] = Exchema.errors(1.0, @extype) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/error_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exchema.ErrorTest do 2 | use ExUnit.Case 3 | 4 | test "we can generate a flattened test report" do 5 | err = {{Predicate, :pred}, nil, :some_error} 6 | nest = fn errs -> {{Predicate, :pred}, nil, {:nested_errors, errs}} end 7 | nested_error = nest.([ 8 | { 9 | :addresses, 10 | [ 11 | nest.([ 12 | { 13 | 0, 14 | [ 15 | nest.([ 16 | {:city,[err]} 17 | ]) 18 | ] 19 | } 20 | ]) 21 | ] 22 | }, 23 | { 24 | :name, 25 | [err] 26 | } 27 | ]) 28 | root_error = err 29 | errors = [nested_error, root_error] 30 | flattened_errors = errors |> Exchema.flatten_errors 31 | 32 | assert [ 33 | {[:addresses, 0, :city], _, _, :some_error}, 34 | {[:name], _, _, :some_error}, 35 | {[], _, _, :some_error} 36 | ] = flattened_errors 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/exchema/types/one_of.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.OneOf do 2 | @moduledoc """ 3 | Represents one of the given types. 4 | Also known as a *sum type*. 5 | 6 | For example: 7 | ``` 8 | iex> alias Exchema.Types, as: T 9 | iex> t = {T.OneOf, [T.String, T.Integer]} 10 | iex> Exchema.is?("string", t) 11 | true 12 | 13 | iex> Exchema.is?(100, t) 14 | true 15 | 16 | iex> Exchema.is?(:atom, t) 17 | false 18 | ``` 19 | 20 | In case it fails, it will just return a invalid_type error. 21 | 22 | If all the types are Structs, use `Exchema.Types.OneStructOf` 23 | """ 24 | 25 | @doc false 26 | def __type__({types}) when is_list(types) do 27 | {:ref, :any, [{{__MODULE__, :predicate}, types}]} 28 | end 29 | 30 | @doc false 31 | def predicate(value, types) do 32 | errors = 33 | types 34 | |> Stream.map(&{&1, Exchema.errors(value, &1)}) 35 | 36 | if Enum.any?(errors, fn {_, errs} -> errs == [] end) do 37 | :ok 38 | else 39 | {:error, {:nested_errors, Enum.to_list(errors)}} 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | However, prior to 0.3.0 we were not following Semantic Versioning, so we broke things. 8 | 9 | ## [Unreleased] 10 | 11 | ## 0.4.0 - 2018-08-14 12 | 13 | ### Bug Fixes 14 | 15 | * Fix error when checking typespec definition on Elixir >= 1.7.0 (#8) 16 | Thanks to @aseigo and @victorolinasc 17 | 18 | ## 0.3.0 - 2018-06-20 19 | 20 | This was the first "stable" release 21 | Prior to that, were just me playing around with concepts. 22 | It has started as a validation library and then we added the concepts such as 23 | Refinement Types and having the type definitions in runtime to enable things 24 | such as Coercion. 25 | 26 | This also removed Coercion from this repository, focusing on the core library only. 27 | 28 | [Unreleased]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.3.0...HEAD 29 | -------------------------------------------------------------------------------- /lib/exchema/types/struct.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.Struct do 2 | @moduledoc """ 3 | Represents a specific struct with some field constraints. 4 | 5 | Normally you won't use this type directly but will instead 6 | define your struct using `Exchema.Struct`. 7 | 8 | Say that you have a struct `Data` with a field `value`. 9 | 10 | If you want to make sure the value is an integer, you can 11 | represent it with 12 | 13 | {Exchema.Types.Struct, 14 | {Data, [ 15 | value: Exchema.Types.Integer 16 | ]} 17 | } 18 | 19 | If you don't care about the field values, you can represent it 20 | with {Exchema.Types.Struct, Data}. 21 | """ 22 | 23 | alias Exchema.{ 24 | Types, 25 | Predicates 26 | } 27 | 28 | @doc false 29 | def __type__({}), do: __type__({:any, []}) 30 | def __type__({mod}), do: __type__({mod, []}) 31 | def __type__({mod, fields}) do 32 | { 33 | :ref, 34 | {Types.Map, {Types.Atom, :any}}, 35 | [ 36 | {{Predicates, :is_struct}, mod}, 37 | {{Predicates, :fields}, fields} 38 | ] 39 | } 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/exchema/type.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Type do 2 | @moduledoc """ 3 | This is the contract of a type module. 4 | 5 | To implement your own type you should just implement 6 | the `___type__/1` callback which receives a tuple with 7 | your type arguments. If you have a concrete type, 8 | then it should match on receiving an empty tuple `{}`. 9 | """ 10 | @type t :: module 11 | 12 | @type predicate_reference :: {module, atom} | atom 13 | @type predicate_spec :: {predicate_reference, any} 14 | @type refined_type :: {:ref, t, [predicate_spec]} 15 | 16 | @type type_params :: tuple 17 | @type type_reference :: Type.t | {Type.t, type_params} 18 | 19 | @type spec :: type_reference | refined_type 20 | 21 | @callback __type__(type_params) :: spec 22 | 23 | @doc "Resolves a type reference into it's definition" 24 | @spec resolve_type(type_reference) :: spec 25 | def resolve_type({type, params}) when is_tuple(params) do 26 | type.__type__(params) 27 | end 28 | def resolve_type({type, param}) do 29 | type.__type__({param}) 30 | end 31 | def resolve_type(type) do 32 | type.__type__({}) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :exchema, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:exchema, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /test/types/one_struct_of_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Types.OneStructOfTest do 2 | use ExUnit.Case 3 | 4 | alias Exchema.Types, as: T 5 | 6 | defmodule Structs do 7 | import Exchema.Notation 8 | structure A, a: T.Integer 9 | structure B, a: T.Float 10 | structure Other, a: T.Integer 11 | end 12 | 13 | @extype {T.OneStructOf, [Structs.A, Structs.B]} 14 | 15 | test "it allows all the specified types" do 16 | assert Exchema.is?(%Structs.A{a: 1}, @extype) 17 | assert Exchema.is?(%Structs.B{a: 1.0}, @extype) 18 | refute Exchema.is?(1, @extype) 19 | refute Exchema.is?(%Structs.A{a: 1.0}, @extype) 20 | refute Exchema.is?(%Structs.B{a: 1}, @extype) 21 | end 22 | 23 | test "it returns a simple error when the type is not a struct" do 24 | assert [{_, _, :invalid_struct}] = Exchema.errors(1.0, @extype) 25 | end 26 | 27 | test "it returns a simple error when the type is not any of the given structs" do 28 | value = %Structs.Other{a: 1} 29 | assert [{_, _, :invalid_struct}] = Exchema.errors(value, @extype) 30 | end 31 | 32 | test "it returns the specific errors of the type" do 33 | value = %Structs.A{a: "1"} 34 | [{_,_,errors}] = Exchema.errors(value, Structs.A) 35 | assert [{ _, _, ^errors }] = Exchema.errors(value, @extype) 36 | end 37 | end -------------------------------------------------------------------------------- /lib/exchema/macros/numeric_type.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Macros.NumericType do 2 | @moduledoc false 3 | 4 | defmacro __using__([type: type]) do 5 | quote do 6 | @doc false 7 | alias __MODULE__, as: ThisType 8 | alias Exchema.Predicates 9 | 10 | @type t :: unquote(type)() 11 | def __type__({}) do 12 | {:ref, :any, [{{Predicates, :is}, unquote(type)}]} 13 | end 14 | 15 | defmodule Positive do 16 | @moduledoc "Represents a positive #{unquote(type)}" 17 | 18 | @type t :: unquote(type)() 19 | def __type__({}) do 20 | {:ref, ThisType, [{{Predicates, :gt}, 0}]} 21 | end 22 | end 23 | 24 | defmodule Negative do 25 | @moduledoc "Represents a negative #{unquote(type)}" 26 | 27 | @type t :: unquote(type)() 28 | def __type__({}) do 29 | {:ref, ThisType, [{{Predicates, :lt}, 0}]} 30 | end 31 | end 32 | 33 | defmodule NonPositive do 34 | @moduledoc "Represents a non positive #{unquote(type)}" 35 | 36 | @type t :: unquote(type)() 37 | def __type__({}) do 38 | {:ref, ThisType, [{{Predicates, :lte}, 0}]} 39 | end 40 | end 41 | 42 | defmodule NonNegative do 43 | @moduledoc "Represents a non negative #{unquote(type)}" 44 | 45 | @type t :: unquote(type)() 46 | def __type__({}) do 47 | {:ref, ThisType, [{{Predicates, :gte}, 0}]} 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :exchema, 7 | version: "0.4.0", 8 | elixir: "~> 1.5", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | package: package(), 12 | docs: docs() 13 | ] 14 | end 15 | 16 | # Run "mix help compile.app" to learn about applications. 17 | def application do 18 | [ 19 | extra_applications: [:logger] 20 | ] 21 | end 22 | 23 | defp docs do 24 | [ 25 | main: "introduction", 26 | groups_for_modules: groups_for_modules(), 27 | extras: [ 28 | "README.md", 29 | "guides/introduction.md", 30 | "guides/types.md", 31 | "guides/checking_types.md", 32 | "guides/predicates.md" 33 | ] 34 | ] 35 | end 36 | 37 | defp deps do 38 | [ 39 | {:credo, "~> 0.8", only: [:dev, :test], runtime: false}, 40 | {:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false}, 41 | {:ex_doc, "~> 0.16", only: :dev, runtime: false} 42 | ] 43 | end 44 | 45 | defp package do 46 | [ 47 | name: "exchema", 48 | description: "Exchema is a library to define data types and validate it", 49 | licenses: ["Apache 2.0"], 50 | maintainers: ["Bernardo Amorim"], 51 | links: %{"GitHub" => "https://github.com/bamorim/exchema"} 52 | ] 53 | end 54 | 55 | defp groups_for_modules do 56 | [ 57 | Types: ~r/^Exchema\.Types/ 58 | ] 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/exchema/notation.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Notation do 2 | @moduledoc """ 3 | A DSL for defining types. 4 | """ 5 | 6 | alias __MODULE__, as: N 7 | @empty {:__block__, [], []} 8 | 9 | defmacro structure(fields) do 10 | __struct(fields) 11 | end 12 | 13 | defmacro structure(mod, fields) do 14 | wrapper(mod, __struct(fields), @empty) 15 | end 16 | 17 | defmacro structure(mod, fields, [do: block]) do 18 | wrapper(mod, __struct(fields), block) 19 | end 20 | 21 | defmacro subtype(suptype, refinements) do 22 | __subtype(suptype, refinements) 23 | end 24 | 25 | defmacro subtype(mod, suptype, refinements) do 26 | wrapper(mod, __subtype(suptype, refinements), @empty) 27 | end 28 | 29 | defmacro subtype(mod, suptype, refinements, [do: block]) do 30 | wrapper(mod, __subtype(suptype, refinements), block) 31 | end 32 | 33 | defmacro refine(refinements) do 34 | N.Subtype.__add_refinements(refinements) 35 | end 36 | 37 | defp wrapper(nil, content, nil), do: content 38 | defp wrapper(nil, content, extra) do 39 | quote do 40 | unquote(content) 41 | unquote(extra) 42 | end 43 | end 44 | defp wrapper(mod, content, extra) do 45 | quote do 46 | defmodule unquote(mod) do 47 | unquote(wrapper(nil, content, extra)) 48 | end 49 | end 50 | end 51 | 52 | defp __struct(fields) do 53 | N.Struct.__struct(fields) 54 | end 55 | 56 | defp __subtype(suptype, refinements) do 57 | N.Subtype.__subtype(suptype, refinements) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/predicate_library_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PrecicatesLibraryTest do 2 | use ExUnit.Case 3 | 4 | test "we can pass an atom as refinement and a predicate library" do 5 | type = {:ref, :any, is_integer: true} 6 | assert Exchema.is?(1, type, predicates: Predicates) 7 | refute Exchema.is?("1", type, predicates: Predicates) 8 | end 9 | 10 | test "we have a default predicate library" do 11 | type = {:ref, :any, is: :integer} 12 | assert Exchema.is?(1, type) 13 | refute Exchema.is?("1", type) 14 | end 15 | 16 | test "we still have access to the default predicate library when passing a custom library" do 17 | type = {:ref, :any, is: :integer} 18 | assert Exchema.is?(1, type, predicates: Predicates) 19 | end 20 | 21 | test "specified predicate library overrides the default ones" do 22 | type = {:ref, :any, is: :integer} 23 | assert [{_, _, :custom_error}] = Exchema.errors("1", type, predicates: Predicates.Overrides) 24 | end 25 | 26 | test "when passing more than one library, it matches the first one" do 27 | type = {:ref, :any, is: :integer} 28 | predicates = [Predicates.Overrides2, Predicates.Overrides] 29 | assert [{_, _, :custom_error_2}] = Exchema.errors("1", type, predicates: predicates) 30 | end 31 | 32 | test "when passing more than one library, all are accessible" do 33 | type = {:ref, :any, is_integer: nil} 34 | predicates = [Predicates.Overrides, Predicates] 35 | assert [{_, _, :not_an_integer}] = Exchema.errors("1", type, predicates: predicates) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/exchema/notation/subtype.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Notation.Subtype do 2 | @moduledoc false 3 | def __subtype(suptype, refinements_spec) do 4 | quote do 5 | @super_type unquote(suptype) 6 | @refinement_count 0 7 | unquote(__add_refinements(refinements_spec)) 8 | @doc false 9 | def __exchema_refinement_0(), do: [] 10 | @before_compile Exchema.Notation.Subtype 11 | end 12 | end 13 | 14 | def __add_refinements(refinements_spec) do 15 | refinements = Macro.escape(refinements_for(refinements_spec)) 16 | base_fname = "__exchema_refinement" 17 | quote bind_quoted: [refinements: refinements, base_fname: base_fname] do 18 | @refinement_count (@refinement_count + 1) 19 | @doc false 20 | def unquote(:"#{base_fname}_#{@refinement_count}")() do 21 | unquote(:"#{base_fname}_#{@refinement_count-1}")() ++ unquote(refinements) 22 | end 23 | end 24 | end 25 | 26 | defmacro __before_compile__(%{module: mod}) do 27 | Exchema.Notation.Typespec.set_typespec(mod) 28 | base_fname = "__exchema_refinement" 29 | quote bind_quoted: [base_fname: base_fname] do 30 | @doc false 31 | def __type__({}) do 32 | { 33 | :ref, 34 | @super_type, 35 | unquote(:"#{base_fname}_#{@refinement_count}")() 36 | } 37 | end 38 | end 39 | end 40 | 41 | defp refinements_for({sym, _, _} = fun) when sym in [:&, :fn] do 42 | quote do 43 | [fun: unquote(fun)] 44 | end 45 | end 46 | defp refinements_for(preds) when is_list(preds), do: preds 47 | defp refinements_for(preds) do 48 | raise "Invalid predicate: #{preds}" 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/exchema.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema do 2 | @moduledoc """ 3 | Exchema is a library for defining data structures using refinement types. 4 | 5 | Exchema is split in some components: 6 | 7 | * `Exchema` - the main module. It does type checking and deal with type checking 8 | errors. 9 | * `Exchema.Predicates` - The default predicate library you can use to refine your 10 | types. It's just a bunch of 2-arity functions that receives the value and some 11 | options. 12 | * `Exchema.Notation` - a DSL for defining types. 13 | 14 | It also comes with a series of pre-defined types you can check under `Exchema.Types` 15 | namespace. 16 | """ 17 | 18 | @type error :: {Type.predicate_spec, any, any} 19 | @type flattened_error :: {[any], Type.predicate_spec, any, any} 20 | 21 | @spec is?(any, Type.spec, [{atom, any}]) :: boolean 22 | def is?(val, type, opts \\ []), do: errors(val, type, opts) == [] 23 | 24 | @spec errors(any, Type.spec, [{atom, any}]) :: [error] 25 | defdelegate errors(val, type, opts \\ []), to: Exchema.Errors 26 | 27 | @doc """ 28 | Flattens a list of errors that follows the `:nested_errors` 29 | pattern where the error returned follow this structure: 30 | 31 | ``` 32 | { 33 | {predicate, predicate_opts, { 34 | :nested_errors, 35 | [ 36 | {key, error}, 37 | {key, error} 38 | ] 39 | } 40 | } 41 | ``` 42 | 43 | The returned result is a list of a 4-tuple where the 44 | first element is the path of keys to reach the error and 45 | the rest is the normal 3-tuple error elements (predicate, 46 | predicate options and the error itself) 47 | """ 48 | @spec flatten_errors([error]) :: [flattened_error] 49 | defdelegate flatten_errors(errors), to: Exchema.Errors 50 | end -------------------------------------------------------------------------------- /test/notation/typespec_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Notation.TypespecTest do 2 | use ExUnit.Case 3 | 4 | defmodule ExposeTypeSpec do 5 | defmacro __before_compile__(_) do 6 | if Version.match?(System.version(), ">= 1.7.0") do 7 | quote do 8 | {_set, bag} = :elixir_module.data_tables(__MODULE__) 9 | @typespecs :ets.lookup_element(bag, :type, 2) 10 | 11 | def __typespec, do: @typespecs 12 | end 13 | else 14 | quote do 15 | def __typespec, do: @type 16 | end 17 | end 18 | end 19 | end 20 | 21 | defmodule Complex do 22 | import Exchema.Notation 23 | alias Exchema.Types, as: T 24 | 25 | structure(SomeStruct, i: T.Integer) 26 | 27 | structure( 28 | key: {T.List, {T.Map, {T.String, {T.Optional, T.DateTime}}}}, 29 | f: T.Float, 30 | pf: T.Float.Positive, 31 | i: T.Integer, 32 | nni: T.Integer.NonNegative, 33 | pi: T.Integer.Positive, 34 | ni: T.Integer.Negative, 35 | s: T.String, 36 | d: T.Date, 37 | dt: T.DateTime, 38 | ndt: T.NaiveDateTime, 39 | t: T.Time, 40 | st: T.Struct, 41 | m: T.Map, 42 | a: T.Atom, 43 | b: T.Boolean, 44 | of: {T.OneOf, [T.Integer, T.Float]}, 45 | osf: {T.OneStructOf, [__MODULE__, SomeStruct]} 46 | ) 47 | 48 | @before_compile ExposeTypeSpec 49 | end 50 | 51 | test "it generates a typespec" do 52 | assert [_ | _] = Complex.__typespec() 53 | end 54 | 55 | test "it contains all structure fields" do 56 | [{_, {_, _, [{_, _, _}, {_, _, [{_, _, _}, {_, _, fields}]}]}, _}] = Complex.__typespec() 57 | keys = fields |> Enum.map(fn {k, _v} -> k end) |> Enum.sort() 58 | 59 | assert [ 60 | :a, 61 | :b, 62 | :d, 63 | :dt, 64 | :f, 65 | :i, 66 | :key, 67 | :m, 68 | :ndt, 69 | :ni, 70 | :nni, 71 | :of, 72 | :osf, 73 | :pf, 74 | :pi, 75 | :s, 76 | :st, 77 | :t 78 | ] = keys 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/exchema/errors.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Errors do 2 | @moduledoc false 3 | 4 | def errors(value, type, opts \\ []) 5 | def errors(_, :any, _), do: [] 6 | def errors(val, {:ref, supertype, predicates}, opts) do 7 | errors(val, supertype, opts) ++ predicates_errors(predicates, val, opts) 8 | end 9 | def errors(val, type_ref, opts) do 10 | errors(val, Exchema.Type.resolve_type(type_ref), opts) 11 | end 12 | 13 | def flatten_errors(errors) do 14 | errors 15 | |> Enum.flat_map(&flatten_error/1) 16 | |> Enum.map(&reverse_path/1) 17 | end 18 | 19 | defp predicates_errors(predicates, val, opts) when is_list(predicates) do 20 | predicates 21 | |> Enum.flat_map(&(predicate_errors(&1, val, opts))) 22 | end 23 | 24 | defp predicate_errors({{mod, fun}, opts}, val, _) do 25 | case apply(mod, fun, [val, opts]) do 26 | false -> 27 | [{{mod, fun}, opts, :invalid}] 28 | errors when is_list(errors) -> 29 | errors 30 | |> Enum.map(&(format_predicate_error(mod, fun, opts, &1))) 31 | {:error, _} = error -> 32 | [format_predicate_error(mod, fun, opts, error)] 33 | _ -> 34 | [] 35 | end 36 | end 37 | defp predicate_errors({pred_key, opts}, val, g_opts) do 38 | predicate_errors( 39 | { 40 | {pred_mod(g_opts, pred_key), pred_key}, 41 | opts 42 | }, 43 | val, 44 | g_opts 45 | ) 46 | end 47 | 48 | defp format_predicate_error(mod, fun, opts, {:error, err}) do 49 | {{mod, fun}, opts, err} 50 | end 51 | 52 | defp pred_mod(g_opts, pred_key) do 53 | g_opts 54 | |> pred_mods 55 | |> Enum.filter(&(:erlang.function_exported(&1, pred_key, 2))) 56 | |> List.first 57 | end 58 | 59 | defp pred_mods(g_opts) do 60 | [ 61 | (Keyword.get(g_opts, :predicates) || []), 62 | (Application.get_env(:exchema, :predicates) || []), 63 | Exchema.Predicates 64 | ] |> List.flatten 65 | end 66 | 67 | defp flatten_error(errors, path \\ []) 68 | defp flatten_error({_, _, {:nested_errors, errors}}, path) do 69 | errors 70 | |> Enum.flat_map(fn {key, key_errors} -> 71 | key_errors 72 | |> Enum.flat_map(&(flatten_error(&1, [key | path]))) 73 | end) 74 | end 75 | defp flatten_error({pred, opt, error}, path) do 76 | [{path, pred, opt, error}] 77 | end 78 | 79 | defp reverse_path({path, pred, opt, error}) do 80 | {Enum.reverse(path), pred, opt, error} 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /test/types/basic_types_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BasicTypesTest do 2 | use ExUnit.Case 3 | 4 | alias Exchema.Types, as: T 5 | import Exchema, only: [is?: 2] 6 | 7 | test "integer" do 8 | assert is?(1, T.Integer) 9 | refute is?(1.0, T.Integer) 10 | refute is?("1", T.Integer) 11 | signal_tests(T.Integer, 1) 12 | end 13 | 14 | test "float" do 15 | assert is?(1.0, T.Float) 16 | refute is?(1, T.Float) 17 | refute is?("1", T.Float) 18 | signal_tests(T.Float, 1.0) 19 | end 20 | 21 | test "number" do 22 | assert is?(1, T.Number) 23 | assert is?(1.0, T.Number) 24 | refute is?("1", T.Number) 25 | signal_tests(T.Integer, 1) 26 | signal_tests(T.Float, 1.0) 27 | end 28 | 29 | test "string" do 30 | assert is?("string", T.String) 31 | refute is?(1, T.String) 32 | end 33 | 34 | test "boolean" do 35 | assert is?(true, T.Boolean) 36 | assert is?(false, T.Boolean) 37 | refute is?("not", T.Boolean) 38 | end 39 | 40 | test "atom" do 41 | assert is?(true, T.Atom) 42 | assert is?(nil, T.Atom) 43 | assert is?(:atom, T.Atom) 44 | refute is?("not", T.Atom) 45 | end 46 | 47 | test "tuple" do 48 | assert is?({}, T.Tuple) 49 | assert is?({1}, T.Tuple) 50 | assert is?({1,2}, T.Tuple) 51 | refute is?([1,2], T.Tuple) 52 | end 53 | 54 | test "DateTime" do 55 | assert is?(DateTime.utc_now, T.DateTime) 56 | refute is?(NaiveDateTime.utc_now, T.DateTime) 57 | end 58 | 59 | test "NaiveDateTime" do 60 | assert is?(NaiveDateTime.utc_now, T.NaiveDateTime) 61 | refute is?(DateTime.utc_now, T.NaiveDateTime) 62 | end 63 | 64 | test "Date" do 65 | assert is?(Date.utc_today, T.Date) 66 | refute is?(DateTime.utc_now, T.Date) 67 | end 68 | 69 | test "Time" do 70 | assert is?(Time.utc_now, T.Time) 71 | refute is?(DateTime.utc_now, T.Time) 72 | end 73 | 74 | defp signal_tests(mod, base) do 75 | assert is?(1 * base, Module.concat(mod, Positive)) 76 | refute is?(0 * base, Module.concat(mod, Positive)) 77 | 78 | assert is?(-1 * base, Module.concat(mod, Negative)) 79 | refute is?(0 * base, Module.concat(mod, Negative)) 80 | 81 | assert is?(1 * base, Module.concat(mod, NonNegative)) 82 | assert is?(0 * base, Module.concat(mod, NonNegative)) 83 | refute is?(-1 * base, Module.concat(mod, NonNegative)) 84 | 85 | assert is?(-1 * base, Module.concat(mod, NonPositive)) 86 | assert is?(0 * base, Module.concat(mod, NonPositive)) 87 | refute is?(1 * base, Module.concat(mod, NonPositive)) 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /test/exchema_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExchemaTest do 2 | use ExUnit.Case 3 | doctest Exchema 4 | 5 | @moduletag :basic 6 | 7 | defmodule IntegerType do 8 | @behaviour Exchema.Type 9 | def __type__(_), do: {:ref, :any, [{{Predicates, :is_integer}, nil}]} 10 | end 11 | 12 | defmodule PositiveIntegerType do 13 | @behaviour Exchema.Type 14 | def __type__(_), do: {:ref, IntegerType, [{{Predicates, :min}, 0}]} 15 | end 16 | 17 | defmodule ListType do 18 | @behaviour Exchema.Type 19 | def __type__({inner_type}) do 20 | {:ref, :any, [{{__MODULE__, :predicate}, inner_type}]} 21 | end 22 | 23 | def predicate(list, inner_type) when is_list(list) do 24 | list 25 | |> Enum.all?(&(Exchema.is?(&1, inner_type))) 26 | |> msg 27 | end 28 | def predicate(_, _), do: msg(false) 29 | 30 | defp msg(true), do: :ok 31 | defp msg(false), do: {:error, :invalid_list_item_type} 32 | end 33 | 34 | 35 | defmodule IntegerListType do 36 | def __type__(_), do: {ListType, IntegerType} 37 | end 38 | 39 | test "basic type check" do 40 | assert Exchema.is?(0, IntegerType) 41 | assert Exchema.is?(-1, IntegerType) 42 | refute Exchema.is?("1", IntegerType) 43 | end 44 | 45 | test "type refinement" do 46 | assert Exchema.is?(0, PositiveIntegerType) 47 | refute Exchema.is?(-1, PositiveIntegerType) 48 | end 49 | 50 | test "type error" do 51 | assert [{{Predicates, :min}, 0, :should_be_bigger}] = Exchema.errors(-1, PositiveIntegerType) 52 | end 53 | 54 | test "parametric types" do 55 | assert Exchema.is?([1,2,3], {ListType, IntegerType}) 56 | assert Exchema.is?([], {ListType, IntegerType}) 57 | refute Exchema.is?(1, {ListType, IntegerType}) 58 | refute Exchema.is?([1, "2", 3], {ListType, IntegerType}) 59 | refute Exchema.is?(["1", "2", "3"], {ListType, IntegerType}) 60 | end 61 | 62 | test "parametric defined types" do 63 | assert Exchema.is?([1,2,3], IntegerListType) 64 | assert Exchema.is?([], IntegerListType) 65 | refute Exchema.is?(1, IntegerListType) 66 | refute Exchema.is?([1, "2", 3], IntegerListType) 67 | refute Exchema.is?(["1", "2", "3"], IntegerListType) 68 | end 69 | 70 | test "we can pass type refinement directly" do 71 | type = {:ref, IntegerType, [{{Predicates, :min}, 0}]} 72 | assert Exchema.is?(1, type) 73 | refute Exchema.is?(-1, type) 74 | end 75 | 76 | test "it aggregates predicate errors" do 77 | type = {:ref, IntegerType, [{{Predicates, :min}, 0}, {{Predicates, :min}, 1}]} 78 | assert [{_,0,:should_be_bigger}, {_, 1, :should_be_bigger}] = Exchema.errors(-1, type) 79 | end 80 | 81 | test "it allos predicates to return a list of errors" do 82 | type_err = {:ref, :any, [{{Predicates, :err_list}, nil}]} 83 | type_ok = {:ref, :any, [{{Predicates, :ok_list}, nil}]} 84 | assert [{_,_,1}, {_,_,2}] = Exchema.errors(1, type_err) 85 | assert [] = Exchema.errors(1, type_ok) 86 | end 87 | 88 | test "it allows predicates to just return false or true" do 89 | type_err = {:ref, :any, [{{Predicates, :err_false}, nil}]} 90 | type_ok = {:ref, :any, [{{Predicates, :ok_true}, nil}]} 91 | assert [{_,_,:invalid}] = Exchema.errors(1, type_err) 92 | assert [] = Exchema.errors(1, type_ok) 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /guides/types.md: -------------------------------------------------------------------------------- 1 | # Types 2 | 3 | The idea of refinement types is based on set theory. We think about 4 | sets of values and how to define narrower sets through types. This 5 | guide does not aim to explain set theory but to help thinking about 6 | refinement types for validation. 7 | 8 | ## `:any` type and anything values 9 | 10 | Think about input data as the set of all possible values. They are 11 | *anything*. So, we can say that they are all contained in the `:any` 12 | set of possible values. 13 | 14 | That is why we call `:any` the **root** parent type as any value derives 15 | from `:any`. 16 | 17 | ## Narrowing the possible values 18 | 19 | In reality we don't want ANY value in our system. Normally we have a very 20 | good idea of the possible values. That is: we want a **subset** of all 21 | possible values. 22 | 23 | Let's think of an example: user ID. Normally we use this for fetching the 24 | user through some REST API. Because we strive for good APIs, we use an 25 | UUID instead of a long. We want to avoid people trying to guess the next 26 | id or the first id. 27 | 28 | Now, normally, an UUID is represented as a string. This is the first 29 | refinement we want to make: *out of all possible values we want only those 30 | that are strings*. 31 | 32 | Even then, there are infinite possible strings that are not valid UUIDs. 33 | This is why we refine it even more. We want a smaller subset of all possible 34 | values: it needs to be a certain group of strings. 35 | 36 | To do that, we can define a subtype of a String: 37 | 38 | ``` elixir 39 | subtype Id, Exchema.Types.String, [] 40 | ``` 41 | 42 | With that declaration we have a name for a new set of possible values. The way 43 | it is now means this subset is exactly the same as the string subset. We need 44 | to refine it further: 45 | 46 | ``` elixir 47 | subtype Id, Exchema.Types.String, fn val -> 48 | String.length(val) == 36 49 | end 50 | ``` 51 | 52 | That creates a subset of all string possible values. Now this subset contains a 53 | finite number of possible values: all strings with length 36. But we can do 54 | better. Let's use the UUID library from hex to narrow it down only to valid 55 | uuids type 4 formatted according to standard notation: 56 | 57 | ``` elixir 58 | subtype( 59 | Id, 60 | Exchema.Types.String, 61 | fn val -> 62 | with {:ok, opts} <- UUID.info(val), # returns details about val 63 | 4 <- opts[:version], # ensure it is version 4 64 | :default <- opts[:type] do # ensure only the standard format 65 | {:ok, val} 66 | else 67 | _ -> 68 | {:error, :not_valid_uuid} 69 | end 70 | end 71 | ) 72 | ``` 73 | 74 | Now we can be sure that this subtype is delimiting all the possible values we 75 | want. Anywhere we declare `Id` type now can be validate to be in the subset of 76 | possible uuid v4 standard formatted values. 77 | 78 | Details about what should be returned in the function are on the Predicates guide. 79 | 80 | ## Defining custom types 81 | 82 | Other than using the `subtype/3` macro, you can define your types implementing 83 | the `__type__/1` function. Let's see, for instance, how the `Exchema.Types.DateTime` 84 | is implemented: 85 | 86 | ``` elixir 87 | defmodule Exchema.Types.DateTime do 88 | def __type__({}) do 89 | {:ref, :any, [{{Exchema.Predicates, :is_struct}, DateTime}]} 90 | end 91 | end 92 | ``` 93 | 94 | More details about the format of the tuple in the Predicates guide. You can also see 95 | the checking types guide for valiadtion errors. 96 | 97 | -------------------------------------------------------------------------------- /guides/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Exchema is a library to define and validate data. It allows 4 | you to check the type for a given value at runtime (it is **not** static 5 | type checking). 6 | 7 | The type definition allows us to build other nice libraries on top of it like [`exchema_coercion`](https://github/bamorim/exchema_coercion) and [`exchema_stream_data`](https://github.com/bamorim/exchema_stream_data) 8 | 9 | It uses the idea of **refinement types**, in which we have a global type 10 | (which all values belong) and can refine that type with the use of 11 | **predicates**. 12 | 13 | The root type is `:any`. From there we can declare subtypes with refinements 14 | to reach the boundaries of possible values we want. The library comes with 15 | several built-in type definitions for native types. 16 | 17 | Let's use the built-in types to show an example: 18 | 19 | ``` elixir 20 | import Exchema.Notation # this is the entrypoint for the DSL 21 | 22 | # Let's declare a type that is a subtype of String 23 | subtype Name, Exchema.Types.String, [] 24 | 25 | # Now we have :any <- String <- Name 26 | 27 | # Let's declare a structure using this type: 28 | # here we use the structure macro which accepts an atom as the 29 | # name for the structure. This will generate a struct so think of 30 | # it as a different `defstruct` call 31 | structure Names, [first: Name, last: Name] 32 | 33 | # Pay attention at the 'default' values. They are type definitions. 34 | # With that we can validate input. 35 | 36 | # That was easy. 37 | 38 | # Let's check if it is valid according to our refinements: 39 | true = Exchema.is?(names, Names) 40 | ``` 41 | 42 | Nice. Although it does not look much, this is very powerful! Let's see 43 | a more complex example. 44 | 45 | Let's define an `Id` type. This will use the library `UUID` from hex just 46 | as an example. 47 | 48 | ``` elixir 49 | subtype( 50 | Id, 51 | Exchema.Types.String, 52 | fn val -> # this is our refinement 53 | with {:ok, opts} <- UUID.info(val), # checks it is proper formed 54 | 4 <- opts[:version], # it is version 4 55 | :default <- opts[:type] do # it is formatted as the default option 56 | {:ok, val} 57 | else 58 | _ -> 59 | {:error, :not_valid_uuid} # not valid UUID 60 | end 61 | end 62 | ) 63 | ``` 64 | 65 | Now if we want to declare a user we can do this: 66 | 67 | ``` elixir 68 | structure User, [ 69 | id: Id, 70 | first_name: Name, 71 | last_name: Name 72 | ] 73 | ``` 74 | 75 | Awesome. Let's again validate: 76 | 77 | ``` elixir 78 | user = %User{id: nil, first_name: "Hello", last_name: "World"} 79 | ``` 80 | 81 | But... wait! `id` nil is valid? Well, let's validate to check: 82 | 83 | ``` elixir 84 | Exchema.is?(user, User) 85 | # false 86 | ``` 87 | 88 | We haven't declared `id` as **optional**. So, the structure is not valid. 89 | 90 | Let's fix our declaration and run again: 91 | 92 | ``` elixir 93 | structure User, [ 94 | id: {Exchema.Types.Optional, Id}, 95 | first_name: Name, 96 | last_name: Name 97 | ] 98 | ``` 99 | 100 | Wow. That is new. That is a *parametric type* (or parameterized type). It is 101 | used for occasions like this one: the parameter can be either `nil` or an 102 | `Id`. 103 | 104 | It is also useful when you want to work with collection of elements like lists 105 | and maps. See the collections guide. 106 | 107 | Let's now validate again: 108 | 109 | ``` elixir 110 | Exchema.is?(user, User) 111 | # true 112 | ``` 113 | 114 | All right! That covers the basics. 115 | 116 | See the Types guide for understanding the core concept behind refinement types. -------------------------------------------------------------------------------- /lib/exchema/notation/typespec.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Notation.Typespec do 2 | def set_typespec(mod) do 3 | unless type_t_defined(mod) do 4 | define_type_t(mod) 5 | end 6 | end 7 | 8 | defp type_t_defined(mod) do 9 | if Version.match?(System.version(), ">= 1.7.0") do 10 | Module.defines_type?(mod, {:t, 0}) 11 | else 12 | mod 13 | |> Module.get_attribute(:type) 14 | |> Enum.any?(&is_type_t/1) 15 | end 16 | end 17 | 18 | defp define_type_t(mod) do 19 | mod 20 | |> Module.get_attribute(:super_type) 21 | |> typespec_for_type() 22 | |> define_type(mod) 23 | end 24 | 25 | defp is_type_t({:type, {:::, _, [{:t, _, _} | _]}, _}), do: true 26 | defp is_type_t(_), do: false 27 | 28 | defp typespec_for_type({Exchema.Types.Struct, {_, fields}}) do 29 | {:%, nil, 30 | [ 31 | {:__MODULE__, [], Elixir}, 32 | {:%{}, [], 33 | Enum.map(fields, fn {field, type} -> 34 | {field, typespec_for_type(type)} 35 | end)} 36 | ]} 37 | end 38 | 39 | defp typespec_for_type({Exchema.Types.List, type}) do 40 | quote do 41 | list(unquote(typespec_for_type(type))) 42 | end 43 | end 44 | 45 | defp typespec_for_type({Exchema.Types.Map, {key, val}}) do 46 | quote do 47 | %{optional(unquote(typespec_for_type(key))) => unquote(typespec_for_type(val))} 48 | end 49 | end 50 | 51 | defp typespec_for_type({Exchema.Types.Optional, type}) do 52 | quote do 53 | nil | unquote(typespec_for_type(type)) 54 | end 55 | end 56 | 57 | defp typespec_for_type({Exchema.Types.OneOf, types}) do 58 | typespec_for_type({Exchema.Types.OneStructOf, types}) 59 | end 60 | 61 | defp typespec_for_type({Exchema.Types.OneStructOf, [type]}) do 62 | typespec_for_type(type) 63 | end 64 | 65 | defp typespec_for_type({Exchema.Types.OneStructOf, types}) do 66 | types 67 | |> Enum.map(&typespec_for_type/1) 68 | |> Enum.reduce(fn x, acc -> 69 | quote do 70 | unquote(x) | unquote(acc) 71 | end 72 | end) 73 | end 74 | 75 | defp typespec_for_type({mod, {single_arg}}) do 76 | typespec_for_type({mod, single_arg}) 77 | end 78 | 79 | defp typespec_for_type(mod) when is_atom(mod) do 80 | case to_string(mod) do 81 | "Elixir.Exchema.Types." <> rest -> 82 | exchema_typespec(rest) 83 | 84 | "Elixir." <> _ -> 85 | quote do 86 | unquote(mod).t 87 | end 88 | 89 | _ -> 90 | {:any, [], []} 91 | end 92 | end 93 | 94 | defp typespec_for_type({:ref, sup, _}) do 95 | typespec_for_type(sup) 96 | end 97 | 98 | defp typespec_for_type(_) do 99 | {:any, [], []} 100 | end 101 | 102 | defp define_type(tspec, mod) do 103 | Module.eval_quoted( 104 | mod, 105 | quote do 106 | @type t :: unquote(tspec) 107 | end 108 | ) 109 | end 110 | 111 | @ex_direct ~w(Atom Boolean Integer Number Map Struct Tuple) 112 | @ex_modules ~w(Date DateTime NaiveDateTime Time) 113 | 114 | defp exchema_typespec(type) when type in @ex_direct, do: from_str(type) 115 | defp exchema_typespec(type) when type in @ex_modules, do: from_mod(type) 116 | defp exchema_typespec("String"), do: simple(:binary) 117 | defp exchema_typespec("Float" <> _), do: simple(:float) 118 | defp exchema_typespec("Integer.Positive"), do: simple(:pos_integer) 119 | defp exchema_typespec("Integer.Negative"), do: simple(:neg_integer) 120 | defp exchema_typespec("Integer.NonNegative"), do: simple(:non_neg_integer) 121 | defp exchema_typespec(_), do: :any 122 | 123 | defp simple(atom) do 124 | {atom, [], []} 125 | end 126 | 127 | defp from_str(str) do 128 | str 129 | |> String.downcase() 130 | |> String.to_atom() 131 | |> simple 132 | end 133 | 134 | defp from_mod(mod) do 135 | mod = :"Elixir.#{mod}" 136 | 137 | quote do 138 | unquote(mod).t 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /guides/checking_types.md: -------------------------------------------------------------------------------- 1 | # Checking types 2 | 3 | For checking input in `Exchema` you call the validation function 4 | `is?/2`. It receives the input and the type for running checks. 5 | 6 | ``` elixir 7 | iex> Exchema.is?("1234", Exchema.Types.String) 8 | true 9 | 10 | iex> Exchema.is?(1234, Exchema.Types.String) 11 | false 12 | 13 | iex> Exchema.is?(1234, Exchema.Types.Integer) 14 | true 15 | ``` 16 | 17 | ## Types 18 | 19 | Now that checking is out of the way. Let's extend to what is a type. 20 | 21 | It can be: 22 | 23 | - the global type `:any` 24 | - a type reference such as `Exchema.Types.String` 25 | - a type refinement such as `{:ref, :any, length: 1}` (more about this in predicates guide) 26 | - a type application (for parametric types) such as `{Exchema.Types.List, Exchema.Types.String}` 27 | 28 | 29 | ## Errors 30 | 31 | When checking returns false, we can see what caused that with the 32 | `errors/2` function: 33 | 34 | ``` elixir 35 | iex> Exchema.errors(1234, Exchema.Types.String) 36 | [{{Exchema.Predicates, :is}, :binary, :not_a_binary}] 37 | ``` 38 | 39 | This is also helpful when you have several errors. Let's check that: 40 | 41 | ``` elixir 42 | # import the DSL 43 | import Exchema.Notation 44 | 45 | # alias the types 46 | alias Exchema.Types, as: T 47 | 48 | # define a User structure 49 | structure User, first_name: T.String, last_name: T.String 50 | 51 | # Type this on iex 52 | iex> Exchema.errors(%User{first_name: 123, last_name: 123}, User) 53 | [ 54 | {{Exchema.Predicates, :fields}, 55 | [first_name: Exchema.Types.String, last_name: Exchema.Types.String], 56 | {:nested_errors, 57 | [ 58 | first_name: [{{Exchema.Predicates, :is}, :binary, :not_a_binary}], 59 | last_name: [{{Exchema.Predicates, :is}, :binary, :not_a_binary}] 60 | ]}} 61 | ] 62 | ``` 63 | 64 | ## Flatten errors 65 | 66 | We can also flatten the errors for better reading: 67 | 68 | ``` elixi 69 | iex> errors = Exchema.errors(%User{first_name: 123, last_name: 123}, User) 70 | # output omitted 71 | iex> Exchema.flatten_errors(errors) 72 | [ 73 | {[:first_name], {Exchema.Predicates, :is}, :binary, :not_a_binary}, 74 | {[:last_name], {Exchema.Predicates, :is}, :binary, :not_a_binary} 75 | ] 76 | ``` 77 | 78 | This is very useful for debugging errors. 79 | 80 | ## Nested types 81 | 82 | You can nest types without issues and check nested errors. Example: 83 | 84 | ```elixir 85 | import Exchema.Notation 86 | 87 | # Name here has no predicate other than its super type. You might want 88 | # to add predicates later like checking if its upcased. 89 | subtype Name, Exchema.Types.String, [] 90 | 91 | # This is a different type of predicate than 'is'. 92 | subtype Country, Exchema.Types.Atom, [inclusion: ~w{brazil canada portugal}a] 93 | 94 | # This extends from `:any` because list and maps descend from any 95 | subtype Metadata, :any, &(is_list(&1) || is_map(&1)) 96 | 97 | structure FullName, [first: Name, last: Name] 98 | 99 | defmodule MyStructure do 100 | structure [ 101 | name: FullName, # nested model 102 | country: Country, 103 | metadata: Metadata # generic data 104 | ] 105 | end 106 | 107 | valid = %MyStructure{ 108 | name: %FullName{ 109 | first: "Bernardo", 110 | last: "Amorim" 111 | }, 112 | country: :brazil, 113 | metadata: %{any: :thing} 114 | } 115 | 116 | invalid = %MyStructure{ 117 | name: %FullName{ 118 | first: 1234, 119 | last: :not_a_string 120 | }, 121 | country: :croatia, 122 | metadata: :not_a_list_nor_a_map 123 | } 124 | 125 | Exchema.is?(valid, MyStructure) 126 | # => true 127 | 128 | Exchema.is?(invalid, MyStructure) 129 | # => false 130 | 131 | Exchema.errors(invalid, MyStructure) 132 | # => [{{Exchema.Predicates, :map},[fields: [...]],{:nested_errors, ...] 133 | 134 | invalid |> Exchema.errors(MyStructure) |> Exchema.Error.flattened 135 | # => [ 136 | # {[:name, :first], {Exchema.Predicates, :is}, :binary, :not_a_binary}, 137 | # {[:name, :last], {Exchema.Predicates, :is}, :binary, :not_a_binary}, 138 | # {[:country], {Exchema.Predicates, :inclusion}, [:brazil, :canada, :portugal], 139 | # :invalid}, 140 | # {[:metadata], {Exchema.Predicates, :fun}, 141 | # #Function<0.33830354/1 in :elixir_compiler_0.__MODULE__/1>, :invalid} 142 | # ] 143 | ``` 144 | 145 | -------------------------------------------------------------------------------- /config/.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any exec using `mix credo -C `. If no exec name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: ["lib/", "src/", "web/", "apps/"], 25 | excluded: [~r"/_build/", ~r"/deps/"] 26 | }, 27 | # 28 | # If you create your own checks, you must specify the source files for 29 | # them here, so they can be loaded by Credo before running the analysis. 30 | # 31 | requires: [], 32 | # 33 | # If you want to enforce a style guide and need a more traditional linting 34 | # experience, you can change `strict` to `true` below: 35 | # 36 | strict: false, 37 | # 38 | # If you want to use uncolored output by default, you can change `color` 39 | # to `false` below: 40 | # 41 | color: true, 42 | # 43 | # You can customize the parameters of any check by adding a second element 44 | # to the tuple. 45 | # 46 | # To disable a check put `false` as second element: 47 | # 48 | # {Credo.Check.Design.DuplicatedCode, false} 49 | # 50 | checks: [ 51 | {Credo.Check.Consistency.ExceptionNames}, 52 | {Credo.Check.Consistency.LineEndings}, 53 | {Credo.Check.Consistency.ParameterPatternMatching}, 54 | {Credo.Check.Consistency.SpaceAroundOperators}, 55 | {Credo.Check.Consistency.SpaceInParentheses}, 56 | {Credo.Check.Consistency.TabsOrSpaces}, 57 | 58 | # You can customize the priority of any check 59 | # Priority values are: `low, normal, high, higher` 60 | # 61 | {Credo.Check.Design.AliasUsage, if_nested_deeper_than: 2}, 62 | 63 | # For some checks, you can also set other parameters 64 | # 65 | # If you don't want the `setup` and `test` macro calls in ExUnit tests 66 | # or the `schema` macro in Ecto schemas to trigger DuplicatedCode, just 67 | # set the `excluded_macros` parameter to `[:schema, :setup, :test]`. 68 | # 69 | {Credo.Check.Design.DuplicatedCode, excluded_macros: []}, 70 | 71 | # You can also customize the exit_status of each check. 72 | # If you don't want TODO comments to cause `mix credo` to fail, just 73 | # set this value to 0 (zero). 74 | # 75 | {Credo.Check.Design.TagTODO, exit_status: 2}, 76 | {Credo.Check.Design.TagFIXME}, 77 | 78 | {Credo.Check.Readability.FunctionNames}, 79 | {Credo.Check.Readability.LargeNumbers}, 80 | {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 100}, 81 | {Credo.Check.Readability.ModuleAttributeNames}, 82 | {Credo.Check.Readability.ModuleDoc}, 83 | {Credo.Check.Readability.ModuleNames}, 84 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs}, 85 | {Credo.Check.Readability.ParenthesesInCondition}, 86 | {Credo.Check.Readability.PredicateFunctionNames}, 87 | {Credo.Check.Readability.PreferImplicitTry}, 88 | {Credo.Check.Readability.RedundantBlankLines}, 89 | {Credo.Check.Readability.StringSigils}, 90 | {Credo.Check.Readability.TrailingBlankLine}, 91 | {Credo.Check.Readability.TrailingWhiteSpace}, 92 | {Credo.Check.Readability.VariableNames}, 93 | {Credo.Check.Readability.Semicolons}, 94 | {Credo.Check.Readability.SpaceAfterCommas}, 95 | 96 | {Credo.Check.Refactor.DoubleBooleanNegation}, 97 | {Credo.Check.Refactor.CondStatements}, 98 | {Credo.Check.Refactor.CyclomaticComplexity}, 99 | {Credo.Check.Refactor.FunctionArity}, 100 | {Credo.Check.Refactor.LongQuoteBlocks}, 101 | {Credo.Check.Refactor.MatchInCondition}, 102 | {Credo.Check.Refactor.NegatedConditionsInUnless}, 103 | {Credo.Check.Refactor.NegatedConditionsWithElse}, 104 | {Credo.Check.Refactor.Nesting}, 105 | {Credo.Check.Refactor.PipeChainStart}, 106 | {Credo.Check.Refactor.UnlessWithElse}, 107 | 108 | {Credo.Check.Warning.BoolOperationOnSameValues}, 109 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck}, 110 | {Credo.Check.Warning.IExPry}, 111 | {Credo.Check.Warning.IoInspect}, 112 | {Credo.Check.Warning.LazyLogging}, 113 | {Credo.Check.Warning.OperationOnSameValues}, 114 | {Credo.Check.Warning.OperationWithConstantResult}, 115 | {Credo.Check.Warning.UnusedEnumOperation}, 116 | {Credo.Check.Warning.UnusedFileOperation}, 117 | {Credo.Check.Warning.UnusedKeywordOperation}, 118 | {Credo.Check.Warning.UnusedListOperation}, 119 | {Credo.Check.Warning.UnusedPathOperation}, 120 | {Credo.Check.Warning.UnusedRegexOperation}, 121 | {Credo.Check.Warning.UnusedStringOperation}, 122 | {Credo.Check.Warning.UnusedTupleOperation}, 123 | {Credo.Check.Warning.RaiseInsideRescue}, 124 | 125 | # Controversial and experimental checks (opt-in, just remove `, false`) 126 | # 127 | {Credo.Check.Refactor.ABCSize, false}, 128 | {Credo.Check.Refactor.AppendSingleItem, false}, 129 | {Credo.Check.Refactor.VariableRebinding, false}, 130 | {Credo.Check.Warning.MapGetUnsafePass, false}, 131 | {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, 132 | 133 | # Deprecated checks (these will be deleted after a grace period) 134 | # 135 | {Credo.Check.Readability.Specs, false}, 136 | {Credo.Check.Warning.NameRedeclarationByAssignment, false}, 137 | {Credo.Check.Warning.NameRedeclarationByCase, false}, 138 | {Credo.Check.Warning.NameRedeclarationByDef, false}, 139 | {Credo.Check.Warning.NameRedeclarationByFn, false}, 140 | 141 | # Custom checks can be created using `mix credo.gen.check`. 142 | # 143 | ] 144 | } 145 | ] 146 | } 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Exchema 2 | 3 | Exchema is a library to define, validate and coerce data. It allows 4 | you to check the type for a given value at runtime (it is not static 5 | type checking). 6 | 7 | It uses the idea of **refinement types**, in which we have a global type 8 | (which all values belong) and can refine that type with the use of 9 | **predicates**. 10 | 11 | Also, check [`exchema_coercion`](https://github.com/bamorim/exchema_coercion) and [`exchema_stream_data`](https://github.com/bamorim/exchema_stream_data) 12 | 13 | It also comes with a neat DSL to help you define your types. 14 | 15 | The macros you need to keep in mind are `subtype/2`, `structure/1` and `refine/1` 16 | 17 | ```elixir 18 | import Exchema.Notation 19 | 20 | defmodule Name, do: subtype(Exchema.Types.String, []) 21 | 22 | defmodule Continent do 23 | subtype(Exchema.Types.Atom, [inclusion: ~w{europe north_america, south_america}a]) 24 | end 25 | 26 | defmodule Country do 27 | subtype(Exchema.Types.Atom, [inclusion: ~w{brazil canada portugal}a]) 28 | def continent_for(country) do 29 | case country do 30 | :brazil -> :south_america, 31 | :canada -> :north_america, 32 | _ -> :europe 33 | end 34 | end 35 | end 36 | 37 | defmodule Metadata, do: subtype(:any, [fun: &(is_list(&1) || is_map(&1))]) 38 | 39 | defmodule FullName, do: structure([first: Name, last: Name]) 40 | 41 | defmodule MyStructure do 42 | structure [ 43 | name: FullName, 44 | country: Country, 45 | continent: Continent, 46 | metadata: Metadata 47 | ] 48 | 49 | refine([fun: fn %{country: country, continent: continent} -> 50 | Country.continent_for(country) == continent 51 | end]) 52 | 53 | def valid do 54 | %MyStructure{ 55 | name: %FullName{ 56 | first: "Bernardo", 57 | last: "Amorim" 58 | }, 59 | country: :brazil, 60 | continent: :south_america, 61 | metadata: %{any: :thing} 62 | } 63 | end 64 | 65 | def invalid do 66 | %MyStructure{ 67 | name: %FullName{ 68 | first: 1234, 69 | last: :not_a_string 70 | }, 71 | country: :croatia, 72 | continent: :oceania, 73 | metadata: :not_a_list_nor_a_map 74 | } 75 | end 76 | end 77 | 78 | Exchema.is?(MyStructure.valid, MyStructure) 79 | # => true 80 | 81 | Exchema.is?(MyStructure.invalid, MyStructure) 82 | # => false 83 | 84 | Exchema.errors(invalid, MyStructure) 85 | # => [{{Exchema.Predicates, :map},[fields: [...]],{:nested_errors, ...] 86 | 87 | invalid |> Exchema.errors(MyStructure) |> Exchema.Error.flattened 88 | # => [ 89 | # {[:name, :first], {Exchema.Predicates, :is}, :binary, :not_a_binary}, 90 | # {[:name, :last], {Exchema.Predicates, :is}, :binary, :not_a_binary}, 91 | # {[:country], {Exchema.Predicates, :inclusion}, [:brazil, :canada, :portugal], 92 | # :invalid}, 93 | # {[:metadata], {Exchema.Predicates, :fun}, 94 | # #Function<0.33830354/1 in :elixir_compiler_0.__MODULE__/1>, :invalid} 95 | # ] 96 | ``` 97 | 98 | ## Simplifying 99 | 100 | Sometimes typing `defmodule` is boring, that is why there are higher-arity versions of the macros. 101 | Also, if the only refinement you want is a function, you can pass it directly (instead of the predicate 102 | `[fun: &my_function/1]` you can pass `&my_function/1` directly) 103 | 104 | You can use this to define the same schema in a different way: 105 | 106 | ```elixir 107 | subtype(Name, Exchema.Types.String, []) 108 | subtype(Continent, Exchema.Types.Atom, inclusion: ~w{europe north_america, south_america}a) 109 | subtype(Country, Exchema.Types.Atom, inclusion: ~w{brazil canada portugal}a) do 110 | def continent_for(country) do 111 | # ... 112 | end 113 | end 114 | subtype(Metadata, :any, &(is_list(&1) || is_map(&1))) 115 | structure(FullName, first: Name, last: Name) 116 | structure( 117 | MyStructure, 118 | [ 119 | name: FullName, 120 | country: Country, 121 | continent: Continent, 122 | metadata: Metadata 123 | ] 124 | ) do 125 | refine([fun: fn %{country: country, continent: continent} -> 126 | Country.continent_for(country) == continent 127 | end]) 128 | end 129 | ``` 130 | 131 | ## Checking types 132 | 133 | Exchema ships with some predefined types that you can check using 134 | `Exchema.is?/2` 135 | 136 | ```elixir 137 | iex> Exchema.is?("1234", Exchema.Types.String) 138 | true 139 | 140 | iex> Exchema.is?(1234, Exchema.Types.String) 141 | false 142 | 143 | iex> Exchema.is?(1234, Exchema.Types.Integer) 144 | true 145 | ``` 146 | 147 | There is also the global type `:any` 148 | 149 | ```elixir 150 | iex> Exchema.is?("1234", :any) 151 | true 152 | 153 | iex> Exchema.is?(1234, :any) 154 | true 155 | ``` 156 | 157 | ## Parametric types 158 | 159 | A type can be specialized, e.g. lists can have an inner type specified, so 160 | `{Exchema.Types.List, Exchema.Types.Integer}` represents a list of integers. 161 | 162 | In the case of list, you can just use and not specify it directly, so 163 | `Exchema.Types.List` is a list of elements of any type, or 164 | `{Exchema.Types.List, :any}`. 165 | 166 | Some types can have multiple parameters, e.g. a map. 167 | `{Exchema.Types.Map, {Exchema.Types.String, Exchema.Types.Integer}}` represents 168 | a map from strings to integer. 169 | 170 | Types with 0 params can be represented just by the module name. 171 | Types with 1 param can be represented by a tuple `{type, argument}` 172 | Types with N params can be represented by a tuple `{type, arguments}` where 173 | arguments is a tuple with N elements. 174 | 175 | ```elixir 176 | iex> Exchema.is?([1,2,3], {Exchema.Types.List, Exchema.Types.Integer}) 177 | true 178 | 179 | iex> Exchema.is?([1, "2", 3], {Exchema.Types.List, Exchema.Types.Integer}) 180 | false 181 | 182 | iex> Exchema.is?(%{a: 1}, {Exchema.Types.Map, {Exchema.Types.Atom, Exchema.Types.Integer}}) 183 | true 184 | ``` 185 | 186 | ## Defining your own types 187 | 188 | When defining types we need to understand `subtype` and `structure` and `refine`. 189 | 190 | ### Subtype 191 | 192 | It defines a subtype given the original type and a list of refinements. 193 | 194 | ```elixir 195 | defmodule ShortString do 196 | import Exchema.Notation 197 | subtype Exchema.Types.String, [] 198 | end 199 | ``` 200 | 201 | ## About Types 202 | 203 | A type can be: 204 | 205 | - the global type `:any` 206 | - a type reference such as `Exchema.Types.String` 207 | - a type refinement such as `{:ref, :any, length: 1}` (more on that later) 208 | - a type application (for parametric types) such as `{Exchema.Types.List, Exchema.Types.String}` 209 | 210 | ## Installation 211 | 212 | Add `exchema` to your list of dependencies in `mix.exs`: 213 | 214 | ```elixir 215 | def deps do 216 | [ 217 | {:exchema, "~> 0.3.0"} 218 | ] 219 | end 220 | ``` 221 | 222 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 223 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 224 | be found at [https://hexdocs.pm/exchema](https://hexdocs.pm/exchema). 225 | -------------------------------------------------------------------------------- /lib/exchema/predicates.ex: -------------------------------------------------------------------------------- 1 | defmodule Exchema.Predicates do 2 | @moduledoc """ 3 | Exschema default predicates library 4 | """ 5 | 6 | @type error :: {:error, any} 7 | @type failure :: false | error | [error, ...] 8 | @type success :: :ok | true | [] 9 | @type result :: failure | success 10 | 11 | @doc """ 12 | Just applies the function as if it was a predicate. 13 | It also checks for exceptions to allow simpler functions. 14 | 15 | ## Examples 16 | 17 | iex> Exchema.Predicates.fun(1, &is_integer/1) 18 | true 19 | 20 | iex> Exchema.Predicates.fun("1", &is_integer/1) 21 | false 22 | 23 | iex> Exchema.Predicates.fun(1, &(&1 > 0)) 24 | true 25 | 26 | iex> Exchema.Predicates.fun(0, &(&1 > 0)) 27 | false 28 | 29 | iex> Exchema.Predicates.fun(1, fn _ -> {:error, :custom_error} end) 30 | {:error, :custom_error} 31 | 32 | iex> Exchema.Predicates.fun(1, fn _ -> raise RuntimeError end) 33 | {:error, :thrown} 34 | 35 | """ 36 | @spec fun(any, ((any) -> result)) :: result 37 | def fun(val, fun) do 38 | fun.(val) 39 | rescue 40 | _ -> {:error, :thrown} 41 | end 42 | 43 | @doc """ 44 | Checks the list type 45 | It can also check the types of the elemsts of the list by 46 | passing the `:element_type` param. 47 | 48 | ## Examples 49 | 50 | iex> Exchema.Predicates.list("", :any) 51 | {:error, :not_a_list} 52 | 53 | iex> Exchema.Predicates.list([], :any) 54 | :ok 55 | 56 | iex> Exchema.Predicates.list(["",1,""], Exchema.Types.Integer) 57 | {:error, { 58 | :nested_errors, 59 | [ 60 | {0, [{{Exchema.Predicates, :is}, :integer, :not_an_integer}]}, 61 | {2, [{{Exchema.Predicates, :is}, :integer, :not_an_integer}]} 62 | ]} 63 | } 64 | 65 | iex> Exchema.Predicates.list([1,2,3], Exchema.Types.Integer) 66 | :ok 67 | 68 | """ 69 | def list(list, _) when not is_list(list) do 70 | {:error, :not_a_list} 71 | end 72 | def list(_list, :any), do: :ok 73 | def list(list, element_type) do 74 | list 75 | |> Enum.with_index 76 | |> Enum.map(fn {e, idx} -> {idx, Exchema.errors(e, element_type)} end) 77 | |> Enum.filter(fn {_, err} -> length(err) > 0 end) 78 | |> nested_errors 79 | end 80 | 81 | defp nested_errors(errors, error_key \\ :nested_errors) 82 | defp nested_errors([], _), do: :ok 83 | defp nested_errors(errors, error_key) do 84 | {:error, {error_key, errors}} 85 | end 86 | 87 | @doc """ 88 | Checks whether or not the given value is a struct or a specific struct. 89 | 90 | Note: It's named `is_struct` to avoid conflict with `Kernel.struct`. 91 | 92 | ## Examples 93 | 94 | iex> Exchema.Predicates.is_struct(%{}, []) 95 | {:error, :not_a_struct} 96 | 97 | iex> Exchema.Predicates.is_struct(nil, []) 98 | {:error, :not_a_struct} 99 | 100 | Also, keep in mind that many internal types are actually structs 101 | 102 | iex> Exchema.Predicates.is_struct(DateTime.utc_now, nil) 103 | :ok 104 | 105 | iex> Exchema.Predicates.is_struct(NaiveDateTime.utc_now, nil) 106 | :ok 107 | 108 | iex> Exchema.Predicates.is_struct(DateTime.utc_now, DateTime) 109 | :ok 110 | 111 | iex> Exchema.Predicates.is_struct(DateTime.utc_now, NaiveDateTime) 112 | {:error, :invalid_struct} 113 | 114 | iex> Exchema.Predicates.is_struct(NaiveDateTime.utc_now, DateTime) 115 | {:error, :invalid_struct} 116 | 117 | iex> Exchema.Predicates.is_struct(DateTime.utc_now, [NaiveDateTime, DateTime]) 118 | :ok 119 | 120 | iex> Exchema.Predicates.is_struct(Date.utc_today, [NaiveDateTime, DateTime]) 121 | {:error, :invalid_struct} 122 | 123 | """ 124 | def is_struct(%{__struct__: real}, expected), do: check_struct(real, expected) 125 | def is_struct(_, _), do: {:error, :not_a_struct} 126 | 127 | defp check_struct(real, expected) when expected == real, do: :ok 128 | defp check_struct(_, expected) when expected in [nil, :any], do: :ok 129 | defp check_struct(real, alloweds) when is_list(alloweds) do 130 | if real in alloweds, do: :ok, else: {:error, :invalid_struct} 131 | end 132 | defp check_struct(_,_), do: {:error, :invalid_struct} 133 | 134 | @doc """ 135 | Checks the key types of a map 136 | 137 | ## Examples 138 | 139 | iex> Exchema.Predicates.key_type("", :any) 140 | {:error, :not_a_map} 141 | 142 | iex > Exchema.Predicates.key_type(%{1 => "value"}, Exchema.Types.Integer) 143 | :ok 144 | 145 | iex > Exchema.Predicates.key_type(%{"key" => 1}, Exchema.Types.Integer) 146 | {:error, { 147 | :key_errors, 148 | [{"key", [{{Exchema.Predicates, :is}, :integer, :not_an_integer}]}] 149 | }} 150 | """ 151 | def key_type(%{} = map, type) do 152 | map 153 | |> Map.keys 154 | |> Enum.flat_map(& Exchema.errors(&1, type)) 155 | |> nested_errors(:key_errors) 156 | end 157 | def key_type(_, _), do: {:error, :not_a_map} 158 | 159 | @doc """ 160 | Checks the types of the values of a Map or Keyword List 161 | 162 | ## Examples 163 | 164 | iex > Exchema.Predicates.value_type(%{"key" => 1}, Exchema.Types.Integer) 165 | :ok 166 | 167 | iex > Exchema.Predicates.value_type([key: 1], Exchema.Types.Integer) 168 | :ok 169 | 170 | iex > Exchema.Predicates.value_type(%{1 => "value"}, Exchema.Types.Integer) 171 | {:error, { 172 | :nested_errors, 173 | [{1, [{{Exchema.Predicates, :is}, :integer, :not_an_integer}]}] 174 | }} 175 | 176 | iex > Exchema.Predicates.value_type([foo: :bar], Exchema.Types.Integer) 177 | {:error, { 178 | :nested_errors, 179 | [{:foo, [{{Exchema.Predicates, :is}, :integer, :not_an_integer}]}] 180 | }} 181 | """ 182 | def value_type(%{} = map, type), do: do_value_type(Map.to_list(map), type) 183 | def value_type(kwlist, type) do 184 | if Keyword.keyword?(kwlist) do 185 | do_value_type(kwlist, type) 186 | else 187 | {:error, :not_list_or_map} 188 | end 189 | end 190 | 191 | defp do_value_type(tuple_list, type) do 192 | tuple_list 193 | |> Enum.flat_map(fn {key, value} -> 194 | case Exchema.errors(value, type) do 195 | [] -> [] 196 | errs -> [{key, errs}] 197 | end 198 | end) 199 | |> nested_errors(:nested_errors) 200 | end 201 | 202 | @doc """ 203 | Checks the types of specific fields of a Map or Keyword List 204 | 205 | ## Examples 206 | 207 | iex> Exchema.Predicates.fields(%{foo: 1}, foo: Exchema.Types.Integer) 208 | :ok 209 | 210 | iex> Exchema.Predicates.fields([foo: 1], foo: Exchema.Types.Integer) 211 | :ok 212 | 213 | iex> Exchema.Predicates.fields(%{foo: :bar}, foo: Exchema.Types.Integer) 214 | {:error, { 215 | :nested_errors, 216 | [{:foo, [{{Exchema.Predicates, :is}, :integer, :not_an_integer}]}] 217 | }} 218 | """ 219 | def fields(%{} = map, fields), do: do_fields(map, &Map.get/2, fields) 220 | def fields(kwlist, fields) do 221 | if Keyword.keyword?(kwlist) do 222 | do_fields(kwlist, &Keyword.get/2, fields) 223 | else 224 | {:error, :not_list_or_map} 225 | end 226 | end 227 | defp do_fields(collection, get_fn, fields) do 228 | fields 229 | |> Enum.flat_map(fn {key, type} -> 230 | case Exchema.errors(get_fn.(collection, key), type) do 231 | [] -> [] 232 | errs -> [{key, errs}] 233 | end 234 | end) 235 | |> nested_errors(:nested_errors) 236 | end 237 | 238 | @doc """ 239 | Checks against system guards like `is_integer` or `is_float`. 240 | 241 | ## Examples 242 | 243 | iex> Exchema.Predicates.is(1, :integer) 244 | :ok 245 | 246 | iex> Exchema.Predicates.is(1.0, :float) 247 | :ok 248 | 249 | iex> Exchema.Predicates.is(1, :nil) 250 | {:error, :not_nil} 251 | 252 | iex> Exchema.Predicates.is(1, :atom) 253 | {:error, :not_an_atom} 254 | 255 | iex> Exchema.Predicates.is(nil, :binary) 256 | {:error, :not_a_binary} 257 | 258 | iex> Exchema.Predicates.is(nil, :bitstring) 259 | {:error, :not_a_bitstring} 260 | 261 | iex> Exchema.Predicates.is(nil, :boolean) 262 | {:error, :not_a_boolean} 263 | 264 | iex> Exchema.Predicates.is(nil, :float) 265 | {:error, :not_a_float} 266 | 267 | iex> Exchema.Predicates.is(nil, :function) 268 | {:error, :not_a_function} 269 | 270 | iex> Exchema.Predicates.is(nil, :integer) 271 | {:error, :not_an_integer} 272 | 273 | iex> Exchema.Predicates.is(nil, :list) 274 | {:error, :not_a_list} 275 | 276 | iex> Exchema.Predicates.is(nil, :map) 277 | {:error, :not_a_map} 278 | 279 | iex> Exchema.Predicates.is(nil, :number) 280 | {:error, :not_a_number} 281 | 282 | iex> Exchema.Predicates.is(nil, :pid) 283 | {:error, :not_a_pid} 284 | 285 | iex> Exchema.Predicates.is(nil, :port) 286 | {:error, :not_a_port} 287 | 288 | iex> Exchema.Predicates.is(nil, :reference) 289 | {:error, :not_a_reference} 290 | 291 | iex> Exchema.Predicates.is(nil, :tuple) 292 | {:error, :not_a_tuple} 293 | 294 | """ 295 | # Explicit nil case becasue Kernel.is_nil is a macro 296 | def is(nil, nil), do: :ok 297 | def is(_, nil), do: {:error, :not_nil} 298 | def is(val, key) do 299 | if apply(Kernel, :"is_#{key}", [val]) do 300 | :ok 301 | else 302 | {:error, is_error_msg(key)} 303 | end 304 | end 305 | 306 | defp is_error_msg(:atom), do: :not_an_atom 307 | defp is_error_msg(:integer), do: :not_an_integer 308 | defp is_error_msg(key), do: :"not_a_#{key}" 309 | 310 | @doc """ 311 | Ensure the value is in a list of values 312 | 313 | ## Examples 314 | 315 | iex> Exchema.Predicates.inclusion("apple", ["apple", "banana"]) 316 | :ok 317 | 318 | iex> Exchema.Predicates.inclusion(5, 1..10) 319 | :ok 320 | 321 | iex> Exchema.Predicates.inclusion("horse", ["apple", "banana"]) 322 | {:error, :invalid} 323 | 324 | """ 325 | def inclusion(val, values) do 326 | if val in values, do: :ok, else: {:error, :invalid} 327 | end 328 | 329 | @doc """ 330 | Ensure the value is not in a list of values 331 | 332 | ## Examples 333 | 334 | iex> Exchema.Predicates.exclusion("apple", ["apple", "banana"]) 335 | {:error, :invalid} 336 | 337 | iex> Exchema.Predicates.exclusion(5, 1..10) 338 | {:error, :invalid} 339 | 340 | iex> Exchema.Predicates.exclusion("horse", ["apple", "banana"]) 341 | :ok 342 | 343 | """ 344 | def exclusion(val, values) do 345 | if val in values, do: {:error, :invalid}, else: :ok 346 | end 347 | 348 | @doc """ 349 | Checks against a specific regex format 350 | 351 | ## Examples 352 | 353 | iex> Exchema.Predicates.format("starts-with", ~r/^starts-/) 354 | :ok 355 | 356 | iex> Exchema.Predicates.format("does-not-starts-with", ~r/^starts-/) 357 | {:error, :invalid} 358 | """ 359 | def format(val, regex) when is_binary(val) do 360 | if Regex.match?(regex, val), do: :ok, else: {:error, :invalid} 361 | end 362 | def format(_, _), do: {:error, :invalid} 363 | 364 | @doc """ 365 | Checks the length of the input. You can pass a max, a min, a range or a specific lenght. 366 | 367 | Can check length of either lists, strings or tuples. 368 | 369 | ## Examples 370 | 371 | iex> Exchema.Predicates.length("123", 3) 372 | :ok 373 | 374 | iex> Exchema.Predicates.length([1,2,3], 3) 375 | :ok 376 | 377 | iex> Exchema.Predicates.length({1,2,3}, 3) 378 | :ok 379 | 380 | iex> Exchema.Predicates.length([1,2,3], min: 2) 381 | :ok 382 | 383 | iex> Exchema.Predicates.length([1,2,3], max: 3) 384 | :ok 385 | 386 | iex> Exchema.Predicates.length([1,2,3], 2..4) 387 | :ok 388 | 389 | iex> Exchema.Predicates.length([1,2,3], min: 2, max: 4) 390 | :ok 391 | 392 | iex> Exchema.Predicates.length([1,2,3], min: 4) 393 | {:error, :invalid_length} 394 | 395 | iex> Exchema.Predicates.length([1,2,3], max: 2) 396 | {:error, :invalid_length} 397 | 398 | iex> Exchema.Predicates.length([1,2,3], min: 1, max: 2) 399 | {:error, :invalid_length} 400 | 401 | iex> Exchema.Predicates.length([1,2,3], 2) 402 | {:error, :invalid_length} 403 | 404 | iex> Exchema.Predicates.length([1,2,3], 1..2) 405 | {:error, :invalid_length} 406 | 407 | """ 408 | def length(val, opts) when is_binary(val) do 409 | compare_length(String.length(val), length_bounds(opts)) 410 | end 411 | def length(val, opts) when is_tuple(val) do 412 | compare_length(val |> Tuple.to_list |> length, length_bounds(opts)) 413 | end 414 | def length(val, opts) when is_list(val) do 415 | compare_length(length(val), length_bounds(opts)) 416 | end 417 | def length(_, _), do: {:error, :invalid} 418 | 419 | defp length_bounds(n) when is_integer(n), do: {n, n} 420 | defp length_bounds(%{__struct__: Range, first: min, last: max}), do: {min, max} 421 | defp length_bounds(opts) when is_list(opts) do 422 | {Keyword.get(opts, :min), Keyword.get(opts, :max)} 423 | end 424 | defp length_bounds(_), do: {nil, nil} 425 | 426 | defp compare_length(_, {nil, nil}), do: :ok 427 | defp compare_length(l, {min, nil}) do 428 | if min > l, do: {:error, :invalid_length}, else: :ok 429 | end 430 | defp compare_length(l, {nil, max}) do 431 | if max < l, do: {:error, :invalid_length}, else: :ok 432 | end 433 | defp compare_length(l, {min, max}) when min > l or max < l, do: {:error, :invalid_length} 434 | defp compare_length(_, _), do: :ok 435 | 436 | @doc """ 437 | Checks if something is greater than a value 438 | 439 | iex> Exchema.Predicates.gt(2, 1) 440 | :ok 441 | 442 | iex> Exchema.Predicates.gt(2, 2) 443 | {:error, :not_greater} 444 | 445 | iex> Exchema.Predicates.gt(2, 3) 446 | {:error, :not_greater} 447 | 448 | iex> Exchema.Predicates.gt("b", "a") 449 | :ok 450 | 451 | iex> Exchema.Predicates.gt("a", "b") 452 | {:error, :not_greater} 453 | """ 454 | def gt(a, b) when a > b, do: :ok 455 | def gt(_, _), do: {:error, :not_greater} 456 | 457 | @doc """ 458 | Checks if something is greater than or equal to a value 459 | 460 | iex> Exchema.Predicates.gte(2, 1) 461 | :ok 462 | 463 | iex> Exchema.Predicates.gte(2, 2) 464 | :ok 465 | 466 | iex> Exchema.Predicates.gte(2, 3) 467 | {:error, :not_greater_or_equal} 468 | 469 | iex> Exchema.Predicates.gte("b", "a") 470 | :ok 471 | 472 | iex> Exchema.Predicates.gte("a", "b") 473 | {:error, :not_greater_or_equal} 474 | """ 475 | def gte(a, b) when a >= b, do: :ok 476 | def gte(_, _), do: {:error, :not_greater_or_equal} 477 | 478 | @doc """ 479 | Checks if something is lesser than a value 480 | 481 | iex> Exchema.Predicates.lt(1, 2) 482 | :ok 483 | 484 | iex> Exchema.Predicates.lt(2, 2) 485 | {:error, :not_lesser} 486 | 487 | iex> Exchema.Predicates.lt(3, 2) 488 | {:error, :not_lesser} 489 | 490 | iex> Exchema.Predicates.lt("a", "b") 491 | :ok 492 | 493 | iex> Exchema.Predicates.lt("b", "a") 494 | {:error, :not_lesser} 495 | """ 496 | def lt(a, b) when a < b, do: :ok 497 | def lt(_, _), do: {:error, :not_lesser} 498 | 499 | @doc """ 500 | Checks if something is lesser than or equal a value 501 | 502 | iex> Exchema.Predicates.lte(1, 2) 503 | :ok 504 | 505 | iex> Exchema.Predicates.lte(2, 2) 506 | :ok 507 | 508 | iex> Exchema.Predicates.lte(3, 2) 509 | {:error, :not_lesser_or_equal} 510 | 511 | iex> Exchema.Predicates.lte("a", "b") 512 | :ok 513 | 514 | iex> Exchema.Predicates.lte("b", "a") 515 | {:error, :not_lesser_or_equal} 516 | """ 517 | def lte(a, b) when a <= b, do: :ok 518 | def lte(_, _), do: {:error, :not_lesser_or_equal} 519 | end 520 | --------------------------------------------------------------------------------