Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
solnic committed Jan 26, 2024
1 parent 5bccce0 commit 66fcf6e
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 12 deletions.
5 changes: 5 additions & 0 deletions lib/drops/type/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defmodule Drops.Type.Compiler do
alias Drops.Types.{
Primitive,
Union,
Number,
List,
Cast,
Map,
Expand All @@ -27,6 +28,10 @@ defmodule Drops.Type.Compiler do
Union.new(visit(left, opts), visit(right, opts))
end

def visit({:type, {:number, predicates}}, opts) do
Number.new(predicates, opts)
end

def visit({:type, {:list, member_type}}, opts)
when is_tuple(member_type) or is_map(member_type) do
List.new(visit(member_type, opts))
Expand Down
27 changes: 27 additions & 0 deletions lib/drops/type/dsl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,33 @@ defmodule Drops.Type.DSL do
type(cast_spec, float(predicates))
end

@doc ~S"""
Returns a number type specification.
## Examples
# a number with no constraints
number()
# a number with constraints
number(gt?: 1.0)
"""

@spec number() :: type()

def number() do
type(:number)
end

def number(predicate) when is_atom(predicate) do
type(:number, [predicate])
end

def number(predicates) when is_list(predicates) do
type(:number, predicates)
end

@doc ~S"""
Returns a boolean type specification.
Expand Down
6 changes: 4 additions & 2 deletions lib/drops/types/map/key.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ defmodule Drops.Types.Map.Key do
Map.has_key?(map, key) and present?(map[key], tail)
end

defp nest_result({:error, {:or, {left, right}}}, root) do
{:error, {:or, {nest_result(left, root), nest_result(right, root)}}}
defp nest_result({:error, {:or, {left, right, opts}}}, root) do
{:error,
{:or,
{nest_result(left, root), nest_result(right, root), Keyword.merge(opts, path: root)}}}
end

defp nest_result({:error, {:list, results}}, root) when is_list(results) do
Expand Down
12 changes: 12 additions & 0 deletions lib/drops/types/number.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule Drops.Types.Number do
@moduledoc ~S"""
Drops.Types.Number is a struct that represents a number type
that can be either an integer or a float
## Examples
"""

@opts name: :number

use(Drops.Type, union([:integer, :float]))
end
21 changes: 18 additions & 3 deletions lib/drops/types/sum.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ defmodule Drops.Types.Union do
"""

defmodule Validator do
def validate(%{left: left, right: right}, input) do
def validate(%{left: left, right: right} = type, input) do
case Drops.Type.Validator.validate(left, input) do
{:ok, value} ->
{:ok, value}
Expand All @@ -31,7 +31,7 @@ defmodule Drops.Types.Union do
{:ok, value}

{:error, _} = right_error ->
{:error, {:or, {left_error, right_error}}}
{:error, {:or, {left_error, right_error, type.opts}}}
end
end
end
Expand All @@ -45,13 +45,28 @@ defmodule Drops.Types.Union do
alias Drops.Type.Compiler
import Drops.Types.Union

def new(predicates, opts) do
type = new(Keyword.merge(@opts, opts))

Map.merge(type, %{
left: constrain(type.left, predicates),
right: constrain(type.right, predicates)
})
end

def new(opts) do
{:union, {left, right}} = unquote(spec)

struct(__MODULE__, %{
left: Compiler.visit(left, opts),
right: Compiler.visit(right, opts),
opts: opts
opts: Keyword.merge(@opts, opts)
})
end

defp constrain(type, predicates) do
Map.merge(type, %{
constraints: type.constraints ++ infer_constraints(predicates)
})
end

Expand Down
23 changes: 17 additions & 6 deletions lib/drops/validator/messages/backend.ex
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,23 @@ defmodule Drops.Validator.Messages.Backend do
%Error.Rule{path: path, text: text}
end

defp error({:error, {:or, {left, right}}}) do
%Error.Union{left: error(left), right: error(right)}
end

defp error({:error, {path, {:or, {left, right}}}}) do
nest(error({:error, {:or, {left, right}}}), path)
defp error({:error, {:or, {left, right, opts}}}) do
if not is_nil(opts[:name]) and not is_nil(opts[:path]) do
meta = Keyword.drop(opts, [:name, :path])

%Error.Type{path: opts[:path], text: text(opts[:name], opts), meta: meta}
else
%Error.Union{left: error(left), right: error(right)}
end
end

defp error({:error, {path, {:or, {left, right, opts}}}}) do
nest(
error(
{:error, {:or, {left, right, Keyword.merge(opts, path: opts[:path] ++ path)}}}
),
path
)
end

defp error({:error, {path, {:cast, error}}}) do
Expand Down
10 changes: 9 additions & 1 deletion lib/drops/validator/messages/default_backend.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,17 @@ defmodule Drops.Validator.Messages.DefaultBackend do
includes?: "must include %input%",
excludes?: "must exclude %input%",
in?: "must be one of: %input%",
not_in?: "must not be one of: %input%"
not_in?: "must not be one of: %input%",

# built-in types
number: "must be a number"
}

@impl true
def text(:number, _opts) do
@text_mapping[:number]
end

@impl true
def text(predicate, _input) do
@text_mapping[predicate]
Expand Down
71 changes: 71 additions & 0 deletions test/contract/types/number_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
defmodule Drops.Contract.Types.IntegerTest do

