Current section

74 Versions

Jump to

Compare versions

17 files changed
+270 additions
-80 deletions
  @@ -2,6 +2,22 @@
2 2
3 3 All notable changes to this project will be documented in this file.
4 4
5 + ## 0.2.2 - 2025-08-06
6 +
7 + ### Added
8 +
9 + - Guides for using `Zoi` in Phoenix controllers
10 + - New `Zoi.tuple/2` type
11 + - New `Zoi.any/1` type
12 + - New `Zoi.nullable/2` type
13 +
14 + ### Changed
15 +
16 + - Improved error messages for all validations and types
17 + - `Zoi.treefy_errors/1` now returns a more human-readable structure
18 + - `Zoi.optional/2` cannot accept `nil` as a value anymore. Use `Zoi.nullable/2` instead.
19 + - `Zoi.optional/2` inside `Zoi.object/2` now handles optional fields correctly
20 +
5 21 ## 0.2.1 - 2025-08-06
6 22
7 23 ### Added
  @@ -2,7 +2,7 @@
2 2 [{<<"Changelog">>,<<"https://hexdocs.pm/zoi/changelog.html">>},
3 3 {<<"GitHub">>,<<"https://github.com/phcurado/zoi">>}]}.
4 4 {<<"name">>,<<"zoi">>}.
5 - {<<"version">>,<<"0.2.1">>}.
5 + {<<"version">>,<<"0.2.2">>}.
6 6 {<<"description">>,
7 7 <<"Zoi is a schema validation library for Elixir, designed to provide a simple and flexible way to define and validate data.">>}.
8 8 {<<"elixir">>,<<"~> 1.14">>}.
  @@ -13,12 +13,14 @@
13 13 <<"lib/zoi/error.ex">>,<<"lib/zoi/refinements.ex">>,<<"lib/zoi/type">>,
14 14 <<"lib/zoi/type/def.ex">>,<<"lib/zoi/types">>,
15 15 <<"lib/zoi/types/integer.ex">>,<<"lib/zoi/types/default.ex">>,
16 - <<"lib/zoi/types/number.ex">>,<<"lib/zoi/types/object.ex">>,
17 - <<"lib/zoi/types/optional.ex">>,<<"lib/zoi/types/float.ex">>,
16 + <<"lib/zoi/types/number.ex">>,<<"lib/zoi/types/nullable.ex">>,
17 + <<"lib/zoi/types/object.ex">>,<<"lib/zoi/types/optional.ex">>,
18 + <<"lib/zoi/types/tuple.ex">>,<<"lib/zoi/types/float.ex">>,
18 19 <<"lib/zoi/types/string.ex">>,<<"lib/zoi/types/boolean.ex">>,
19 20 <<"lib/zoi/types/enum.ex">>,<<"lib/zoi/types/array.ex">>,
20 21 <<"lib/zoi/types/union.ex">>,<<"lib/zoi/types/meta.ex">>,
21 - <<"lib/zoi/type.ex">>,<<"lib/zoi/errors.ex">>,<<"lib/zoi/transforms.ex">>,
22 - <<"mix.exs">>,<<"README.md">>,<<"CHANGELOG.md">>]}.
22 + <<"lib/zoi/types/any.ex">>,<<"lib/zoi/type.ex">>,<<"lib/zoi/errors.ex">>,
23 + <<"lib/zoi/transforms.ex">>,<<"mix.exs">>,<<"README.md">>,
24 + <<"CHANGELOG.md">>]}.
23 25 {<<"requirements">>,[]}.
24 26 {<<"build_tools">>,[<<"mix">>]}.
  @@ -43,7 +43,7 @@ defmodule Zoi do
