respect 0.1.0

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