kwalify 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +340 -0
- data/ChangeLog +62 -0
- data/README.txt +38 -0
- data/bin/kwalify +185 -0
- data/doc/docstyle.css +188 -0
- data/doc/users-guide.html +706 -0
- data/examples/address-book/Makefile +4 -0
- data/examples/address-book/address-book.schema.yaml +43 -0
- data/examples/address-book/address-book.yaml +36 -0
- data/examples/invoice/Makefile +3 -0
- data/examples/invoice/invoice.schema.yaml +59 -0
- data/examples/invoice/invoice.yaml +32 -0
- data/lib/kwalify.rb +10 -0
- data/lib/kwalify/error-msg.rb +41 -0
- data/lib/kwalify/errors.rb +112 -0
- data/lib/kwalify/meta-validator.rb +137 -0
- data/lib/kwalify/types.rb +70 -0
- data/lib/kwalify/util/assert-diff.rb +44 -0
- data/lib/kwalify/util/option-parser.rb +220 -0
- data/lib/kwalify/util/yaml-helper.rb +82 -0
- data/lib/kwalify/validator.rb +286 -0
- data/setup.rb +1331 -0
- data/test/test.rb +364 -0
- data/todo.txt +21 -0
- metadata +66 -0
@@ -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
|
+
|