rails-param-validation 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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