kwalify 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.
@@ -0,0 +1,70 @@
1
+ ###
2
+ ### $Rev: 7 $
3
+ ### $Release: 0.1.0 $
4
+ ### copyright(c) 2005 kuwata-lab all rights reserved.
5
+ ###
6
+
7
+ require 'date'
8
+
9
+
10
+ module Kwalify
11
+ module Boolean
12
+ end
13
+ end
14
+ #module Boolean; end
15
+ class TrueClass
16
+ include Kwalify::Boolean
17
+ #include Boolean
18
+ end
19
+ class FalseClass
20
+ include Kwalify::Boolean
21
+ #include Boolean
22
+ end
23
+
24
+
25
+ module Kwalify
26
+ module Text
27
+ end
28
+ end
29
+ #module Text; end
30
+ class String
31
+ include Kwalify::Text
32
+ #include Text
33
+ end
34
+ class Numeric
35
+ include Kwalify::Text
36
+ #include Text
37
+ end
38
+
39
+
40
+ module Kwalify
41
+
42
+ DEFAULT_TYPE = "string" ## use "string" as default of @type
43
+
44
+ @@type_table = {
45
+ "seq" => Array,
46
+ "sequence" => Array,
47
+ #"list" => Array,
48
+ #"array" => Array,
49
+ "map" => Hash,
50
+ "mapping" => Hash,
51
+ #"hash" => Hash,
52
+ "text" => Text,
53
+ "string" => String,
54
+ "integer" => Integer,
55
+ "float" => Float,
56
+ "number" => Numeric,
57
+ #"numeric" => Numeric,
58
+ "date" => Date,
59
+ "time" => Time,
60
+ "boolean" => Boolean,
61
+ #"bool" => Boolean,
62
+ "object" => Object,
63
+ "any" => Object,
64
+ }
65
+
66
+ def self.type_table
67
+ return @@type_table
68
+ end
69
+
70
+ end
@@ -0,0 +1,44 @@
1
+ ###
2
+ ### $Rev: 5 $
3
+ ### $Release: 0.1.0 $
4
+ ### copyright(c) 2005 kuwata-lab all rights reserved.
5
+ ###
6
+
7
+ require 'test/unit'
8
+ require 'tempfile'
9
+
10
+ module Test
11
+ module Unit
12
+ class TestCase
13
+ def assert_equal_with_diff(expected, actual, message=nil, diffopt='-u', flag_cut=true)
14
+ if expected == actual
15
+ assert(true)
16
+ return
17
+ end
18
+
19
+ if expected[-1] != ?\n || actual[-1] != ?\n
20
+ expected += "\n"
21
+ actual += "\n"
22
+ end
23
+ begin
24
+ expfile = Tempfile.new(".expected.")
25
+ expfile.write(expected); expfile.flush()
26
+ actfile = Tempfile.new(".actual.")
27
+ actfile.write(actual); actfile.flush()
28
+ diff = `diff #{diffopt} #{expfile.path} #{actfile.path}`
29
+ ensure
30
+ expfile.close(true) if expfile
31
+ actfile.close(true) if actfile
32
+ end
33
+
34
+ # cut 1st & 2nd lines
35
+ message = (flag_cut ? diff.gsub(/\A.*\n.*\n/, '') : diff) unless message
36
+ #raise Test::Unit::AssertionFailedError.new(message)
37
+ assert_block(message) { false } # or assert(false, message)
38
+ end
39
+ #def assert_equal(expected, actual, message=nil)
40
+ # return assert_equal_with_diff(expected, actual, message)
41
+ #end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,220 @@
1
+ ###
2
+ ### $Rev: 6 $
3
+ ### $Release: 0.1.0 $
4
+ ### copyright(c) 2005 kuwata-lab all rights reserved.
5
+ ###
6
+
7
+ class CommandOptionError < StandardError
8
+ def initialize(option, error_symbol, message=nil)
9
+ if !message
10
+ case error_symbol
11
+ when :no_argument
12
+ message = "-%s: argument required." % option
13
+ when :unknown_option
14
+ message = "-%s: unknown option." % option
15
+ when :invalid_property
16
+ message = "-%s: invalid property." % option
17
+ else
18
+ message = "*** internal error(optchar=#{option}, error_symbol=#{error_symbol}) ***"
19
+ end
20
+ end
21
+ super(message)
22
+ @option = option
23
+ @error_symbol = error_symbol
24
+ end
25
+
26
+ attr_reader :option, :error_symbol
27
+ end
28
+
29
+
30
+ ##
31
+ ## ex.
32
+ ## ## create parser
33
+ ## arg_none = "hv" # ex. -h -v
34
+ ## arg_required = "xf" # ex. -x suffix -f filename
35
+ ## arg_optional = "i" # ex. -i (or -i10)
36
+ ## parser = CommandOptionParser.new(arg_none, arg_required, arg_optional)
37
+ ##
38
+ ## ## parse options
39
+ ## argv = %w[-h -v -f filename -i 10 aaa bbb]
40
+ ## options, properties = parser.parse(argv)
41
+ ## p options #=> { ?h=>true, ?v=>true, ?f=>"filename", ?i=>true }
42
+ ## p argv #=> ["10", "aaa", "bbb"]
43
+ ##
44
+ ## ## parse options #2
45
+ ## argv = %w[-hvx.txt -ffilename -i10 aaa bbb]
46
+ ## options, properties = parser.parse(argv)
47
+ ## p options #=> { ?h=>true, ?v=>true, ?x=>".txt", ?f=>"filename", ?i=>10 }
48
+ ## p argv #=> ["aaa", "bbb"]
49
+ ##
50
+ ## ## parse properties
51
+ ## argv = %w[-hi --index=10 --user-name=foo --help]
52
+ ## options, properties = parser.parse(argv)
53
+ ## p options #=> {?h=>true, ?i=>true}
54
+ ## p properties #=> {"index"=>"10", "user-name"=>"foo", "help"=>nil}
55
+ ##
56
+ ## ## parse properties with auto-convert
57
+ ## argv = %w[-hi --index=10 --user-name=foo --help]
58
+ ## options, properties = parser.parse(argv, true)
59
+ ## p options #=> {?h=>true, ?i=>true}
60
+ ## p properties #=> {:index=>10, :user_name=>foo, :help=>true}
61
+ ##
62
+ ## ## -a: unknown option.
63
+ ## argv = %w[-abc]
64
+ ## begin
65
+ ## options, properties = parser.parse(argv)
66
+ ## rescue CommandOptionError => ex
67
+ ## $stderr.puts ex.message # -a: unknown option.
68
+ ## end
69
+ ##
70
+ ## ## -f: argument required.
71
+ ## argv = %w[-f]
72
+ ## begin
73
+ ## options, properties = parser.parse(argv)
74
+ ## rescue CommandOptionError => ex
75
+ ## $stderr.puts ex.message # -f: argument required.
76
+ ## end
77
+ ##
78
+ ## ## --@prop=10: invalid property.
79
+ ## argv = %w[--@prop=10]
80
+ ## begin
81
+ ## options, properties = parser.parse(argv)
82
+ ## rescue CommandOptionError => ex
83
+ ## $stderr.puts ex.message # --@prop=10: invalid property.
84
+ ## end
85
+ ##
86
+
87
+ class CommandOptionParser
88
+
89
+ ## arg_none: option string which takes no argument
90
+ ## arg_required: option string which takes argument
91
+ ## arg_otpional: option string which may takes argument optionally
92
+ def initialize(arg_none=nil, arg_required=nil, arg_optional=nil)
93
+ @arg_none = arg_none || ""
94
+ @arg_required = arg_required || ""
95
+ @arg_optional = arg_optional || ""
96
+ end
97
+
98
+
99
+ def self.to_value(str)
100
+ case str
101
+ when nil, "null", "nil" ; return nil
102
+ when "true", "yes" ; return true
103
+ when "false", "no" ; return false
104
+ when /\A\d+\z/ ; return str.to_i
105
+ when /\A\d+\.\d+\z/ ; return str.to_f
106
+ when /\/(.*)\// ; return Regexp.new($1)
107
+ when /\A'.*'\z/, /\A".*"\z/ ; return eval(str)
108
+ else ; return str
109
+ end
110
+ end
111
+
112
+
113
+ ## argv:: array of string
114
+ ## auto_convert:: if true, convert properties value to int, boolean, string, regexp, ... (default false)
115
+ def parse(argv, auto_convert=false)
116
+ options = {}
117
+ properties = {}
118
+ while argv[0] && argv[0][0] == ?-
119
+ optstr = argv.shift
120
+ optstr = optstr[1, optstr.length-1]
121
+ #
122
+ if optstr[0] == ?- ## property
123
+ unless optstr =~ /\A\-([-\w]+)(?:=(.*))?/
124
+ raise CommandOptionError.new(optstr, :invalid_property)
125
+ end
126
+ prop_name = $1; prop_value = $2
127
+ if auto_convert
128
+ key = prop_name.gsub(/-/, '_').intern
129
+ value = prop_value == nil ? true : CommandOptionParser.to_value(prop_value)
130
+ properties[key] = value
131
+ else
132
+ properties[prop_name] = prop_value
133
+ end
134
+ #
135
+ else ## options
136
+ while optstr && !optstr.empty?
137
+ optchar = optstr[0]
138
+ optstr[0,1] = ""
139
+ #puts "*** debug: optchar=#{optchar.chr}, optstr=#{optstr.inspect}"
140
+ if @arg_none.include?(optchar)
141
+ options[optchar] = true
142
+ elsif @arg_required.include?(optchar)
143
+ arg = optstr.empty? ? argv.shift : optstr
144
+ raise CommandOptionError.new(optchar.chr, :no_argument) unless arg
145
+ options[optchar] = arg
146
+ optstr = nil
147
+ elsif @arg_optional.include?(optchar)
148
+ arg = optstr.empty? ? true : optstr
149
+ options[optchar] = arg
150
+ optstr = nil
151
+ else
152
+ raise CommandOptionError.new(optchar.chr, :unknown_option)
153
+ end
154
+ end
155
+ end
156
+ #
157
+ end # end of while
158
+
159
+ return options, properties
160
+ end
161
+
162
+ end
163
+
164
+
165
+ if __FILE__ == $0
166
+ ## create parser
167
+ arg_none = "hv" # ex. -h -v
168
+ arg_required = "xf" # ex. -x suffix -f filename
169
+ arg_optional = "i" # ex. -i (or -i10)
170
+ parser = CommandOptionParser.new(arg_none, arg_required, arg_optional)
171
+
172
+ ## parse options
173
+ argv = %w[-h -v -f filename -i 10 aaa bbb]
174
+ options, properties = parser.parse(argv)
175
+ p options #=> { ?h=>true, ?v=>true, ?f=>"filename", ?i=>true }
176
+ p argv #=> ["10", "aaa", "bbb"]
177
+
178
+ ## parse options #2
179
+ argv = %w[-hvx.txt -ffilename -i10 aaa bbb]
180
+ options, properties = parser.parse(argv)
181
+ p options #=> { ?h=>true, ?v=>true, ?x=>".txt", ?f=>"filename", ?i=>"10" }
182
+ p argv #=> ["aaa", "bbb"]
183
+
184
+ ## parse properties
185
+ argv = %w[-hi --index=10 --user-name=foo --help]
186
+ options, properties = parser.parse(argv)
187
+ p options #=> {?h=>true, ?i=>true}
188
+ p properties #=> {"index"=>"10", "user-name"=>"foo", "help"=>nil}
189
+
190
+ ## parse properties with auto-convert
191
+ argv = %w[-hi --index=10 --user-name=foo --help]
192
+ options, properties = parser.parse(argv, true)
193
+ p options #=> {?h=>true, ?i=>true}
194
+ p properties #=> {:index=>10, :user_name=>foo, :help=>true}
195
+
196
+ ## -a: unknown option.
197
+ argv = %w[-abc]
198
+ begin
199
+ options, properties = parser.parse(argv)
200
+ rescue CommandOptionError => ex
201
+ $stderr.puts ex.message # -a: unknown option.
202
+ end
203
+
204
+ ## -f: argument required.
205
+ argv = %w[-f]
206
+ begin
207
+ options, properties = parser.parse(argv)
208
+ rescue CommandOptionError => ex
209
+ $stderr.puts ex.message # -f: argument required.
210
+ end
211
+
212
+ ## --@prop=10: invalid property.
213
+ argv = %w[--@prop=10]
214
+ begin
215
+ options, properties = parser.parse(argv)
216
+ rescue CommandOptionError => ex
217
+ $stderr.puts ex.message # --@prop=10: invalid property.
218
+ end
219
+
220
+ end
@@ -0,0 +1,82 @@
1
+ ###
2
+ ### $Rev: 7 $
3
+ ### $Release: 0.1.0 $
4
+ ### copyright(c) 2005 kuwata-lab all rights reserved.
5
+ ###
6
+
7
+ require 'yaml'
8
+
9
+ module YamlHelper
10
+
11
+ ##
12
+ ## expand tab character to spaces
13
+ ##
14
+ ## ex.
15
+ ## untabified_str = YAML::Helper.untabify(tabbed_str)
16
+ ##
17
+ ## input:: String or IO
18
+ ##
19
+ def self.untabify(input)
20
+ s = ''
21
+ input.each_line do |line|
22
+ s << line.gsub(/([^\t]{8})|([^\t]*)\t/n) { [$+].pack("A8") }
23
+ end
24
+ return s
25
+ end
26
+
27
+
28
+ ##
29
+ ## create a hash table from list of hash with primary key.
30
+ ##
31
+ ## ex.
32
+ ## hashlist = [
33
+ ## { "name"=>"Foo", "gender"=>"M", "age"=>20, },
34
+ ## { "name"=>"Bar", "gender"=>"F", "age"=>25, },
35
+ ## { "name"=>"Baz", "gender"=>"M", "age"=>30, },
36
+ ## ]
37
+ ## hashtable = YAML::Helper.create_hashtable(hashlist, "name")
38
+ ## p hashtable
39
+ ## # => { "Foo" => { "name"=>"Foo", "gender"=>"M", "age"=>20, },
40
+ ## "Bar" => { "name"=>"Bar", "gender"=>"F", "age"=>25, },
41
+ ## "Baz" => { "name"=>"Baz", "gender"=>"M", "age"=>30, }, }
42
+ ##
43
+ def self.create_hashtable(hashlist, primarykey, flag_duplicate_check=true)
44
+ hashtable = {}
45
+ hashlist.each do |hash|
46
+ key = hash[primarykey]
47
+ unless key
48
+ riase "primary key '#{key}' not found."
49
+ end
50
+ if flag_duplicate_check && hashtable.key?(key)
51
+ raise "primary key '#{key}' duplicated (value '#{hashtable[key]}')"
52
+ end
53
+ hashtable[key] = hash
54
+ end if hashlist
55
+ return hashtable
56
+ end
57
+
58
+
59
+ ##
60
+ ## get nested value directly.
61
+ ##
62
+ ## ex.
63
+ ## val = YAML::Helper.get_value(obj, ['aaa', 0, 'xxx'])
64
+ ##
65
+ ## This is equal to the following:
66
+ ## begin
67
+ ## val = obj['aaa'][0]['xxx']
68
+ ## rescue NameError
69
+ ## val = nil
70
+ ## end
71
+ ##
72
+ def self.get_value(obj, path)
73
+ val = obj
74
+ path.each do |key|
75
+ val = val[key]
76
+ return val unless val
77
+ end if path
78
+ return val
79
+ end
80
+
81
+ end
82
+
@@ -0,0 +1,286 @@
1
+ ###
2
+ ### $Rev: 9 $
3
+ ### $Release: 0.1.0 $
4
+ ### copyright(c) 2005 kuwata-lab all rights reserved.
5
+ ###
6
+
7
+ require 'kwalify/error-msg'
8
+ require 'kwalify/errors'
9
+ require 'kwalify/types'
10
+ require 'pp'
11
+
12
+ module Kwalify
13
+
14
+
15
+ ##
16
+ ## ex.
17
+ ## s = File.read('schema.yaml')
18
+ ## validator = Kwalify::Validator.new(YAML.load(s))
19
+ ## s = File.read('document.yaml')
20
+ ## document = YAML.load(s)
21
+ ## error_list = validator.validate(document)
22
+ ## unless error_list.empty?
23
+ ## error_list.each { |error| print error.message }
24
+ ## end
25
+ ##
26
+ class Validator
27
+ def initialize(hash, &block)
28
+ raise KwalifyError.new("Validator#initalize(arg): arg is not a hash(==#{hash.class.name}).") unless hash.is_a?(Hash)
29
+ @schema = Schema.new(hash)
30
+ @block = block
31
+ end
32
+ attr_reader :schema
33
+
34
+ def validate(obj)
35
+ @schema.validate(obj, &@block)
36
+ end
37
+
38
+ def inspect
39
+ @schema.inspect
40
+ end
41
+ end
42
+
43
+
44
+ class Schema
45
+ include Kwalify::Errors
46
+
47
+ def initialize(hash, path="", schema_table={})
48
+ schema_table[hash.__id__] = self
49
+ hash.each do |key, val|
50
+ case key
51
+ #when "id"
52
+ # @id = val
53
+ when "name"
54
+ @name = val.to_s
55
+ when "desc"
56
+ @desc = val.to_s
57
+ when "type"
58
+ @type = val
59
+ @klass = Kwalify::type_table[val.downcase]
60
+ if @klass == nil
61
+ begin
62
+ @klass = Kernel.const_get(val)
63
+ rescue NameError
64
+ raise schema_error(:type_not_found, self, path)
65
+ end
66
+ end
67
+ when "required"
68
+ unless val.is_a?(Boolean)
69
+ raise schema_error(:required_not_boolean, self, path)
70
+ else
71
+ @required = val
72
+ end
73
+ when "pattern"
74
+ pat = (val =~ /\A\/(.*)\/\z/ ? $1 : val)
75
+ begin
76
+ @pattern = Regexp.compile(pat)
77
+ rescue RegexpError => ex
78
+ raise schema_error(:regexp_error, self, path)
79
+ end
80
+ when "enum"
81
+ unless val.is_a?(Array)
82
+ raise schema_error(:enum_not_seq, self, path)
83
+ else
84
+ @enum = val
85
+ end
86
+ when "sequence"
87
+ if !val.is_a?(Array)
88
+ raise schema_error(:sequence_not_seq, self, path)
89
+ elsif val.length == 0
90
+ raise schema_error(:sequence_no_elem, self, path)
91
+ elsif val.length > 1
92
+ raise schema_error(:sequence_too_many, self, path)
93
+ else
94
+ elem = val[0]
95
+ elem ||= {}
96
+ schema = schema_table[elem.__id__]
97
+ i = 1
98
+ schema ||= Schema.new(elem, "#{path}/#{i}", schema_table)
99
+ @sequence = [ schema ]
100
+ end
101
+ when "mapping"
102
+ if !val.is_a?(Hash)
103
+ raise schema_error(:mapping_not_map, self, path)
104
+ elsif val.empty?
105
+ raise schema_error(:mapping_no_elem, self, path)
106
+ else
107
+ @mapping = {}
108
+ val.each do |key, elem|
109
+ raise schema_error(:duplicate_key, self, path, key) if @mapping.key?(key)
110
+ elem ||= {}
111
+ schema = schema_table[elem.__id__]
112
+ schema ||= Schema.new(elem, "#{path}/#{key}", schema_table)
113
+ if key == '*'
114
+ @mapping.default = schema
115
+ else
116
+ @mapping[key] = schema
117
+ end
118
+ end
119
+ end
120
+ else
121
+ raise schema_error(:unexpected_key, self, path, key)
122
+ end
123
+ end
124
+
125
+ if @type == nil
126
+ if @sequence
127
+ @type = 'seq'
128
+ elsif @mapping
129
+ @type = 'map'
130
+ else
131
+ @type = Kwalify::DEFAULT_TYPE
132
+ end
133
+ @klass = Kwalify::type_table[@type]
134
+ end
135
+
136
+ if @klass == Array
137
+ raise schema_error(:seq_has_enum, self, path) if @enum
138
+ raise schema_error(:seq_has_pattern, self, path) if @pattern
139
+ raise schema_error(:seq_has_mapping, self, path) if @mapping
140
+ elsif @klass == Hash
141
+ raise schema_error(:map_has_enum, self, path) if @enum
142
+ raise schema_error(:map_has_pattern, self, path) if @pattern
143
+ raise schema_error(:map_has_sequence, self, path) if @sequence
144
+ else
145
+ raise schema_error(:scalar_has_sequence, self, path) if @sequence
146
+ raise schema_error(:scalar_has_mapping, self, path) if @mapping
147
+ end
148
+
149
+ end
150
+
151
+ #attr_reader :id
152
+ attr_accessor :name
153
+ attr_accessor :desc
154
+ attr_accessor :enum
155
+ attr_accessor :required
156
+ attr_accessor :type
157
+ attr_accessor :klass
158
+ attr_accessor :pattern
159
+ attr_accessor :elements
160
+ #attr_accessor :constraints
161
+
162
+
163
+ def inspect()
164
+ str = ""; level = 0; done = {}
165
+ _inspect(str, level, done)
166
+ return str
167
+ end
168
+
169
+ def _inspect(str, level, done)
170
+ done[self.__id__] = true
171
+ str << " " * level << "name: #{@name}\n" if @name != nil
172
+ str << " " * level << "type: #{@type}\n" if @type != nil
173
+ str << " " * level << "klass: #{@klass.name}\n" if @klass != nil
174
+ str << " " * level << "required: #{@required}\n" if @required != nil
175
+ str << " " * level << "pattern: #{@pattern.inspect}\n" if @pattern != nil
176
+ if @enum != nil
177
+ str << " " * level << "enum:\n"
178
+ @enum.each do |item|
179
+ str << " " * (level+1) << "- #{item}\n"
180
+ end
181
+ end
182
+ @sequence.each do |schema|
183
+ if done[schema.__id__]
184
+ str << " " * (level+1) << "- ...\n"
185
+ else
186
+ str << " " * (level+1) << "- \n"
187
+ schema._inspect(str, level+2, done)
188
+ end
189
+ end if @sequence
190
+ @mapping.each do |key, schema|
191
+ if done[schema.__id__]
192
+ str << " " * (level+1) << key << ": ...\n"
193
+ else
194
+ str << " " * (level+1) << key << ":\n"
195
+ schema._inspect(str, level+2, done)
196
+ end
197
+ end if @mapping
198
+ end
199
+ protected :_inspect
200
+
201
+
202
+ def validate(obj, &block)
203
+ errors = []; path = ""; done = {}
204
+ _validate(obj, errors, path, done, &block)
205
+ return errors
206
+ end
207
+
208
+
209
+ def _validate(obj, errors=[], path="", done={}, &block)
210
+ return true if done[obj.__id__]
211
+ done[obj.__id__] = true
212
+ if @required && obj == nil
213
+ errors << validate_error(:missing_value, self, path, obj)
214
+ return
215
+ end
216
+ if @klass && obj != nil && !obj.is_a?(@klass)
217
+ errors << validate_error(:invalid_type, self, path, obj)
218
+ return
219
+ end
220
+ #
221
+ n = errors.length
222
+ if @sequence
223
+ _validate_sequence(obj, errors, path, done, &block)
224
+ elsif @mapping
225
+ _validate_mapping(obj, errors, path, done, &block)
226
+ else
227
+ _validate_scalar(obj, errors, path, done, &block)
228
+ end
229
+ return unless errors.length == n
230
+ #
231
+ yield(self, obj, errors, path) if block
232
+ end
233
+ protected :_validate
234
+
235
+
236
+ def _validate_scalar(obj, errors, path, done, &block)
237
+ assert_error("@sequence.class==#{@sequence.class.name} (expected NilClass)") if @sequence
238
+ assert_error("@mapping.class==#{@mapping.class.name} (expected NilClass)") if @mapping
239
+ return if obj == nil
240
+ if @enum && !@enum.include?(obj)
241
+ errors << validate_error(:invalid_enum, self, path, obj)
242
+ end
243
+ if @pattern && obj.to_s !~ @pattern
244
+ errors << validate_error(:invalid_pattern, self, path, obj)
245
+ end
246
+ end
247
+ private :_validate_scalar
248
+
249
+
250
+ def _validate_sequence(list, errors, path, done, &block)
251
+ assert_error("@sequence.class==#{@sequence.class.name} (expected Array)") unless @sequence.is_a?(Array)
252
+ assert_error("@sequence.length==#{@sequence.length} (expected 1)") unless @sequence.length == 1
253
+ return if list == nil
254
+ schema = @sequence[0]
255
+ i = 0
256
+ list.each do |obj|
257
+ i += 1
258
+ schema._validate(obj, errors, "#{path}/#{i}", done, &block) ## validate recursively
259
+ end
260
+ end
261
+ private :_validate_sequence
262
+
263
+
264
+ def _validate_mapping(hash, errors, path, done, &block)
265
+ assert_error("@mapping.class==#{@mapping.class.name} (expected Hash)") unless @mapping.is_a?(Hash)
266
+ return if hash == nil
267
+ @mapping.each do |key, schema|
268
+ if schema.required && !hash.key?(key)
269
+ errors << validate_error(:missing_key, schema, path, hash, key)
270
+ end
271
+ end
272
+ hash.each do |key, value|
273
+ schema = @mapping[key]
274
+ if schema
275
+ ret = schema._validate(value, errors, "#{path}/#{key}", done, &block) ## validate recursively
276
+ else
277
+ errors << validate_error(:invalid_key, schema, path, hash, key)
278
+ end
279
+ end
280
+ end
281
+ private :_validate_mapping
282
+
283
+
284
+ end
285
+ end
286
+