kwalify 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+