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
@@ -0,0 +1,19 @@
1
+ module Respect
2
+ # Validate a string containing a regexp but also accept Regexp object.
3
+ class RegexpSchema < StringSchema
4
+
5
+ def validate_type(object)
6
+ case object
7
+ when NilClass
8
+ if allow_nil?
9
+ nil
10
+ else
11
+ raise ValidationError, "object is nil but this #{self.class} does not allow nil"
12
+ end
13
+ else
14
+ FormatValidator.new(:regexp).validate(object)
15
+ end
16
+ end
17
+
18
+ end # class RegexpSchema
19
+ end # module Respect
@@ -0,0 +1,285 @@
1
+ module Respect
2
+ # Base class for all object schema.
3
+ #
4
+ # A schema defines the expected structure and format for a given object.
5
+ # It is similar in spirit to {json-schema.org}(http://json-schema.org/)
6
+ # specification but uses Ruby DSL as definition. Using the DSL is not
7
+ # mandatory since you can also defines a schema using its own methods.
8
+ #
9
+ # Almost all {Schema} sub-classes has an associated statement available in
10
+ # the DSL for defining it. This statement is named after the class name
11
+ # (see {Schema.statement_name}). However all sub-classes do not have a statement
12
+ # associated (see {Respect.schema_for}).
13
+ #
14
+ # You can define such a schema using the
15
+ # {define} method. The {#validate} method allow you to check whether
16
+ # the given object is valid according to this schema.
17
+ # Various options can be passed to the schema when initializing it.
18
+ #
19
+ # While validating an object the schema build a sanitized
20
+ # version of this object including all the validated part.
21
+ # The value presents in this sanitized object have generally a
22
+ # type specific to the contents they represents. For instance,
23
+ # a URI would be represented as a string in the original
24
+ # object but as a URI object in the sanitized object.
25
+ # There is a she-bang version of the validation method which
26
+ # update the value of the given object in-place with the value from
27
+ # the sanitized object if the validation succeeded.
28
+ #
29
+ # You can pass several options when creating a Schema:
30
+ # required:: whether this property associated to this schema is
31
+ # required in the hash schema (+true+ by default).
32
+ # default:: the default value to use for the associated property
33
+ # if it is not present. Setting a default value make
34
+ # the property private. (+nil+ by default)
35
+ # doc:: the documentation of this schema (+nil+ by default).
36
+ # A documentation is composed of a title followed by
37
+ # an empty line and an optional long description.
38
+ # If set to false, then this schema is considered
39
+ # as an implementation details that should not be
40
+ # publicly documented. Thus, it will not be dumped as
41
+ # {json-schema.org}[http://json-schema.org/].
42
+ # allow_nil:: whether the schema accept +nil+ as validation value.
43
+ # (+false+ by default). This option is not supported
44
+ # yet by the json-schema.org standard.
45
+ # These options applies to all schema sub-classes.
46
+ #
47
+ # In addition to these options, you can configure any defined
48
+ # {Validator}. Validators are run during validation process by
49
+ # certain schema class like {IntegerSchema}, {StringSchema},
50
+ # etc... They are mostly non-containers schema. In the
51
+ # following code the {GreaterThanValidator} will be run at
52
+ # validation time with the value +o+:
53
+ #
54
+ # IntegerSchema.define greater_than: 0
55
+ #
56
+ # This class is _abstract_. You cannot instantiate it directly.
57
+ # Use one of its sub-classes instead.
58
+ class Schema
59
+ include DocHelper
60
+
61
+ class << self
62
+ # Make this class abstract.
63
+ private :new
64
+
65
+ # If a corresponding _def_ class exists for this class
66
+ # (see {def_class}) it defines a new schema by evaluating the given
67
+ # +block+ in the context of this definition class. It behaves as an alias
68
+ # for new if no block is given.
69
+ #
70
+ # If there is no associated _def_ class the block is passed to the constructor.
71
+ def define(*args, &block)
72
+ def_class = self.def_class
73
+ if def_class
74
+ if block
75
+ def_class.eval(*args, &block)
76
+ else
77
+ self.new(*args)
78
+ end
79
+ else
80
+ self.new(*args, &block)
81
+ end
82
+ end
83
+
84
+ # Return the associated _def_ class name for this class.
85
+ # Example:
86
+ # ArraySchema.def_class_name #=> "ArrayDef"
87
+ # HashSchema.def_class_name #=> "HashDef"
88
+ # Schema.def_class_name #=> "SchemaDef"
89
+ def def_class_name
90
+ if self == Schema
91
+ "Respect::SchemaDef"
92
+ else
93
+ self.name.sub(/Schema$/, 'Def')
94
+ end
95
+ end
96
+
97
+ # Return the definition class symbol for this schema class or nil
98
+ # if there is no class (see {def_class_name})
99
+ def def_class
100
+ self.def_class_name.safe_constantize
101
+ end
102
+
103
+ # Build a statement name from this class name.
104
+ #
105
+ # Example:
106
+ # Schema.statement_name #=> "schema"
107
+ # HashSchema.statement_name #=> "hash"
108
+ def statement_name
109
+ self.name.underscore.sub(/^.*\//, '').sub(/_schema$/, '')
110
+ end
111
+
112
+ # Return the default options for this schema class.
113
+ # If you overwrite this method in sub-classes, call super and merge the
114
+ # result with your default options.
115
+ def default_options
116
+ {
117
+ required: true,
118
+ default: nil,
119
+ doc: nil,
120
+ allow_nil: false,
121
+ }.freeze
122
+ end
123
+
124
+ end
125
+
126
+ # Create a new schema using the given _options_.
127
+ def initialize(options = {})
128
+ @sanitized_object = nil
129
+ @options = self.class.default_options.merge(options)
130
+ end
131
+
132
+ def initialize_copy(other)
133
+ @options = other.options.dup
134
+ end
135
+
136
+ # Returns the sanitized object. It is +nil+ as long as you have not
137
+ # validated any object. It is overwritten every times you call
138
+ # {#validate}. If the validation failed it will be reset to +nil+.
139
+ attr_reader :sanitized_object
140
+
141
+ # Returns the hash of options.
142
+ attr_reader :options
143
+
144
+ # Returns the documentation of this schema.
145
+ def documentation
146
+ @options[:doc]
147
+ end
148
+
149
+ alias_method :doc, :documentation
150
+
151
+ # Returns whether this schema must be documented (i.e. not ignored
152
+ # when dumped).
153
+ def documented?
154
+ @options[:doc] != false
155
+ end
156
+
157
+ # Whether this schema is required. (opposite of optional?)
158
+ def required?
159
+ @options[:required] && !has_default?
160
+ end
161
+
162
+ # Whether this schema is optional.
163
+ def optional?
164
+ !required?
165
+ end
166
+
167
+ # Returns the default value used when this schema is missing.
168
+ def default
169
+ @options[:default]
170
+ end
171
+
172
+ # Returns whether this schema has a default value defined.
173
+ def has_default?
174
+ @options[:default] != nil
175
+ end
176
+
177
+ # Returns whether this schema accept +nil+ as validation value.
178
+ def allow_nil?
179
+ !!@options[:allow_nil]
180
+ end
181
+
182
+ # Return whether the given +object+ validates this schema.
183
+ # You can get the validation error via {#last_error}.
184
+ def validate?(object)
185
+ begin
186
+ validate(object)
187
+ true
188
+ rescue ValidationError => e
189
+ @last_error = e
190
+ false
191
+ end
192
+ end
193
+
194
+ # Return the last validation error that happens during the
195
+ # validation process. (set by {#validate?}).
196
+ # Reset each time {#validate?} is called.
197
+ attr_reader :last_error
198
+
199
+ # Raise a {ValidationError} if the given +object+ is not validated by this schema.
200
+ # Returns true otherwise. A sanitized version of the object is built during
201
+ # this process and you can access it via {#sanitized_object}.
202
+ # Rewrite it in sub-classes.
203
+ def validate(object)
204
+ raise NoMethodError, "overwrite me in sub-classes"
205
+ end
206
+
207
+ # Return +true+ or +false+ whether this schema validates the given +object+.
208
+ # If it does +object+ is updated in-place with the sanitized value.
209
+ # This method does not raise a {ValidationError}. You can access the error
210
+ # using {#last_error}.
211
+ def validate!(object)
212
+ valid = validate?(object)
213
+ if valid
214
+ sanitize_object!(object)
215
+ end
216
+ valid
217
+ end
218
+
219
+ # Sanitize the given +object+ *in-place* if it validates this schema. The sanitized object
220
+ # is returned. {ValidationError} is raised on error.
221
+ def sanitize!(object)
222
+ validate(object)
223
+ sanitize_object!(object)
224
+ end
225
+
226
+ # A shortcut for {Respect.sanitize_object!}.
227
+ def sanitize_object!(object)
228
+ Respect.sanitize_object!(object, self.sanitized_object)
229
+ end
230
+
231
+ # Returns a string containing a human-readable representation of this schema.
232
+ def inspect
233
+ "#<%s:0x%x %s>" % [
234
+ self.class.name,
235
+ self.object_id,
236
+ instance_variables.map{|v| "#{v}=#{instance_variable_get(v).inspect}" }.join(", ")
237
+ ]
238
+ end
239
+
240
+ # Returns a string containing ruby code defining this schema. Theoretically, you can
241
+ # evaluate it and get the same schema afterward.
242
+ def to_s
243
+ DslDumper.new(self).dump
244
+ end
245
+
246
+ # Serialize this schema to a JSON string following the given +format+.
247
+ def to_json(format = :org3)
248
+ case format
249
+ when :org3
250
+ self.to_h(:org3).to_json
251
+ else
252
+ raise ArgumentError, "unknown format '#{format}'"
253
+ end
254
+ end
255
+
256
+ # Return the options with no default value.
257
+ # (Useful when writing a dumper)
258
+ def non_default_options
259
+ @options.select{|opt, value| value != self.class.default_options[opt] }
260
+ end
261
+
262
+ # Convert this schema to a hash representation following the given
263
+ # +format+.
264
+ def to_h(format = :org3)
265
+ case format
266
+ when :org3
267
+ Org3Dumper.new(self).dump
268
+ else
269
+ raise ArgumentError, "unknown format '#{format}'"
270
+ end
271
+ end
272
+
273
+ # Two schema are equal if they have the same type and the set of options.
274
+ # Sub-class definition may include more attributes.
275
+ def ==(other)
276
+ self.class == other.class && @options == other.options
277
+ end
278
+
279
+ private
280
+
281
+ # Used by sub-classes to set the formatted object.
282
+ attr_writer :sanitized_object
283
+
284
+ end
285
+ end
@@ -0,0 +1,16 @@
1
+ module Respect
2
+ class SchemaDef < GlobalDef
3
+ include_core_statements
4
+ include DefWithoutName
5
+
6
+ private
7
+
8
+ def evaluation_result
9
+ @schema
10
+ end
11
+
12
+ def update_context(name, schema)
13
+ @schema = schema
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,21 @@
1
+ module Respect
2
+ class StringSchema < Schema
3
+ include HasConstraints
4
+
5
+ public_class_method :new
6
+
7
+ def validate_type(object)
8
+ case object
9
+ when NilClass
10
+ if allow_nil?
11
+ nil
12
+ else
13
+ raise ValidationError, "object is nil but this #{self.class} does not allow nil"
14
+ end
15
+ else
16
+ object.to_s
17
+ end
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,37 @@
1
+ module Respect
2
+ module UnitTestHelper
3
+ def assert_schema_validation_is(expected, schema, object, msg = nil)
4
+ valid = schema.validate?(object)
5
+ if expected
6
+ if !valid
7
+ if msg
8
+ message = msg
9
+ else
10
+ message = "Schema:\n#{schema}expected to validate object <#{object.inspect}> but failed with \"#{schema.last_error.context.join(" ")}\"."
11
+ end
12
+ assert false, message
13
+ end
14
+ else
15
+ if valid
16
+ if msg
17
+ message = msg
18
+ else
19
+ message = "Schema:\n#{schema}expected to invalidate object <#{object.inspect}> but succeed."
20
+ end
21
+ assert false, message
22
+ end
23
+ end
24
+ end
25
+
26
+ def assert_schema_validate(schema, object, msg = nil)
27
+ assert_schema_validation_is(true, schema, object, msg)
28
+ end
29
+
30
+ def assert_schema_invalidate(schema, object, msg = nil)
31
+ assert_schema_validation_is(false, schema, object, msg)
32
+ end
33
+
34
+ end # module UnitTestHelper
35
+ end # module Respect
36
+
37
+ Test::Unit::TestCase.send :include, Respect::UnitTestHelper
@@ -0,0 +1,23 @@
1
+ require 'uri'
2
+
3
+ module Respect
4
+ # Validate a string containing an URI but also accepts URI object.
5
+ class URISchema < StringSchema
6
+
7
+ def validate_type(object)
8
+ case object
9
+ when NilClass
10
+ if allow_nil?
11
+ nil
12
+ else
13
+ raise ValidationError, "object is nil but this #{self.class} does not allow nil"
14
+ end
15
+ when URI
16
+ object
17
+ else
18
+ FormatValidator.new(:uri).validate(object)
19
+ end
20
+ end
21
+
22
+ end # class URISchema
23
+ end # module Respect
@@ -0,0 +1,17 @@
1
+ module Respect
2
+ # A UTC time. It creates a Time object.
3
+ class UTCTimeSchema < NumericSchema
4
+
5
+ def validate_type(object)
6
+ value = super
7
+ unless value.nil?
8
+ if value < 0
9
+ raise ValidationError,
10
+ "UTC time value #{value} cannot be negative"
11
+ end
12
+ Time.at(value)
13
+ end
14
+ end
15
+
16
+ end # class UTCTimeSchema
17
+ end # module Respect
@@ -0,0 +1,51 @@
1
+ module Respect
2
+ # A schema validator.
3
+ #
4
+ # Validator are an extensible way to validate certain properties of
5
+ # a schema. There are many validators available in this library (see
6
+ # all the sub-classes of this class).
7
+ #
8
+ # You can attach a validator to any schema through its options
9
+ # parameters when initializing it. Any schema including the
10
+ # {HasConstraints} module will execute them when its {Schema#validate}
11
+ # method is called.
12
+ #
13
+ # Example:
14
+ # # Will call GreaterThanValidator.new(42).validate(-1)
15
+ # IntegerSchema.define(greater_than: 42).validate?(-1) #=> true
16
+ #
17
+ # The validator API is *experimental* so it is not recommended to
18
+ # write your own.
19
+ class Validator
20
+
21
+ class << self
22
+ # Turn this validator class name into a constraint name.
23
+ def constraint_name
24
+ self.name.sub(/^.*::/, '').sub(/Validator$/, '').underscore
25
+ end
26
+ end
27
+
28
+ def validate(value)
29
+ true
30
+ end
31
+
32
+ # Convert this validator to a Hash using the given +format+.
33
+ def to_h(format = :org3)
34
+ case format
35
+ when :org3
36
+ to_h_org3
37
+ else
38
+ raise ArgumentError, "unknown format '#{format}'"
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ # Called when {#to_h} is called with +:org3+ format.
45
+ # Sub-classes are supposed to overwrite this methods and to return
46
+ # their conversion to the JSON schema standard draft v3.
47
+ def to_h_org3
48
+ raise NoMethodError, "overwrite me in sub-classes"
49
+ end
50
+ end
51
+ end