diff --git a/README.md b/README.md index d1b060c..e3feb5d 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ defmodule MyApp.Schema do arg(:max, :integer, description: "Maximum value allowed") arg(:min_items, :integer, description: "Minimum number of items allowed in a list") arg(:max_items, :integer, description: "Maximum number of items allowed in a list") + arg(:regex, :string, description: "Pattern to match for a string") end query do @@ -101,7 +102,7 @@ field :my_list, list_of(:integer) do end field :my_field, :integer do - arg :my_arg, non_null(:string), directives: [constraints: [min: 10]] + arg :my_arg, non_null(:string), directives: [constraints: [min: 10, regex: "^[a-zA-Z]+$"]] resolve(&MyResolver.resolve/3) end diff --git a/lib/constraints/max.ex b/lib/constraints/max.ex index 5504957..5f570f9 100644 --- a/lib/constraints/max.ex +++ b/lib/constraints/max.ex @@ -3,7 +3,7 @@ defmodule AbsintheHelpers.Constraints.Max do @behaviour AbsintheHelpers.Constraint - def call(node = %{items: _items}, {:max, _min}) do + def call(node = %{items: _items}, {:max, _max}) do {:ok, node} end diff --git a/lib/constraints/regex.ex b/lib/constraints/regex.ex new file mode 100644 index 0000000..0020ca1 --- /dev/null +++ b/lib/constraints/regex.ex @@ -0,0 +1,15 @@ +defmodule AbsintheHelpers.Constraints.Regex do + @moduledoc """ + Applies regex constraint on node data. This constraint can only be applied to String types. + """ + + @behaviour AbsintheHelpers.Constraint + + def call(node = %{data: data}, {:regex, regex}) do + if data =~ Regex.compile!(regex) do + {:ok, node} + else + {:error, :invalid_format, %{regex: regex}} + end + end +end diff --git a/lib/directives/constraints.ex b/lib/directives/constraints.ex index 486672a..37b0b35 100644 --- a/lib/directives/constraints.ex +++ b/lib/directives/constraints.ex @@ -5,11 +5,12 @@ defmodule AbsintheHelpers.Directives.Constraints do Supports: - `:min`, `:max`: For numbers and string lengths - `:min_items`, `:max_items`: For lists + - `:regex`: For strings Applicable to scalars (:string, :integer, :float, :decimal) and lists. Example: - field :username, :string, directives: [constraints: [min: 3, max: 20]] + field :username, :string, directives: [constraints: [min: 3, max: 20, regex: "^[a-zA-Z]+$"]] arg :tags, list_of(:string), directives: [constraints: [max_items: 5, max: 10]] Constraints are automatically enforced during query execution. @@ -20,7 +21,7 @@ defmodule AbsintheHelpers.Directives.Constraints do alias Absinthe.Blueprint.TypeReference.{List, NonNull} @constraints %{ - string: [:min, :max], + string: [:min, :max, :regex], number: [:min, :max], list: [:min, :max, :min_items, :max_items] } @@ -30,9 +31,12 @@ defmodule AbsintheHelpers.Directives.Constraints do arg(:min, :integer, description: "Minimum value allowed") arg(:max, :integer, description: "Maximum value allowed") + arg(:min_items, :integer, description: "Minimum number of items allowed in a list") arg(:max_items, :integer, description: "Maximum number of items allowed in a list") + arg(:regex, :string, description: "Pattern to match for a string") + expand(&__MODULE__.expand_constraints/2) end diff --git a/mix.exs b/mix.exs index 2ea3ffa..8b88171 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule AbsintheHelpers.MixProject do def project do [ app: :absinthe_helpers, - version: "0.1.8", + version: "0.1.9", elixir: "~> 1.16", start_permanent: Mix.env() == :prod, deps: deps(), diff --git a/test/absinthe_helpers/constraints/regex_test.exs b/test/absinthe_helpers/constraints/regex_test.exs new file mode 100644 index 0000000..0af0208 --- /dev/null +++ b/test/absinthe_helpers/constraints/regex_test.exs @@ -0,0 +1,17 @@ +defmodule AbsintheHelpers.Constraints.RegexTest do + use ExUnit.Case, async: true + + alias AbsintheHelpers.Constraints.Regex + + test "returns :ok tuple on regex match" do + input = %{data: "username"} + + assert {:ok, %{data: "username"}} = Regex.call(input, {:regex, "^[a-z]*$"}) + end + + test "returns invalid_format error on regex match failure" do + input = %{data: "user.name"} + + assert {:error, :invalid_format, %{regex: "^[a-z]*$"}} = Regex.call(input, {:regex, "^[a-z]*$"}) + end +end diff --git a/test/absinthe_helpers/phases/apply_constraints_test.exs b/test/absinthe_helpers/phases/apply_constraints_test.exs index d1d888e..f224d0e 100644 --- a/test/absinthe_helpers/phases/apply_constraints_test.exs +++ b/test/absinthe_helpers/phases/apply_constraints_test.exs @@ -31,7 +31,7 @@ defmodule AbsintheHelpers.Phases.ApplyConstraintsTest do field(:cost, :decimal, directives: [constraints: [min: 10, max: 1000]]) field(:description, :string) do - directive(:constraints, min: 5, max: 50) + directive(:constraints, regex: "^[a-zA-Z\s]+$", min: 5, max: 50) end field(:override_ids, non_null(list_of(non_null(:integer)))) do @@ -112,5 +112,32 @@ defmodule AbsintheHelpers.Phases.ApplyConstraintsTest do ] }} = TestSchema.run_query(query) end + + test "returns invalid_format on strings that do not match regex pattern" do + query = """ + mutation { + create_booking( + customer_id: 1, + service: { + cost: "150.75", + description: "invalid-description", + override_ids: [6, 7, 8], + location_ids: [8, 9, 10], + commission_ids: [] + } + ) + } + """ + + assert {:ok, + %{ + errors: [ + %{ + message: :invalid_format, + details: %{field: "description", regex: "^[a-zA-Z\s]+$"} + } + ] + }} = TestSchema.run_query(query) + end end end