respect 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 (97) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +289 -0
  3. data/RELATED_WORK.md +40 -0
  4. data/RELEASE_NOTES.md +23 -0
  5. data/Rakefile +31 -0
  6. data/STATUS_MATRIX.html +137 -0
  7. data/lib/respect.rb +231 -0
  8. data/lib/respect/any_schema.rb +22 -0
  9. data/lib/respect/array_def.rb +28 -0
  10. data/lib/respect/array_schema.rb +203 -0
  11. data/lib/respect/boolean_schema.rb +32 -0
  12. data/lib/respect/composite_schema.rb +86 -0
  13. data/lib/respect/core_statements.rb +206 -0
  14. data/lib/respect/datetime_schema.rb +27 -0
  15. data/lib/respect/def_without_name.rb +6 -0
  16. data/lib/respect/divisible_by_validator.rb +20 -0
  17. data/lib/respect/doc_helper.rb +24 -0
  18. data/lib/respect/doc_parser.rb +37 -0
  19. data/lib/respect/dsl_dumper.rb +181 -0
  20. data/lib/respect/equal_to_validator.rb +20 -0
  21. data/lib/respect/fake_name_proxy.rb +116 -0
  22. data/lib/respect/float_schema.rb +27 -0
  23. data/lib/respect/format_validator.rb +136 -0
  24. data/lib/respect/global_def.rb +79 -0
  25. data/lib/respect/greater_than_or_equal_to_validator.rb +19 -0
  26. data/lib/respect/greater_than_validator.rb +19 -0
  27. data/lib/respect/has_constraints.rb +34 -0
  28. data/lib/respect/hash_def.rb +40 -0
  29. data/lib/respect/hash_schema.rb +218 -0
  30. data/lib/respect/in_validator.rb +19 -0
  31. data/lib/respect/integer_schema.rb +27 -0
  32. data/lib/respect/ip_addr_schema.rb +23 -0
  33. data/lib/respect/ipv4_addr_schema.rb +27 -0
  34. data/lib/respect/ipv6_addr_schema.rb +27 -0
  35. data/lib/respect/items_def.rb +21 -0
  36. data/lib/respect/json_schema_html_formatter.rb +143 -0
  37. data/lib/respect/less_than_or_equal_to_validator.rb +19 -0
  38. data/lib/respect/less_than_validator.rb +19 -0
  39. data/lib/respect/match_validator.rb +19 -0
  40. data/lib/respect/max_length_validator.rb +20 -0
  41. data/lib/respect/min_length_validator.rb +20 -0
  42. data/lib/respect/multiple_of_validator.rb +10 -0
  43. data/lib/respect/null_schema.rb +26 -0
  44. data/lib/respect/numeric_schema.rb +33 -0
  45. data/lib/respect/org3_dumper.rb +213 -0
  46. data/lib/respect/regexp_schema.rb +19 -0
  47. data/lib/respect/schema.rb +285 -0
  48. data/lib/respect/schema_def.rb +16 -0
  49. data/lib/respect/string_schema.rb +21 -0
  50. data/lib/respect/unit_test_helper.rb +37 -0
  51. data/lib/respect/uri_schema.rb +23 -0
  52. data/lib/respect/utc_time_schema.rb +17 -0
  53. data/lib/respect/validator.rb +51 -0
  54. data/lib/respect/version.rb +3 -0
  55. data/test/any_schema_test.rb +79 -0
  56. data/test/array_def_test.rb +113 -0
  57. data/test/array_schema_test.rb +487 -0
  58. data/test/boolean_schema_test.rb +89 -0
  59. data/test/composite_schema_test.rb +30 -0
  60. data/test/datetime_schema_test.rb +83 -0
  61. data/test/doc_helper_test.rb +34 -0
  62. data/test/doc_parser_test.rb +109 -0
  63. data/test/dsl_dumper_test.rb +395 -0
  64. data/test/fake_name_proxy_test.rb +138 -0
  65. data/test/float_schema_test.rb +146 -0
  66. data/test/format_validator_test.rb +224 -0
  67. data/test/hash_def_test.rb +126 -0
  68. data/test/hash_schema_test.rb +613 -0
  69. data/test/integer_schema_test.rb +142 -0
  70. data/test/ip_addr_schema_test.rb +78 -0
  71. data/test/ipv4_addr_schema_test.rb +71 -0
  72. data/test/ipv6_addr_schema_test.rb +71 -0
  73. data/test/json_schema_html_formatter_test.rb +214 -0
  74. data/test/null_schema_test.rb +46 -0
  75. data/test/numeric_schema_test.rb +294 -0
  76. data/test/org3_dumper_test.rb +784 -0
  77. data/test/regexp_schema_test.rb +54 -0
  78. data/test/respect_test.rb +108 -0
  79. data/test/schema_def_test.rb +405 -0
  80. data/test/schema_test.rb +290 -0
  81. data/test/string_schema_test.rb +209 -0
  82. data/test/support/circle.rb +11 -0
  83. data/test/support/color.rb +24 -0
  84. data/test/support/point.rb +11 -0
  85. data/test/support/respect/circle_schema.rb +16 -0
  86. data/test/support/respect/color_def.rb +19 -0
  87. data/test/support/respect/color_schema.rb +33 -0
  88. data/test/support/respect/point_schema.rb +19 -0
  89. data/test/support/respect/rgba_schema.rb +20 -0
  90. data/test/support/respect/universal_validator.rb +25 -0
  91. data/test/support/respect/user_macros.rb +12 -0
  92. data/test/support/rgba.rb +11 -0
  93. data/test/test_helper.rb +90 -0
  94. data/test/uri_schema_test.rb +54 -0
  95. data/test/utc_time_schema_test.rb +63 -0
  96. data/test/validator_test.rb +22 -0
  97. metadata +288 -0
