Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
* [#2808](https://github.com/ruby-grape/grape/pull/2808): Internalize `build_params_with` and `auth` behind `Grape::Util::InheritableSetting` accessors - [@ericproulx](https://github.com/ericproulx).
* [#2807](https://github.com/ruby-grape/grape/pull/2807): Internalize routing scope flags behind `Grape::Util::InheritableSetting` accessors (`do_not_route_head`, `do_not_route_options`, `do_not_document`, `lint` — `!` writers / `?` readers), and drop the vestigial `namespace_inheritable` hash copy in `Grape::API::Instance`'s route-config collection - [@ericproulx](https://github.com/ericproulx).
* [#2810](https://github.com/ruby-grape/grape/pull/2810): Make `Grape::Util::InheritableSetting`'s raw stores internal where the ecosystem allows: `namespace_inheritable` is now protected and `namespace_stackable_with_hash` private, while `namespace_stackable` stays public for grape-swagger; `Router::Pattern::Path` reads a dedicated `path_settings` snapshot, and `mount_paths` exposes the full mount-path stack - [@ericproulx](https://github.com/ericproulx).
* [#2822](https://github.com/ruby-grape/grape/pull/2822): Add a `numericality` validator (`greater_than`, `greater_than_or_equal_to`, `less_than`, `less_than_or_equal_to`, `equal_to`, `other_than`, `only_integer`, `odd`, `even`) - [@ericproulx](https://github.com/ericproulx).
* Your contribution here.

#### Fixes
Expand Down
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1630,6 +1630,37 @@ params do
end
```

#### `numericality`

Numeric parameters can be restricted to a range or a shape with the `:numericality` option. It accepts any combination of `:greater_than`, `:greater_than_or_equal_to`, `:less_than`, `:less_than_or_equal_to`, `:equal_to`, `:other_than`, `:only_integer`, `:odd` and `:even`.

```ruby
params do
requires :quantity, type: Integer, numericality: { greater_than: 0 }
requires :discount, type: Float, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }
requires :rating, type: Integer, numericality: { equal_to: 5 }
requires :page, type: Integer, numericality: { greater_than: 0, only_integer: true }
end
```

`:only_integer` accepts whole-numbered `Float`/`BigDecimal` values as well as `Integer`, so it also works on non-integer types:

```ruby
params do
requires :amount, type: Float, numericality: { only_integer: true } # 5.0 is valid, 5.5 is not
end
```

When applied to a typed array, every element is checked individually:

```ruby
params do
requires :numbers, type: [Integer], numericality: { greater_than: 0 }
end
```

`:equal_to` cannot be combined with any other comparison option, `:greater_than`/`:greater_than_or_equal_to` cannot be combined with each other (same for the `:less_than` pair), and `:odd`/`:even` are mutually exclusive; combining them raises an `ArgumentError` when the params are defined. Values that aren't `Numeric` (e.g. because no `:type`/coercion was declared) are left untouched by this validator — pair it with `:type` to enforce numeric-ness.

#### `regexp`

Parameters can be restricted to match a specific regular expression with the `:regexp` option. If the value does not match the regular expression an error will be returned. Note that this is true for both `requires` and `optional` parameters.
Expand Down Expand Up @@ -2022,6 +2053,14 @@ params do
end
```

#### `numericality`

```ruby
params do
requires :rating, type: Integer, numericality: { equal_to: 5, message: 'must be a perfect score' }
end
```

#### `all_or_none_of`

```ruby
Expand Down
9 changes: 9 additions & 0 deletions lib/grape/locale/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ en:
resolution: 'eg: version ''v1'', using: :header, vendor: ''twitter'''
summary: 'when version using header, you must specify :vendor option'
mutual_exclusion: 'are mutually exclusive'
numericality_equal_to: 'must be equal to %{count}'
numericality_even: 'must be even'
numericality_greater_than: 'must be greater than %{count}'
numericality_greater_than_or_equal_to: 'must be greater than or equal to %{count}'
numericality_less_than: 'must be less than %{count}'
numericality_less_than_or_equal_to: 'must be less than or equal to %{count}'
numericality_odd: 'must be odd'
numericality_only_integer: 'must be an integer'
numericality_other_than: 'must be other than %{count}'
oneof: 'does not match any of the allowed schemas'
presence: 'is missing'
regexp: 'is invalid'
Expand Down
89 changes: 89 additions & 0 deletions lib/grape/validations/validators/numericality_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# frozen_string_literal: true

module Grape
module Validations
module Validators
class NumericalityValidator < Base
COMPARISON_OPTIONS = %i[greater_than greater_than_or_equal_to less_than less_than_or_equal_to equal_to other_than].freeze

def initialize(attrs, options, required, scope, opts)
super

@greater_than, @greater_than_or_equal_to, @less_than, @less_than_or_equal_to, @equal_to, @other_than =
options.values_at(*COMPARISON_OPTIONS)
@only_integer, @odd, @even = options.values_at(:only_integer, :odd, :even)

validate_options!
end

def validate_param!(attr_name, params)
return unless hash_like?(params)

Array.wrap(params[attr_name]).each { |val| check_value!(attr_name, val) }
end

private

def check_value!(attr_name, val)
return unless val.is_a?(Numeric)

check_shape!(attr_name, val)
check_comparisons!(attr_name, val)
end

def check_shape!(attr_name, val)
fail!(attr_name, :only_integer) if @only_integer && !whole?(val)
fail!(attr_name, :odd) if @odd && val.respond_to?(:odd?) && val.even?
fail!(attr_name, :even) if @even && val.respond_to?(:even?) && val.odd?
end

def check_comparisons!(attr_name, val)
fail!(attr_name, :equal_to, @equal_to) if @equal_to && val != @equal_to
fail!(attr_name, :other_than, @other_than) if @other_than && val == @other_than
fail!(attr_name, :greater_than, @greater_than) if @greater_than && val <= @greater_than
fail!(attr_name, :greater_than_or_equal_to, @greater_than_or_equal_to) if @greater_than_or_equal_to && val < @greater_than_or_equal_to
fail!(attr_name, :less_than, @less_than) if @less_than && val >= @less_than
fail!(attr_name, :less_than_or_equal_to, @less_than_or_equal_to) if @less_than_or_equal_to && val > @less_than_or_equal_to
end

def whole?(val)
val.is_a?(Integer) || (val.respond_to?(:to_i) && val == val.to_i)
end

def fail!(attr_name, key, count = nil)
validation_error!(attr_name, message { translate(:"numericality_#{key}", count:) })
end

def validate_options!
validate_numeric_option!(:greater_than, @greater_than)
validate_numeric_option!(:greater_than_or_equal_to, @greater_than_or_equal_to)
validate_numeric_option!(:less_than, @less_than)
validate_numeric_option!(:less_than_or_equal_to, @less_than_or_equal_to)
validate_numeric_option!(:equal_to, @equal_to)
validate_numeric_option!(:other_than, @other_than)

raise ArgumentError, 'greater_than cannot be combined with greater_than_or_equal_to' if @greater_than && @greater_than_or_equal_to
raise ArgumentError, 'less_than cannot be combined with less_than_or_equal_to' if @less_than && @less_than_or_equal_to
raise ArgumentError, 'odd cannot be combined with even' if @odd && @even

other_comparisons_given = @greater_than || @greater_than_or_equal_to || @less_than || @less_than_or_equal_to || @other_than
raise ArgumentError, 'equal_to cannot be combined with other comparison options' if @equal_to && other_comparisons_given

validate_range!
end

def validate_numeric_option!(name, val)
raise ArgumentError, "#{name} must be a Numeric value" if !val.nil? && !val.is_a?(Numeric)
end

def validate_range!
lower = @greater_than || @greater_than_or_equal_to
upper = @less_than || @less_than_or_equal_to
return unless lower && upper && lower > upper

raise ArgumentError, "#{lower} cannot be greater than #{upper}"
end
end
end
end
end
Loading
Loading