Skip to content

Commit

Permalink
Implement regex constraint for String fields (#9)
Browse files Browse the repository at this point in the history
* Implement regex constraint for Strings

* Add a test case for regex match on string field

* Update README with regex example

* Bump patch version to 0.1.9
  • Loading branch information
andreyuhai authored Oct 22, 2024
1 parent 3ea0189 commit 6b2b379
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 6 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/constraints/max.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 15 additions & 0 deletions lib/constraints/regex.ex
Original file line number Diff line number Diff line change
@@ -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
8 changes: 6 additions & 2 deletions lib/directives/constraints.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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]
}
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
17 changes: 17 additions & 0 deletions test/absinthe_helpers/constraints/regex_test.exs
Original file line number Diff line number Diff line change
@@ -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
29 changes: 28 additions & 1 deletion test/absinthe_helpers/phases/apply_constraints_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

0 comments on commit 6b2b379

Please sign in to comment.