Skip to content

Commit

Permalink
feat: add regex pattern constraint (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
DReigada authored Dec 5, 2023
1 parent da9fc58 commit e403bb7
Show file tree
Hide file tree
Showing 6 changed files with 43 additions and 24 deletions.
27 changes: 12 additions & 15 deletions lib/absinthe_constraints/directive.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,39 +24,36 @@ defmodule AbsintheConstraints.Directive do
alias Absinthe.Blueprint.TypeReference.List
alias Absinthe.Blueprint.TypeReference.NonNull

@string_args [:min_length, :max_length, :format]
@string_args [:min_length, :max_length, :format, :pattern]
@number_args [:min, :max]
@list_args [:min_items, :max_items]

directive :constraints do
on([:argument_definition, :field_definition])

arg(:min, non_null(:integer), description: "Ensure value is greater than or equal to")
arg(:max, non_null(:integer), description: "Ensure value is less than or equal to")
arg(:min, :integer, description: "Ensure value is greater than or equal to")
arg(:max, :integer, description: "Ensure value is less than or equal to")

arg(:format, non_null(:string), description: "Restricts the string to a specific format")
arg(:format, :string, description: "Restricts the string to a specific format")
arg(:pattern, :string, description: "Ensure value matches regex")

arg(:min_length, non_null(:integer), description: "Restrict to a minimum length")
arg(:max_length, non_null(:integer), description: "Restrict to a maximum length")
arg(:min_length, :integer, description: "Restrict to a minimum length")
arg(:max_length, :integer, description: "Restrict to a maximum length")

arg(:min_items, non_null(:integer), description: "Restrict to a minimum number of items")
arg(:max_items, non_null(:integer), description: "Restrict to a maximum number of items")
arg(:min_items, :integer, description: "Restrict to a minimum number of items")
arg(:max_items, :integer, description: "Restrict to a maximum number of items")

expand(&__MODULE__.expand_constraints/2)
end

def expand_constraints(args, %{type: %List{}} = node),
do: do_expand(args, node, get_args(:list))

def expand_constraints(args, %{type: %NonNull{of_type: type}} = node),
def expand_constraints(args, %{type: type} = node),
do: do_expand(args, node, get_args(type))

def expand_constraints(args, %{type: type} = node), do: do_expand(args, node, get_args(type))

defp get_args(:string), do: @string_args
defp get_args(:integer), do: @number_args
defp get_args(:float), do: @number_args
defp get_args(:list), do: @list_args
defp get_args(%List{}), do: @list_args
defp get_args(%NonNull{of_type: of_type}), do: get_args(of_type)
defp get_args(type), do: raise("Unsupported type: #{inspect(type)}")

defp do_expand(args, node, args_list) do
Expand Down
6 changes: 6 additions & 0 deletions lib/absinthe_constraints/validator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,10 @@ defmodule AbsintheConstraints.Validator do
do: [],
else: ["must be a valid email address"]
end

def handle_constraint({:pattern, regex}, value) do
if String.match?(value, Regex.compile!(regex)),
do: [],
else: ["must match regular expression `#{regex}`"]
end
end
6 changes: 1 addition & 5 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,7 @@ defmodule AbsintheConstraints.MixProject do

defp deps do
[
# We require this version for a fix introduced in master that is not yet released.
{:absinthe,
github: "absinthe-graphql/absinthe",
ref: "a0afbe1aa3d7e2d47a88370b78d62fa9c002e72c",
override: true},
{:absinthe, ">= 1.7.6"},
{:elixir_uuid, ">= 1.2.1"},
{:ex_doc, "~> 0.14", only: :dev, runtime: false},
{:dialyxir, "~> 1.3", only: [:dev], runtime: false}
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
%{
"absinthe": {:git, "https://github.com/absinthe-graphql/absinthe.git", "a0afbe1aa3d7e2d47a88370b78d62fa9c002e72c", [ref: "a0afbe1aa3d7e2d47a88370b78d62fa9c002e72c"]},
"absinthe": {:hex, :absinthe, "1.7.6", "0b897365f98d068cfcb4533c0200a8e58825a4aeeae6ec33633ebed6de11773b", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7626951ca5eec627da960615b51009f3a774765406ff02722b1d818f17e5778"},
"dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"},
"earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"},
"elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"},
Expand Down
8 changes: 8 additions & 0 deletions test/absinthe_constraints/validator_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ defmodule AbsintheConstraints.ValidatorTest do
assert handle_constraint({:format, "email"}, "[email protected]") == []
end

test "should validate regex pattern" do
assert handle_constraint({:pattern, "^[A-Z]*$"}, "asdf") == [
"must match regular expression `^[A-Z]*$`"
]

assert handle_constraint({:pattern, "^[A-Z]*$"}, "ASDF") == []
end

test "should validate min_items" do
assert handle_constraint({:min_items, 5}, ["1", "2", "3", "4"]) == [
"must have at least 5 items"
Expand Down
18 changes: 15 additions & 3 deletions test/integration/api_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,16 @@ defmodule AbsintheConstraints.Integration.APITest do

query do
field :test, non_null(:string) do
arg(:list, list_of(:integer), directives: [constraints: [min_items: 2]])
arg(:list, non_null(list_of(non_null(:integer))),
directives: [constraints: [min_items: 2]]
)

arg(:number, :integer, directives: [constraints: [min: 2]])

arg(:regex_field, non_null(:string),
directives: [constraints: [pattern: "^[A-Z][0-9a-z]*$"]]
)

resolve(fn _, _ -> {:ok, "test_result"} end)
end
end
Expand All @@ -39,7 +47,7 @@ defmodule AbsintheConstraints.Integration.APITest do

test "should return success on valid query arguments" do
assert {:ok, %{data: %{"test" => "test_result"}}} ==
TestSchema.run_query("{ test(list: [1, 3], number: 5) }")
TestSchema.run_query("{ test(list: [1, 3], number: 5, regexField: \"A123aa\") }")
end

test "should return errors on invalid query arguments" do
Expand All @@ -53,10 +61,14 @@ defmodule AbsintheConstraints.Integration.APITest do
%{
message: "\"number\" must be greater than or equal to 2",
locations: [%{line: 1, column: 19}]
},
%{
message: "\"regexField\" must match regular expression `^[A-Z][0-9a-z]*$`",
locations: [%{line: 1, column: 30}]
}
]
}} ==
TestSchema.run_query("{ test(list: [1], number: 1) }")
TestSchema.run_query("{ test(list: [1], number: 1, regexField: \"invalid\") }")
end

test "should return errors on valid mutation arguments" do
Expand Down

0 comments on commit e403bb7

Please sign in to comment.