sinatra-param-validator 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +37 -0
  4. data/Gemfile +0 -7
  5. data/Gemfile.lock +28 -5
  6. data/README.md +159 -2
  7. data/lib/sinatra/param_validator/camelize.rb +12 -0
  8. data/lib/sinatra/param_validator/definitions.rb +24 -0
  9. data/lib/sinatra/param_validator/helpers.rb +25 -0
  10. data/lib/sinatra/param_validator/identifier.rb +15 -0
  11. data/lib/sinatra/param_validator/invalid_parameter_error.rb +9 -0
  12. data/lib/sinatra/param_validator/parameter/array.rb +25 -0
  13. data/lib/sinatra/param_validator/parameter/boolean.rb +26 -0
  14. data/lib/sinatra/param_validator/parameter/common.rb +98 -0
  15. data/lib/sinatra/param_validator/parameter/date.rb +24 -0
  16. data/lib/sinatra/param_validator/parameter/float.rb +23 -0
  17. data/lib/sinatra/param_validator/parameter/hash.rb +25 -0
  18. data/lib/sinatra/param_validator/parameter/integer.rb +23 -0
  19. data/lib/sinatra/param_validator/parameter/string.rb +27 -0
  20. data/lib/sinatra/param_validator/parameter/time.rb +24 -0
  21. data/lib/sinatra/param_validator/parameter.rb +28 -0
  22. data/lib/sinatra/param_validator/parser.rb +56 -0
  23. data/lib/sinatra/param_validator/rule/all_or_none_of.rb +31 -0
  24. data/lib/sinatra/param_validator/rule/any_of.rb +29 -0
  25. data/lib/sinatra/param_validator/rule/one_of.rb +30 -0
  26. data/lib/sinatra/param_validator/rule.rb +23 -0
  27. data/lib/sinatra/param_validator/snake_case.rb +12 -0
  28. data/lib/sinatra/param_validator/validation_failed_error.rb +15 -0
  29. data/lib/sinatra/param_validator/validator/form.rb +37 -0
  30. data/lib/sinatra/param_validator/validator/url_param.rb +14 -0
  31. data/lib/sinatra/param_validator/validator.rb +42 -0
  32. data/lib/sinatra/param_validator/version.rb +1 -1
  33. data/lib/sinatra/param_validator.rb +40 -2
  34. data/sinatra-param-validator.gemspec +9 -3
  35. metadata +140 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c121f205eb49bdae18ff06dd346292bd8c9fc9018468002cf26836728a466449
4
- data.tar.gz: eceddebc3e9bcfeef60e69c6e190fc33e360b14458613e20951f7f8fc0b952a2
3
+ metadata.gz: f474d7b6ff2e2ed3de6f58e3bd0ab0b328cc11886ac36701c5a1557a8713e5be
4
+ data.tar.gz: 378f0b0bb00df8adca7f951175133833d68ce457066f79423aadb52cab1ba6fb
5
5
  SHA512:
6
- metadata.gz: 5e629007dc0aa4cf0e6edf751992031d366b6eceea1c615c23b05ad0d05fe3681bfae9346c1139c9c4ad9b0f3c40cf41a815e9b95cf426a772aa4aedd4ecd999
7
- data.tar.gz: 89ba7f100e4314453d3fa0d1bee0f6237f56c970fa6580e1cd2a73451f05e9ae2645ff056c7c7eafadb4be531362bdfbc1f8911de48f38ea8eca4229e2df1664
6
+ metadata.gz: 045a26ac9992b92e1dab3c63527b6ebfd2b70c84b554d20a7f38abfa070f0402da47776d663619c0ae1ff95c681a52ccfb7da34fa2f5c10928c28626c77f75a9
7
+ data.tar.gz: cefd05d4f4babca139755e05d891802e375732bc944d278a8313e14759e87cd58b300c567e236ccf844897233072ce37e79d9c70f27385babd8b35435008a5c3
data/.rubocop.yml CHANGED
@@ -18,3 +18,6 @@ Metrics/BlockLength:
18
18
  # Extend permitted line length
19
19
  Layout/LineLength:
20
20
  Max: 120
