Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce minimum, maximum and numeric types #25

Merged
merged 2 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ It is currently used in production in several projects (mainly as request parame
- [`float(error_key = nil)`](#floaterror_key--nil)
- [`hash_value(error_key = nil)`](#hash_valueerror_key--nil)
- [`integer(error_key = nil)`](#integererror_key--nil)
- [`numeric(error_key = nil)`](#numericerror_key--nil)
- [`string(error_key = nil)`](#stringerror_key--nil)
- [Convenience types](#convenience-types)
- [`hash_with_symbolized_keys(error_key = nil)`](#hash_with_symbolized_keyserror_key--nil)
- [`integer32(error_key = nil)`](#integer32error_key--nil)
- [`maximum(max, error_key = nil, inclusive: true)`](#maximummax-error_key--nil-inclusive-true)
- [`minimum(min, error_key = nil, inclusive: true)`](#minimummin-error_key--nil-inclusive-true)
- [`non_empty_string(error_key = nil)`](#non_empty_stringerror_key--nil)
- [`pattern(regexp, error_key = nil)`](#patternregexp-error_key--nil)
- [`uuid(error_key = nil)`](#uuiderror_key--nil)
Expand Down Expand Up @@ -500,6 +503,12 @@ Returns ValidResult if and only if provided value is an integer. Doesn't transfo

I18n keys: `error_key`, `'.integer'`, `'datacaster.errors.integer'`.

#### `numeric(error_key = nil)`

Returns ValidResult if and only if provided value is a number (Ruby's `Numeric`). Doesn't transform the value.

I18n keys: `error_key`, `'.numeric'`, `'datacaster.errors.numeric'`.

#### `string(error_key = nil)`

Returns ValidResult if and only if provided value is a string. Doesn't transform the value.
Expand All @@ -523,6 +532,26 @@ I18n keys:
* not an integer – `error_key`, `'.integer'`, `'datacaster.errors.integer'`
* too big – `error_key`, `'.integer32'`, `'datacaster.errors.integer32'`

#### `maximum(max, error_key = nil, inclusive: true)`

Returns ValidResult if and only if provided value is a number and is less than `max`. If `inclusive` set to true, provided value should be less than or equal to `max`. Doesn't transform the value.

I18n keys:

* not a number – `error_key`, `'.numeric'`, `'datacaster.errors.numeric'`
* is less (when `inclusive` is `true`) – `error_key`, `'.maximum.lteq'`, `'datacaster.errors.maximum.lteq'`
* is less (when `inclusive` is `false`) – `error_key`, `'.maximum.lt'`, `'datacaster.errors.maximum.lt'`

#### `minimum(min, error_key = nil, inclusive: true)`

Returns ValidResult if and only if provided value is a number and is greater than `min`. If `inclusive` set to true, provided value should be greater than or equal to `min`. Doesn't transform the value.

I18n keys:

* not a number – `error_key`, `'.numeric'`, `'datacaster.errors.numeric'`
* is greater (when `inclusive` is `true`) – `error_key`, `'.minimum.gteq'`, `'datacaster.errors.minimum.gteq'`
* is greater (when `inclusive` is `false`) – `error_key`, `'.minimum.gt'`, `'datacaster.errors.minimum.gt'`

#### `non_empty_string(error_key = nil)`

Returns ValidResult if and only if provided value is a string and is not empty. Doesn't transform the value.
Expand Down
7 changes: 7 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ en:
integer: is not an integer
integer32: is not a 32-bit integer
iso8601: is not a string with ISO-8601 date and time
maximum:
lt: '%{value} should be less than %{max}'
lteq: '%{value} should be less than or equal to %{max}'
minimum:
gt: '%{value} should be greater than %{min}'
gteq: '%{value} should be greater than or equal to %{min}'
must_be: "is not %{reference}"
numeric: is not a number
pattern: has invalid format
relate: "%{left} should be %{op} %{right}"
responds_to: "does not respond to %{reference}"
Expand Down
42 changes: 42 additions & 0 deletions lib/datacaster/predefined.rb
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,12 @@ def with(keys, caster)

# Strict types

def numeric(error_key = nil)
error_keys = ['.numeric', 'datacaster.errors.numeric']
error_keys.unshift(error_key) if error_key
check { |x| x.is_a?(Numeric) }.i18n_key(*error_keys)
end

def decimal(digits = 8, error_key = nil)
error_keys = ['.decimal', 'datacaster.errors.decimal']
error_keys.unshift(error_key) if error_key
Expand Down Expand Up @@ -355,6 +361,42 @@ def integer32(error_key = nil)
integer(error_key) & check { |x| x.abs <= 2_147_483_647 }.i18n_key(*error_keys)
end

def maximum(max, error_key = nil, inclusive: true)
subkey = 'lt'
subkey += 'eq' if inclusive

error_keys = [".maximum.#{subkey}", "datacaster.errors.maximum.#{subkey}"]

error_keys.unshift(error_key) if error_key

caster =
if inclusive
check { |x| x <= max }
else
check { |x| x < max }
end

numeric(error_key) & caster.i18n_key(*error_keys, max:)
end

def minimum(min, error_key = nil, inclusive: true)
subkey = 'gt'
subkey += 'eq' if inclusive

error_keys = [".minimum.#{subkey}", "datacaster.errors.minimum.#{subkey}"]

error_keys.unshift(error_key) if error_key

caster =
if inclusive
check { |x| x >= min }
else
check { |x| x > min }
end

numeric(error_key) & caster.i18n_key(*error_keys, min:)
end

def string(error_key = nil)
error_keys = ['.string', 'datacaster.errors.string']
error_keys.unshift(error_key) if error_key
Expand Down
2 changes: 1 addition & 1 deletion lib/datacaster/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Datacaster
VERSION = "4.1.0"
VERSION = "4.2.0"
end
152 changes: 152 additions & 0 deletions spec/datacaster_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,34 @@
end
end

describe "numeric typecasting" do
subject { described_class.schema { numeric } }

it "passes integers" do
expect(subject.(2_147_483_647).to_dry_result).to eq Success(2_147_483_647)
end

it "passes floats" do
expect(subject.(1.33).to_dry_result).to eq Success(1.33)
end

it "passes decimals" do
expect(subject.(1.33.to_d).to_dry_result).to eq Success(1.33.to_d)
end

it "passes rationals" do
expect(subject.(2/3r).to_dry_result).to eq Success(2/3r)
end

it "passes complex numbers" do
expect(subject.(3i).to_dry_result).to eq Success(3i)
end

it "returns Failure on string numbers" do
expect(subject.("100").to_dry_result).to eq Failure(["is not a number"])
end
end

describe "decimal typecasting" do
subject { described_class.schema { decimal } }

Expand Down Expand Up @@ -184,6 +212,130 @@
end
end

describe "maximum typecasting" do
context "when inclusive implicitly" do
subject { described_class.schema { maximum(4, inclusive: true) } }

it "passes less than maximum" do
expect(subject.(3).to_dry_result).to eq Success(3)
end

it "passes equal to maximum" do
expect(subject.(4).to_dry_result).to eq Success(4)
end

it "doesn't pass greater than maximum" do
expect(subject.(5).to_dry_result).to eq Failure(["5 should be less than or equal to 4"])
end

it "doesn't pass non-numerics" do
expect(subject.(:'123').to_dry_result).to eq Failure(["is not a number"])
end
end

context "when inclusive explicitly" do
subject { described_class.schema { maximum(4) } }

it "passes less than maximum" do
expect(subject.(3).to_dry_result).to eq Success(3)
end

it "passes equal to maximum" do
expect(subject.(4).to_dry_result).to eq Success(4)
end

it "doesn't pass greater than maximum" do
expect(subject.(5).to_dry_result).to eq Failure(["5 should be less than or equal to 4"])
end

it "doesn't pass non-numerics" do
expect(subject.(:'123').to_dry_result).to eq Failure(["is not a number"])
end
end

context "when exclusive" do
subject { described_class.schema { maximum(4, inclusive: false) } }

it "passes less than maximum" do
expect(subject.(3).to_dry_result).to eq Success(3)
end

it "doesn't pass equal to maximum" do
expect(subject.(4).to_dry_result).to eq Failure(["4 should be less than 4"])
end

it "doesn't pass greater than maximum" do
expect(subject.(5).to_dry_result).to eq Failure(["5 should be less than 4"])
end

it "doesn't pass non-numerics" do
expect(subject.(:'123').to_dry_result).to eq Failure(["is not a number"])
end
end
end

describe "minimum typecasting" do
context "when inclusive implicitly" do
subject { described_class.schema { minimum(4, inclusive: true) } }

it "passes greater than minimum" do
expect(subject.(5).to_dry_result).to eq Success(5)
end

it "passes equal to minimum" do
expect(subject.(4).to_dry_result).to eq Success(4)
end

it "doesn't pass less than minimum" do
expect(subject.(3).to_dry_result).to eq Failure(["3 should be greater than or equal to 4"])
end

it "doesn't pass non-numerics" do
expect(subject.(:'123').to_dry_result).to eq Failure(["is not a number"])
end
end

context "when inclusive explicitly" do
subject { described_class.schema { minimum(4) } }

it "passes greater than minimum" do
expect(subject.(5).to_dry_result).to eq Success(5)
end

it "passes equal to minimum" do
expect(subject.(4).to_dry_result).to eq Success(4)
end

it "doesn't pass less than minimum" do
expect(subject.(3).to_dry_result).to eq Failure(["3 should be greater than or equal to 4"])
end

it "doesn't pass non-numerics" do
expect(subject.(:'123').to_dry_result).to eq Failure(["is not a number"])
end
end

context "when exclusive" do
subject { described_class.schema { minimum(4, inclusive: false) } }

it "passes greater than minimum" do
expect(subject.(5).to_dry_result).to eq Success(5)
end

it "doesn't pass equal to minimum" do
expect(subject.(4).to_dry_result).to eq Failure(["4 should be greater than 4"])
end

it "doesn't pass less than minimum" do
expect(subject.(3).to_dry_result).to eq Failure(["3 should be greater than 4"])
end

it "doesn't pass non-numerics" do
expect(subject.(:'123').to_dry_result).to eq Failure(["is not a number"])
end
end
end

describe "pattern typecasting" do
subject { described_class.schema { pattern(/\A\d+\z/) } }

Expand Down