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.
- 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
|
+
|