diff --git a/lib/absinthe_constraints/directive.ex b/lib/absinthe_constraints/directive.ex index dfac505..2347575 100644 --- a/lib/absinthe_constraints/directive.ex +++ b/lib/absinthe_constraints/directive.ex @@ -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 diff --git a/lib/absinthe_constraints/validator.ex b/lib/absinthe_constraints/validator.ex index 69015cd..8560ba1 100644 --- a/lib/absinthe_constraints/validator.ex +++ b/lib/absinthe_constraints/validator.ex @@ -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 diff --git a/mix.exs b/mix.exs index 2d7ab00..1f63291 100644 --- a/mix.exs +++ b/mix.exs @@ -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} diff --git a/mix.lock b/mix.lock index 0f20d74..1d43932 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, diff --git a/test/absinthe_constraints/validator_test.exs b/test/absinthe_constraints/validator_test.exs index c31fda2..e2681df 100644 --- a/test/absinthe_constraints/validator_test.exs +++ b/test/absinthe_constraints/validator_test.exs @@ -49,6 +49,14 @@ defmodule AbsintheConstraints.ValidatorTest do assert handle_constraint({:format, "email"}, "email@example.com") == [] 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" diff --git a/test/integration/api_test.exs b/test/integration/api_test.exs index 3402a9f..407a8b4 100644 --- a/test/integration/api_test.exs +++ b/test/integration/api_test.exs @@ -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 @@ -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 @@ -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