pylon / spect

Type specification extensions for Elixir
21 stars 3 forks source link

Spect can't handle negative Decimals #19

Open 23Skidoo opened 2 years ago

23Skidoo commented 2 years ago

Using the decimal-2.0.0 package:

iex(20)> Spect.to_spec(Decimal.new("1.23"), Decimal)       
{:ok, #Decimal<1.23>}

iex(21)> Spect.to_spec(Decimal.new("-1.23"), Decimal)
{:error,
 %Spect.ConvertError{
   message: "expected: union of [{:integer, 0, 1}, {:op, 74, :-, {:integer, 74, 1}}], found: -1"
 }}

My guess is that it's due to sign() in decimal-2.0.0 being defined as

@type sign :: 1 | -1

which seems to be an invalid typespec.

23Skidoo commented 2 years ago

The following type definition works as expected, however:

defmodule MyDecimal do
  import TypedStruct

  @type sign() :: 1 | 2 | 3

  typedstruct do
    field :sign, sign()
    field :num, integer()
  end
end

[...]

iex(3)> Spect.to_spec(%MyDecimal{sign: 1, num: 2}, MyDecimal)
{:ok, %MyDecimal{num: 2, sign: 1}}
iex(4)> Spect.to_spec(%MyDecimal{sign: 2, num: 2}, MyDecimal)
{:ok, %MyDecimal{num: 2, sign: 2}}
iex(5)> Spect.to_spec(%MyDecimal{sign: 3, num: 2}, MyDecimal)
{:ok, %MyDecimal{num: 2, sign: 3}}
wojtekmach commented 2 years ago

The problem is spect doesn't currently support negative integer literals. I haven't tested this thoroughly but here's an attempt at a patch:

diff --git a/lib/spect.ex b/lib/spect.ex
index d54a774..921aec8 100644
--- a/lib/spect.ex
+++ b/lib/spect.ex
@@ -156,6 +156,10 @@ defmodule Spect do
     to_kind!(data, module, type, params)
   end

+  defp to_kind!(data, _module, {:op, _, :-, {:integer, _, value}}, _params) do
+    to_lit!(data, :integer, -value)
+  end
+
   defp to_kind!(data, _module, {kind, _line, value}, _params) do
     to_lit!(data, kind, value)
   end
diff --git a/test/spect_test.exs b/test/spect_test.exs
index 4ea4407..dbaf09f 100644
--- a/test/spect_test.exs
+++ b/test/spect_test.exs
@@ -36,6 +36,9 @@ defmodule Spect.Test do
     assert to_spec(1, Specs, :literal_1) === {:ok, 1}
     {:error, %ConvertError{}} = to_spec(2, Specs, :literal_1)

+    assert to_spec(-1, Specs, :literal_minus_1) === {:ok, -1}
+    {:error, %ConvertError{}} = to_spec(-2, Specs, :literal_minus_1)
+
     assert to_spec([], Specs, :literal_list) === {:ok, []}
     {:error, %ConvertError{}} = to_spec(1, Specs, :literal_list)

diff --git a/test/support/specs.ex b/test/support/specs.ex
index 1ee4b03..f45ca62 100644
--- a/test/support/specs.ex
+++ b/test/support/specs.ex
@@ -6,6 +6,7 @@ defmodule Spect.Support.Specs do
   @type literal_true :: true
   @type literal_false :: false
   @type literal_1 :: 1
+  @type literal_minus_1 :: -1
   @type literal_list :: []
   @type literal_map :: %{}

feel free to go with it if you want!