21
+
22
+ RSpec/NestedGroups:
23
+ Max: 4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2022-06-09
4
+
5
+ - Allow custom error messages to be used when validation fails
6
+ - Ensure running multiple validations for a single parameter merges the errors correctly
7
+ - Allow validations to run code if successful
8
+ - Allow exceptions to be raised by the parameter block to indicate failure
9
+ - Allow parameters to be passed to validators
10
+
11
+ ## [0.3.0] - 2022-06-08
12
+
13
+ - Don't create entries in `params` for parameters that are not passed
14
+ - Don't set validator type during definition
15
+ - Add unique validator conditionals for each validator:
16
+ - validate
17
+ - validate_form
18
+ - validate_url_param
19
+
20
+ ## [0.2.0] - 2022-06-08
21
+
22
+ - Add validators:
23
+ - Standard
24
+ - URL Parameter
25
+ - Form
26
+ - Add parameters:
27
+ - Array
28
+ - Boolean
29
+ - Date
30
+ - Float
31
+ - Hash
32
+ - Integer
33
+ - String
34
+ - Time
35
+ - Add rules:
36
+ - All or none of
37
+ - Any of
38
+ - One of
39
+
3
40
  ## [0.1.0] - 2022-05-16
4
41
 
5
42
  - Initial release
data/Gemfile CHANGED
@@ -4,10 +4,3 @@ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in sinatra-param_validator-validator.gemspec
6
6
  gemspec