data/lib/respect.rb ADDED
@@ -0,0 +1,231 @@
1
+ require 'active_support/dependencies/autoload'
2
+ require 'active_support/core_ext/string/inflections'
3
+ require 'active_support/core_ext/integer/inflections'
4
+ require 'active_support/core_ext/hash/indifferent_access'
5
+ require 'active_support/core_ext/string/strip'
6
+
7
+ # Setup inflection rules for our acronyms
8
+ ActiveSupport::Inflector.inflections do |inflect|
9
+ inflect.acronym "URI"
10
+ inflect.acronym "UTC"
11
+ inflect.acronym "IP"
12
+ inflect.acronym "JSON"
13
+ end
14
+
15
+ # Provide methods and classes to define, validate, sanitize and dump object schema.
16
+ #
17
+ # Classes in this module are split in 5 groups:
18
+ # * The _schema_ classes are the core of this module since they support the validation
19
+ # process and are the internal representation of schema specification (see {Schema}).
20
+ # * The _definition_ classes (aka _def_ classes) are the front-end of this module since
21
+ # they implement the schema definition DSL (see {GlobalDef}).
22
+ # * The _validator_ classes implement validation routine you can attach to your schema.
23
+ # accessible via the schema's options (see {Validator}).
24
+ # * The _dumper_ classes are the back-end of this module since they implement the
25
+ # convertion of the internal schema representation to different formats.
26
+ # * The _miscellaneous_ classes provides various support for the other categories.
27
+ #
28
+ # You can extend this library in many ways:
29
+ #
30
+ # 1. If you want to add your own schema class, you can sub-class the {CompositeSchema}
31
+ # class. Sub-classing of the {Schema} class is not well supported yet as it may have
32
+ # some issues with the current dumpers (see {DslDumper} and {Org3Dumper}). Fortunately,
33
+ # most of the cases can be handled by {CompositeSchema}.
34
+ # 1. If you want to simply add new statements to the schema definition DSL, you can just
35
+ # bundle them in a module and call {Respect.extend_dsl_with} (see {CoreStatements} for
36
+ # further information).
37
+ #
38
+ # Extension of the _validator_ and _dumper_ classes is still experimental. Also, creating
39
+ # custom _definition_ classes is not recommended yet.
40
+ module Respect
41
+ extend ActiveSupport::Autoload
42
+
43
+ # Schema classes
44
+ autoload :Schema
45
+ autoload :HashSchema
46
+ autoload :IntegerSchema
47
+ autoload :FloatSchema
48
+ autoload :NumericSchema
49
+ autoload :StringSchema
50
+ autoload :ArraySchema
51
+ autoload :AnySchema
52
+ autoload :BooleanSchema
53
+ autoload :NullSchema
54
+ autoload :URISchema
55
+ autoload :RegexpSchema
56
+ autoload :DatetimeSchema
57
+ autoload :IPAddrSchema
58
+ autoload :Ipv4AddrSchema
59
+ autoload :Ipv6AddrSchema
60
+ autoload :UTCTimeSchema
61
+ autoload :HasConstraints
62
+ autoload :CompositeSchema
63
+ # Validator classes
64
+ autoload :Validator
65
+ autoload :EqualToValidator
66
+ autoload :GreaterThanValidator
67
+ autoload :GreaterThanOrEqualToValidator
68
+ autoload :LessThanValidator
69
+ autoload :LessThanOrEqualToValidator
70
+ autoload :DivisibleByValidator
71
+ autoload :MultipleOfValidator
72
+ autoload :InValidator
73
+ autoload :MatchValidator
74
+ autoload :MinLengthValidator
75
+ autoload :MaxLengthValidator
76
+ autoload :FormatValidator
77
+ # DSL classes
78
+ autoload :SchemaDef
79
+ autoload :ArrayDef
80
+ autoload :HashDef
81
+ autoload :GlobalDef
82
+ autoload :ItemsDef
83
+ autoload :CoreStatements
84
+ autoload :DefWithoutName
85
+ autoload :FakeNameProxy
86
+ # Dumper classes
87
+ autoload :DslDumper
88
+ autoload :Org3Dumper
89
+ # Miscellaneous classes
90
+ autoload :DocParser
91
+ autoload :DocHelper
92
+ autoload :JSONSchemaHTMLFormatter
93
+
94
+ # Base error of all errors raised by this module.
95
+ class RespectError < StandardError
96
+ end
97
+
98
+ # Raised when the validation process has failed.
99
+ class ValidationError < RespectError
100
+ def initialize(message)
101
+ super
102
+ @context = [ message ]
103
+ end
104
+
105
+ # An array of error messages to help you track where
106
+ # the error happened. Use it as a back-trace but in
107
+ # your validated object instead of your code.
108
+ attr_reader :context
109
+ end
110
+
111
+ # Raised when you did an illegal operation while defining
112
+ # a schema. See it as an ArgumentError but more specific.
113
+ class InvalidSchemaError < RespectError
114
+ end
115
+
116
+ class << self
117
+
118
+ # Extend the schema definition DSL with the statements defined in the given
119
+ # module +mod+. Its methods would be available to each definition class
120
+ # calling {GlobalDef.include_core_statements}.
121
+ def extend_dsl_with(mod)
122
+ raise ArugmentError, "cannot extend DSL with CoreStatements" if mod == CoreStatements
123
+ CoreStatements.send(:include, mod)
124
+ # We must "refresh" all the classes include "CoreStatements" by re-including it to
125
+ # work around the
126
+ # {dynamic module include problem}[http://eigenclass.org/hiki/The+double+inclusion+problem]
127
+ GlobalDef.core_contexts.each{|c| c.send(:include, CoreStatements) }
128
+ end
129
+
130
+ STATEMENT_NAME_REGEXP = /^[a-z_][a-z_0-9]*$/
131
+
132
+ # Build a schema class name from the given +statement_name+.
133
+ def schema_name_for(statement_name)
134
+ unless statement_name =~ STATEMENT_NAME_REGEXP
135
+ raise ArgumentError, "statement '#{statement_name}' name must match #{STATEMENT_NAME_REGEXP.inspect}"
136
+ end
137
+ const_name = statement_name.to_s
138
+ if const_name == "schema"
139
+ "#{self.name}::Schema"
140
+ else
141
+ "#{self.name}::#{const_name.camelize}Schema"
142
+ end
143
+ end
144
+
145
+ # Return the schema class associated to the given +statement_name+.
146
+ #
147
+ # A "valid" schema class must verify the following properties:
148
+ # * Named like +StatementNameSchema+ in {Respect} module.
149
+ # * Be a sub-class of {Schema}.
150
+ # * Be concrete (i.e. have a public method +new+)
151
+ def schema_for(statement_name)
152
+ klass = Respect.schema_name_for(statement_name).safe_constantize
153
+ if klass && klass < Schema && klass.public_methods.include?(:new)
154
+ klass
155
+ else
156
+ nil
157
+ end
158
+ end
159
+
160
+ # Test whether a schema is defined for the given +statement_name+.
161
+ def schema_defined_for?(statement_name)
162
+ !!schema_for(statement_name)
163
+ end
164
+
165
+ # Turn the given string (assuming it is a constraint name) into a
166
+ # validator class name string.
167
+ def validator_name_for(constraint_name)
168
+ "#{self.name}::#{constraint_name.to_s.camelize}Validator"
169
+ end
170
+
171
+ # Turn the given +constraint_name+ into a validator class symbol.
172
+ # Return nil if the validator class does not exist.
173
+ def validator_for(constraint_name)
174
+ validator_name_for(constraint_name).safe_constantize
175
+ end
176
+
177
+ # Test whether a validator is defined for the given +constraint_name+.
178
+ def validator_defined_for?(constraint_name)
179
+ !!validator_for(constraint_name)
180
+ end
181
+
182
+ # Sanitize the given +object+ *in-place* according to the given +sanitized_object+.
183
+ # A sanitized object contains value with more specific data type. Like a URI
184
+ # object instead of a plain string.
185
+ #
186
+ # Non-sanitized value are not touch (i.e. values present in +object+ but not in
187
+ # +sanitized_object+). However, +object["key"]+ and +object[:key]+ are considered as
188
+ # referring to the same value, but they original key would be preserved.
189
+ #
190
+ # Example:
191
+ # object = { "int" => "42" }
192
+ # Respect.sanitize_object!(object, { "int" => 42 }
193
+ # object #=> { "int" => 42 }
194
+ # object = { :int => "42" }
195
+ # Respect.sanitize_object!(object, { "int" => 42 }
196
+ # object #=> { :int => 42 }
197
+ #
198
+ # The sanitized object is accessible via the {Schema#sanitized_object} method after a
199
+ # successful validation.
200
+ def sanitize_object!(object, sanitized_object)
201
+ case object
202
+ when Hash
203
+ if sanitized_object.is_a? Hash
204
+ sanitized_object.each do |name, value|
205
+ if object.has_key?(name)
206
+ object[name] = sanitize_object!(object[name], value)
207
+ else
208
+ object[name.to_sym] = sanitize_object!(object[name.to_sym], value)
209
+ end
210
+ end
211
+ object
212
+ else
213
+ sanitized_object
214
+ end
215
+ when Array
216
+ if sanitized_object.is_a? Array
217
+ sanitized_object.each_with_index do |value, index|
218
+ object[index] = sanitize_object!(object[index], value)
219
+ end
220
+ object
221
+ else
222
+ sanitized_object
223
+ end
224
+ else
225
+ sanitized_object
226
+ end
227
+ end
228
+
229
+ end
230
+
231
+ end
@@ -0,0 +1,22 @@
1
+ module Respect
2
+ class AnySchema < Schema
3
+
4
+ public_class_method :new
5
+
6
+ def validate(object)
7
+ case object
8
+ when Hash, Array, TrueClass, FalseClass, Numeric, NilClass, String
9
+ self.sanitized_object = object
10
+ true
11
+ else
12
+ raise ValidationError,
13
+ "object is not of a valid type but a #{object.class}"
14
+ end
15
+ rescue ValidationError => e
16
+ # Reset sanitized object.
17
+ self.sanitized_object = nil
18
+ raise e
19
+ end
20
+
21
+ end # class AnySchema
22
+ end # module Respect
@@ -0,0 +1,28 @@
1
+ module Respect
2
+ class ArrayDef < GlobalDef
3
+ include_core_statements
4
+ include DefWithoutName
5
+
6
+ def initialize(options = {})
7
+ @array_schema = ArraySchema.new(options)
8
+ end
9
+
10
+ def items(&block)
11
+ @array_schema.items = ItemsDef.eval(&block)
12
+ end
13
+
14
+ def extra_items(&block)
15
+ @array_schema.extra_items = ItemsDef.eval(&block)
16
+ end
17
+
18
+ private
19
+
20
+ def evaluation_result
21
+ @array_schema
22
+ end
23
+
24
+ def update_context(name, schema)
25
+ @array_schema.item = schema
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,203 @@
1
+ module Respect
2
+ # A schema to specify the structure of an array.
3
+ #
4
+ # They are two approaches to specify the structure of an array.
5
+ #
6
+ # If the items of your array have all the same structure then you
7
+ # should use the {#item=} method to set their schema.
8
+ #
9
+ # Example:
10
+ # # An array where all items are integer greater than 42.
11
+ # s = ArraySchema.define do |s|
12
+ # s.item do |s|
13
+ # s.integer greater_than: 42
14
+ # end
15
+ # end
16
+ # s.validate?([]) #=> true
17
+ # s.validate?([ 43 ]) #=> true
18
+ # s.validate?([ 43, 44 ]) #=> true
19
+ # s.validate?([ 43, 44, 30 ]) #=> false
20
+ #
21
+ # Otherwise, you should use the {#items=} and {#extra_items=}. This is called
22
+ # "tuple" typing.
23
+ #
24
+ # Example:
25
+ # # An array where first item is an integer and the second one
26
+ # # is a string.
27
+ # ArraySchema.define do |s|
28
+ # s.items do |s|
29
+ # s.integer
30
+ # s.string
31
+ # end
32
+ # end
33
+ # s.validate?([]) #=> false
34
+ # s.validate?([ 43 ]) #=> false
35
+ # s.validate?([ 43, "foo" ]) #=> true
36
+ # s.validate?([ 43, 44 ]) #=> false
37
+ #
38
+ # You cannot mix tuple typing and single item typing.
39
+ #
40
+ # You can pass several options when creating an {ArraySchema}:
41
+ # uniq:: if +true+, duplicated items are forbidden (+false+ by default).
42
+ # min_size:: if set the array must have at least the given number of items
43
+ # (+nil+ by default). This option apply only in non-tuple typing.
44
+ # max_size:: if set the array must have at most the given number of items
45
+ # (+nil+ by default). This option apply only in non-tuple typing.
46
+ class ArraySchema < Schema
47
+
48
+ public_class_method :new
49
+
50
+ class << self
51
+ # Overwritten method. See {Schema.default_options}
52
+ def default_options
53
+ super().merge({
54
+ uniq: false,
55
+ }).freeze
56
+ end
57
+ end
58
+
59
+ def initialize(options = {})
60
+ super(self.class.default_options.merge(options))
61
+ end
62
+
63
+ def initialize_copy(other)
64
+ super
65
+ @items = other.items.dup unless other.items.nil?
66
+ @extra_items = other.extra_items.dup unless other.extra_items.nil?
67
+ end
68
+
69
+ # Set the schema that all items in the array must validate.
70
+ def item=(item)
71
+ if @items
72
+ raise InvalidSchemaError,
73
+ "cannot mix single item and multiple items validation"
74
+ end
75
+ @item = item
76
+ end
77
+
78
+ # Get the schema that all items in the array must validate.
79
+ attr_reader :item
80
+
81
+ # Set the array of schema that the corresponding items must validate.
82
+ def items=(items)
83
+ if @item
84
+ raise InvalidSchemaError,
85
+ "cannot mix single item and multiple items validation"
86
+ end
87
+ @items = items
88
+ end
89
+
90
+ # Get the array of schema that the corresponding items must validate.
91
+ attr_reader :items
92
+
93
+ # Set extra schema items. These are optional. If they are not in the
94
+ # object the validation pass anyway.
95
+ def extra_items=(extra_items)
96
+ return @extra_items unless extra_items
97
+ if @item
98
+ raise InvalidSchemaError,
99
+ "cannot mix single item and extra items validation"
100
+ end
101
+ @items = [] if @items.nil?
102
+ @extra_items = extra_items
103
+ end
104
+
105
+ # Get the extra schema items.
106
+ attr_reader :extra_items
107
+
108
+ # Overwritten method. See {Schema#validate}
109
+ def validate(object)
110
+ # Handle nil case.
111
+ if object.nil?
112
+ if allow_nil?
113
+ self.sanitized_object = nil
114
+ return true
115
+ else
116
+ raise ValidationError, "object is nil but this #{self.class} does not allow nil"
117
+ end
118
+ end
119
+ # Validate type.
120
+ unless object.is_a?(Array)
121
+ raise ValidationError, "object is not an array but a #{object.class}"
122
+ end
123
+ # At this point we are sure @item and (@items or @extra_items) cannot be
124
+ # defined both. (see the setters).
125
+ sanitized_object = []
126
+ # Validate expected item.
127
+ if @item
128
+ if options[:min_size] && object.size < options[:min_size]
129
+ raise ValidationError,
130
+ "expected at least #{options[:min_size]} item(s) but got #{object.size}"
131
+ end
132
+ if options[:max_size] && object.size > options[:max_size]
133
+ raise ValidationError,
134
+ "expected at most #{options[:min_size]} item(s) but got #{object.size}"
135
+ end
136
+ object.each_with_index do |item, i|
137
+ validate_item(i, @item, object, sanitized_object)
138
+ end
139
+ end
140
+ # Validate object items count.
141
+ if @items || @extra_items
142
+ if @extra_items
143
+ min_size = @items ? @items.size : 0
144
+ unless min_size <= object.size
145
+ raise ValidationError,
146
+ "array size should be at least #{min_size} but is #{object.size}"
147
+ end
148
+ else
149
+ if @items.size != object.size
150
+ raise ValidationError,
151
+ "array size should be #{@items.size} but is #{object.size}"
152
+ end
153
+ end
154
+ end
155
+ # Validate expected multiple items.
156
+ if @items
157
+ @items.each_with_index do |schema, i|
158
+ validate_item(i, schema, object, sanitized_object)
159
+ end
160
+ end
161
+ # Validate extra items.
162
+ if @extra_items
163
+ @extra_items.each_with_index do |schema, i|
164
+ if @items.size + i < object.size
165
+ validate_item(@items.size + i, schema, object, sanitized_object)
166
+ end
167
+ end
168
+ end
169
+ # Validate all items are unique.
170
+ if options[:uniq]
171
+ s = Set.new
172
+ object.each_with_index do |e, i|
173
+ if s.add?(e).nil?
174
+ raise ValidationError,
175
+ "duplicated item number #{i}"
176
+ end
177
+ end
178
+ end
179
+ self.sanitized_object = sanitized_object
180
+ true
181
+ rescue ValidationError => e
182
+ # Reset sanitized object.
183
+ self.sanitized_object = nil
184
+ raise e
185
+ end
186
+
187
+ def ==(other)
188
+ super && @item == other.item && @items == other.items && @extra_items == other.extra_items
189
+ end
190
+
191
+ private
192
+
193
+ def validate_item(index, schema, object, sanitized_object)
194
+ begin
195
+ schema.validate(object[index])
196
+ sanitized_object << schema.sanitized_object
197
+ rescue ValidationError => e
198
+ e.context << "in array #{index.ordinalize} item"
199
+ raise e
200
+ end
201
+ end
202
+ end
203
+ end