validate_my_routes 1.0.0

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