Skip to content

Commit 83ccb56

Browse files
committed
Support repeated validation and add some tests.
1 parent 873381d commit 83ccb56

File tree

18 files changed

+570
-85
lines changed

18 files changed

+570
-85
lines changed

lib/dsl.ex

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,18 @@ defmodule ProtoValidator.DSL do
44

55
quote bind_quoted: [validator: validator] do
66
Enum.map(@validations, fn {field, rules} ->
7-
rules = apply(validator, :translate_rules, [field, rules])
7+
rules = apply(validator, :translate_rules, [rules])
88

9-
def validate_field(unquote(field) = field, data) do
10-
apply(unquote(validator), :validate_field, [data, field, unquote(rules)])
9+
def validate_value(unquote(field) = field, value) do
10+
apply(
11+
unquote(validator),
12+
:validate_value,
13+
[field, value, unquote(Macro.escape(rules))]
14+
)
1115
end
1216
end)
1317

14-
def validate_field(field, _data), do: true
15-
16-
def validate(data) do
17-
@validations
18-
|> Stream.map(fn {field, _options} ->
19-
validate_field(field, data)
20-
end)
21-
|> Stream.filter(&Kernel.match?({:error, msg}, &1))
22-
|> Enum.at(0, :ok)
23-
end
18+
def validate_value(field, _value), do: :ok
2419
end
2520
end
2621

lib/proto_validator.ex

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
defprotocol ProtoValidator.Verifiable do
22
@doc ""
3+
@fallback_to_any true
34
def validate(data)
45
end
56

