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