Skip to content

Commit ca6ae92

Browse files
committed
Add BitcrowdEcto.FixedWidthInteger
1 parent 55e8f8a commit ca6ae92

File tree

2 files changed

+106
-0
lines changed

2 files changed

+106
-0
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
defmodule BitcrowdEcto.FixedWidthInteger do
2+
@moduledoc """
3+
An Ecto type that automatically validates that the given integer fits the underlying DB type.
4+
5+
This turns the ugly Postgrex errors into neat `validation: :cast` changeset errors without
6+
having to manually `validate_number` all `:integer` fields.
7+
8+
Named widths are based on Postgres' integer types.
9+
10+
https://www.postgresql.org/docs/current/datatype-numeric.html
11+
"""
12+
13+
use Ecto.ParameterizedType
14+
15+
@impl true
16+
def init(opts) do
17+
case Keyword.get(opts, :width, 4) do
18+
2 -> -32_768..32_767
19+
4 -> -2_147_483_648..2_147_483_647
20+
8 -> -9_223_372_036_854_775_808..9_223_372_036_854_775_807
21+
:smallint -> -32_768..32_767
22+
:integer -> -2_147_483_648..2_147_483_647
23+
:bigint -> -9_223_372_036_854_775_808..9_223_372_036_854_775_807
24+
:smallserial -> 1..32_767
25+
:serial -> 1..2_147_483_647
26+
:bigserial -> 1..9_223_372_036_854_775_807
27+
end
28+
end
29+
30+
@impl true
31+
def type(_range), do: :integer
32+
33+
@impl true
34+
def cast(value, range) do
35+
if is_integer(value) and value not in range do
36+
:error
37+
else
38+
Ecto.Type.cast(:integer, value)
39+
end
40+
end
41+
42+
@impl true
43+
def load(value, loader, _range) do
44+
Ecto.Type.load(:integer, value, loader)
45+
end
46+
47+
@impl true
48+
def dump(value, dumper, _range) do
49+
Ecto.Type.dump(:integer, value, dumper)
50+
end
51+
52+
@impl true
53+
def equal?(a, b, _range) do
54+
a == b
55+
end
56+
end
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
defmodule BitcrowdEcto.FixedWidthIntegerTest do
2+
use ExUnit.Case, async: true
3+
import BitcrowdEcto.Assertions
4+
import Ecto.Changeset
5+
6+
defmodule TestSchema do
7+
use Ecto.Schema
8+
9+
embedded_schema do
10+
field(:int_4, BitcrowdEcto.FixedWidthInteger, width: 4)
11+
field(:int_smallint, BitcrowdEcto.FixedWidthInteger, width: :smallint)
12+
field(:int_bigserial, BitcrowdEcto.FixedWidthInteger, width: :bigserial)
13+
end
14+
end
15+
16+
test "casting an out-of-range value results in a changeset error" do
17+
for ok <- [-2, 2, 0, -2_147_483_648, 2_147_483_647] do
18+
cs = cast(%TestSchema{}, %{int_4: ok}, [:int_4])
19+
assert cs.valid?
20+
end
21+
22+
for not_ok <- [-2_147_483_649, 2_147_483_648] do
23+
cs = cast(%TestSchema{}, %{int_4: not_ok}, [:int_4])
24+
refute cs.valid?
25+
assert_error_on(cs, :int_4, :cast)
26+
end
27+
28+
for ok <- [-2, 2, 0, -32768, 32767] do
29+
cs = cast(%TestSchema{}, %{int_smallint: ok}, [:int_smallint])
30+
assert cs.valid?
31+
end
32+
33+
for not_ok <- [-32769, 32768] do
34+
cs = cast(%TestSchema{}, %{int_smallint: not_ok}, [:int_smallint])
35+
refute cs.valid?
36+
assert_error_on(cs, :int_smallint, :cast)
37+
end
38+
39+
for ok <- [1, 9_223_372_036_854_775_807] do
40+
cs = cast(%TestSchema{}, %{int_bigserial: ok}, [:int_bigserial])
41+
assert cs.valid?
42+
end
43+
44+
for not_ok <- [-1, 0, 9_223_372_036_854_775_808] do
45+
cs = cast(%TestSchema{}, %{int_bigserial: not_ok}, [:int_bigserial])
46+
refute cs.valid?
47+
assert_error_on(cs, :int_bigserial, :cast)
48+
end
49+
end
50+
end

0 commit comments

Comments
 (0)