Check failure on line 1 in test/contract/types/number_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (1.14.5, 25.3)

** (CompileError) test/contract/types/number_test.exs:1: cannot define module Drops.Contract.Types.IntegerTest because it is currently being defined in test/contract/types/integer_test.exs:1

Check warning on line 1 in test/contract/types/number_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (1.15.7, 26.1)

redefining module Drops.Contract.Types.IntegerTest (current version defined in memory)

Check warning on line 1 in test/contract/types/number_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (1.14.5, 26.2)

redefining module Drops.Contract.Types.IntegerTest (current version defined in memory)

Check warning on line 1 in test/contract/types/number_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (1.14.5, 26.1)

redefining module Drops.Contract.Types.IntegerTest (current version defined in memory)
use Drops.ContractCase

describe "number/0" do
contract do
schema do
%{required(:test) => number()}
end
end

test "returns success with valid data", %{contract: contract} do
assert {:ok, %{test: 312}} = contract.conform(%{test: 312})
end

test "returns error with invalid data", %{contract: contract} do
assert_errors(["test must be a number"], contract.conform(%{test: :invalid}))
end
end

describe "number/1 with an extra predicate" do
contract do
schema do
%{required(:test) => number(:odd?)}
end
end

test "returns success with valid data", %{contract: contract} do
assert {:ok, %{test: 311}} = contract.conform(%{test: 311})
end

test "returns error with invalid data", %{contract: contract} do

Check failure on line 31 in test/contract/types/number_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (1.15.7, 26.1)

test number/1 with an extra predicate returns error with invalid data (Drops.Contract.Types.IntegerTest)

Check failure on line 31 in test/contract/types/number_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (1.14.5, 26.2)

test number/1 with an extra predicate returns error with invalid data (Drops.Contract.Types.IntegerTest)

Check failure on line 31 in test/contract/types/number_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (1.14.5, 26.2)

test number/1 with an extra predicate returns error with invalid data (Drops.Contract.Types.IntegerTest)

Check failure on line 31 in test/contract/types/number_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (1.14.5, 26.1)

test number/1 with an extra predicate returns error with invalid data (Drops.Contract.Types.IntegerTest)

Check failure on line 31 in test/contract/types/number_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (1.14.5, 26.1)

test number/1 with an extra predicate returns error with invalid data (Drops.Contract.Types.IntegerTest)
assert_errors(["test must be a number"], contract.conform(%{test: :invalid}))
assert_errors(["test must be odd"], contract.conform(%{test: 312}))
end
end

describe "number/1 with an extra predicate with args" do
contract do
schema do
%{required(:test) => number(gt?: 2)}
end
end

test "returns success with valid data", %{contract: contract} do
assert {:ok, %{test: 312}} = contract.conform(%{test: 312})
end

test "returns error with invalid data", %{contract: contract} do

Check failure on line 48 in test/contract/types/number_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (1.15.7, 26.1)

test number/1 with an extra predicate with args returns error with invalid data (Drops.Contract.Types.IntegerTest)

Check failure on line 48 in test/contract/types/number_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (1.14.5, 26.2)

test number/1 with an extra predicate with args returns error with invalid data (Drops.Contract.Types.IntegerTest)

Check failure on line 48 in test/contract/types/number_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (1.14.5, 26.2)

test number/1 with an extra predicate with args returns error with invalid data (Drops.Contract.Types.IntegerTest)

Check failure on line 48 in test/contract/types/number_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (1.14.5, 26.1)

test number/1 with an extra predicate with args returns error with invalid data (Drops.Contract.Types.IntegerTest)

Check failure on line 48 in test/contract/types/number_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (1.14.5, 26.1)

test number/1 with an extra predicate with args returns error with invalid data (Drops.Contract.Types.IntegerTest)
assert_errors(["test must be a number"], contract.conform(%{test: :invalid}))
assert_errors(["test must be greater than 2"], contract.conform(%{test: 0}))
end
end

describe "number/1 with extra predicates" do
contract do
schema do
%{required(:test) => number([:even?, gt?: 2])}
end
end

test "returns success with valid data", %{contract: contract} do
assert {:ok, %{test: 312}} = contract.conform(%{test: 312})
end

test "returns error with invalid data", %{contract: contract} do

Check failure on line 65 in test/contract/types/number_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (1.15.7, 26.1)

test number/1 with extra predicates returns error with invalid data (Drops.Contract.Types.IntegerTest)

Check failure on line 65 in test/contract/types/number_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (1.14.5, 26.2)

test number/1 with extra predicates returns error with invalid data (Drops.Contract.Types.IntegerTest)

Check failure on line 65 in test/contract/types/number_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (1.14.5, 26.2)

test number/1 with extra predicates returns error with invalid data (Drops.Contract.Types.IntegerTest)

Check failure on line 65 in test/contract/types/number_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (1.14.5, 26.1)

test number/1 with extra predicates returns error with invalid data (Drops.Contract.Types.IntegerTest)

Check failure on line 65 in test/contract/types/number_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (1.14.5, 26.1)

test number/1 with extra predicates returns error with invalid data (Drops.Contract.Types.IntegerTest)
assert_errors(["test must be a number"], contract.conform(%{test: :invalid}))
assert_errors(["test must be even"], contract.conform(%{test: 311}))
assert_errors(["test must be greater than 2"], contract.conform(%{test: 0}))
end
end
end

0 comments on commit 66fcf6e

Please sign in to comment.