@@ -16,24 +17,59 @@ defmodule ProtoValidator do
1617
"""
1718

1819
defmacro __using__(opts) do
19-
entity_module = Keyword.get(opts, :entity)
20+
# entity_module = Keyword.get(opts, :entity)
2021

2122
quote location: :keep do
22-
import unquote(__MODULE__), except: [validate: 2]
2323
import ProtoValidator.DSL, only: [validate: 2]
2424

2525
@options unquote(opts)
2626
Module.register_attribute(__MODULE__, :validations, accumulate: true)
2727

2828
validator_module = __MODULE__
2929

30-
defimpl ProtoValidator.Verifiable, for: unquote(entity_module) do
30+
entity_module = Keyword.get(@options, :entity)
31+
32+
defimpl ProtoValidator.Verifiable, for: entity_module do
3133
@validator_module validator_module
3234
def validate(data) do
3335
apply(@validator_module, :validate, [data])
3436
end
3537
end
3638

39+
def validate(%{__struct__: protobuf_module} = data) do
40+
props = protobuf_module.__message_props__()
41+
42+
props
43+
|> Map.get(:field_props)
44+
|> Stream.filter(fn {_, %{oneof: oneof}} -> is_nil(oneof) end)
45+
|> ProtoValidator.Utils.pipe_validates(fn {_, %{name_atom: field} = field_prop} ->
46+
value = Map.get(data, field)
47+
48+
case validate_value(field, value) do
49+
:ok -> validate_field(field_prop, value)
50+
{:error, msg} -> {:error, msg}
51+
end
52+
end)
53+
end
54+
55+
def validate(data) when is_map(data) do
56+
@options |> Keyword.get(:entity) |> struct(data) |> validate()
57+
end
58+
59+
def validate(_), do: :ok
60+
61+
def validate_field(_, nil), do: :ok
62+
63+
def validate_field(%{type: type, repeated?: true}, values) do
64+
ProtoValidator.Utils.pipe_validates(values, fn value ->
65+
ProtoValidator.validate(type, value)
66+
end)
67+
end
68+
69+
def validate_field(%{type: type} = field_prop, value) do
70+
ProtoValidator.validate(type, value)
71+
end
72+
3773
@before_compile ProtoValidator.DSL
3874
end
3975
end
@@ -42,9 +78,19 @@ defmodule ProtoValidator do
4278
ProtoValidator.Verifiable.validate(data)
4379
end
4480

45-
def validate(module, data) do
81+
def validate(_), do: :ok
82+
83+
def validate(_module, %_{} = data) do
84+
validate(data)
85+
end
86+
87+
def validate(module, data) when is_map(data) do
4688
module
4789
|> struct(data)
4890
|> ProtoValidator.Verifiable.validate()
91+
rescue
92+
err -> {:error, inspect(err)}
4993
end
94+
95+
def validate(_, _), do: :ok
5096
end

lib/proto_validator/protoc/cli.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ defmodule ProtoValidator.Protoc.CLI do
3232
|> Enum.map(fn file_metadata ->
3333
ProtoValidator.Protoc.Generator.generate(file_metadata)
3434
end)
35+
|> Enum.reject(&is_nil/1)
3536

3637
Google.Protobuf.Compiler.CodeGeneratorResponse.new(file: files)
3738
end)

lib/proto_validator/protoc/generator.ex

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@ defmodule ProtoValidator.Protoc.Generator do
22
@moduledoc false
33

44
alias Protobuf.Protoc.Metadata
5+
alias ProtoValidator.Protoc.Utils
6+
alias Google.Protobuf.Compiler.CodeGeneratorResponse
57

68
def generate(file) do
79
name = new_file_name(file.fqn)
810

9-
Google.Protobuf.Compiler.CodeGeneratorResponse.File.new(
10-
name: name,
11-
content: generate_content(file)
12-
)
11+
case generate_content(file) do
12+
nil ->
13+
nil
14+
15+
content ->
16+
CodeGeneratorResponse.File.new(name: name, content: content)
17+
end
1318
end
1419

1520
defp new_file_name(name) do
@@ -20,8 +25,16 @@ defmodule ProtoValidator.Protoc.Generator do
2025
file
2126
|> Metadata.File.all_messages()
2227
|> Enum.map(fn msg -> generate_message(msg) end)
23-
|> Enum.join("\n")
24-
|> Protobuf.Protoc.Generator.format_code()
28+
|> Enum.reject(&is_nil/1)
29+
|> case do
30+
[] ->
31+
nil
32+
33+
validations ->
34+
validations
35+
|> Enum.join("\n")
36+
|> Protobuf.Protoc.Generator.format_code()
37+
end
2538
end
2639

2740
# TODO: put get_str functions to a separate module
@@ -74,25 +87,8 @@ defmodule ProtoValidator.Protoc.Generator do
7487
defp gen_validation([]), do: nil
7588

7689
defp gen_validation(validations) do
77-
Enum.map(validations, fn {name, %Validate.FieldRules{message: message, type: type}} ->
78-
message_rule_str_list = get_rule_str_list(message)
79-
type_rule_str_list = get_rule_str_list(type)
80-
rules_str = "#{message_rule_str_list}#{type_rule_str_list}"
81-
82-
":#{name}, #{rules_str}"
90+
Enum.map(validations, fn {name, rules} ->
91+
":#{name}, #{Utils.get_rule_str(rules)}"
8392
end)
8493
end
85-
86-
defp get_rule_str_list(nil), do: nil
87-
88-
defp get_rule_str_list({type, type_rules}) do
89-
", #{type}: [#{get_rule_str_list(type_rules)}]"
90-
end
91-
92-
defp get_rule_str_list(rules) do
93-
rules
94-
|> Map.from_struct()
95-
|> Enum.map(fn {k, v} -> "#{k}: #{v}" end)
96-
|> Enum.join(", ")
97-
end
9894
end
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
defmodule ProtoValidator.Protoc.Utils do
2+
@moduledoc """
3+
"""
4+
5+
@doc """
6+
convert rules map to string:
7+
From:
8+
%Validate.FieldRules{
9+
message: %Validate.MessageRules{required: true},
10+
type: {
11+
:repeated,
12+
%Validate.RepeatedRules{
13+
items: %Validate.FieldRules{
14+
message: nil,
15+
type: {:uint64, %Validate.UInt64Rules{gt: 0, lt: 90}}
16+
},
17+
max_items: nil,
18+
min_items: 0,
19+
unique: true
20+
}
21+
}
22+
}
23+
24+
To:
25+
"required: true, repeated: [items: [uint64: [gt: 0, lt: 90]], min_items: 0, unique: true]"
26+
"""
27+
def get_rule_str(%{message: message, type: type}) do
28+
[get_rule_str(message), get_rule_str(type)]
29+
|> Enum.reject(&is_nil/1)
30+
|> Enum.join(", ")
31+
end
32+
33+
def get_rule_str(nil), do: nil
34+
35+
def get_rule_str(%_{} = rules) when is_map(rules) do
36+
rules |> Map.from_struct() |> get_rule_str()
37+
end
38+
39+
def get_rule_str(rules) when is_map(rules) do
40+
rule_str =
41+
rules
42+
|> Enum.map(fn
43+
{_k, nil} -> nil
44+
{k, v} when is_map(v) -> "#{k}: [#{get_rule_str(v)}]"
45+
{k, v} -> "#{k}: #{get_rule_str(v)}"
46+
end)
47+
|> Enum.reject(&is_nil/1)
48+
|> Enum.join(", ")
49+
50+
"#{rule_str}"
51+
end
52+
53+
def get_rule_str({type, type_rules}) do
54+
"#{type}: [#{get_rule_str(type_rules)}]"
55+
end
56+
57+
def get_rule_str(v), do: to_string(v)
58+
end

lib/utils.ex

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
defmodule ProtoValidator.Utils do
2+
@moduledoc ""
3+
4+
def pipe_validates(objects, validate_fun) do
5+
objects
6+
|> Stream.map(fn object -> validate_fun.(object) end)
7+
|> Stream.filter(&Kernel.match?({:error, _msg}, &1))
8+
|> Enum.at(0, :ok)
9+
end
10+
end

lib/validate.pb.ex

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ defmodule Validate.FieldRules do
1212

1313
field(:message, 17, optional: true, type: Validate.MessageRules)
1414
field(:uint64, 6, optional: true, type: Validate.UInt64Rules, oneof: 0)
15+
field(:repeated, 18, optional: true, type: Validate.RepeatedRules, oneof: 0)
1516
end
1617

1718
defmodule Validate.MessageRules do
@@ -40,6 +41,24 @@ defmodule Validate.UInt64Rules do
4041
field(:gt, 4, optional: true, type: :uint64)
4142
end
4243

44+
defmodule Validate.RepeatedRules do
45+
@moduledoc false
46+
use Protobuf, syntax: :proto2
47+
48+
@type t :: %__MODULE__{
49+
min_items: non_neg_integer,
50+
max_items: non_neg_integer,
51+
unique: boolean,
52+
items: Validate.FieldRules.t() | nil
53+
}
54+
defstruct [:min_items, :max_items, :unique, :items]
55+
56+
field(:min_items, 1, optional: true, type: :uint64)
57+
field(:max_items, 2, optional: true, type: :uint64)
58+
field(:unique, 3, optional: true, type: :bool)
59+
field(:items, 4, optional: true, type: Validate.FieldRules)
60+
end
61+
4362
defmodule Validate.PbExtension do
4463
@moduledoc false
4564
use Protobuf, syntax: :proto2

lib/validator.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,10 @@ defmodule ProtoValidator.Validator do
66
end
77

88
defp get_validator(:vex), do: ProtoValidator.Validator.Vex
9+
10+
def validate_uniq(nil), do: :ok
11+
12+
def validate_uniq(value) do
13+
if Enum.uniq(value) == value, do: :ok, else: {:error, "values should be uniq"}
14+
end
915
end

0 commit comments

Comments
 (0)