rails-param-validation 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.gitlab-ci.yml +34 -0
  4. data/Gemfile +8 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +63 -0
  7. data/Rakefile +6 -0
  8. data/bin/.keep +0 -0
  9. data/docs/_config.yml +3 -0
  10. data/docs/annotations.md +62 -0
  11. data/docs/getting-started.md +32 -0
  12. data/docs/image/error-screenshot.png +0 -0
  13. data/docs/index.md +61 -0
  14. data/docs/main-idea.md +72 -0
  15. data/docs/openapi.md +39 -0
  16. data/docs/type-definition.md +178 -0
  17. data/lib/rails-param-validation/errors/missing_parameter_annotation.rb +9 -0
  18. data/lib/rails-param-validation/errors/no_matching_factory.rb +9 -0
  19. data/lib/rails-param-validation/errors/param_validation_failed_error.rb +12 -0
  20. data/lib/rails-param-validation/errors/type_not_found.rb +9 -0
  21. data/lib/rails-param-validation/rails/action_definition.rb +66 -0
  22. data/lib/rails-param-validation/rails/annotation_manager.rb +40 -0
  23. data/lib/rails-param-validation/rails/config.rb +48 -0
  24. data/lib/rails-param-validation/rails/extensions/annotation_extension.rb +95 -0
  25. data/lib/rails-param-validation/rails/extensions/custom_type_extension.rb +13 -0
  26. data/lib/rails-param-validation/rails/extensions/error.template.html.erb +86 -0
  27. data/lib/rails-param-validation/rails/extensions/validation_extension.rb +105 -0
  28. data/lib/rails-param-validation/rails/helper.rb +9 -0
  29. data/lib/rails-param-validation/rails/openapi/openapi.rb +128 -0
  30. data/lib/rails-param-validation/rails/openapi/routing_helper.rb +40 -0
  31. data/lib/rails-param-validation/rails/rails.rb +31 -0
  32. data/lib/rails-param-validation/rails/tasks/openapi.rake +32 -0
  33. data/lib/rails-param-validation/types/types.rb +100 -0
  34. data/lib/rails-param-validation/validator.rb +51 -0
  35. data/lib/rails-param-validation/validator_factory.rb +37 -0
  36. data/lib/rails-param-validation/validators/alternatives.rb +42 -0
  37. data/lib/rails-param-validation/validators/array.rb +49 -0
  38. data/lib/rails-param-validation/validators/boolean.rb +38 -0
  39. data/lib/rails-param-validation/validators/constant.rb +38 -0
  40. data/lib/rails-param-validation/validators/custom_type.rb +28 -0
  41. data/lib/rails-param-validation/validators/date.rb +39 -0
  42. data/lib/rails-param-validation/validators/datetime.rb +39 -0
  43. data/lib/rails-param-validation/validators/float.rb +39 -0
  44. data/lib/rails-param-validation/validators/hash.rb +52 -0
  45. data/lib/rails-param-validation/validators/integer.rb +39 -0
  46. data/lib/rails-param-validation/validators/object.rb +63 -0
  47. data/lib/rails-param-validation/validators/optional.rb +44 -0
  48. data/lib/rails-param-validation/validators/regex.rb +37 -0
  49. data/lib/rails-param-validation/validators/string.rb +31 -0
  50. data/lib/rails-param-validation/validators/uuid.rb +39 -0
  51. data/lib/rails-param-validation/version.rb +3 -0
  52. data/lib/rails-param-validation.rb +42 -0
  53. data/rails-param-validation.gemspec +33 -0
  54. metadata +100 -0
