validate_my_routes 1.0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7bbe49d25595b0b18e832e1296bef3e8ba29cc6d
4
+ data.tar.gz: de5cbfe90139663a05c9055b6696e7a846fa6f30
5
+ SHA512:
6
+ metadata.gz: fdb2afba0936e7f025cc4f375a01dcc1312e7bfa466c001e1ea4c7031f6474af968919d6d5151fbed68df6fc374e5cbd3a77582e606f00fa035d577627c72976
7
+ data.tar.gz: fb517bac0d2b6c368e00b024ad5f35ef3f2aa9a8d575cbf5712b3274764938050a385a96d9174b8afe911c61513a195871513dfed0cd6f47410dc0f3be8b3be3
@@ -0,0 +1,9 @@
1
+ require_relative 'validate_my_routes/version'
2
+ require_relative 'validate_my_routes/errors'
3
+ require_relative 'validate_my_routes/validation_rules'
4
+ require_relative 'validate_my_routes/validate'
5
+ require_relative 'validate_my_routes/validatable'
6
+
7
+ # General module that defines the base access to ValidateMyRoutes
8
+ module ValidateMyRoutes
9
+ end
@@ -0,0 +1,42 @@
1
+ module ValidateMyRoutes
2
+ # Defining errors
3
+ module Errors
4
+ # Base error for validate_my_routes
5
+ class Error < ::RuntimeError; end
6
+
7
+ # Raised when type conversion fails
8
+ class InvalidTypeError < Error; end
9
+
10
+ # Raised when rule is not following required protocol
11
+ class UnsupportedRuleError < Error; end
12
+
13
+ # Raised when using validation DSL and missing validation block
14
+ class MissingValidationDeclarationBlock < Error; end
15
+
16
+ # Raised when using validation DSL when validation rule is already defined
17
+ class ValidationRuleNamingConflict < Error; end
18
+
19
+ # Raised when rule is missused
20
+ class MissusedRuleError < Error; end
21
+
22
+ # Basic error raised for validation failures
23
+ class ValidationError < Error
24
+ attr_reader :status_code, :message
25
+ def initialize(message, status_code)
26
+ super(message)
27
+ @status_code = status_code
28
+ @message = message
29
+ end
30
+ end
31
+
32
+ # Raised when soft failure occurs
33
+ class ConditionalValidationError < ValidationError; end
34
+
35
+ # Raised when exceptions occurs in validation block
36
+ class ValidationRaisedAnExceptionError < ValidationError
37
+ def initialize(custom_exception, status_code)
38
+ super(custom_exception.to_s, status_code)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,102 @@
1
+ module ValidateMyRoutes
2
+ # Route parameters validation extension
3
+ # To start using it, the extension needs to be registered
4
+ #
5
+ # register ValidateMyRoutes::Validatable
6
+ #
7
+ # Registering Validatable extension adds two conditions:
8
+ # - validate_all_params - a list of rules that need to be applied to all parameters together
9
+ # - validate_params - a hash with parameter names as keys and rules with extra information
10
+ # in values
11
+ module Validatable
12
+ class << self
13
+ def registered(app)
14
+ add_validate_params_condition_to app
15
+ add_validate_all_params_condition_to app
16
+ end
17
+
18
+ private
19
+
20
+ def add_validate_all_params_condition_to(app)
21
+ app.set(:validate_all_params) do |*rules|
22
+ condition do
23
+ rules.all? { |rule| Validate.validate!(self, rule, params, false) }
24
+ end
25
+ end
26
+ end
27
+
28
+ # rubocop:disable AbcSize
29
+ def add_validate_params_condition_to(app)
30
+ app.set(:validate_params) do |*validations|
31
+ condition do
32
+ path_validations = validations.select { |_, rule| rule[:path_param] }
33
+ query_validations = validations.reject { |_, rule| rule[:path_param] }
34
+
35
+ [path_validations, query_validations].all? do |param_validations|
36
+ param_validations.select { |_, rule| rule[:path_param] }.all? do |param_name, rule|
37
+ value = params[param_name]
38
+ Validate.validate!(self, rule[:rule], value, rule[:path_param], param_name)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ # rubocop:enable AbcSize
46
+
47
+ # Define path parameter with validation for all routes (including nested routes)
48
+ #
49
+ # param_validation :service_id, from_enum(%w[a b c])
50
+ def param_validation(name, rule)
51
+ Validate::Rules.validate_single_param_rule! rule
52
+ (@param_validations ||= {})[name.to_sym] = rule
53
+ end
54
+
55
+ # Define all parameters validation for a single route
56
+ #
57
+ # all_params_validation at_least_one_of(%i[version class status owner])
58
+ # get '/' do
59
+ # # params contain at least one of :version, :class, :status or :owner parameter
60
+ # end
61
+ def all_params_validation(rule)
62
+ Validate::Rules.validate_all_params_rule! rule
63
+ (@all_params_validation ||= []) << rule
64
+ end
65
+
66
+ # Hook into .route Sinatra method to add validation for parameters
67
+ def route(verb, route_pattern, conditions = {}, &block)
68
+ route_path_parameters(route_pattern).each do |name|
69
+ next unless param_validations.key? name
70
+
71
+ rule = param_validations[name]
72
+ # Add path parameter validation if it was specified
73
+ (conditions[:validate_params] ||= {})[name] ||= { path_param: true, rule: rule }
74
+ end
75
+
76
+ # Add all params validation if it was specified
77
+ conditions[:validate_all_params] = @all_params_validation if @all_params_validation
78
+ @all_params_validation = nil # remove params validation as it is defined on per-route bases
79
+
80
+ super(verb, route_pattern, conditions, &block)
81
+ end
82
+
83
+ def route_path_parameters(route_pattern)
84
+ path_parameters = route_pattern.split('/').map do |part|
85
+ part.start_with?(':') ? part[1..-1].to_sym : nil
86
+ end
87
+
88
+ path_parameters.flatten.compact.uniq.map(&:to_sym)
89
+ end
90
+
91
+ protected
92
+
93
+ def param_validations
94
+ parameters_defined_in_superclass = {}
95
+ # For nested routes we need to look for defined parameters with validation in
96
+ # superclass also.
97
+ parameters_defined_in_superclass = superclass.param_validations \
98
+ if superclass.respond_to?(:param_validations, true)
99
+ parameters_defined_in_superclass.merge(@param_validations || {})
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,27 @@
1
+ require_relative 'validate/convert_to_type'
2
+ require_relative 'validate/rules'
3
+
4
+ module ValidateMyRoutes
5
+ # Module for validation. Provides method to validate value by specified rule.
6
+ module Validate
7
+ class << self
8
+ # Perform validation of a single rule in-place
9
+ # Note: this method is not validating that rule is for all parameters or just a single
10
+ # Example:
11
+ #
12
+ # get 'some/:id' do |id|
13
+ # ValidateMyRoutes::Validate.validate!(self, greater_than(5), id.to_i, 'id') do |msg|
14
+ # halt 400, "Id <#{id}> failed validation: #{msg}"
15
+ # end
16
+ # end
17
+ def validate!(app, rule, *args)
18
+ rule.validate!(app, *args)
19
+ rescue Errors::ConditionalValidationError
20
+ false
21
+ rescue Errors::ValidationError => failure
22
+ app.halt failure.status_code, failure.message unless block_given?
23
+ yield failure.message
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,80 @@
1
+ require 'time'
2
+
3
+ module ValidateMyRoutes
4
+ module Validate
5
+ # ConvertToType module provides single method convert_to_type to convert value into type
6
+ # to_type. Conversion can fail with InvalidTypeError.
7
+ module ConvertToType
8
+ class << self
9
+ Boolean = :Boolean # rubocop:disable Naming/ConstantName
10
+ SIMPLE_TYPES = [Float, String, Date, Time, DateTime, Integer].freeze
11
+ COMPOSITE_TYPES = [Array, Hash].freeze
12
+ BOOLEAN_TYPES = [Boolean, TrueClass, FalseClass].freeze
13
+
14
+ def convert_to_type(value, to_type)
15
+ return value if already_of_type?(value, to_type)
16
+
17
+ if SIMPLE_TYPES.include?(to_type)
18
+ parse_simple_type(value, to_type)
19
+ elsif COMPOSITE_TYPES.include?(to_type)
20
+ parse_composite_type(value, to_type)
21
+ elsif BOOLEAN_TYPES.include?(to_type)
22
+ parse_boolean(value)
23
+ else
24
+ raise_unknown_type(to_type)
25
+ end
26
+ rescue ArgumentError
27
+ raise_with_invalid_type(value, to_type)
28
+ end
29
+
30
+ private
31
+
32
+ def already_of_type?(value, typ)
33
+ (typ.is_a?(Class) || typ.is_a?(Module)) && value.is_a?(typ)
34
+ end
35
+
36
+ def parse_simple_type(value, to_type)
37
+ if to_type == Integer
38
+ Integer(value)
39
+ elsif [Float, String].include?(to_type)
40
+ Kernel.send(to_type.to_s.to_sym, value)
41
+ elsif to_type.respond_to? :parse
42
+ to_type.parse(value)
43
+ else
44
+ raise_unknown_type(to_type)
45
+ end
46
+ end
47
+
48
+ def parse_composite_type(value, to_type)
49
+ if to_type == Array
50
+ value.split(',')
51
+ elsif to_type == Hash
52
+ Hash[value.split(',').map { |item| item.split(':') }]
53
+ else
54
+ raise_unknown_type(to_type)
55
+ end
56
+ end
57
+
58
+ def parse_boolean(value)
59
+ if value.to_s.casecmp('false').zero?
60
+ false
61
+ elsif value.to_s.casecmp('true').zero?
62
+ true
63
+ else
64
+ raise_with_invalid_type(value, Boolean)
65
+ end
66
+ end
67
+
68
+ def raise_with_invalid_type(value, type)
69
+ raise ValidateMyRoutes::Errors::InvalidTypeError,
70
+ "'#{value}' is not a valid '#{type}'"
71
+ end
72
+
73
+ def raise_unknown_type(type)
74
+ raise ValidateMyRoutes::Errors::InvalidTypeError,
75
+ "don't know how to convert type '#{type}'"
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,51 @@
1
+ module ValidateMyRoutes
2
+ module Validate
3
+ # Helper functions to provide a DSL for creating validation rules
4
+ module Macros
5
+ # Define all parameters validation
6
+ #
7
+ # validate do |params|
8
+ # params.key? 'some_parameter_name'
9
+ # end
10
+ def validate(&block)
11
+ define_method(:validate, &block)
12
+ end
13
+
14
+ # Customize validation rule description
15
+ #
16
+ # description do
17
+ # 'this is my custom validation rule description'
18
+ # end
19
+ def description(&block)
20
+ define_method(:description, &block)
21
+ end
22
+
23
+ # Customize message returned when validation fails for all parameters
24
+ #
25
+ # failure_message do |params|
26
+ # "oh no! validation failed for #{params}"
27
+ # end
28
+ def failure_message(&block)
29
+ define_method(:failure_message, &block)
30
+ end
31
+
32
+ # Customize message returned when opposite rule validation fails (not of type A)
33
+ #
34
+ # failure_message_when_negated do |params|
35
+ # "oh no! validation failed for #{params}, but it was not expected"
36
+ # end
37
+ def failure_message_when_negated(&block)
38
+ define_method(:failure_message_when_negated, &block)
39
+ end
40
+
41
+ # Customize http status code of the failure
42
+ #
43
+ # failure_code do |in_path|
44
+ # in_path ? 404 : 400
45
+ # end
46
+ def failure_code(&block)
47
+ define_method(:failure_code, &block)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,30 @@
1
+ module ValidateMyRoutes
2
+ module Validate
3
+ # Helper functions to provide a DSL for creating validation rules
4
+ module RulesCombinators
5
+ # Negate the rule to validate opposite expectation
6
+ #
7
+ # is_an_integer = of_type(Integer)
8
+ # is_not_an_integer = of_type(Integer).negate
9
+ def negate
10
+ ValidateMyRoutes::ValidationRules.not self
11
+ end
12
+
13
+ # Chain rule with another one to perform both validations
14
+ # Note that if first rule fails validation, second is ignored
15
+ #
16
+ # required.and of_type(Integer)
17
+ def and(other_rule)
18
+ ValidateMyRoutes::ValidationRules.and(self, other_rule)
19
+ end
20
+
21
+ # Chain rule with another one to perform one or another validations
22
+ # Note that second validation will be performed only if first fails
23
+ #
24
+ # eql('all').or of_type(Integer)
25
+ def or(other_rule)
26
+ ValidateMyRoutes::ValidationRules.or(self, other_rule)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,61 @@
1
+ require_relative 'validation_rule'
2
+
3
+ require_relative 'rules/compound'
4
+ require_relative 'rules/anything'
5
+ require_relative 'rules/comparable'
6
+ require_relative 'rules/conditional'
7
+ require_relative 'rules/of_type'
8
+ require_relative 'rules/required'
9
+ require_relative 'rules/transforms'
10
+ require_relative 'rules/enum'
11
+ require_relative 'rules/all_parameters'
12
+
13
+ module ValidateMyRoutes
14
+ module Validate
15
+ # Module provides methods to validate if rule is a rule and if it can be used for single
16
+ # or all parameters validation.
17
+ module Rules
18
+ class << self
19
+ REQUIRED_RULE_METHODS = %i[validate! description rule_type].freeze
20
+
21
+ def single_param_rule?(rule)
22
+ validation_rule?(rule) && %i[single_param general].include?(rule.rule_type)
23
+ end
24
+
25
+ def all_params_rule?(rule)
26
+ validation_rule?(rule) && %i[all_params general].include?(rule.rule_type)
27
+ end
28
+
29
+ # Validate that rule can be used for single parameter validation
30
+ #
31
+ # Example:
32
+ #
33
+ # Rules.validate_single_param_rule! required(:q) # => throws an exception
34
+ def validate_single_param_rule!(rule)
35
+ return if Rules.single_param_rule?(rule)
36
+ raise ValidateMyRoutes::Errors::UnsupportedRuleError,
37
+ "rule #{rule} must implement #{REQUIRED_RULE_METHODS.join(', ')} " \
38
+ 'and be either :generic or :single_param rule type.'
39
+ end
40
+
41
+ # Validate that rule can be used for all parameters validation
42
+ #
43
+ # Example:
44
+ #
45
+ # Rules.validate_all_params_rule! of_type(Integer) # => throws an exception
46
+ def validate_all_params_rule!(rule)
47
+ return if Rules.all_params_rule?(rule)
48
+ raise ValidateMyRoutes::Errors::UnsupportedRuleError,
49
+ "rule #{rule} must implement #{REQUIRED_RULE_METHODS.join(', ')} " \
50
+ 'and be either :generic or :all_params rule type.'
51
+ end
52
+
53
+ private
54
+
55
+ def validation_rule?(rule)
56
+ REQUIRED_RULE_METHODS.all? { |method_name| rule.respond_to? method_name }
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,75 @@
1
+ module ValidateMyRoutes
2
+ module Validate
3
+ module Rules
4
+ # Validation rules that designed to be used for validating all parameters
5
+ # in the route.
6
+ #
7
+ # For example if we need to allow specifying "all" or "search" criteria,
8
+ # but not both at the same time.
9
+ module AllParameters
10
+ ValidateMyRoutes::ValidationRules.def_all_params_validator :only_one_of do |names|
11
+ raise Errors::MissusedRuleError, 'names must be an array' unless names.is_a? Array
12
+
13
+ validate do |params|
14
+ present_parameters_count = names.count { |name| params.key? name.to_s }
15
+ present_parameters_count <= 1
16
+ end
17
+
18
+ description { "only one of <#{names.join(', ')}> parameters" }
19
+
20
+ failure_message do |actual|
21
+ "was expected to have only one of <#{names.join(', ')}> parameters, " \
22
+ "but <#{actual.keys.join(', ')}> #{actual.size > 1 ? 'were' : 'was'} provided"
23
+ end
24
+
25
+ failure_message_when_negated do |actual|
26
+ "was expected to have all of <#{names.join(', ')}> parameters, " \
27
+ "but <#{actual.keys.join(', ')}> #{actual.size > 1 ? 'were' : 'was'} provided"
28
+ end
29
+ end
30
+
31
+ ValidateMyRoutes::ValidationRules.def_all_params_validator :exactly_one_of do |names|
32
+ raise Errors::MissusedRuleError, 'names must be an array' unless names.is_a? Array
33
+
34
+ validate do |params|
35
+ present_parameters_count = names.count { |name| params.key? name.to_s }
36
+ present_parameters_count == 1
37
+ end
38
+
39
+ description { "exactly one of <#{names.join(', ')}> parameters" }
40
+
41
+ failure_message do |actual|
42
+ "was expected to have exactly one of <#{names.join(', ')}> parameters, " \
43
+ "but <#{actual.keys.join(', ')}> #{actual.size > 1 ? 'were' : 'was'} provided"
44
+ end
45
+
46
+ failure_message_when_negated do |actual|
47
+ "was expected to have none or more than one of <#{names.join(', ')}>, " \
48
+ "but <#{actual.keys.join(', ')}> #{actual.size > 1 ? 'were' : 'was'} provided"
49
+ end
50
+ end
51
+
52
+ ValidateMyRoutes::ValidationRules.def_all_params_validator :at_least_one_of do |names|
53
+ raise Errors::MissusedRuleError, 'names must be an array' unless names.is_a? Array
54
+
55
+ validate do |params|
56
+ present_parameters_count = names.count { |name| params.key? name.to_s }
57
+ present_parameters_count >= 1
58
+ end
59
+
60
+ description { "at least one of <#{names.join(', ')}> parameters" }
61
+
62
+ failure_message do |actual|
63
+ "was expected to have at least one of <#{names.join(', ')}> parameters, " \
64
+ "but <#{actual.keys.join(', ')}> #{actual.size > 1 ? 'were' : 'was'} provided"
65
+ end
66
+
67
+ failure_message_when_negated do |actual|
68
+ "was expected to have none of <#{names.join(', ')}>, " \
69
+ "but <#{actual.keys.join(', ')}> #{actual.size > 1 ? 'were' : 'was'} provided"
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,13 @@
1
+ module ValidateMyRoutes
2
+ module Validate
3
+ module Rules
4
+ # Always successful validation rule
5
+ module Anything
6
+ ValidateMyRoutes::ValidationRules.def_validation_rule :anything do
7
+ validate { |*_| true }
8
+ description { 'anything' }
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,82 @@
1
+ module ValidateMyRoutes
2
+ module Validate
3
+ module Rules
4
+ # List of comparison rules
5
+ module Comparable
6
+ VR = ValidateMyRoutes::ValidationRules
7
+
8
+ VR.def_single_param_validator :eql do |expected|
9
+ validate { |actual, _| actual == expected }
10
+ description { "equal to <#{expected}>" }
11
+ failure_message do |actual, name|
12
+ "was expected #{name} parameter to equal <#{expected}>, but was <#{actual}>"
13
+ end
14
+ failure_message_when_negated do |actual, name|
15
+ "was expected #{name} parameter to not equal <#{expected}>, but was <#{actual}>"
16
+ end
17
+ end
18
+
19
+ VR.def_single_param_validator :greater_than do |expected|
20
+ validate { |actual, _| actual > expected }
21
+ description { "greater than <#{expected}>" }
22
+ failure_message do |actual, name|
23
+ "was expected #{name} parameter to be greater than <#{expected}>, but was <#{actual}>"
24
+ end
25
+ failure_message_when_negated do |actual, name|
26
+ "was expected #{name} parameter to be less than or equal to <#{expected}>, " \
27
+ "but was <#{actual}>"
28
+ end
29
+ end
30
+
31
+ VR.def_single_param_validator :greater_than_or_equal_to do |expected|
32
+ validate { |actual, _| actual >= expected }
33
+ description { "greater than or equal to <#{expected}>" }
34
+ failure_message do |actual, name|
35
+ "was expected #{name} parameter to be greater than or equal to <#{expected}>, " \
36
+ "but was <#{actual}>"
37
+ end
38
+ failure_message_when_negated do |actual, name|
39
+ "was expected #{name} parameter to be less than <#{expected}>, but was <#{actual}>"
40
+ end
41
+ end
42
+
43
+ VR.def_single_param_validator :less_than do |expected|
44
+ validate { |actual, _| actual < expected }
45
+ description { "less than <#{expected}>" }
46
+ failure_message do |actual, name|
47
+ "was expected #{name} parameter to be less than <#{expected}>, but was <#{actual}>"
48
+ end
49
+ failure_message_when_negated do |actual, name|
50
+ "was expected #{name} parameter to be greater than or equal to <#{expected}>, " \
51
+ "but was <#{actual}>"
52
+ end
53
+ end
54
+
55
+ VR.def_single_param_validator :less_than_or_equal_to do |expected|
56
+ validate { |actual, _| actual <= expected }
57
+ description { "less than or equal to <#{expected}>" }
58
+ failure_message do |actual, name|
59
+ "was expected #{name} parameter to be less than or equal to <#{expected}>, " \
60
+ "but was <#{actual}>"
61
+ end
62
+ failure_message_when_negated do |actual, name|
63
+ "was expected #{name} parameter to be greater than <#{expected}>, but was <#{actual}>"
64
+ end
65
+ end
66
+
67
+ VR.def_single_param_validator :between do |min, max|
68
+ validate { |actual, _| actual >= min && actual <= max }
69
+ description { "between <#{min}> and <#{max}>" }
70
+ failure_message do |actual, name|
71
+ "was expected #{name} parameter to be between <#{min}> and <#{max}>, " \
72
+ "but was <#{actual}>"
73
+ end
74
+ failure_message_when_negated do |actual, name|
75
+ "was expected #{name} parameter to be less than <#{min}> or greater than <#{max}>, " \
76
+ "but was <#{actual}>"
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,41 @@
1
+ module ValidateMyRoutes
2
+ module Validate
3
+ module Rules
4
+ # Rules combinators
5
+ # TODO: add validations to the rules before combining them
6
+ module Compound
7
+ ValidateMyRoutes::ValidationRules.def_validation_rule :not do |rule|
8
+ validate do |*args|
9
+ begin
10
+ !check(rule, *args)
11
+ rescue Errors::ValidationError
12
+ true
13
+ end
14
+ end
15
+
16
+ failure_message { |*args| rule.failure_message_when_negated(*args) }
17
+ failure_message_when_negated { |*args| rule.failure_message(*args) }
18
+ description { "NOT #{rule.description}" }
19
+ failure_code { |*args| rule.failure_code(*args) }
20
+ end
21
+
22
+ ValidateMyRoutes::ValidationRules.def_validation_rule :and do |first_rule, second_rule|
23
+ validate { |*args| check(first_rule, *args) && check(second_rule, *args) }
24
+ description { "(#{first_rule.description} AND #{second_rule.description})" }
25
+ end
26
+
27
+ ValidateMyRoutes::ValidationRules.def_validation_rule :or do |first_rule, second_rule|
28
+ validate do |*args|
29
+ begin
30
+ check(first_rule, *args) || check(second_rule, *args)
31
+ rescue Errors::ValidationError
32
+ check(second_rule, *args)
33
+ end
34
+ end
35
+
36
+ description { "(#{first_rule.description} OR #{second_rule.description})" }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,21 @@
1
+ module ValidateMyRoutes
2
+ module Validate
3
+ module Rules
4
+ # Validation rule that on failure instruct Sinatra to search for another route instead of
5
+ # failing with validation error
6
+ module Conditional
7
+ ValidateMyRoutes::ValidationRules.def_validation_rule :conditional do |rule|
8
+ validate do |*args|
9
+ begin
10
+ check(rule, *args)
11
+ rescue Errors::ValidationError => error
12
+ raise Errors::ConditionalValidationError.new(error.message, error.status_code)
13
+ end
14
+ end
15
+
16
+ description { "conditional, #{rule.description}" }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module ValidateMyRoutes
2
+ module Validate
3
+ module Rules
4
+ # Always successful validation rule
5
+ module Enum
6
+ ValidateMyRoutes::ValidationRules.def_single_param_validator :from_enum do |values|
7
+ unless values.respond_to? :include?
8
+ raise Errors::MissusedRuleError, 'from_enum rule requires #include? method on ' \
9
+ 'expectation'
10
+ end
11
+ validate { |actual, _| values.include? actual }
12
+ failure_message do |actual, name|
13
+ "parameter <#{name}> was expected to have one of following values: " \
14
+ "<#{values.join ', '}>, but was <#{actual}>"
15
+ end
16
+ description { "of enum type with values: #{values.join(', ')}" }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ require_relative '../convert_to_type'
2
+
3
+ module ValidateMyRoutes
4
+ module Validate
5
+ module Rules
6
+ # Rule to validate type of parameter
7
+ module OfType
8
+ ValidateMyRoutes::ValidationRules.def_single_param_validator :of_type do |typ|
9
+ validate do |actual, _|
10
+ begin
11
+ ConvertToType.convert_to_type(actual, typ)
12
+ true
13
+ rescue ValidateMyRoutes::Errors::InvalidTypeError
14
+ false
15
+ end
16
+ end
17
+
18
+ description { "of a type <#{typ}>" }
19
+
20
+ failure_message do |actual, name|
21
+ "was expected #{name} parameter to be of a type <#{typ}>, but was <#{actual}>"
22
+ end
23
+
24
+ failure_message_when_negated do |actual, name|
25
+ "was expected #{name} parameter to not be of a type <#{typ}>, but was <#{actual}>"
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ module ValidateMyRoutes
2
+ module Validate
3
+ module Rules
4
+ # Validation rule that fails if parameter was not provided
5
+ module Required
6
+ ValidateMyRoutes::ValidationRules.def_all_params_validator :required do |parameter_name|
7
+ validate { |params| params.key? parameter_name.to_s }
8
+ description { "parameter <#{parameter_name}> is required" }
9
+ failure_message { |_| "parameter <#{parameter_name}> was expected to be present" }
10
+ failure_message_when_negated do |_|
11
+ "parameter <#{parameter_name}> was expected not to be present"
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,50 @@
1
+ module ValidateMyRoutes
2
+ module Validate
3
+ module Rules
4
+ # Rules that perform transformation of value before sending it to another rule
5
+ module Transforms
6
+ ValidateMyRoutes::ValidationRules.def_single_param_validator :value_as do |typ, rule|
7
+ validate do |actual, name|
8
+ begin
9
+ converted_value = ConvertToType.convert_to_type(actual, typ)
10
+ check(rule, converted_value, name)
11
+ rescue ValidateMyRoutes::Errors::InvalidTypeError
12
+ false
13
+ end
14
+ end
15
+
16
+ description { rule.description }
17
+
18
+ failure_message do |actual, name|
19
+ "was expected #{name} parameter to be of type <#{typ}>, but was <#{actual}>"
20
+ end
21
+
22
+ failure_message_when_negated do |_, _|
23
+ # TODO: make sure that not(value_as(...)) can not be used instead of failing here
24
+ raise Errors::MissusedRuleError, 'value_as does not support negate operation'
25
+ end
26
+
27
+ failure_code { |*args| rule.failure_code(*args) }
28
+ end
29
+
30
+ ValidateMyRoutes::ValidationRules.def_validation_rule :transform do |transformation, rule|
31
+ description { rule.description }
32
+ validate do |*args|
33
+ if args.size == 2
34
+ # this means single parameter validation with value and name
35
+ check(rule, transformation.call(args[0]), args[1])
36
+ elsif args.size == 1
37
+ # this means all parameters validation with value only
38
+ check(rule, transformation.call(args[0]))
39
+ else
40
+ raise Errors::MissusedRuleError, "Received #{args.size} instead of 1 or 2"
41
+ end
42
+ end
43
+ failure_message { |*args| rule.failure_message(*args) }
44
+ failure_message_when_negated { |*args| rule.failure_message_when_negated(*args) }
45
+ failure_code { |*args| rule.failure_code(*args) }
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,123 @@
1
+ require_relative 'mixins/macros'
2
+ require_relative 'mixins/rules_combinators'
3
+
4
+ module ValidateMyRoutes
5
+ module Validate
6
+ # ValidationRule is a base class for all rules
7
+ class ValidationRule
8
+ # Add DSL support for declarations in constructor
9
+ extend Macros
10
+ include RulesCombinators
11
+
12
+ attr_reader :rule_type
13
+
14
+ def initialize(rule_name, rule_type, *expected, declarations)
15
+ self.rule_name = rule_name
16
+ self.rule_type = rule_type
17
+ self.app = nil # this is a Sinatra application instance
18
+ singleton_class.class_exec(*expected, &declarations)
19
+ end
20
+
21
+ # Current method can be used for validation
22
+ def validate!(app, value, path_param, *args)
23
+ # save current Sinatra app instance for method lookup on it
24
+ self.app = app
25
+
26
+ self.value = value
27
+ self.path_param = path_param == true
28
+
29
+ validate(value, *args) || fail_validation(failure_message(value, *args))
30
+ rescue Errors::ValidationError
31
+ # validation failed, so just re-raise an error to buble it up to the root
32
+ # re-raising is needed in order to catch all other exceptions to wrap them in
33
+ # special error
34
+ raise
35
+ rescue => ex # rubocop:disable Style/RescueStandardError
36
+ # unexpected exception happened in validation block, so we should wrap it in special error
37
+ raise Errors::ValidationRaisedAnExceptionError.new(ex, failure_code(path_param?))
38
+ end
39
+
40
+ def validate(*_args)
41
+ raise Errors::MissusedRuleError, 'validate method not implemented'
42
+ end
43
+
44
+ def description
45
+ rule_name.to_s.capitalize.tr('_', ' ')
46
+ end
47
+
48
+ def failure_code(in_path)
49
+ in_path ? 404 : 400
50
+ end
51
+
52
+ def failure_message(*args)
53
+ if args.size == 1
54
+ "parameters were expected to satisfy: #{description} but were <#{args[0]}>"
55
+ elsif args.size == 2
56
+ "parameter #{args[1]} was expected to satisfy: #{description} but was <#{args[0]}>"
57
+ else
58
+ raise Errors::MissusedRuleError, "failure_message method called with #{args.size} " \
59
+ 'arguments'
60
+ end
61
+ end
62
+
63
+ def failure_message_when_negated(*args)
64
+ if args.size == 1
65
+ "parameters were expected not to satisfy: #{description} but were <#{args[0]}>"
66
+ elsif args.size == 2
67
+ "parameter #{args[1]} was expected not to satisfy: #{description} but was <#{args[0]}>"
68
+ else
69
+ raise Errors::MissusedRuleError, 'failure_message_when_negated method called with ' \
70
+ "#{args.size} arguments"
71
+ end
72
+ end
73
+
74
+ # Expand method lookup to the application scope
75
+ def method_missing(method_name, *args, &block)
76
+ app && app.respond_to?(method_name) ? app.send(method_name, *args, &block) : super
77
+ end
78
+
79
+ def respond_to_missing?(method_name, include_private = false)
80
+ super || app.respond_to?(method_name) || super
81
+ end
82
+
83
+ private
84
+
85
+ attr_accessor :app, :value, :path_param, :rule_name
86
+ attr_writer :rule_type
87
+
88
+ # Helper method to perform validation of other rules inside validation block
89
+ #
90
+ # validate do |params|
91
+ # # make use of built in validation rules in custom validations
92
+ # check(of_type(Integer), params[:id], :id)
93
+ # # or validate all parameters
94
+ # check(required(:id), params)
95
+ # end
96
+ def check(rule, value, *args)
97
+ if args.empty?
98
+ ValidateMyRoutes::Validate::Rules.validate_all_params_rule! rule
99
+ elsif args.size == 1
100
+ ValidateMyRoutes::Validate::Rules.validate_single_param_rule! rule
101
+ else
102
+ raise Errors::MissusedRuleError, "check method called with #{args.size} arguments"
103
+ end
104
+
105
+ rule.validate!(app, value, path_param?, *args)
106
+ end
107
+
108
+ # Helper method to fail validation
109
+ #
110
+ # validate do |params|
111
+ # fail_validation 'no!' if params.size > 1
112
+ # end
113
+ def fail_validation(message, code = nil)
114
+ code ||= failure_code(path_param?)
115
+ raise Errors::ValidationError.new(message, code)
116
+ end
117
+
118
+ def path_param?
119
+ path_param
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,29 @@
1
+ require_relative './validate/validation_rule'
2
+
3
+ module ValidateMyRoutes
4
+ # Mixin to add custom rules to the application.
5
+ #
6
+ # To create custom rule you can extend your class with ValidationRules:
7
+ # extend ValidateMyRoutes::ValidationRules
8
+ module ValidationRules
9
+ module_function
10
+
11
+ def def_single_param_validator(name, &declarations)
12
+ def_validation_rule name, :single_param, &declarations
13
+ end
14
+
15
+ def def_all_params_validator(name, &declarations)
16
+ def_validation_rule name, :all_params, &declarations
17
+ end
18
+
19
+ def def_validation_rule(name, typ = :general, &declarations)
20
+ raise Errors::MissingValidationDeclarationBlock unless block_given?
21
+ raise Errors::ValidationRuleNamingConflict, name.to_sym if respond_to? name.to_sym
22
+
23
+ rule = ->(*expected) { Validate::ValidationRule.new(name, typ, *expected, declarations) }
24
+
25
+ define_method(name, rule)
26
+ define_singleton_method(name, rule)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,4 @@
1
+ # ValidateMyRoutes version
2
+ module ValidateMyRoutes
3
+ VERSION = '1.0.0'.freeze
4
+ end
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: validate_my_routes
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Workday, Ltd.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-03-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack-test
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sinatra
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: ValidateMyRoutes provides a way to annotate Sinatra routes and validate
98
+ parameters before executing the route
99
+ email:
100
+ - prd.eng.os@workday.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - lib/validate_my_routes.rb
106
+ - lib/validate_my_routes/errors.rb
107
+ - lib/validate_my_routes/validatable.rb
108
+ - lib/validate_my_routes/validate.rb
109
+ - lib/validate_my_routes/validate/convert_to_type.rb
110
+ - lib/validate_my_routes/validate/mixins/macros.rb
111
+ - lib/validate_my_routes/validate/mixins/rules_combinators.rb
112
+ - lib/validate_my_routes/validate/rules.rb
113
+ - lib/validate_my_routes/validate/rules/all_parameters.rb
114
+ - lib/validate_my_routes/validate/rules/anything.rb
115
+ - lib/validate_my_routes/validate/rules/comparable.rb
116
+ - lib/validate_my_routes/validate/rules/compound.rb
117
+ - lib/validate_my_routes/validate/rules/conditional.rb
118
+ - lib/validate_my_routes/validate/rules/enum.rb
119
+ - lib/validate_my_routes/validate/rules/of_type.rb
120
+ - lib/validate_my_routes/validate/rules/required.rb
121
+ - lib/validate_my_routes/validate/rules/transforms.rb
122
+ - lib/validate_my_routes/validate/validation_rule.rb
123
+ - lib/validate_my_routes/validation_rules.rb
124
+ - lib/validate_my_routes/version.rb
125
+ homepage: https://github.com/Workday/validate_my_routes
126
+ licenses:
127
+ - MIT
128
+ metadata: {}
129
+ post_install_message:
130
+ rdoc_options: []
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ requirements: []
144
+ rubyforge_project:
145
+ rubygems_version: 2.4.8
146
+ signing_key:
147
+ specification_version: 4
148
+ summary: A simple gem to validate Sinatra routes
149
+ test_files: []