43 43 ## Coercion
44 44 By default, `Zoi` will not attempt to infer input data to match the expected type. For example, if you define a schema that expects a string, passing an integer will result in an error.
45 45 iex> Zoi.string() |> Zoi.parse(123)
46 - {:error, [%Zoi.Error{message: "invalid string type"}]}
46 + {:error, [%Zoi.Error{message: "invalid type: must be a string"}]}
47 47
48 48 If you need coercion, you can enable it by passing the `:coerce` option:
49 49
  @@ -192,6 +192,24 @@ defmodule Zoi do
192 192 @doc group: "Basic Types"
193 193 defdelegate boolean(opts \\ []), to: Zoi.Types.Boolean, as: :new
194 194
195 + @doc """
196 + Defines a schema that accepts any type of input.
197 +
198 + This is useful when you want to allow any data type without validation.
199 +
200 + ## Example
201 +
202 + iex> schema = Zoi.any()
203 + iex> Zoi.parse(schema, "hello")
204 + {:ok, "hello"}
205 + iex> Zoi.parse(schema, 42)
206 + {:ok, 42}
207 + iex> Zoi.parse(schema, %{key: "value"})
208 + {:ok, %{key: "value"}}
209 + """
210 + @doc group: "Basic Types"
211 + defdelegate any(opts \\ []), to: Zoi.Types.Any, as: :new
212 +
195 213 @doc """
196 214 Makes the schema optional for the `Zoi.object/2` type.
197 215
  @@ -204,6 +222,19 @@ defmodule Zoi do
204 222 @doc group: "Encapsulated Types"
205 223 defdelegate optional(opts \\ []), to: Zoi.Types.Optional, as: :new
206 224
225 + @doc """
226 + Defines a schema that allows `nil` values.
227 +
228 + ## Examples
229 + iex> schema = Zoi.string() |> Zoi.nullable()
230 + iex> Zoi.parse(schema, nil)
231 + {:ok, nil}
232 + iex> Zoi.parse(schema, "hello")
233 + {:ok, "hello"}
234 + """
235 + @doc group: "Encapsulated Types"
236 + defdelegate nullable(opts \\ []), to: Zoi.Types.Nullable, as: :new
237 +
207 238 @doc """
208 239 Creates a default value for the schema.
209 240
  @@ -291,6 +322,20 @@ defmodule Zoi do
