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
@@ -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