Skip to content

Commit 5f5c37c

Browse files
committed
feat: peri validation for Lexicons
array validator
1 parent 252a325 commit 5f5c37c

File tree

12 files changed

+431
-7
lines changed

12 files changed

+431
-7
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ atex-*.tar
2424

2525
.envrc
2626
.direnv
27-
.vscode/
27+
.vscode/
28+
.elixir_ls

.vscode/settings.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +0,0 @@
1-
{
2-
"git.enabled": false
3-
}

lib/atex/did.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
defmodule Atex.DID do
2+
@re ~r/^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/
3+
@blessed_re ~r/^did:(?:plc|web):[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/
4+
5+
@spec re() :: Regex.t()
6+
def re, do: @re
7+
8+
@spec match?(String.t()) :: boolean()
9+
def match?(value), do: Regex.match?(@re, value)
10+
11+
@spec blessed_re() :: Regex.t()
12+
def blessed_re, do: @blessed_re
13+
14+
@spec match_blessed?(String.t()) :: boolean()
15+
def match_blessed?(value), do: Regex.match?(@blessed_re, value)
16+
end

lib/atex/handle.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
defmodule Atex.Handle do
2+
@re ~r/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/
3+
4+
@spec re() :: Regex.t()
5+
def re, do: @re
6+
7+
@spec match?(String.t()) :: boolean()
8+
def match?(value), do: Regex.match?(@re, value)
9+
end

lib/atex/lexicon/validators.ex

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
defmodule Atex.Lexicon.Validators do
2+
alias Atex.Lexicon.Validators
3+
4+
@type blob_option() :: {:accept, list(String.t())} | {:max_size, integer()}
5+
6+
@type blob_t() ::
7+
%{
8+
"$type": String.t(),
9+
req: %{"$link": String.t()},
10+
mimeType: String.t(),
11+
size: integer()
12+
}
13+
| %{}
14+
15+
@spec string(list(Validators.String.option())) :: Peri.custom_def()
16+
def string(options \\ []), do: {:custom, {Validators.String, :validate, [options]}}
17+
18+
@spec integer(list(Validators.Integer.option())) :: Peri.custom_def()
19+
def integer(options \\ []), do: {:custom, {Validators.Integer, :validate, [options]}}
20+
21+
@spec array(Peri.schema_def(), list(Validators.Array.option())) :: Peri.custom_def()
22+
def array(inner_type, options \\ []) do
23+
{:ok, ^inner_type} = Peri.validate_schema(inner_type)
24+
{:custom, {Validators.Array, :validate, [inner_type, options]}}
25+
end
26+
27+
@spec blob(list(blob_option())) :: Peri.schema_def()
28+
def blob(options \\ []) do
29+
options = Keyword.validate!(options, accept: nil, max_size: nil)
30+
accept = Keyword.get(options, :accept)
31+
max_size = Keyword.get(options, :max_size)
32+
33+
mime_type =
34+
{:required,
35+
if(accept,
36+
do: {:string, {:regex, strings_to_re(accept)}},
37+
else: {:string, {:regex, ~r"^.+/.+$"}}
38+
)}
39+
40+
{
41+
:either,
42+
{
43+
# Newer blobs
44+
%{
45+
"$type": {:required, {:literal, "blob"}},
46+
ref: {:required, %{"$link": {:required, :string}}},
47+
mimeType: mime_type,
48+
size: {:required, if(max_size != nil, do: {:integer, {:lte, max_size}}, else: :integer)}
49+
},
50+
# Old deprecated blobs
51+
%{
52+
cid: {:reqiured, :string},
53+
mimeType: mime_type
54+
}
55+
}
56+
}
57+
end
58+
59+
@spec boolean_validate(boolean(), String.t(), keyword() | map()) ::
60+
Peri.validation_result()
61+
def boolean_validate(success?, error_message, context \\ []) do
62+
if success? do
63+
:ok
64+
else
65+
{:error, error_message, context}
66+
end
67+
end
68+
69+
@spec strings_to_re(list(String.t())) :: Regex.t()
70+
defp strings_to_re(strings) do
71+
strings
72+
|> Enum.map(&String.replace(&1, "*", ".+"))
73+
|> Enum.join("|")
74+
|> then(&~r/^(#{&1})$/)
75+
end
76+
end
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
defmodule Atex.Lexicon.Validators.Array do
2+
@type option() :: {:min_length, non_neg_integer()} | {:max_length, non_neg_integer()}
3+
4+
@option_keys [:min_length, :max_length]
5+
6+
# Needs type input
7+
@spec validate(Peri.schema_def(), term(), list(option())) :: Peri.validation_result()
8+
def validate(inner_type, value, options) when is_list(value) do
9+
# TODO: validate inner_type with Peri to make sure it's correct?
10+
11+
options
12+
|> Keyword.validate!(min_length: nil, max_length: nil)
13+
|> Stream.map(&validate_option(value, &1))
14+
|> Enum.find(:ok, fn x -> x != :ok end)
15+
|> case do
16+
:ok ->
17+
value
18+
|> Stream.map(&Peri.validate(inner_type, &1))
19+
|> Enum.find({:ok, nil}, fn
20+
{:ok, _} -> false
21+
{:error, _} -> true
22+
end)
23+
|> case do
24+
{:ok, _} -> :ok
25+
e -> e
26+
end
27+
28+
e ->
29+
e
30+
end
31+
end
32+
33+
def validate(_inner_type, value, _options),
34+
do: {:error, "expected type of `array`, received #{value}", [expected: :array, actual: value]}
35+
36+
@spec validate_option(list(), option()) :: Peri.validation_result()
37+
defp validate_option(value, option)
38+
39+
defp validate_option(_value, {option, nil}) when option in @option_keys, do: :ok
40+
41+
defp validate_option(value, {:min_length, expected}) when length(value) >= expected,
42+
do: :ok
43+
44+
defp validate_option(value, {:min_length, expected}) when length(value) < expected,
45+
do: {:error, "should have a minimum length of #{expected}", [length: expected]}
46+
47+
defp validate_option(value, {:max_length, expected}) when length(value) <= expected,
48+
do: :ok
49+
50+
defp validate_option(value, {:max_length, expected}) when length(value) > expected,
51+
do: {:error, "should have a maximum length of #{expected}", [length: expected]}
52+
end
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
defmodule Atex.Lexicon.Validators.Integer do
2+
alias Atex.Lexicon.Validators
3+
4+
@type option() ::
5+
{:minimum, integer()}
6+
| {:maximum, integer()}
7+
| {:enum, list(integer())}
8+
| {:const, integer()}
9+
10+
@option_keys [:minimum, :maximum, :enum, :const]
11+
12+
@spec validate(term(), list(option())) :: Peri.validation_result()
13+
def validate(value, options) when is_integer(value) do
14+
options
15+
|> Keyword.validate!(
16+
minimum: nil,
17+
maximum: nil,
18+
enum: nil,
19+
const: nil
20+
)
21+
|> Stream.map(&validate_option(value, &1))
22+
|> Enum.find(:ok, fn x -> x != :ok end)
23+
end
24+
25+
def validate(value, _options),
26+
do:
27+
{:error, "expected type of `integer`, received #{value}",
28+
[expected: :integer, actual: value]}
29+
30+
@spec validate_option(integer(), option()) :: Peri.validation_result()
31+
defp validate_option(value, option)
32+
33+
defp validate_option(_value, {option, nil}) when option in @option_keys, do: :ok
34+
35+
defp validate_option(value, {:minimum, expected}) when value >= expected, do: :ok
36+
37+
defp validate_option(value, {:minimum, expected}) when value < expected,
38+
do: {:error, "", [value: expected]}
39+
40+
defp validate_option(value, {:maximum, expected}) when value <= expected, do: :ok
41+
42+
defp validate_option(value, {:maximum, expected}) when value > expected,
43+
do: {:error, "", [value: expected]}
44+
45+
defp validate_option(value, {:enum, values}),
46+
do:
47+
Validators.boolean_validate(value in values, "should be one of the expected values",
48+
enum: values
49+
)
50+
51+
defp validate_option(value, {:const, expected}) when value == expected, do: :ok
52+
53+
defp validate_option(value, {:const, expected}),
54+
do: {:error, "should match constant value", [actual: value, expected: expected]}
55+
end

0 commit comments

Comments
 (0)