respect 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.md +289 -0
- data/RELATED_WORK.md +40 -0
- data/RELEASE_NOTES.md +23 -0
- data/Rakefile +31 -0
- data/STATUS_MATRIX.html +137 -0
- data/lib/respect.rb +231 -0
- data/lib/respect/any_schema.rb +22 -0
- data/lib/respect/array_def.rb +28 -0
- data/lib/respect/array_schema.rb +203 -0
- data/lib/respect/boolean_schema.rb +32 -0
- data/lib/respect/composite_schema.rb +86 -0
- data/lib/respect/core_statements.rb +206 -0
- data/lib/respect/datetime_schema.rb +27 -0
- data/lib/respect/def_without_name.rb +6 -0
- data/lib/respect/divisible_by_validator.rb +20 -0
- data/lib/respect/doc_helper.rb +24 -0
- data/lib/respect/doc_parser.rb +37 -0
- data/lib/respect/dsl_dumper.rb +181 -0
- data/lib/respect/equal_to_validator.rb +20 -0
- data/lib/respect/fake_name_proxy.rb +116 -0
- data/lib/respect/float_schema.rb +27 -0
- data/lib/respect/format_validator.rb +136 -0
- data/lib/respect/global_def.rb +79 -0
- data/lib/respect/greater_than_or_equal_to_validator.rb +19 -0
- data/lib/respect/greater_than_validator.rb +19 -0
- data/lib/respect/has_constraints.rb +34 -0
- data/lib/respect/hash_def.rb +40 -0
- data/lib/respect/hash_schema.rb +218 -0
- data/lib/respect/in_validator.rb +19 -0
- data/lib/respect/integer_schema.rb +27 -0
- data/lib/respect/ip_addr_schema.rb +23 -0
- data/lib/respect/ipv4_addr_schema.rb +27 -0
- data/lib/respect/ipv6_addr_schema.rb +27 -0
- data/lib/respect/items_def.rb +21 -0
- data/lib/respect/json_schema_html_formatter.rb +143 -0
- data/lib/respect/less_than_or_equal_to_validator.rb +19 -0
- data/lib/respect/less_than_validator.rb +19 -0
- data/lib/respect/match_validator.rb +19 -0
- data/lib/respect/max_length_validator.rb +20 -0
- data/lib/respect/min_length_validator.rb +20 -0
- data/lib/respect/multiple_of_validator.rb +10 -0
- data/lib/respect/null_schema.rb +26 -0
- data/lib/respect/numeric_schema.rb +33 -0
- data/lib/respect/org3_dumper.rb +213 -0
- data/lib/respect/regexp_schema.rb +19 -0
- data/lib/respect/schema.rb +285 -0
- data/lib/respect/schema_def.rb +16 -0
- data/lib/respect/string_schema.rb +21 -0
- data/lib/respect/unit_test_helper.rb +37 -0
- data/lib/respect/uri_schema.rb +23 -0
- data/lib/respect/utc_time_schema.rb +17 -0
- data/lib/respect/validator.rb +51 -0
- data/lib/respect/version.rb +3 -0
- data/test/any_schema_test.rb +79 -0
- data/test/array_def_test.rb +113 -0
- data/test/array_schema_test.rb +487 -0
- data/test/boolean_schema_test.rb +89 -0
- data/test/composite_schema_test.rb +30 -0
- data/test/datetime_schema_test.rb +83 -0
- data/test/doc_helper_test.rb +34 -0
- data/test/doc_parser_test.rb +109 -0
- data/test/dsl_dumper_test.rb +395 -0
- data/test/fake_name_proxy_test.rb +138 -0
- data/test/float_schema_test.rb +146 -0
- data/test/format_validator_test.rb +224 -0
- data/test/hash_def_test.rb +126 -0
- data/test/hash_schema_test.rb +613 -0
- data/test/integer_schema_test.rb +142 -0
- data/test/ip_addr_schema_test.rb +78 -0
- data/test/ipv4_addr_schema_test.rb +71 -0
- data/test/ipv6_addr_schema_test.rb +71 -0
- data/test/json_schema_html_formatter_test.rb +214 -0
- data/test/null_schema_test.rb +46 -0
- data/test/numeric_schema_test.rb +294 -0
- data/test/org3_dumper_test.rb +784 -0
- data/test/regexp_schema_test.rb +54 -0
- data/test/respect_test.rb +108 -0
- data/test/schema_def_test.rb +405 -0
- data/test/schema_test.rb +290 -0
- data/test/string_schema_test.rb +209 -0
- data/test/support/circle.rb +11 -0
- data/test/support/color.rb +24 -0
- data/test/support/point.rb +11 -0
- data/test/support/respect/circle_schema.rb +16 -0
- data/test/support/respect/color_def.rb +19 -0
- data/test/support/respect/color_schema.rb +33 -0
- data/test/support/respect/point_schema.rb +19 -0
- data/test/support/respect/rgba_schema.rb +20 -0
- data/test/support/respect/universal_validator.rb +25 -0
- data/test/support/respect/user_macros.rb +12 -0
- data/test/support/rgba.rb +11 -0
- data/test/test_helper.rb +90 -0
- data/test/uri_schema_test.rb +54 -0
- data/test/utc_time_schema_test.rb +63 -0
- data/test/validator_test.rb +22 -0
- 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,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
|