291 322 @doc group: "Complex Types"
292 323 defdelegate object(fields, opts \\ []), to: Zoi.Types.Object, as: :new
293 324
325 + @doc """
326 + Defines a tuple type schema.
327 +
328 + Use `Zoi.tuple(fields)` to define a tuple with specific types for each element:
329 +
330 + iex> schema = Zoi.tuple({Zoi.string(), Zoi.integer()})
331 + iex> Zoi.parse(schema, {"hello", 42})
332 + {:ok, {"hello", 42}}
333 + iex> Zoi.parse(schema, {"hello", "world"})
334 + {:error, [%Zoi.Error{message: "invalid type: must be an integer", path: [1]}]}
335 + """
336 + @doc group: "Complex Types"
337 + defdelegate tuple(fields, opts \\ []), to: Zoi.Types.Tuple, as: :new
338 +
294 339 @doc """
295 340 Defines a array type schema.
296 341
  @@ -601,7 +646,7 @@ defmodule Zoi do
601 646 @spec treefy_errors([Zoi.Error.t()]) :: map()
602 647 def treefy_errors(errors) when is_list(errors) do
603 648 Enum.reduce(errors, %{}, fn %Zoi.Error{path: path} = error, acc ->
604 - insert_error(acc, path, error)
649 + insert_error(acc, path, error.message)
605 650 end)
606 651 end
  @@ -5,7 +5,7 @@ defmodule Zoi.Refinements do
5 5 if String.length(input) >= min do
6 6 :ok
7 7 else
8 - {:error, "minimum length is #{min}"}
8 + {:error, "too small: must have at least #{min} characters"}
9 9 end
10 10 end
11 11
  @@ -13,7 +13,7 @@ defmodule Zoi.Refinements do
13 13 if input >= min do
14 14 :ok
15 15 else
16 - {:error, "minimum value is #{min}"}
16 + {:error, "too small: must be at least #{min}"}
17 17 end
18 18 end
19 19
  @@ -21,7 +21,7 @@ defmodule Zoi.Refinements do
21 21 if input >= min do
22 22 :ok
23 23 else
24 - {:error, "minimum value is #{min}"}
24 + {:error, "too small: must be at least #{min}"}
25 25 end
26 26 end
27 27
  @@ -29,7 +29,7 @@ defmodule Zoi.Refinements do
29 29 if length(input) >= min do
30 30 :ok
31 31 else
32 - {:error, "minimum array length is #{min}"}
32 + {:error, "too small: must have at least #{min} items"}
33 33 end
34 34 end
35 35
  @@ -37,7 +37,7 @@ defmodule Zoi.Refinements do
37 37 if String.length(input) <= max do
38 38 :ok
39 39 else
40 - {:error, "maximum length is #{max}"}
40 + {:error, "too big: must have at most #{max} characters"}
41 41 end
42 42 end
43 43
  @@ -45,7 +45,7 @@ defmodule Zoi.Refinements do
45 45 if input <= max do
46 46 :ok
47 47 else
48 - {:error, "maximum value is #{max}"}
48 + {:error, "too big: must be at most #{max}"}
49 49 end
50 50 end
51 51
  @@ -53,7 +53,7 @@ defmodule Zoi.Refinements do
53 53 if input <= max do
54 54 :ok
55 55 else
56 - {:error, "maximum value is #{max}"}
56 + {:error, "too big: must be at most #{max}"}
57 57 end
58 58 end
59 59
  @@ -61,7 +61,7 @@ defmodule Zoi.Refinements do
61 61 if length(input) <= max do
62 62 :ok
63 63 else
64 - {:error, "maximum length is #{max}"}
64 + {:error, "too big: must have at most #{max} items"}
65 65 end
66 66 end
67 67
  @@ -69,7 +69,7 @@ defmodule Zoi.Refinements do
69 69 if String.length(input) == length do
70 70 :ok
71 71 else
72 - {:error, "length must be #{length}"}
72 + {:error, "Invalid length: must have #{length} characters"}
73 73 end
74 74 end
75 75
  @@ -77,12 +77,13 @@ defmodule Zoi.Refinements do
77 77 if length(input) == length do
78 78 :ok
79 79 else
80 - {:error, "length must be #{length}"}
80 + {:error, "Invalid length: must have #{length} items"}
81 81 end
82 82 end
83 83
84 84 def refine(%Zoi.Types.String{}, input, [regex: regex], opts) do
85 - message = Keyword.get(opts, :message, "regex does not match")
85 + message =
86 + Keyword.get(opts, :message, "Invalid string: must match a patterh #{inspect(regex)}")
86 87
87 88 if String.match?(input, regex) do
88 89 :ok
  @@ -95,7 +96,7 @@ defmodule Zoi.Refinements do
95 96 if String.starts_with?(input, prefix) do
96 97 :ok
97 98 else
98 - {:error, "must start with '#{prefix}'"}
99 + {:error, "Invalid string: must start with '#{prefix}'"}
99 100 end
100 101 end
101 102
  @@ -103,7 +104,7 @@ defmodule Zoi.Refinements do
103 104 if String.ends_with?(input, suffix) do
104 105 :ok
105 106 else
106 - {:error, "must end with '#{suffix}'"}
107 + {:error, "Invalid string: must end with '#{suffix}'"}
107 108 end
108 109 end
  @@ -0,0 +1,12 @@
1 + defmodule Zoi.Types.Any do
2 + @moduledoc false
3 + use Zoi.Type.Def
4 +
5 + def new(opts \\ []) do
6 + apply_type(opts)
7 + end
8 +
9 + defimpl Zoi.Type do
10 + def parse(%Zoi.Types.Any{}, value, _opts), do: {:ok, value}
11 + end
12 + end
Loading more files…