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.
- 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,79 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Respect
|
4
|
+
# Global context of the schema definition DSL.
|
5
|
+
#
|
6
|
+
# This is the base class of all DSL evaluation context. It provides
|
7
|
+
# minimal evaluation support. Any methods added to this class will
|
8
|
+
# be available in every context of DSL.
|
9
|
+
#
|
10
|
+
# You can evaluate a block using the {#eval} method. Sub-classes must
|
11
|
+
# implement the +evalulation_result+ methods (which must returns the
|
12
|
+
# result of the evaluation) or provides their own +eval+ methods.
|
13
|
+
#
|
14
|
+
# End-users are not supposed to sub-class this class yet. Its API is
|
15
|
+
# *experimental*.
|
16
|
+
class GlobalDef
|
17
|
+
|
18
|
+
# Remove methods inherited from Object and conflicting with the
|
19
|
+
# dynamic methods in CoreStatement.
|
20
|
+
%w{hash}.each do |name|
|
21
|
+
undef_method name
|
22
|
+
end
|
23
|
+
|
24
|
+
class << self
|
25
|
+
|
26
|
+
# Instantiate this evaluation context using the given +args+
|
27
|
+
# and evaluate the given +block+ within it.
|
28
|
+
def eval(*args, &block)
|
29
|
+
new(*args).eval(&block)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Return whether the statements declared in this context accept a name
|
33
|
+
# as first argument. All classes not including {DefWithoutName}
|
34
|
+
# accept names.
|
35
|
+
def accept_name?
|
36
|
+
!(self < DefWithoutName)
|
37
|
+
end
|
38
|
+
|
39
|
+
@@core_contexts = Set.new
|
40
|
+
|
41
|
+
# Call this method in "def" class willing to offer core statements.
|
42
|
+
# Do not include {CoreStatements} directly.
|
43
|
+
def include_core_statements
|
44
|
+
@@core_contexts << self
|
45
|
+
include CoreStatements
|
46
|
+
end
|
47
|
+
|
48
|
+
# Return the list of all classes including {CoreStatements}.
|
49
|
+
def core_contexts
|
50
|
+
@@core_contexts
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
# Shortcut to {GlobalDef.accept_name?}.
|
56
|
+
def accept_name?
|
57
|
+
self.class.accept_name?
|
58
|
+
end
|
59
|
+
|
60
|
+
# Evaluate the given +block+ in the context of this class through
|
61
|
+
# a {FakeNameProxy} with this class as target.
|
62
|
+
# {#evaluation_result} is called at the end to return the
|
63
|
+
# result of this evaluation.
|
64
|
+
def eval(&block)
|
65
|
+
@def_evaluator ||= FakeNameProxy.new(self)
|
66
|
+
@def_evaluator.eval(&block)
|
67
|
+
evaluation_result
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# Overwrite this method in sub-classes to return the result value
|
73
|
+
# of this evaluation context.
|
74
|
+
def evaluation_result
|
75
|
+
raise NoMethodError, "overwrite me in sub-classes"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Respect
|
2
|
+
class GreaterThanOrEqualToValidator < Validator
|
3
|
+
def initialize(min)
|
4
|
+
@min = min
|
5
|
+
end
|
6
|
+
|
7
|
+
def validate(value)
|
8
|
+
unless value >= @min
|
9
|
+
raise ValidationError, "#{value} is not greater than or equal to #@min"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def to_h_org3
|
16
|
+
{ "minimum" => @min }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Respect
|
2
|
+
class GreaterThanValidator < Validator
|
3
|
+
def initialize(min)
|
4
|
+
@min = min
|
5
|
+
end
|
6
|
+
|
7
|
+
def validate(value)
|
8
|
+
unless value > @min
|
9
|
+
raise ValidationError, "#{value} is not greater than #@min"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def to_h_org3
|
16
|
+
{ "minimum" => @min, "exclusiveMinimum" => true }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Respect
|
2
|
+
# Module supporting execution of validators referred in options.
|
3
|
+
#
|
4
|
+
# Classes including this module must fulfill the following requirements:
|
5
|
+
# * Respond to +options+ and returned a hash of options where keys
|
6
|
+
# refers to validator name (i.e. +greater_than+ for {GreaterThanValidator}).
|
7
|
+
# * Respond to +validate_type(object)+ which must returns the sanitized object.
|
8
|
+
module HasConstraints
|
9
|
+
|
10
|
+
# Validate all the constraints listed in +options+ to the
|
11
|
+
# given +value+.
|
12
|
+
def validate_constraints(value)
|
13
|
+
options.each do |option, arg|
|
14
|
+
if validator_class = Respect.validator_for(option)
|
15
|
+
validator_class.new(arg).validate(value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Call +validate_type+ with the given +object+, apply the constraints
|
21
|
+
# and assign the sanitized object.
|
22
|
+
def validate(object)
|
23
|
+
sanitized_object = validate_type(object)
|
24
|
+
validate_constraints(sanitized_object) unless sanitized_object.nil? && allow_nil?
|
25
|
+
self.sanitized_object = sanitized_object
|
26
|
+
true
|
27
|
+
rescue ValidationError => e
|
28
|
+
# Reset sanitized object.
|
29
|
+
self.sanitized_object = nil
|
30
|
+
raise e
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Respect
|
2
|
+
class HashDef < GlobalDef
|
3
|
+
include_core_statements
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
@hash_schema = HashSchema.new(options)
|
7
|
+
end
|
8
|
+
|
9
|
+
def extra(&block)
|
10
|
+
with_options(required: false, &block)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Shortcut to say a schema +key+ must be equal to a given +value+. When it
|
14
|
+
# does not recognize the value type it creates a "any" schema.
|
15
|
+
#
|
16
|
+
# Example:
|
17
|
+
# HashSchema.define do |s|
|
18
|
+
# s["a_string"] = "value" # equivalent to: s.string("a_string", equal_to: "value")
|
19
|
+
# s["a_key"] = 0..5 # equivalent to: s.any("a_key", equal_to: "0..5")
|
20
|
+
# end
|
21
|
+
def []=(key, value)
|
22
|
+
case value
|
23
|
+
when String
|
24
|
+
string(key, equal_to: value.to_s)
|
25
|
+
else
|
26
|
+
any(key, equal_to: value.to_s)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def evaluation_result
|
33
|
+
@hash_schema
|
34
|
+
end
|
35
|
+
|
36
|
+
def update_context(name, schema)
|
37
|
+
@hash_schema[name] = schema
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
module Respect
|
2
|
+
# A schema to specify the structure of a hash.
|
3
|
+
#
|
4
|
+
# This schema defines the structure of a hash by listing
|
5
|
+
# the expected property name and they associated schema.
|
6
|
+
#
|
7
|
+
# Property can be define by using a symbol, a string or a regular
|
8
|
+
# expression. In the later case, the associated schema will be used
|
9
|
+
# to validate the value of all the properties matching the regular
|
10
|
+
# expression. You can get the list of all pattern properties
|
11
|
+
# using the {#pattern_properties} method.
|
12
|
+
#
|
13
|
+
# You can specify optional property by either setting the "required"
|
14
|
+
# option to false or y setting a non nil default value.
|
15
|
+
# You can get the list of all optional properties using the
|
16
|
+
# {#optional_properties} method.
|
17
|
+
#
|
18
|
+
# Access to the object's value being validated is done using either
|
19
|
+
# string key or symbol key. In other word +{ i: "42" }+ and
|
20
|
+
# +{ "i" => "42" }+ are the same object for the {#validate} method.
|
21
|
+
# The object passed is left untouched. The sanitized object
|
22
|
+
# is a hash with indifferent access. Note that when an object
|
23
|
+
# is sanitized in-place, its original keys are kept
|
24
|
+
# (see {Respect.sanitize_object!}). Only validated keys are included
|
25
|
+
# in the sanitized object.
|
26
|
+
#
|
27
|
+
# You can pass several options when creating an {HashSchema}:
|
28
|
+
# strict:: if set to +true+ the hash must not have any extra
|
29
|
+
# properties to be validated. (+false+ by default)
|
30
|
+
class HashSchema < Schema
|
31
|
+
include Enumerable
|
32
|
+
|
33
|
+
class << self
|
34
|
+
# Overwritten method. See Schema::default_options
|
35
|
+
def default_options
|
36
|
+
super().merge({
|
37
|
+
strict: false,
|
38
|
+
}).freeze
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
public_class_method :new
|
43
|
+
|
44
|
+
def initialize(options = {})
|
45
|
+
super(self.class.default_options.merge(options))
|
46
|
+
@properties = {}
|
47
|
+
end
|
48
|
+
|
49
|
+
def initialize_copy(other)
|
50
|
+
super
|
51
|
+
@properties = other.properties.dup
|
52
|
+
end
|
53
|
+
|
54
|
+
# Get the schema for the given property +name+.
|
55
|
+
def [](name)
|
56
|
+
@properties[name]
|
57
|
+
end
|
58
|
+
|
59
|
+
# Set the given +schema+ for the given property +name+. A name can be
|
60
|
+
# a Symbol, a String or a Regexp.
|
61
|
+
def []=(name, schema)
|
62
|
+
case name
|
63
|
+
when Symbol, String, Regexp
|
64
|
+
if @properties.has_key?(name)
|
65
|
+
raise InvalidSchemaError, "property '#{name}' already defined"
|
66
|
+
end
|
67
|
+
@properties[name] = schema
|
68
|
+
else
|
69
|
+
raise InvalidSchemaError, "unsupported property name type #{name}:#{name.class}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns the set of properties of this schema index by their name.
|
74
|
+
attr_reader :properties
|
75
|
+
|
76
|
+
# Overwritten method. See {Schema#validate}.
|
77
|
+
def validate(object)
|
78
|
+
# Handle nil case.
|
79
|
+
if object.nil?
|
80
|
+
if allow_nil?
|
81
|
+
self.sanitized_object = nil
|
82
|
+
return true
|
83
|
+
else
|
84
|
+
raise ValidationError, "object is nil but this #{self.class} does not allow nil"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
# Validate object format.
|
88
|
+
unless object.is_a?(Hash)
|
89
|
+
raise ValidationError, "object is not a hash but a #{object.class}"
|
90
|
+
end
|
91
|
+
sanitized_object = {}.with_indifferent_access
|
92
|
+
# Validate expected properties.
|
93
|
+
@properties.each do |name, schema|
|
94
|
+
case name
|
95
|
+
when Symbol
|
96
|
+
validate_property_with_options(name.to_s, schema, object, sanitized_object)
|
97
|
+
when String
|
98
|
+
validate_property_with_options(name, schema, object, sanitized_object)
|
99
|
+
when Regexp
|
100
|
+
object.select{|prop, schema| prop =~ name }.each do |prop, value|
|
101
|
+
validate_property(prop, schema, object, sanitized_object)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
if options[:strict]
|
106
|
+
# Check whether there are extra properties.
|
107
|
+
object.each do |name, schema|
|
108
|
+
unless sanitized_object.has_key? name
|
109
|
+
raise ValidationError, "unexpected key `#{name}'"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
self.sanitized_object = sanitized_object
|
114
|
+
true
|
115
|
+
rescue ValidationError => e
|
116
|
+
# Reset sanitized object.
|
117
|
+
self.sanitized_object = nil
|
118
|
+
raise e
|
119
|
+
end
|
120
|
+
|
121
|
+
def validate_property_with_options(name, schema, object, sanitized_object)
|
122
|
+
if object_has_key?(object, name)
|
123
|
+
validate_property(name, schema, object, sanitized_object)
|
124
|
+
else
|
125
|
+
if schema.required?
|
126
|
+
raise ValidationError, "missing key `#{name}'"
|
127
|
+
else
|
128
|
+
if schema.has_default?
|
129
|
+
sanitized_object[name] = schema.default
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
private :validate_property_with_options
|
135
|
+
|
136
|
+
def object_has_key?(object, key)
|
137
|
+
if object.has_key?(key)
|
138
|
+
true
|
139
|
+
elsif object.has_key?(key.to_sym)
|
140
|
+
true
|
141
|
+
else
|
142
|
+
false
|
143
|
+
end
|
144
|
+
end
|
145
|
+
private :object_has_key?
|
146
|
+
|
147
|
+
def validate_property(name, schema, object, sanitized_object)
|
148
|
+
begin
|
149
|
+
schema.validate(object_get_key(object, name))
|
150
|
+
sanitized_object[name] = schema.sanitized_object
|
151
|
+
rescue ValidationError => e
|
152
|
+
e.context << "in hash property `#{name}'"
|
153
|
+
raise e
|
154
|
+
end
|
155
|
+
end
|
156
|
+
private :validate_property
|
157
|
+
|
158
|
+
def object_get_key(object, key)
|
159
|
+
if object.has_key?(key)
|
160
|
+
object[key]
|
161
|
+
elsif object.has_key?(key.to_sym)
|
162
|
+
object[key.to_sym]
|
163
|
+
else
|
164
|
+
object.default(key)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
private :object_get_key
|
168
|
+
|
169
|
+
# Return the optional properties (e.g. those that are not required).
|
170
|
+
def optional_properties
|
171
|
+
@properties.select{|name, schema| schema.optional? }
|
172
|
+
end
|
173
|
+
|
174
|
+
# Return all the properties identified by a regular expression.
|
175
|
+
def pattern_properties
|
176
|
+
@properties.select{|name, schema| name.is_a?(Regexp) }
|
177
|
+
end
|
178
|
+
|
179
|
+
# In-place version of {#merge}. This schema is returned.
|
180
|
+
def merge!(hash_schema)
|
181
|
+
@options.merge!(hash_schema.options)
|
182
|
+
@properties.merge!(hash_schema.properties)
|
183
|
+
self
|
184
|
+
end
|
185
|
+
|
186
|
+
# Merge the given +hash_schema+ with this object schema. It works like
|
187
|
+
# +Hash.merge+.
|
188
|
+
def merge(hash_schema)
|
189
|
+
self.dup.merge!(hash_schema)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Return whether +property_name+ is defined in this hash schema.
|
193
|
+
def has_property?(property_name)
|
194
|
+
@properties.has_key?(property_name)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Evaluate the given block as a hash schema definition (i.e. in the context of
|
198
|
+
# {Respect::HashDef}) and merge the result with this hash schema.
|
199
|
+
# This is a way to "re-open" this hash schema definition to add some more.
|
200
|
+
def eval(&block)
|
201
|
+
self.merge!(HashSchema.define(&block))
|
202
|
+
end
|
203
|
+
|
204
|
+
# Return all the properties with a non-false documentation.
|
205
|
+
def documented_properties
|
206
|
+
@properties.select{|name, schema| schema.documented? }
|
207
|
+
end
|
208
|
+
|
209
|
+
def ==(other)
|
210
|
+
super && @properties == other.properties
|
211
|
+
end
|
212
|
+
|
213
|
+
# FIXME(Nicolas Despres): Add a test for me.
|
214
|
+
def each(&block)
|
215
|
+
@properties.each(&block)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Respect
|
2
|
+
class InValidator < Validator
|
3
|
+
def initialize(set)
|
4
|
+
@set = set
|
5
|
+
end
|
6
|
+
|
7
|
+
def validate(value)
|
8
|
+
unless @set.include?(value)
|
9
|
+
raise ValidationError, "#{value.inspect} is not included in #@set"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def to_h_org3
|
16
|
+
{ 'enum' => @set.dup }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Respect
|
2
|
+
class IntegerSchema < NumericSchema
|
3
|
+
|
4
|
+
def validate_type(object)
|
5
|
+
case object
|
6
|
+
when String
|
7
|
+
if object =~ /^[-+]?\d+$/
|
8
|
+
object.to_i
|
9
|
+
else
|
10
|
+
raise ValidationError,
|
11
|
+
"malformed integer value: `#{object}'"
|
12
|
+
end
|
13
|
+
when Integer
|
14
|
+
object
|
15
|
+
when NilClass
|
16
|
+
if allow_nil?
|
17
|
+
nil
|
18
|
+
else
|
19
|
+
raise ValidationError, "object is nil but this #{self.class} does not allow nil"
|
20
|
+
end
|
21
|
+
else
|
22
|
+
raise ValidationError, "object is not an integer but a '#{object.class}'"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end # class IntegerSchema
|
27
|
+
end
|