@@ -0,0 +1,100 @@
1
+ module RailsParamValidation
2
+
3
+ module AnnotationTypes
4
+ class AnnotationT
5
+ attr_reader :inner_type
6
+
7
+ def initialize(inner_type)
8
+ @inner_type = inner_type
9
+ end
10
+ end
11
+
12
+ class ArrayT < AnnotationT
13
+ def initialize(inner_type)
14
+ super(inner_type)
15
+ end
16
+ end
17
+
18
+ class HashT < AnnotationT
19
+ attr_reader :key_type
20
+
21
+ def initialize(inner_type, key_type = String)
22
+ super(inner_type)
23
+
24
+ @key_type = key_type
25
+ end
26
+ end
27
+
28
+ class OptionalT < AnnotationT
29
+ attr_reader :default
30
+
31
+ def initialize(inner_type, default)
32
+ super(inner_type)
33
+
34
+ @default = default
35
+ end
36
+ end
37
+
38
+ class CustomT < AnnotationT
39
+ attr_reader :type
40
+
41
+ def initialize(type)
42
+ @type = type
43
+ end
44
+
45
+ def schema
46
+ CustomT.registry[@type]
47
+ end
48
+
49
+ def self.register(type, schema)
50
+ registry[type] = schema
51
+ end
52
+
53
+ def self.registered(type)
54
+ raise TypeNotFound.new(type) unless registry.key? type
55
+ registry[type]
56
+ end
57
+
58
+ def self.types
59
+ registry.keys
60
+ end
61
+
62
+ private
63
+
64
+ def self.registry
65
+ @@types ||= {}
66
+ end
67
+ end
68
+ end
69
+
70
+ module Types
71
+ def ArrayType(inner_type)
72
+ AnnotationTypes::ArrayT.new(inner_type)
73
+ end
74
+
75
+ def HashType(inner_type, key_type = String)
76
+ AnnotationTypes::HashT.new(inner_type, key_type)
77
+ end
78
+
79
+ def Optional(inner_type, default)
80
+ AnnotationTypes::OptionalT.new(inner_type, default)
81
+ end
82
+
83
+ def Type(type)
84
+ AnnotationTypes::CustomT.new(type)
85
+ end
86
+ end
87
+
88
+ end
89
+
90
+ begin
91
+ class Boolean; end unless Module.const_get("Boolean").is_a?(Class)
92
+ rescue NameError
93
+ class Boolean; end
94
+ end
95
+
96
+ begin
97
+ class Uuid; end unless Module.const_get("Uuid").is_a?(Class)
98
+ rescue NameError
99
+ class Uuid; end
100
+ end
@@ -0,0 +1,51 @@
1
+ module RailsParamValidation
2
+
3
+ class MatchResult
4
+ attr_reader :errors, :matching, :value
5
+
6
+ def initialize(value, path = nil, error = nil)
7
+ @value = value
8
+ @errors = []
9
+
10
+ unless error.nil?
11
+ @errors.push(path: path, message: error)
12
+ end
13
+
14
+ @matching = error.nil?
15
+ end
16
+
17
+ def matches?
18
+ @matching
19
+ end
20
+
21
+ # @param [MatchResult] other
22
+ # @return [MatchResult]
23
+ def merge!(other)
24
+ @matching = @matching && other.matching
25
+ @errors += other.errors
26
+
27
+ self
28
+ end
29
+
30
+ def error_messages
31
+ @errors.map { |e| { path: e[:path].join('/'), message: e[:message] } }
32
+ end
33
+ end
34
+
35
+ class Validator
36
+ attr_reader :schema
37
+
38
+ def initialize(schema)
39
+ @schema = schema
40
+ end
41
+
42
+ # @return [MatchData]
43
+ def matches?(path, structure)
44
+ end
45
+
46
+ def to_openapi
47
+ raise NotImplementedError
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,37 @@
1
+ require_relative "./errors/no_matching_factory"
2
+ require_relative "./validator"
3
+
4
+ module RailsParamValidation
5
+
6
+ class ValidatorFactory
7
+
8
+ # @param [ValidatorFactory] factory
9
+ def self.register(factory)
10
+ factories.push factory
11
+ end
12
+
13
+ # @return [Array<ValidatorFactory>]
14
+ def self.factories
15
+ @@factories ||= []
16
+ end
17
+
18
+ # @return [Validator]
19
+ def self.create(schema)
20
+ factory = factories.detect { |f| f.supports? schema }
21
+
22
+ if factory.nil?
23
+ raise NoMatchingFactory.new(schema)
24
+ end
25
+
26
+ factory.create schema
27
+ end
28
+
29
+ # @return [Boolean]
30
+ def supports?(schema)
31
+ end
32
+
33
+ # @return Validator
34
+ def create(schema)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,42 @@
1
+ module RailsParamValidation
2
+
3
+ class AlternativesValidator < Validator
4
+ # @param [Array] schema
5
+ def initialize(schema)
6
+ super schema
7
+
8
+ @inner_validators = schema.map { |value| ValidatorFactory.create(value) }
9
+ end
10
+
11
+ def matches?(path, data)
12
+ result = MatchResult.new nil, path, "The value did not match any of the alternatives"
13
+
14
+ @inner_validators.each_with_index do |validator, idx|
15
+ match = validator.matches?(path + ["[#{idx}]"], data)
16
+
17
+ if match.matches?
18
+ return match
19
+ else
20
+ result.merge! match
21
+ end
22
+ end
23
+
24
+ result
25
+ end
26
+
27
+ def to_openapi
28
+ { oneOf: @inner_validators.map(&:to_openapi) }
29
+ end
30
+ end
31
+
32
+ class AlternativesValidatorFactory < ValidatorFactory
33
+ def supports?(schema)
34
+ schema.is_a? Array
35
+ end
36
+
37
+ def create(schema)
38
+ AlternativesValidator.new schema
39
+ end
40
+ end
41
+
42
+ end
@@ -0,0 +1,49 @@
1
+ module RailsParamValidation
2
+
3
+ class ArrayValidator < Validator
4
+ # @param [ArrayT] schema
5
+ def initialize(schema)
6
+ super schema
7
+
8
+ @inner_validator = ValidatorFactory.create schema.inner_type
9
+ end
10
+
11
+ def matches?(path, data)
12
+ # Don't proceed if it is not an array at all
13
+ unless data.is_a? Array
14
+ return MatchResult.new nil, path, "Expected an array"
15
+ end
16
+
17
+ value = []
18
+ result = MatchResult.new nil
19
+
20
+ # Verify each entry
21
+ data.each_with_index do |entry, index|
22
+ match = @inner_validator.matches?(path + [index.to_s], entry)
23
+
24
+ if match.matches?
25
+ value.push match.value
26
+ else
27
+ result.merge! match
28
+ end
29
+ end
30
+
31
+ result.matches? ? MatchResult.new(value) : result
32
+ end
33
+
34
+ def to_openapi
35
+ { type: :array, items: @inner_validator.to_openapi }
36
+ end
37
+ end
38
+
39
+ class ArrayValidatorFactory < ValidatorFactory
40
+ def supports?(schema)
41
+ schema.is_a? AnnotationTypes::ArrayT
42
+ end
43
+
44
+ def create(schema)
45
+ ArrayValidator.new schema
46
+ end
47
+ end
48
+
49
+ end
@@ -0,0 +1,38 @@
1
+ module RailsParamValidation
2
+
3
+ class BooleanValidator < Validator
4
+ def initialize(schema)
5
+ super schema
6
+ end
7
+
8
+ def matches?(path, data)
9
+ if data.is_a?(TrueClass) || data.is_a?(FalseClass)
10
+ return MatchResult.new data
11
+ end
12
+
13
+ case data
14
+ when "true"
15
+ return MatchResult.new true
16
+ when "false"
17
+ return MatchResult.new false
18
+ else
19
+ return MatchResult.new(nil, path, "Expected a boolean (true, false)")
20
+ end
21
+ end
22
+
23
+ def to_openapi
24
+ { type: :boolean }
25
+ end
26
+ end
27
+
28
+ class BooleanValidatorFactory < ValidatorFactory
29
+ def supports?(schema)
30
+ schema == Boolean
31
+ end
32
+
33
+ def create(schema)
34
+ BooleanValidator.new schema
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,38 @@
1
+ module RailsParamValidation
2
+
3
+ class ConstantValidator < Validator
4
+ def initialize(schema)
5
+ super schema
6
+
7
+ @constant = schema
8
+ end
9
+
10
+ def matches?(path, data)
11
+
12
+ if data.to_s == @constant.to_s
13
+ if @constant.is_a?(String)
14
+ MatchResult.new @constant.clone
15
+ else
16
+ MatchResult.new @constant
17
+ end
18
+ else
19
+ MatchResult.new nil, path, "Expected value #{@constant.to_s}"
20
+ end
21
+ end
22
+
23
+ def to_openapi
24
+ ValidatorFactory.create(schema.class).to_openapi.merge(enum: [schema])
25
+ end
26
+ end
27
+
28
+ class ConstantValidatorFactory < ValidatorFactory
29
+ def supports?(schema)
30
+ ![String, Symbol, Numeric, TrueClass, FalseClass].detect { |klass| schema.is_a? klass }.nil?
31
+ end
32
+
33
+ def create(schema)
34
+ ConstantValidator.new schema
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,28 @@
1
+ module RailsParamValidation
2
+
3
+ class CustomTypeValidator < Validator
4
+ def initialize(type)
5
+ super type
6
+ @validator = ValidatorFactory.create type.schema
7
+ end
8
+
9
+ def matches?(path, data)
10
+ @validator.matches? path, data
11
+ end
12
+
13
+ def to_openapi
14
+ { '$ref': "#/components/schemas/#{schema.type}" }
15
+ end
16
+ end
17
+
18
+ class CustomTypeValidatorFactory < ValidatorFactory
19
+ def supports?(schema)
20
+ schema.is_a? AnnotationTypes::CustomT
21
+ end
22
+
23
+ def create(schema)
24
+ CustomTypeValidator.new schema
25
+ end
26
+ end
27
+
28
+ end
@@ -0,0 +1,39 @@
1
+ module RailsParamValidation
2
+
3
+ class DateValidator < Validator
4
+ def initialize(schema)
5
+ super schema
6
+ end
7
+
8
+ def matches?(path, data)
9
+ if data.is_a?(DateTime)
10
+ MatchResult.new data.to_date
11
+ elsif data.is_a?(Time)
12
+ MatchResult.new data.to_date
13
+ elsif data.is_a?(String)
14
+ begin
15
+ MatchResult.new(data.to_date || raise(ArgumentError))
16
+ rescue ArgumentError
17
+ MatchResult.new nil, path, "Expected a date"
18
+ end
19
+ else
20
+ MatchResult.new nil, path, "Expected a date"
21
+ end
22
+ end
23
+
24
+ def to_openapi
25
+ { type: :string, format: 'date' }
26
+ end
27
+ end
28
+
29
+ class DateValidatorFactory < ValidatorFactory
30
+ def supports?(schema)
31
+ schema == Date
32
+ end
33
+
34
+ def create(schema)
35
+ DateValidator.new schema
36
+ end
37
+ end
38
+
39
+ end
@@ -0,0 +1,39 @@
1
+ module RailsParamValidation
2
+
3
+ class DateTimeValidator < Validator
4
+ def initialize(schema)
5
+ super schema
6
+ end
7
+
8
+ def matches?(path, data)
9
+ if data.is_a?(DateTime)
10
+ MatchResult.new data
11
+ elsif data.is_a?(Time)
12
+ MatchResult.new data.to_datetime
13
+ elsif data.is_a?(String)
14
+ begin
15
+ MatchResult.new(data.to_datetime || raise(ArgumentError))
16
+ rescue ArgumentError
17
+ MatchResult.new nil, path, "Expected a date-time"
18
+ end
19
+ else
20
+ MatchResult.new nil, path, "Expected a date-time"
21
+ end
22
+ end
23
+
24
+ def to_openapi
25
+ { type: :string, format: 'date-time' }
26
+ end
27
+ end
28
+
29
+ class DateTimeValidatorFactory < ValidatorFactory
30
+ def supports?(schema)
31
+ schema == DateTime
32
+ end
33
+
34
+ def create(schema)
35
+ DateTimeValidator.new schema
36
+ end
37
+ end
38
+
39
+ end
@@ -0,0 +1,39 @@
1
+ module RailsParamValidation
2
+
3
+ class FloatValidator < Validator
4
+ def initialize(schema)
5
+ super schema
6
+ end
7
+
8
+ def matches?(path, data)
9
+ if data.is_a? Numeric
10
+ return MatchResult.new data.to_f
11
+ end
12
+
13
+ unless data.is_a? String
14
+ return MatchResult.new(nil, path, "Expected a float")
15
+ end
16
+
17
+ begin
18
+ return MatchResult.new(Float(data))
19
+ rescue ArgumentError
20
+ return MatchResult.new(nil, path, "Expected a float")
21
+ end
22
+ end
23
+
24
+ def to_openapi
25
+ { type: :number, format: :double }
26
+ end
27
+ end
28
+
29
+ class FloatValidatorFactory < ValidatorFactory
30
+ def supports?(schema)
31
+ schema == Float
32
+ end
33
+
34
+ def create(schema)
35
+ FloatValidator.new schema
36
+ end
37
+ end
38
+
39
+ end
@@ -0,0 +1,52 @@
1
+ module RailsParamValidation
2
+
3
+ class HashValidator < Validator
4
+ # @param [HashT] schema
5
+ def initialize(schema)
6
+ super schema
7
+
8
+ @value_validator = ValidatorFactory.create schema.inner_type
9
+ @key_validator = ValidatorFactory.create schema.key_type
10
+ end
11
+
12
+ def matches?(path, data)
13
+ # Don't proceed if it is not an array at all
14
+ unless data.is_a? Hash
15
+ return MatchResult.new nil, path, "Expected a hash"
16
+ end
17
+
18
+ value = {}
19
+ result = MatchResult.new nil
20
+
21
+ # Verify each entry
22
+ data.each do |key, entry|
23
+ match_key = @value_validator.matches?(path + ["#{key}[key]"], key)
24
+ match_value = @value_validator.matches?(path + [key], entry)
25
+
26
+ if match_value.matches? && match_key.matches?
27
+ value[match_key.value] = match_value.value
28
+ else
29
+ result.merge! match_key unless match_key.matches?
30
+ result.merge! match_value unless match_value.matches?
31
+ end
32
+ end
33
+
34
+ result.matches? ? MatchResult.new(value) : result
35
+ end
36
+
37
+ def to_openapi
38
+ { type: :object, additionalProperties: @value_validator.to_openapi }
39
+ end
40
+ end
41
+
42
+ class HashValidatorFactory < ValidatorFactory
43
+ def supports?(schema)
44
+ schema.is_a? AnnotationTypes::HashT
45
+ end
46
+
47
+ def create(schema)
48
+ HashValidator.new schema
49
+ end
50
+ end
51
+
52
+ end
@@ -0,0 +1,39 @@
1
+ module RailsParamValidation
2
+
3
+ class IntegerValidator < Validator
4
+ def initialize(schema)
5
+ super schema
6
+ end
7
+
8
+ def matches?(path, data)
9
+ if data.is_a? Integer
10
+ return MatchResult.new data
11
+ end
12
+
13
+ unless data.is_a? String
14
+ return MatchResult.new(nil, path, "Expected an integer")
15
+ end
16
+
17
+ begin
18
+ return MatchResult.new(Integer(data))
19
+ rescue ArgumentError
20
+ return MatchResult.new(nil, path, "Expected an integer")
21
+ end
22
+ end
23
+
24
+ def to_openapi
25
+ { type: :integer }
26
+ end
27
+ end
28
+
29
+ class IntegerValidatorFactory < ValidatorFactory
30
+ def supports?(schema)
31
+ schema == Integer
32
+ end
33
+
34
+ def create(schema)
35
+ IntegerValidator.new schema
36
+ end
37
+ end
38
+
39
+ end
@@ -0,0 +1,63 @@
1
+ module RailsParamValidation
2
+
3
+ class ObjectValidator < Validator
4
+ # @param [Hash] schema
5
+ def initialize(schema)
6
+ super schema
7
+
8
+ @inner_validators = schema.map { |key, value| [key, ValidatorFactory.create(value)] }.to_h
9
+ end
10
+
11
+ def matches?(path, data)
12
+ # Don't proceed if it is not hash at all
13
+ unless data.is_a? Hash
14
+ return MatchResult.new nil, path, "Expected an object"
15
+ end
16
+
17
+ value = {}
18
+ result = MatchResult.new nil
19
+
20
+ # Verify each entry
21
+ @inner_validators.each do |property, validator|
22
+ match = validator.matches?(path + [property], data.key?(property.to_s) ? data[property.to_s] : data[property.to_sym])
23
+
24
+ if match.matches?
25
+ value[property] = match.value
26
+ else
27
+ result.merge! match
28
+ end
29
+ end
30
+
31
+ additional_properties = data.keys.reject { |k| @inner_validators.key?(k.to_s) || @inner_validators.key?(k.to_sym) }
32
+ additional_properties.each do |property|
33
+ result.merge! MatchResult.new(nil, path + [property], "Unknown property")
34
+ end
35
+
36
+ result.matches? ? MatchResult.new(value) : result
37
+ end
38
+
39
+ def to_openapi
40
+ openapi = { type: :object, properties: {} }
41
+ required = []
42
+ schema.each do |key, _|
43
+ validator = @inner_validators[key]
44
+ openapi[:properties][key] = validator.to_openapi
45
+ required.push key unless validator.is_a? OptionalValidator
46
+ end
47
+
48
+ openapi[:required] = required if required.any?
49
+ openapi
50
+ end
51
+ end
52
+
53
+ class ObjectValidatorFactory < ValidatorFactory
54
+ def supports?(schema)
55
+ schema.is_a? Hash
56
+ end
57
+
58
+ def create(schema)
59
+ ObjectValidator.new schema
60
+ end
61
+ end
62
+
63
+ end
@@ -0,0 +1,44 @@
1
+ module RailsParamValidation
2
+
3
+ class OptionalValidator < Validator
4
+ def initialize(schema)
5
+ super schema
6
+
7
+ @inner_validator = ValidatorFactory.create schema.inner_type
8
+ @default = schema.default
9
+ end
10
+
11
+ def matches?(path, data)
12
+ if data.nil?
13
+ if @default.is_a? Proc
14
+ MatchResult.new @default.call
15
+ else
16
+ MatchResult.new @default
17
+ end
18
+ else
19
+ @inner_validator.matches? path, data
20
+ end
21
+ end
22
+
23
+ def to_openapi
24
+ child = @inner_validator.to_openapi
25
+ child[:nullable] = true
26
+ if child.key? :enum
27
+ child[:enum].push nil
28
+ end
29
+
30
+ child
31
+ end
32
+ end
33
+
34
+ class OptionalValidatorFactory < ValidatorFactory
35
+ def supports?(schema)
36
+ schema.is_a? AnnotationTypes::OptionalT
37
+ end
38
+
39
+ def create(schema)
40
+ OptionalValidator.new schema
41
+ end
42
+ end
43
+
44
+ end