7
-
8
- gem 'rake', '~> 13.0'
9
- gem 'rspec', '~> 3.0'
10
- gem 'rubocop', '~> 1.21'
11
- gem 'rubocop-performance', '~> 1.0'
12
- gem 'rubocop-rake', '~> 0.5'
13
- gem 'rubocop-rspec', '~> 2.0'
data/Gemfile.lock CHANGED
@@ -1,19 +1,27 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sinatra-param-validator (0.1.0)
4
+ sinatra-param-validator (0.4.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
9
  ast (2.4.2)
10
10
  diff-lcs (1.5.0)
11
+ multi_json (1.15.0)
12
+ mustermann (1.1.1)
13
+ ruby2_keywords (~> 0.0.1)
11
14
  parallel (1.22.1)
12
15
  parser (3.1.2.0)
13
16
  ast (~> 2.4.1)
17
+ rack (2.2.3)
18
+ rack-protection (2.2.0)
19
+ rack
20
+ rack-test (1.1.0)
21
+ rack (>= 1.0, < 3)
14
22
  rainbow (3.1.1)
15
23
  rake (13.0.6)
16
- regexp_parser (2.3.1)
24
+ regexp_parser (2.4.0)
17
25
  rexml (3.2.5)
18
26
  rspec (3.11.0)
19
27
  rspec-core (~> 3.11.0)
@@ -28,7 +36,7 @@ GEM
28
36
  diff-lcs (>= 1.2.0, < 2.0)
29
37
  rspec-support (~> 3.11.0)
30
38
  rspec-support (3.11.0)
31
- rubocop (1.29.0)
39
+ rubocop (1.29.1)
32
40
  parallel (~> 1.10)
33
41
  parser (>= 3.1.0.0)
34
42
  rainbow (>= 2.2.2, < 4.0)
@@ -37,7 +45,7 @@ GEM
37
45
  rubocop-ast (>= 1.17.0, < 2.0)
38
46
  ruby-progressbar (~> 1.7)
39
47
  unicode-display_width (>= 1.4.0, < 3.0)
40
- rubocop-ast (1.17.0)
48
+ rubocop-ast (1.18.0)
41
49
  parser (>= 3.1.1.0)
42
50
  rubocop-performance (1.13.3)
43
51
  rubocop (>= 1.7.0, < 2.0)
@@ -47,18 +55,33 @@ GEM
47
55
  rubocop-rspec (2.10.0)
48
56
  rubocop (~> 1.19)
49
57
  ruby-progressbar (1.11.0)
58
+ ruby2_keywords (0.0.5)
59
+ sinatra (2.2.0)
60
+ mustermann (~> 1.0)
61
+ rack (~> 2.2)
62
+ rack-protection (= 2.2.0)
63
+ tilt (~> 2.0)
64
+ sinatra-contrib (2.2.0)
65
+ multi_json
66
+ mustermann (~> 1.0)
67
+ rack-protection (= 2.2.0)
68
+ sinatra (= 2.2.0)
69
+ tilt (~> 2.0)
70
+ tilt (2.0.10)
50
71
  unicode-display_width (2.1.0)
51
72
 
52
73
  PLATFORMS
53
74
  x86_64-linux
54
75
 
55
76
  DEPENDENCIES
77
+ rack-test (~> 1.1)
56
78
  rake (~> 13.0)
57
79
  rspec (~> 3.0)
58
- rubocop (~> 1.21)
80
+ rubocop (~> 1.0)
59
81
  rubocop-performance (~> 1.0)
60
82
  rubocop-rake (~> 0.5)
61
83
  rubocop-rspec (~> 2.0)
84
+ sinatra-contrib (~> 2.0)
62
85
  sinatra-param-validator!
63
86
 
64
87
  BUNDLED WITH
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # Sinatra::Param::Validator
2
2
 
3
+ Validate parameters in a Sinatra app.
3
4
 
4
5
  ## Installation
5
6
 
@@ -11,9 +12,165 @@ If bundler is not being used to manage dependencies, install the gem by executin
11
12
 
12
13
  $ gem install sinatra-param-validator
13
14
 
14
- ## Usage
15
+ ## Sample Usage
16
+
17
+ ```ruby
18
+ validator :user_id do
19
+ param :id, Integer, required: true
20
+ end
21
+
22
+ get '/user/:id', validate: :user_id do
23
+ # ...
24
+ end
25
+
26
+ validator :new_user do
27
+ param :name, String, required: true
28
+ param :age, Integer, required: true, min: 0
29
+ end
30
+
31
+ post '/new-user', validate: :new_user do
32
+ # ...
33
+ end
34
+ ```
35
+
36
+ ## Parameter Types
37
+
38
+ The following parameter types are built-in,
39
+ and values will be coerced to an object of that type.
40
+
41
+ * `Array`
42
+ * Accepts a comma-separated list of values, as well as an array
43
+ * e.g. `a,b,c`
44
+ * `Boolean`
45
+ * `false|f|no|n|0` or `true|t|yes|y|1`
46
+ * `Date`
47
+ * All formats accepted by `Date.parse`
48
+ * `Float`
49
+ * `Hash`
50
+ * Accepts a comma-separated list of colon-separated key-value pairs
51
+ * e.g. `a:1,b:2,c:3`
52
+ * `Integer`
53
+ * `String`
54
+ * `Time`
55
+ * All formats accepted by `Time.parse`
56
+
57
+ Types can be defined using class names or symbols:
58
+
59
+ ```ruby
60
+ param :name, String
61
+ param :tick_box, :boolean
62
+ ```
63
+
64
+ ## Parameter Validations
65
+
66
+ ```ruby
67
+ param :number, Integer, required: true, in: 0..100
68
+ ```
69
+
70
+ All parameters have the following validations available:
71
+
72
+ * `nillable`
73
+ * If this is set, all other validations are skipped if the value is nil
74
+ * `required`
75
+ * The parameter must be present and cannot be nil
76
+ * `in`
77
+ * The value is in the given array / range
78
+ * `is`
79
+ * Match a specific value
80
+
81
+ `Array`, `Hash` and `String` have the following validations:
82
+
83
+ * `min_length` / `max_length`
84
+
85
+ `Date`, `Time`, `Float` and `Integer` have the following validations:
86
+
87
+ * `min` / `max`
88
+
89
+ ## Custom Messages
90
+
91
+ It is possible to return a custom error message when a validation fails:
92
+
93
+ ```ruby
94
+ param :number, Integer, required: true, message: 'The number is required'
95
+ ```
96
+
97
+ It is also possible to run multiple validations against a single parameter.
98
+ This can be useful if different failures require different messages.
99
+
100
+ ```ruby
101
+ param :number, Integer, required: true, message: 'The number is required'
102
+ param :number, Integer, min: 100, message: 'The number is not large enough'
103
+ ```
104
+
105
+ ## Validation blocks
106
+
107
+ It is possible to run code after a validation succeeds, by passing a block to `param`:
108
+
109
+ ```ruby
110
+ param :number, Integer, required: true do
111
+ # ...
112
+ end
113
+ ```
114
+
115
+ If you wish to indicate a validation failure within a block, raise `Sinatra::ParameterValidator::InvalidParameterError`
116
+ with a message, and it will be passed through as an error for the parameter.
117
+
118
+ ## Rules
119
+
120
+ Rules work on multiple parameters:
121
+
122
+ ```ruby
123
+ rule :all_or_none_of, :a, :b
124
+ ```
125
+
126
+ * `all_or_none_of`
127
+ * `any_of`
128
+ * At least one of the given fields must be present
129
+ * `one_of`
130
+ * Only one of the given fields can be present
131
+
132
+ ## Validator Types
133
+
134
+ The default validator will raise `Sinatra::ParamValidator::ValidationFailedError` when validation fails.
135
+
136
+ There are two other provided validators, that handle failure differently:
137
+
138
+ * `url_param`
139
+ * will `halt 403`
140
+ * `form`
141
+ * if [sinatra-flash](https://github.com/SFEley/sinatra-flash) is available, it will flash the errors and `redirect back`
142
+ * will provide a JSON object with errors to an XHR request
143
+ * will `halt 400`
144
+
145
+ These validators can be invoked with a different conditional on the route:
146
+
147
+ ```ruby
148
+ post '/new-user', validate_form: :new_user do
149
+ # ...
150
+ end
151
+
152
+
153
+ get '/user/:id', validate_url_param: :user_id do
154
+ # ...
155
+ end
156
+ ```
157
+
158
+ ## Validators with parameters
159
+
160
+ It is possible to define a validator with a parameter.
161
+ To call the validator, you can use the `vi` helper to wrap a validator identifier with arguments:
162
+
163
+ ```ruby
164
+ validator :number do |min|
165
+ param :id, Integer, min: min
166
+ end
167
+
168
+ post '/number', validate: vi(:new_user, 10) do
169
+ # ...
170
+ end
171
+ ```
172
+
15
173
 
16
- TODO: Write usage instructions here
17
174
 
18
175
  ## Development
19
176
 
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sinatra
4
+ module ParamValidator
5
+ # Helpers for validating parameters
6
+ module Camelize
7
+ def camelize(symbol)
8
+ symbol.to_s.split('_').map(&:capitalize).join
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sinatra
4
+ module ParamValidator
5
+ # Store of valid definitions
6
+ class Definitions
7
+ def initialize
8
+ @definitions = {}
9
+ end
10
+
11
+ def add(identifier, validator)
12
+ raise "Validator already defined: '#{identifier}'" if @definitions.key? identifier
13
+
14
+ @definitions[identifier] = validator
15
+ end
16
+
17
+ def get(identifier)
18
+ raise "Unknown validator: '#{identifier}'" unless @definitions.key? identifier
19
+
20
+ @definitions[identifier]
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sinatra
4
+ module ParamValidator
5
+ # Helpers for validating parameters
6
+ module Helpers
7
+ def filter_params
8
+ params.each do |(param, value)|
9
+ params[param] = nil if value == ''
10
+ params[param] = [] if value == ['']
11
+ end
12
+ rescue StandardError => e
13
+ raise "Filter params failed: #{e}"
14
+ end
15
+
16
+ def validate(klass, identifier)
17
+ identifier = Identifier.new(identifier) if identifier.is_a? Symbol
18
+ definition = settings.validator_definitions.get(identifier.identifier)
19
+ validator = klass.new(&definition)
20
+ validator.run(self, *identifier.args)
21
+ validator.handle_failure(self) unless validator.success?
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sinatra
4
+ module ParamValidator
5
+ # Class to hold a validator identifier plus arguments
6
+ class Identifier
7
+ attr_reader :identifier, :args
8
+
9
+ def initialize(identifier, *args)
10
+ @identifier = identifier
11
+ @args = args
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sinatra
4
+ module ParamValidator
5
+ # Error raised when validation fails
6
+ class InvalidParameterError < StandardError
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'common'
4
+
5
+ module Sinatra
6
+ module ParamValidator
7
+ class Parameter
8
+ # Validation for arrays
9
+ class Array
10
+ include Common
11
+ include CommonMinMaxLength
12
+
13
+ private
14
+
15
+ def coerce(value)
16
+ return nil if value.nil?
17
+ return value if value.is_a? ::Array
18
+ return value.split(',') if value.is_a? ::String
19
+
20
+ raise ArgumentError
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'common'
4
+
5
+ module Sinatra
6
+ module ParamValidator
7
+ class Parameter
8
+ # Validation for booleans
9
+ class Boolean
10
+ include Common
11
+
12
+ private
13
+
14
+ def coerce(value)
15
+ return nil if value.nil?
16
+
17
+ case value.to_s
18
+ when /^(false|f|no|n|0)$/i then false
19
+ when /^(true|t|yes|y|1)$/i then true
20
+ else raise ArgumentError
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sinatra
4
+ module ParamValidator
5
+ class Parameter
6
+ # Common validation methods shared between parameters
7
+ module Common
8
+ attr_reader :coerced, :errors
9
+
10
+ def initialize(value, **options)
11
+ @errors = []
12
+ @coerced = coerce value
13
+ @options = options
14
+
15
+ validate_options
16
+ validate unless nil_and_ok?
17
+ rescue ArgumentError
18
+ @errors.push "'#{value}' is not a valid #{self.class}"
19
+ end
20
+
21
+ def valid?
22
+ @errors.empty?
23
+ end
24
+
25
+ def validate_options
26
+ @options.each { |key, _| raise "Unknown option '#{key}' for #{self.class}" unless respond_to? key }
27
+ end
28
+ private :validate_options
29
+
30
+ def validate
31
+ @options.each { |key, value| method(key).call(value) }
32
+ end
33
+ private :validate
34
+
35
+ def in(options)
36
+ @errors.push "Parameter must be within #{options}" unless in? options
37
+ end
38
+
39
+ def in?(options)
40
+ case options
41
+ when Range
42
+ options.include? @coerced
43
+ else
44
+ Array(options).include? @coerced
45
+ end
46
+ end
47
+ private :in?
48
+
49
+ def is(option_value)
50
+ @errors.push "Parameter must be #{option_value}" unless @coerced == option_value
51
+ end
52
+
53
+ def nillable(_)
54
+ # Does nothing. Allows other tests to ignore nil values if present in the options
55
+ end
56
+
57
+ def nil_and_ok?
58
+ @options.key?(:nillable) && @coerced.nil?
59
+ end
60
+ private :nil_and_ok?
61
+
62
+ def required(enabled)
63
+ @errors.push 'Parameter is required' if enabled && @coerced.nil?
64
+ end
65
+ end
66
+
67
+ # min/max tests
68
+ module CommonMinMax
69
+ def max(maximum)
70
+ return if @coerced.respond_to?(:<=) && @coerced <= maximum
71
+
72
+ @errors.push "Parameter cannot be greater than #{maximum}"
73
+ end
74
+
75
+ def min(minimum)
76
+ return if @coerced.respond_to?(:>=) && @coerced >= minimum
77
+
78
+ @errors.push "Parameter cannot be less than #{minimum}"
79
+ end
80
+ end
81
+
82
+ # min/max length tests
83
+ module CommonMinMaxLength
84
+ def max_length(length)
85
+ return if @coerced.respond_to?(:length) && @coerced.length <= length
86
+
87
+ @errors.push "Parameter cannot have length greater than #{length}"
88
+ end
89
+
90
+ def min_length(length)
91
+ return if @coerced.respond_to?(:length) && @coerced.length >= length
92
+
93
+ @errors.push "Parameter cannot have length less than #{length}"
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require_relative 'common'
5
+
6
+ module Sinatra
7
+ module ParamValidator
8
+ class Parameter
9
+ # Validation for dates
10
+ class Date
11
+ include Common
12
+ include CommonMinMax
13
+
14
+ private
15
+
16
+ def coerce(value)
17
+ return nil if value.nil?
18
+
19
+ ::Date.parse(value)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'common'
4
+
5
+ module Sinatra
6
+ module ParamValidator
7
+ class Parameter
8
+ # Validation for floats
9
+ class Float
10
+ include Common
11
+ include CommonMinMax
12
+
13
+ private
14
+
15
+ def coerce(value)
16
+ return nil if value.nil?
17
+
18
+ Float(value)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'common'
4
+
5
+ module Sinatra
6
+ module ParamValidator
7
+ class Parameter
8
+ # Validation for hashes
9
+ class Hash
10
+ include Common
11
+ include CommonMinMaxLength
12
+
13
+ private
14
+
15
+ def coerce(value)
16
+ return nil if value.nil?
17
+ return value if value.is_a? ::Hash
18
+ return value.split(',').to_h { |s| s.split(':') } if value.is_a? ::String
19
+
20
+ raise ArgumentError
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'common'
4
+
5
+ module Sinatra
6
+ module ParamValidator
7
+ class Parameter
8
+ # Validation for integers
9
+ class Integer
10
+ include Common
11
+ include CommonMinMax
12
+
13
+ private
14
+
15
+ def coerce(value)
16
+ return nil if value.nil?
17
+
18
+ Integer(value, 10)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'common'
4
+
5
+ module Sinatra
6
+ module ParamValidator
7
+ class Parameter
8
+ # Validation for strings
9
+ class String
10
+ include Common
11
+ include CommonMinMaxLength
12
+
13
+ def format(format_string)
14
+ @errors.push "Parameter must match the format #{format_string}" unless @coerced&.match?(format_string)
15
+ end
16
+
17
+ private
18
+
19
+ def coerce(value)
20
+ return nil if value.nil?
21
+
22
+ String(value)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require_relative 'common'
5
+
6
+ module Sinatra
7
+ module ParamValidator
8
+ class Parameter
9
+ # Validation for times
10
+ class Time
11
+ include Common
12
+ include CommonMinMax
13
+
14
+ private
15
+
16
+ def coerce(value)
17
+ return nil if value.nil?
18
+
19
+ ::Time.parse(value)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'camelize'
4
+ require_relative 'parameter/array'
5
+ require_relative 'parameter/boolean'
6
+ require_relative 'parameter/date'
7
+ require_relative 'parameter/float'
8
+ require_relative 'parameter/hash'
9
+ require_relative 'parameter/integer'
10
+ require_relative 'parameter/string'
11
+ require_relative 'parameter/time'
12
+
13
+ module Sinatra
14
+ module ParamValidator
15
+ # Load and validate a single parameter
16
+ class Parameter
17
+ class << self
18
+ include Camelize
19
+
20
+ def new(value, type, **args)
21
+ type = camelize(type) if type.is_a? Symbol
22
+ klass = Object.const_get "Sinatra::ParamValidator::Parameter::#{type}"
23
+ klass.new(value, **args)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+
5
+ require_relative 'invalid_parameter_error'
6
+ require_relative 'parameter'
7
+ require_relative 'rule'
8
+
9
+ module Sinatra
10
+ module ParamValidator
11
+ # Run the definition in the given scope
12
+ class Parser < SimpleDelegator
13
+ attr_reader :errors
14
+
15
+ def initialize(definition, context, *args)
16
+ super(context)
17
+ @context = context
18
+ @errors = {}
19
+
20
+ instance_exec(*args, &definition)
21
+ end
22
+
23
+ def add_error(key, error)
24
+ @errors[key] = @errors.fetch(key, []).concat(Array(error))
25
+ end
26
+
27
+ def param(key, type, message: nil, **args, &block)
28
+ parameter = Parameter.new(@context.params[key], type, **args)
29
+ @context.params[key] = parameter.coerced if @context.params.key?(key) && parameter.coerced
30
+ if parameter.valid?
31
+ run_block(key, block) if block
32
+ else
33
+ add_error key, message || parameter.errors
34
+ end
35
+ rescue NameError
36
+ raise 'Invalid parameter type'
37
+ end
38
+
39
+ def rule(name, *args, **kwargs)
40
+ rule = Rule.new(name, @context.params, *args, **kwargs)
41
+ unless rule.passes?
42
+ @errors[:rules] ||= []
43
+ @errors[:rules].push(rule.errors)
44
+ end
45
+ rescue NameError
46
+ raise 'Invalid rule type'
47
+ end
48
+
49
+ def run_block(key, block)
50
+ @context.instance_exec(&block)
51
+ rescue InvalidParameterError => e
52
+ add_error key, e.message
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sinatra
4
+ module ParamValidator
5
+ class Rule
6
+ # Rule to enforce all given params, or none of them
7
+ class AllOrNoneOf
8
+ attr_reader :errors
9
+
10
+ def initialize(params, *fields, **_kwargs)
11
+ @errors = []
12
+ @params = params
13
+ @fields = fields
14
+
15
+ validate(fields)
16
+ end
17
+
18
+ def passes?
19
+ @errors.empty?
20
+ end
21
+
22
+ def validate(fields)
23
+ count = fields.count { |f| @params.key? f }
24
+ return if count.zero? || count == fields.count
25
+
26
+ @errors.push "All or none of [#{fields.join ', '}] must be provided"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sinatra
4
+ module ParamValidator
5
+ class Rule
6
+ # Rule to enforce at least one of the given params exists
7
+ class AnyOf
8
+ attr_reader :errors
9
+
10
+ def initialize(params, *fields, **_kwargs)
11
+ @errors = []
12
+ @params = params
13
+ @fields = fields
14
+
15
+ validate(fields)
16
+ end
17
+
18
+ def passes?
19
+ @errors.empty?
20
+ end
21
+
22
+ def validate(fields)
23
+ count = fields.count { |f| @params.key? f }
24
+ @errors.push "One of [#{fields.join ', '}] must be provided" if count < 1
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sinatra
4
+ module ParamValidator
5
+ class Rule
6
+ # Rule to enforce only one of the given params has been given
7
+ class OneOf
8
+ attr_reader :errors
9
+
10
+ def initialize(params, *fields, **_kwargs)
11
+ @errors = []
12
+ @params = params
13
+ @fields = fields
14
+
15
+ validate(fields)
16
+ end
17
+
18
+ def passes?
19
+ @errors.empty?
20
+ end
21
+
22
+ def validate(fields)
23
+ count = fields.count { |f| @params.key? f }
24
+ @errors.push "Only one of [#{fields.join ', '}] is allowed" if count > 1
25
+ @errors.push "One of [#{fields.join ', '}] must be provided" if count < 1
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'camelize'
4
+ require_relative 'rule/all_or_none_of'
5
+ require_relative 'rule/any_of'
6
+ require_relative 'rule/one_of'
7
+
8
+ module Sinatra
9
+ module ParamValidator
10
+ # Class to check a single rule
11
+ class Rule
12
+ class << self
13
+ include Camelize
14
+
15
+ def new(name, params, *args, **kwargs)
16
+ name = camelize(name) if name.is_a? Symbol
17
+ klass = Object.const_get "Sinatra::ParamValidator::Rule::#{name}"
18
+ klass.new(params, *args, **kwargs)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sinatra
4
+ module ParamValidator
5
+ # Helpers for validating parameters
6
+ module SnakeCase
7
+ def snake_case(string)
8
+ string.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sinatra
4
+ module ParamValidator
5
+ # Error raised when validation fails
6
+ class ValidationFailedError < StandardError
7
+ attr_reader :errors
8
+
9
+ def initialize(errors)
10
+ @errors = errors
11
+ super("Validation failed: #{errors}")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sinatra
4
+ module ParamValidator
5
+ class Validator
6
+ # A form validator
7
+ class Form < Validator
8
+ def handle_failure(context)
9
+ case context.request.preferred_type.to_s
10
+ when 'application/json' then return json_failure(context)
11
+ when 'text/html'
12
+ return flash_failure(context) if defined? Sinatra::Flash
13
+ end
14
+
15
+ context.halt 400
16
+ end
17
+
18
+ def run(context)
19
+ @original_params = context.params
20
+ super(context)
21
+ end
22
+
23
+ private
24
+
25
+ def json_failure(context)
26
+ context.halt 400, { error: 'Validation failed', fields: @errors }.to_json
27
+ end
28
+
29
+ def flash_failure(context)
30
+ context.flash[:params] = @original_params
31
+ context.flash[:form_errors] = @errors
32
+ context.redirect context.back
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sinatra
4
+ module ParamValidator
5
+ class Validator
6
+ # A URL parameter; handle validation failure with
7
+ class UrlParam < Validator
8
+ def handle_failure(context)
9
+ context.halt 403
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sinatra
4
+ module ParamValidator
5
+ # Definition of a single validator
6
+ class Validator
7
+ attr_reader :errors
8
+
9
+ def initialize(&definition)
10
+ @definition = definition
11
+ @errors = {}
12
+ end
13
+
14
+ def handle_failure(_context)
15
+ raise ValidationFailedError, @errors
16
+ end
17
+
18
+ def run(context, *args)
19
+ @errors = Parser.new(@definition, context, *args).errors
20
+ end
21
+
22
+ def success?
23
+ @errors.empty?
24
+ end
25
+
26
+ @validators = []
27
+
28
+ class << self
29
+ attr_reader :validators
30
+
31
+ def inherited(subclass)
32
+ super
33
+ @validators << subclass
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ require_relative 'validation_failed_error'
41
+ require_relative 'validator/form'
42
+ require_relative 'validator/url_param'
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sinatra
4
4
  module ParamValidator
5
- VERSION = '0.1.0'
5
+ VERSION = '0.4.0'
6
6
  end
7
7
  end
@@ -1,10 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'param_validator/camelize'
4
+ require_relative 'param_validator/definitions'
5
+ require_relative 'param_validator/helpers'
6
+ require_relative 'param_validator/identifier'
7
+ require_relative 'param_validator/parser'
8
+ require_relative 'param_validator/snake_case'
9
+ require_relative 'param_validator/validator'
3
10
  require_relative 'param_validator/version'
4
11
 
5
12
  module Sinatra
6
- # Validator for param
13
+ # Module to register in Sinatra app
7
14
  module ParamValidator
8
- # Your code goes here...
15
+ include Camelize
16
+
17
+ def validator(identifier, &definition)
18
+ settings.validator_definitions.add(identifier, definition)
19
+ end
20
+
21
+ def vi(identifier, *args)
22
+ Identifier.new(identifier, *args)
23
+ end
24
+
25
+ class << self
26
+ include SnakeCase
27
+
28
+ def registered(app)
29
+ app.helpers Helpers
30
+ app.before { filter_params }
31
+ app.set(:validator_definitions, Definitions.new)
32
+ validator_conditional app, :validate, Sinatra::ParamValidator::Validator
33
+
34
+ Sinatra::ParamValidator::Validator.validators.each do |validator|
35
+ validator_conditional app, :"validate_#{snake_case(validator.to_s.split('::').last)}", validator
36
+ end
37
+ end
38
+
39
+ def validator_conditional(app, name, klass)
40
+ app.set(name) do |*identifiers|
41
+ condition do
42
+ identifiers.each { |identifier| validate klass, identifier }
43
+ end
44
+ end
45
+ end
46
+ end
9
47
  end
10
48
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'lib/sinatra/param_validator'
3
+ require_relative 'lib/sinatra/param_validator/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = 'sinatra-param-validator'
@@ -27,6 +27,12 @@ Gem::Specification.new do |spec|
27
27
  end
28
28
  spec.require_paths = ['lib']
29
29
 
30
- # Uncomment to register a new dependency of your gem
31
- # spec.add_dependency "example-gem", "~> 1.0"
30
+ spec.add_development_dependency 'rack-test', '~> 1.1'
31
+ spec.add_development_dependency 'rake', '~> 13.0'
32
+ spec.add_development_dependency 'rspec', '~> 3.0'
33
+ spec.add_development_dependency 'rubocop', '~> 1.0'
34
+ spec.add_development_dependency 'rubocop-performance', '~> 1.0'
35
+ spec.add_development_dependency 'rubocop-rake', '~> 0.5'
36
+ spec.add_development_dependency 'rubocop-rspec', '~> 2.0'
37
+ spec.add_development_dependency 'sinatra-contrib', '~> 2.0'
32
38
  end
metadata CHANGED
@@ -1,15 +1,127 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sinatra-param-validator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rick Selby
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-05-16 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2022-06-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack-test
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop-performance
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.5'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.5'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: sinatra-contrib
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '2.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '2.0'
13
125
  description:
14
126
  email:
15
127
  - rick@selby-family.co.uk
@@ -27,6 +139,31 @@ files:
27
139
  - README.md
28
140
  - Rakefile
29
141
  - lib/sinatra/param_validator.rb
142
+ - lib/sinatra/param_validator/camelize.rb
143
+ - lib/sinatra/param_validator/definitions.rb
144
+ - lib/sinatra/param_validator/helpers.rb
145
+ - lib/sinatra/param_validator/identifier.rb
146
+ - lib/sinatra/param_validator/invalid_parameter_error.rb
147
+ - lib/sinatra/param_validator/parameter.rb
148
+ - lib/sinatra/param_validator/parameter/array.rb
149
+ - lib/sinatra/param_validator/parameter/boolean.rb
150
+ - lib/sinatra/param_validator/parameter/common.rb
151
+ - lib/sinatra/param_validator/parameter/date.rb
152
+ - lib/sinatra/param_validator/parameter/float.rb
153
+ - lib/sinatra/param_validator/parameter/hash.rb
154
+ - lib/sinatra/param_validator/parameter/integer.rb
155
+ - lib/sinatra/param_validator/parameter/string.rb
156
+ - lib/sinatra/param_validator/parameter/time.rb
157
+ - lib/sinatra/param_validator/parser.rb
158
+ - lib/sinatra/param_validator/rule.rb
159
+ - lib/sinatra/param_validator/rule/all_or_none_of.rb
160
+ - lib/sinatra/param_validator/rule/any_of.rb
161
+ - lib/sinatra/param_validator/rule/one_of.rb
162
+ - lib/sinatra/param_validator/snake_case.rb
163
+ - lib/sinatra/param_validator/validation_failed_error.rb
164
+ - lib/sinatra/param_validator/validator.rb
165
+ - lib/sinatra/param_validator/validator/form.rb
166
+ - lib/sinatra/param_validator/validator/url_param.rb
30
167
  - lib/sinatra/param_validator/version.rb
31
168
  - sinatra-param-validator.gemspec
32
169
  homepage: https://github.com/rickselby/sinatra-param_validator-validator