sinatra-param-validator 0.1.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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