Skip to content

Commit

Permalink
Add BitcrowdEcto.FixedWidthInteger
Browse files Browse the repository at this point in the history
  • Loading branch information
maltoe committed Dec 6, 2023
1 parent 55e8f8a commit ca6ae92
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 0 deletions.
56 changes: 56 additions & 0 deletions lib/bitcrowd_ecto/fixed_width_integer.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
defmodule BitcrowdEcto.FixedWidthInteger do
@moduledoc """
An Ecto type that automatically validates that the given integer fits the underlying DB type.
This turns the ugly Postgrex errors into neat `validation: :cast` changeset errors without
having to manually `validate_number` all `:integer` fields.
Named widths are based on Postgres' integer types.
https://www.postgresql.org/docs/current/datatype-numeric.html
"""

use Ecto.ParameterizedType

@impl true
def init(opts) do

Check warning on line 16 in lib/bitcrowd_ecto/fixed_width_integer.ex

View workflow job for this annotation

GitHub Actions / Build and test (23.1.1, 1.12.2)

Function is too complex (cyclomatic complexity is 10, max is 9).
case Keyword.get(opts, :width, 4) do
2 -> -32_768..32_767
4 -> -2_147_483_648..2_147_483_647
8 -> -9_223_372_036_854_775_808..9_223_372_036_854_775_807
:smallint -> -32_768..32_767
:integer -> -2_147_483_648..2_147_483_647
:bigint -> -9_223_372_036_854_775_808..9_223_372_036_854_775_807
:smallserial -> 1..32_767
:serial -> 1..2_147_483_647
:bigserial -> 1..9_223_372_036_854_775_807
end
end

@impl true
def type(_range), do: :integer

@impl true
def cast(value, range) do
if is_integer(value) and value not in range do
:error
else
Ecto.Type.cast(:integer, value)
end
end

@impl true
def load(value, loader, _range) do
Ecto.Type.load(:integer, value, loader)
end

@impl true
def dump(value, dumper, _range) do
Ecto.Type.dump(:integer, value, dumper)
end

@impl true
def equal?(a, b, _range) do
a == b
end
end
50 changes: 50 additions & 0 deletions test/bitcrowd_ecto/fixed_width_integer_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
defmodule BitcrowdEcto.FixedWidthIntegerTest do
use ExUnit.Case, async: true
import BitcrowdEcto.Assertions
import Ecto.Changeset

defmodule TestSchema do
use Ecto.Schema

embedded_schema do
field(:int_4, BitcrowdEcto.FixedWidthInteger, width: 4)
field(:int_smallint, BitcrowdEcto.FixedWidthInteger, width: :smallint)
field(:int_bigserial, BitcrowdEcto.FixedWidthInteger, width: :bigserial)
end
end

test "casting an out-of-range value results in a changeset error" do
for ok <- [-2, 2, 0, -2_147_483_648, 2_147_483_647] do
cs = cast(%TestSchema{}, %{int_4: ok}, [:int_4])
assert cs.valid?
end

for not_ok <- [-2_147_483_649, 2_147_483_648] do
cs = cast(%TestSchema{}, %{int_4: not_ok}, [:int_4])
refute cs.valid?
assert_error_on(cs, :int_4, :cast)
end

for ok <- [-2, 2, 0, -32768, 32767] do

Check warning on line 28 in test/bitcrowd_ecto/fixed_width_integer_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (23.1.1, 1.12.2)

Numbers larger than 9999 should be written with underscores: 32_767

Check warning on line 28 in test/bitcrowd_ecto/fixed_width_integer_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (23.1.1, 1.12.2)

Numbers larger than 9999 should be written with underscores: 32_768
cs = cast(%TestSchema{}, %{int_smallint: ok}, [:int_smallint])
assert cs.valid?
end

for not_ok <- [-32769, 32768] do

Check warning on line 33 in test/bitcrowd_ecto/fixed_width_integer_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (23.1.1, 1.12.2)

Numbers larger than 9999 should be written with underscores: 32_768

Check warning on line 33 in test/bitcrowd_ecto/fixed_width_integer_test.exs

View workflow job for this annotation

GitHub Actions / Build and test (23.1.1, 1.12.2)

Numbers larger than 9999 should be written with underscores: 32_769
cs = cast(%TestSchema{}, %{int_smallint: not_ok}, [:int_smallint])
refute cs.valid?
assert_error_on(cs, :int_smallint, :cast)
end

for ok <- [1, 9_223_372_036_854_775_807] do
cs = cast(%TestSchema{}, %{int_bigserial: ok}, [:int_bigserial])
assert cs.valid?
end

for not_ok <- [-1, 0, 9_223_372_036_854_775_808] do
cs = cast(%TestSchema{}, %{int_bigserial: not_ok}, [:int_bigserial])
refute cs.valid?
assert_error_on(cs, :int_bigserial, :cast)
end
end
end

0 comments on commit ca6ae92

Please sign in to comment.