Skip to content

Commit 6a79f48

Browse files
committed
Add BitcrowdEcto.FixedWidthInteger
1 parent 8e70c8e commit 6a79f48

2 files changed

Lines changed: 116 additions & 0 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
@postgres_type_ranges %{
16+
smallint: -32_768..32_767,
17+
integer: -2_147_483_648..2_147_483_647,
18+
bigint: -9_223_372_036_854_775_808..9_223_372_036_854_775_807,
19+
smallserial: 1..32_767,
20+
serial: 1..2_147_483_647,
21+
bigserial: 1..9_223_372_036_854_775_807
22+
}
23+
24+
@generic_byte_size_ranges %{
25+
2 => -32_768..32_767,
26+
4 => -2_147_483_648..2_147_483_647,
27+
8 => -9_223_372_036_854_775_808..9_223_372_036_854_775_807
28+
}
29+
30+
@impl true
31+
def init(opts) do
32+
opts
33+
|> Keyword.get(:width, 4)
34+
|> width_to_range()
35+
end
36+
37+
defp width_to_range(type) when is_atom(type), do: Map.fetch!(@postgres_type_ranges, type)
38+
defp width_to_range(size) when is_integer(size), do: Map.fetch!(@generic_byte_size_ranges, size)
39+
40+
@impl true
41+
def type(_range), do: :integer
42+
43+
@impl true
44+
def cast(value, range) do
45+
if is_integer(value) and value not in range do
46+
:error
47+
else
48+
Ecto.Type.cast(:integer, value)
49+
end
50+
end
51+
52+
@impl true
53+
def load(value, loader, _range) do
54+
Ecto.Type.load(:integer, value, loader)
55+
end
56+
57+
@impl true
58+
def dump(value, dumper, _range) do
59+
Ecto.Type.dump(:integer, value, dumper)
60+
end
61+
62+
@impl true
63+
def equal?(a, b, _range) do
64+
a == b
65+
end
66+
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, -32_768, 32_767] do
29+
cs = cast(%TestSchema{}, %{int_smallint: ok}, [:int_smallint])
30+
assert cs.valid?
31+
end
32+
33+
for not_ok <- [-32_769, 32_768] 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)