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