kwalify 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/ChangeLog +6 -1
- data/README.txt +1 -1
- data/bin/kwalify +1 -1
- data/doc/users-guide.html +328 -62
- data/examples/address-book/Makefile +2 -2
- data/examples/address-book/address-book.schema.yaml +1 -1
- data/examples/invoice/Makefile +2 -2
- data/examples/invoice/invoice.schema.yaml +1 -1
- data/examples/tapkit/Makefile +2 -2
- data/examples/tapkit/tapkit.schema.yaml +1 -1
- data/lib/kwalify.rb +4 -3
- data/lib/kwalify/errors.rb +13 -3
- data/lib/kwalify/main-program.rb +60 -21
- data/lib/kwalify/messages.rb +1 -1
- data/lib/kwalify/meta-validator.rb +7 -7
- data/lib/kwalify/parser.rb +759 -0
- data/lib/kwalify/rule.rb +13 -7
- data/lib/kwalify/types.rb +12 -11
- data/lib/kwalify/util/assert-diff.rb +1 -1
- data/lib/kwalify/util/option-parser.rb +1 -1
- data/lib/kwalify/util/yaml-helper.rb +1 -1
- data/lib/kwalify/validator.rb +55 -47
- data/test/test-metavalidator.rb +16 -11
- data/test/test-parser.rb +1276 -0
- data/test/test-validator.rb +172 -87
- data/test/test.rb +13 -16
- data/todo.txt +1 -1
- metadata +57 -50
- data/test/test-metavalidator.rb.error +0 -490
data/examples/invoice/Makefile
CHANGED
data/examples/tapkit/Makefile
CHANGED
data/lib/kwalify.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
###
|
2
|
-
### $Rev:
|
3
|
-
### $Release: 0.
|
2
|
+
### $Rev: 26 $
|
3
|
+
### $Release: 0.4.0 $
|
4
4
|
### copyright(c) 2005 kuwata-lab all rights reserved.
|
5
5
|
###
|
6
6
|
|
7
7
|
|
8
8
|
module Kwalify
|
9
9
|
|
10
|
-
RELEASE = ("$Release: 0.
|
10
|
+
RELEASE = ("$Release: 0.4.0 $" =~ /[.\d]+/) && $&
|
11
11
|
|
12
12
|
end
|
13
13
|
|
@@ -17,3 +17,4 @@ require 'kwalify/errors'
|
|
17
17
|
require 'kwalify/rule'
|
18
18
|
require 'kwalify/validator'
|
19
19
|
require 'kwalify/meta-validator'
|
20
|
+
require 'kwalify/parser'
|
data/lib/kwalify/errors.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
###
|
2
|
-
### $Rev:
|
3
|
-
### $Release: 0.
|
2
|
+
### $Rev: 27 $
|
3
|
+
### $Release: 0.4.0 $
|
4
4
|
### copyright(c) 2005 kuwata-lab all rights reserved.
|
5
5
|
###
|
6
6
|
|
@@ -28,6 +28,7 @@ module Kwalify
|
|
28
28
|
@value = value
|
29
29
|
end
|
30
30
|
attr_reader :error_symbol, :rule, :path, :value
|
31
|
+
attr_accessor :linenum
|
31
32
|
|
32
33
|
def path
|
33
34
|
return @path == '' ? "/" : @path
|
@@ -59,6 +60,15 @@ module Kwalify
|
|
59
60
|
end
|
60
61
|
|
61
62
|
|
63
|
+
class ParseError < KwalifyError
|
64
|
+
def initialize(msg, linenum)
|
65
|
+
super("line #{linenum}: #{msg}")
|
66
|
+
@linenum = linenum
|
67
|
+
end
|
68
|
+
attr_accessor :linenum
|
69
|
+
end
|
70
|
+
|
71
|
+
|
62
72
|
module ErrorHelper
|
63
73
|
|
64
74
|
def assert_error(message="")
|
@@ -77,7 +87,7 @@ module Kwalify
|
|
77
87
|
msg = Kwalify.msg(error_symbol)
|
78
88
|
assert_error("error_symbol=#{error_symbol.inspect}") unless msg
|
79
89
|
msg = msg % args if args
|
80
|
-
msg = "'#{val.to_s.gsub(/\n/, '\n')}': #{msg}" if val != nil && Kwalify.
|
90
|
+
msg = "'#{val.to_s.gsub(/\n/, '\n')}': #{msg}" if val != nil && Kwalify.scalar?(val)
|
81
91
|
return error_klass.new(msg, path, val, rule, error_symbol)
|
82
92
|
end
|
83
93
|
|
data/lib/kwalify/main-program.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
###
|
2
|
-
### $Rev:
|
3
|
-
### $Release: 0.
|
2
|
+
### $Rev: 26 $
|
3
|
+
### $Release: 0.4.0 $
|
4
4
|
### copyright(c) 2005 kuwata-lab all rights reserved.
|
5
5
|
###
|
6
6
|
|
@@ -62,6 +62,19 @@ module Kwalify
|
|
62
62
|
private
|
63
63
|
|
64
64
|
|
65
|
+
def _load_yaml(str)
|
66
|
+
str = YamlHelper.untabify(tr) if @options[:untabify]
|
67
|
+
if @options[:linenum]
|
68
|
+
@parser = Kwalify::Parser.new(str)
|
69
|
+
doc = @parser.parse()
|
70
|
+
else
|
71
|
+
doc = YAML.load(str)
|
72
|
+
doc = doc.value if doc.is_a?(YAML::Syck::DomainType)
|
73
|
+
end
|
74
|
+
return doc
|
75
|
+
end
|
76
|
+
|
77
|
+
|
65
78
|
def _meta_validate(filenames)
|
66
79
|
filenames = [ nil ] if filenames.empty?
|
67
80
|
meta_validator = @options[:meta2] ? nil : Kwalify.meta_validator()
|
@@ -76,9 +89,7 @@ module Kwalify
|
|
76
89
|
|
77
90
|
def __meta_validate(meta_validator, filename=nil)
|
78
91
|
str = filename ? File.open(filename) { |f| f.read() } : $stdin.read()
|
79
|
-
|
80
|
-
schema = YAML.load(str)
|
81
|
-
schema = schema.value if schema.is_a?(YAML::Syck::DomainType)
|
92
|
+
schema = _load_yaml(str)
|
82
93
|
filename ||= "(stdin)"
|
83
94
|
if !schema || schema.empty?
|
84
95
|
#* key=:meta_schema_empty msg="%s: empty.\n"
|
@@ -101,7 +112,25 @@ module Kwalify
|
|
101
112
|
else
|
102
113
|
#* key=:meta_validation_invalid msg="%s: NG!\n"
|
103
114
|
s << (Kwalify.msg(:meta_validation_invalid) % filename)
|
104
|
-
|
115
|
+
s << _errors_to_str(errors)
|
116
|
+
end
|
117
|
+
return s
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
def _errors_to_str(errors)
|
122
|
+
s = ''
|
123
|
+
flag_linenum = @options[:linenum]
|
124
|
+
if flag_linenum
|
125
|
+
@parser.set_error_linenums(errors)
|
126
|
+
errors = errors.sort { |e1, e2| e1.linenum <=> e2.linenum }
|
127
|
+
end
|
128
|
+
errors.each do |error|
|
129
|
+
if flag_linenum
|
130
|
+
s << " - (line #{error.linenum}) [#{error.path}] #{error.message}\n"
|
131
|
+
else
|
132
|
+
s << " - [#{error.path}] #{error.message}\n"
|
133
|
+
end
|
105
134
|
end
|
106
135
|
return s
|
107
136
|
end
|
@@ -110,9 +139,7 @@ module Kwalify
|
|
110
139
|
def _validate(filenames)
|
111
140
|
schema_filename = @options[:schema]
|
112
141
|
str = File.open(schema_filename) { |f| f.read() }
|
113
|
-
|
114
|
-
schema = YAML.load(str)
|
115
|
-
schema = schema.value if schema.is_a?(YAML::Syck::DomainType)
|
142
|
+
schema = _load_yaml(str)
|
116
143
|
validator = Kwalify::Validator.new(schema)
|
117
144
|
#
|
118
145
|
filenames = [ nil ] if filenames.empty?
|
@@ -126,14 +153,11 @@ module Kwalify
|
|
126
153
|
|
127
154
|
def __validate(validator, filename=nil)
|
128
155
|
str = filename ? File.open(filename) { |f| f.read() } : $stdin.read()
|
129
|
-
str = YamlHelper.untabify(str) if @options[:untabify]
|
130
|
-
stream = YAML::load_stream(str)
|
131
156
|
filename ||= "(stdin)"
|
132
|
-
i = -1 # or 0? *index*
|
133
157
|
s = ''
|
134
|
-
|
158
|
+
i = -1
|
159
|
+
_each_yaml_documents(str) do |document|
|
135
160
|
i += 1
|
136
|
-
document = document.value if document.is_a?(YAML::Syck::DomainType)
|
137
161
|
if !document || document.empty?
|
138
162
|
#* key=:schema_empty msg="%s#%d: empty.\n"
|
139
163
|
s << (Kwalify.msg(:schema_empty) % i)
|
@@ -146,19 +170,32 @@ module Kwalify
|
|
146
170
|
else
|
147
171
|
#* key=:validation_invalid msg="%s#%d: INVALID\n"
|
148
172
|
s << (Kwalify.msg(:validation_invalid) % [filename, i])
|
149
|
-
|
173
|
+
s << _errors_to_str(errors)
|
150
174
|
end
|
151
175
|
end
|
152
176
|
return s
|
153
177
|
end
|
154
178
|
|
155
179
|
|
180
|
+
def _each_yaml_documents(str, &block)
|
181
|
+
str = YamlHelper.untabify(str) if @options[:untabify]
|
182
|
+
if @options[:linenum]
|
183
|
+
@parser = Kwalify::Parser.new(str)
|
184
|
+
doc = @parser.parse()
|
185
|
+
yield(doc)
|
186
|
+
else
|
187
|
+
YAML::load_documents(str) do |doc|
|
188
|
+
doc = doc.value if doc.is_a?(YAML::Syck::DomainType)
|
189
|
+
yield(doc)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
|
156
195
|
def _inspect_schema(schema_filename)
|
157
196
|
filename = schema_filename
|
158
197
|
str = filename ? File.open(filename) { |f| f.read() } : $stdin.read()
|
159
|
-
|
160
|
-
schema = YAML.load(str)
|
161
|
-
schema = schema.value if schema.is_a?(YAML::Syck::DomainType)
|
198
|
+
schema = _load_yaml(str)
|
162
199
|
return nil if !schema || schema.empty?
|
163
200
|
validator = Kwalify::Validator.new(schema) # error raised when schema is wrong
|
164
201
|
s = validator._inspect()
|
@@ -169,14 +206,15 @@ module Kwalify
|
|
169
206
|
|
170
207
|
def _usage()
|
171
208
|
s = ''
|
172
|
-
s << "Usage1: #{@command} [-
|
173
|
-
s << "Usage2: #{@command} [-
|
209
|
+
s << "Usage1: #{@command} [-hvstl] -f schema.yaml document.yaml [document2.yaml ...]\n"
|
210
|
+
s << "Usage2: #{@command} [-hvstl] -m schema.yaml [schema2.yaml ...]\n"
|
174
211
|
s << " -h, --help : help\n"
|
175
212
|
s << " -v : version\n"
|
176
213
|
s << " -s : silent\n"
|
177
214
|
s << " -f schema.yaml : schema definition file\n"
|
178
215
|
s << " -m : meta-validation mode\n"
|
179
216
|
s << " -t : expand tab character automatically\n"
|
217
|
+
s << " -l : show linenumber when errored (experimental)\n"
|
180
218
|
return s
|
181
219
|
end
|
182
220
|
|
@@ -187,7 +225,7 @@ module Kwalify
|
|
187
225
|
|
188
226
|
|
189
227
|
def _parse_argv(argv)
|
190
|
-
opt_parser = CommandOptionParser.new("
|
228
|
+
opt_parser = CommandOptionParser.new("hvstlmMD", "f")
|
191
229
|
#opts, props = opt_parser.parse(argv)
|
192
230
|
#properties = {}
|
193
231
|
#props.each do |pname, pvalue|
|
@@ -202,6 +240,7 @@ module Kwalify
|
|
202
240
|
options[:version] = opts[?v] || properties[:version]
|
203
241
|
options[:silent] = opts[?s]
|
204
242
|
options[:schema] = opts[?f]
|
243
|
+
options[:linenum] = opts[?l]
|
205
244
|
options[:meta] = opts[?m]
|
206
245
|
options[:meta2] = opts[?M]
|
207
246
|
options[:untabify] = opts[?t]
|
data/lib/kwalify/messages.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
###
|
2
|
-
### $Rev:
|
3
|
-
### $Release: 0.
|
2
|
+
### $Rev: 35 $
|
3
|
+
### $Release: 0.4.0 $
|
4
4
|
### copyright(c) 2005 kuwata-lab all rights reserved.
|
5
5
|
###
|
6
6
|
|
@@ -98,7 +98,7 @@ mapping: &main-rule
|
|
98
98
|
name: MAPPING
|
99
99
|
type: map
|
100
100
|
mapping:
|
101
|
-
|
101
|
+
=:
|
102
102
|
type: map
|
103
103
|
mapping: *main-rule
|
104
104
|
name: MAIN
|
@@ -110,7 +110,7 @@ END
|
|
110
110
|
## ex.
|
111
111
|
## meta_validator = Kwalify.meta_validator()
|
112
112
|
## schema = File.load_file('schema.yaml')
|
113
|
-
## errors
|
113
|
+
## errors = meta_validator.validate(schema)
|
114
114
|
## if !errors.empty?
|
115
115
|
## errors.each do |error|
|
116
116
|
## puts "[#{error.path}] #{error.message}"
|
@@ -269,9 +269,9 @@ END
|
|
269
269
|
if hash.key?('mapping')
|
270
270
|
val = hash['mapping']
|
271
271
|
if val != nil && !val.is_a?(Hash)
|
272
|
-
errors << validate_error(:mapping_notmap,
|
273
|
-
elsif val == nil || val.empty?
|
274
|
-
errors << validate_error(:mapping_noelem,
|
272
|
+
errors << validate_error(:mapping_notmap, rule, "#{path}/mapping", val)
|
273
|
+
elsif val == nil || (val.empty? && !val.default)
|
274
|
+
errors << validate_error(:mapping_noelem, rule, "#{path}/mapping", val)
|
275
275
|
end
|
276
276
|
end
|
277
277
|
#
|
@@ -0,0 +1,759 @@
|
|
1
|
+
###
|
2
|
+
### $Rev: 35 $
|
3
|
+
### $Release: 0.4.0 $
|
4
|
+
### copyright(c) 2005 kuwata-lab all rights reserved.
|
5
|
+
###
|
6
|
+
|
7
|
+
require 'kwalify/messages'
|
8
|
+
require 'kwalify/errors'
|
9
|
+
require 'kwalify/types'
|
10
|
+
|
11
|
+
require 'date'
|
12
|
+
|
13
|
+
|
14
|
+
module Kwalify
|
15
|
+
|
16
|
+
|
17
|
+
##
|
18
|
+
## ex.
|
19
|
+
## str = ARGF.read()
|
20
|
+
## parser = Kwalify::PlainParser.new(str)
|
21
|
+
## doc = parser.parse()
|
22
|
+
## p doc
|
23
|
+
##
|
24
|
+
class PlainParser
|
25
|
+
|
26
|
+
class Alias
|
27
|
+
def initialize(label, linenum)
|
28
|
+
@label = label
|
29
|
+
@linenum = linenum
|
30
|
+
end
|
31
|
+
attr_reader :label, :linenum
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def initialize(yaml_str)
|
36
|
+
@lines = yaml_str.to_a()
|
37
|
+
@line = nil
|
38
|
+
@linenum = 0
|
39
|
+
@anchors = {}
|
40
|
+
@aliases = {}
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def parse()
|
45
|
+
data = parse_child(0)
|
46
|
+
if data == nil && @end_flag == '---'
|
47
|
+
data = parse_child(0)
|
48
|
+
end
|
49
|
+
resolve_aliases(data) unless @aliases.empty?
|
50
|
+
return data
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
|
57
|
+
def create_sequence(linenum=nil)
|
58
|
+
return []
|
59
|
+
end
|
60
|
+
|
61
|
+
def add_to_seq(seq, value, linenum)
|
62
|
+
seq << value
|
63
|
+
end
|
64
|
+
|
65
|
+
def set_seq_at(seq, i, value, linenum)
|
66
|
+
seq[i] = value
|
67
|
+
end
|
68
|
+
|
69
|
+
def create_mapping(linenum=nil)
|
70
|
+
return {}
|
71
|
+
end
|
72
|
+
|
73
|
+
def add_to_map(map, key, value, linenum)
|
74
|
+
map[key] = value
|
75
|
+
end
|
76
|
+
|
77
|
+
def set_map_with(map, key, value, linenum)
|
78
|
+
map[key] = value
|
79
|
+
end
|
80
|
+
|
81
|
+
def set_default(map, value, linenum)
|
82
|
+
map.value = value
|
83
|
+
end
|
84
|
+
|
85
|
+
def merge_map(map, map2, linenum)
|
86
|
+
map2.each do |key, val|
|
87
|
+
map[key] = value unless map.key?(key)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def create_scalar(value, linenum=nil)
|
92
|
+
return value
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
def current_line
|
97
|
+
return @line
|
98
|
+
end
|
99
|
+
|
100
|
+
def current_linenum
|
101
|
+
return @linenum
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
|
108
|
+
def getline
|
109
|
+
line = _getline()
|
110
|
+
line = _getline() while line && line =~ /^\s*($|\#)/
|
111
|
+
return line
|
112
|
+
end
|
113
|
+
|
114
|
+
def _getline
|
115
|
+
@line = @lines[@linenum]
|
116
|
+
@linenum += 1
|
117
|
+
case @line
|
118
|
+
when nil ; @end_flag = 'EOF'
|
119
|
+
when /^\.\.\.$/ ; @end_flag = '...'; @line = nil
|
120
|
+
when /^---(\s+.*)?$/ ; @end_flag = '---'; @line = nil
|
121
|
+
else ; @end_flag = nil
|
122
|
+
end
|
123
|
+
return @line
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
def reset_sbuf(str)
|
128
|
+
@sbuf = str[-1] == ?\n ? str : str + "\n"
|
129
|
+
@index = -1
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
def _getchar
|
134
|
+
@index += 1
|
135
|
+
ch = @sbuf[@index]
|
136
|
+
while ch == nil
|
137
|
+
break if (line = getline()) == nil
|
138
|
+
reset_sbuf(line)
|
139
|
+
@index += 1
|
140
|
+
ch = @sbuf[@index]
|
141
|
+
end
|
142
|
+
return ch
|
143
|
+
end
|
144
|
+
|
145
|
+
def getchar
|
146
|
+
ch = _getchar()
|
147
|
+
ch = _getchar() while ch && white?(ch)
|
148
|
+
return ch
|
149
|
+
end
|
150
|
+
|
151
|
+
def getchar_or_nl
|
152
|
+
ch = _getchar()
|
153
|
+
ch = _getchar() while ch && white?(ch) && ch != ?\n
|
154
|
+
return ch
|
155
|
+
end
|
156
|
+
|
157
|
+
def current_char
|
158
|
+
return @sbuf[@index]
|
159
|
+
end
|
160
|
+
|
161
|
+
|
162
|
+
def syntax_error(msg, linenum=@linenum)
|
163
|
+
return Kwalify::ParseError.new(msg, linenum)
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
def parse_child(column)
|
168
|
+
line = getline()
|
169
|
+
return create_scalar(nil) if !line
|
170
|
+
line =~ /^( *)(.*)/
|
171
|
+
indent = $1.length
|
172
|
+
return create_scalar(nil) if indent < column
|
173
|
+
value = $2
|
174
|
+
return parse_value(column, value, indent)
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
def parse_value(column, value, value_start_column)
|
179
|
+
case value
|
180
|
+
when /^-( |$)/
|
181
|
+
data = parse_sequence(value_start_column, value)
|
182
|
+
when /^(:?[-.\w]+|'.*?'|".*?"|=|<<) *:( |$)/
|
183
|
+
#when /^:?["']?[-.\w]+["']? *:( |$)/ #'
|
184
|
+
data = parse_mapping(value_start_column, value)
|
185
|
+
when /^\[/, /^\{/
|
186
|
+
data = parse_flowstyle(column, value)
|
187
|
+
when /^\&[-\w]+( |$)/
|
188
|
+
data = parse_anchor(column, value)
|
189
|
+
when /^\*[-\w]+( |$)/
|
190
|
+
data = parse_alias(column, value)
|
191
|
+
when /^[|>]/
|
192
|
+
data = parse_block_text(column, value)
|
193
|
+
when /^!/
|
194
|
+
data = parse_tag(column, value)
|
195
|
+
when /^\#/
|
196
|
+
data = parse_child(column)
|
197
|
+
else
|
198
|
+
data = parse_scalar(column, value)
|
199
|
+
end
|
200
|
+
return data
|
201
|
+
end
|
202
|
+
|
203
|
+
def white?(ch)
|
204
|
+
return ch == ?\ || ch == ?\t || ch == ?\n || ch == ?\r
|
205
|
+
end
|
206
|
+
|
207
|
+
|
208
|
+
##
|
209
|
+
## flowstyle ::= flow_seq | flow_map | flow_scalar
|
210
|
+
##
|
211
|
+
## flow_seq ::= '[' [ flow_seq_item { ',' sp flow_seq_item } ] ']'
|
212
|
+
## flow_seq_item ::= flowstyle
|
213
|
+
##
|
214
|
+
## flow_map ::= '{' [ flow_map_item { ',' sp flow_map_item } ] '}'
|
215
|
+
## flow_map_item ::= flowstyle ':' sp flowstyle
|
216
|
+
##
|
217
|
+
## flow_scalar ::= string | number | boolean | symbol | date
|
218
|
+
##
|
219
|
+
|
220
|
+
def parse_flowstyle(column, value)
|
221
|
+
reset_sbuf(value)
|
222
|
+
getchar()
|
223
|
+
data = parse_flow(0)
|
224
|
+
ch = current_char
|
225
|
+
assert ch == ?] || ch == ?}
|
226
|
+
ch = getchar_or_nl()
|
227
|
+
unless ch == ?\n || ch == ?# || ch == nil
|
228
|
+
raise syntax_error("flow style sequence is closed but got '#{ch.chr}'.")
|
229
|
+
end
|
230
|
+
getline() if ch != nil
|
231
|
+
return data
|
232
|
+
end
|
233
|
+
|
234
|
+
def parse_flow(depth)
|
235
|
+
ch = current_char()
|
236
|
+
#ch = getchar()
|
237
|
+
if ch == nil
|
238
|
+
rase syntax_error("found EOF when parsing flow style.")
|
239
|
+
end
|
240
|
+
if ch == ?[
|
241
|
+
data = parse_flow_seq(depth)
|
242
|
+
elsif ch == ?{
|
243
|
+
data = parse_flow_map(depth)
|
244
|
+
else
|
245
|
+
data = parse_flow_scalar(depth)
|
246
|
+
end
|
247
|
+
return data
|
248
|
+
end
|
249
|
+
|
250
|
+
def parse_flow_seq(depth)
|
251
|
+
assert current_char() == ?[
|
252
|
+
seq = create_sequence() # []
|
253
|
+
ch = getchar()
|
254
|
+
if ch != ?}
|
255
|
+
linenum = current_linenum()
|
256
|
+
#seq << parse_flow_seq_item(depth + 1)
|
257
|
+
add_to_seq(seq, parse_flow_seq_item(depth + 1), linenum)
|
258
|
+
while (ch = current_char()) == ?,
|
259
|
+
ch = getchar()
|
260
|
+
if ch == ?]
|
261
|
+
raise syntax_error("sequence item required (or last comma is extra).")
|
262
|
+
end
|
263
|
+
#break if ch == ?]
|
264
|
+
linenum = current_linenum()
|
265
|
+
#seq << parse_flow_seq_item(depth + 1)
|
266
|
+
add_to_seq(seq, parse_flow_seq_item(depth + 1), linenum)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
unless current_char() == ?]
|
270
|
+
raise syntax_error("flow style sequence requires ']'.")
|
271
|
+
end
|
272
|
+
getchar() if depth > 0
|
273
|
+
return seq
|
274
|
+
end
|
275
|
+
|
276
|
+
def parse_flow_seq_item(depth)
|
277
|
+
return parse_flow(depth)
|
278
|
+
end
|
279
|
+
|
280
|
+
def parse_flow_map(depth)
|
281
|
+
assert current_char() == ?{ #}
|
282
|
+
map = create_mapping() # {}
|
283
|
+
ch = getchar()
|
284
|
+
if ch != ?}
|
285
|
+
linenum = current_linenum()
|
286
|
+
key, value = parse_flow_map_item(depth + 1)
|
287
|
+
#map[key] = value
|
288
|
+
add_to_map(map, key, value, linenum)
|
289
|
+
while (ch = current_char()) == ?,
|
290
|
+
ch = getchar()
|
291
|
+
if ch == ?}
|
292
|
+
raise syntax_error("mapping item required (or last comma is extra).")
|
293
|
+
end
|
294
|
+
#break if ch == ?}
|
295
|
+
linenum = current_linenum()
|
296
|
+
key, value = parse_flow_map_item(depth + 1)
|
297
|
+
#map[key] = value
|
298
|
+
add_to_map(map, key, value, linenum)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
unless current_char() == ?}
|
302
|
+
raise syntax_error("flow style mapping requires '}'.")
|
303
|
+
end
|
304
|
+
getchar() if depth > 0
|
305
|
+
return map
|
306
|
+
end
|
307
|
+
|
308
|
+
def parse_flow_map_item(depth)
|
309
|
+
key = parse_flow(depth)
|
310
|
+
unless (ch = current_char()) == ?:
|
311
|
+
s = ch ? "'#{ch.chr}'" : "EOF"
|
312
|
+
raise syntax_error("':' expected but got #{s}.")
|
313
|
+
end
|
314
|
+
getchar()
|
315
|
+
value = parse_flow(depth)
|
316
|
+
return key, value
|
317
|
+
end
|
318
|
+
|
319
|
+
def parse_flow_scalar(depth)
|
320
|
+
case ch = current_char()
|
321
|
+
when ?", ?' #"
|
322
|
+
endch = ch
|
323
|
+
s = ''
|
324
|
+
while (ch = _getchar()) != nil && ch != endch
|
325
|
+
s << ch.chr
|
326
|
+
end
|
327
|
+
getchar()
|
328
|
+
scalar = s
|
329
|
+
else
|
330
|
+
s = ch.chr
|
331
|
+
while (ch = _getchar()) != nil && ch != ?: && ch != ?, && ch != ?] && ch != ?}
|
332
|
+
s << ch.chr
|
333
|
+
end
|
334
|
+
scalar = to_scalar(s.strip)
|
335
|
+
end
|
336
|
+
return create_scalar(scalar)
|
337
|
+
end
|
338
|
+
|
339
|
+
|
340
|
+
def parse_tag(column, value)
|
341
|
+
assert value =~ /^!\S+/
|
342
|
+
value =~ /^!(\S+)((\s+)(.*))?$/
|
343
|
+
tag = $1
|
344
|
+
space = $3
|
345
|
+
value2 = $4
|
346
|
+
if value2 && !value2.empty?
|
347
|
+
value_start_column = column + 1 + tag.length + space.length
|
348
|
+
data = parse_value(column, value2, value_start_column)
|
349
|
+
else
|
350
|
+
data = parse_child(column)
|
351
|
+
end
|
352
|
+
return data
|
353
|
+
end
|
354
|
+
|
355
|
+
|
356
|
+
def parse_anchor(column, value)
|
357
|
+
assert value =~ /^\&([-\w]+)(( *)(.*))?$/
|
358
|
+
label = $1
|
359
|
+
space = $3
|
360
|
+
value2 = $4
|
361
|
+
if value2 && !value2.empty?
|
362
|
+
#column2 = column + 1 + label.length + space.length
|
363
|
+
#data = parse_value(column2, value2)
|
364
|
+
value_start_column = column + 1 + label.length + space.length
|
365
|
+
data = parse_value(column, value2, value_start_column)
|
366
|
+
else
|
367
|
+
#column2 = column + 1
|
368
|
+
#data = parse_child(column2)
|
369
|
+
data = parse_child(column)
|
370
|
+
end
|
371
|
+
register_anchor(label, data)
|
372
|
+
return data
|
373
|
+
end
|
374
|
+
|
375
|
+
def register_anchor(label, data)
|
376
|
+
if @anchors[label]
|
377
|
+
raise syntax_error("anchor '#{label}' is already used.")
|
378
|
+
end
|
379
|
+
@anchors[label] = data
|
380
|
+
end
|
381
|
+
|
382
|
+
def parse_alias(column, value)
|
383
|
+
assert value =~ /^\*([-\w]+)(( *)(.*))?$/
|
384
|
+
label = $1
|
385
|
+
space = $3
|
386
|
+
value2 = $4
|
387
|
+
if value2 && !value2.empty? && value2[0] != ?\#
|
388
|
+
raise syntax_error("alias cannot take any data.")
|
389
|
+
end
|
390
|
+
data = @anchors[label]
|
391
|
+
unless data
|
392
|
+
data = register_alias(label)
|
393
|
+
#raise syntax_error("anchor '#{label}' not found (cannot refer to backward or child anchor).")
|
394
|
+
end
|
395
|
+
getline()
|
396
|
+
return data
|
397
|
+
end
|
398
|
+
|
399
|
+
def register_alias(label)
|
400
|
+
@aliases[label] ||= 0
|
401
|
+
@aliases[label] += 1
|
402
|
+
return Alias.new(label, @linenum)
|
403
|
+
end
|
404
|
+
|
405
|
+
|
406
|
+
def resolve_aliases(data)
|
407
|
+
@resolved ||= {}
|
408
|
+
return if @resolved[data.__id__]
|
409
|
+
@resolved[data.__id__] = data
|
410
|
+
case data
|
411
|
+
when Array
|
412
|
+
seq = data
|
413
|
+
seq.each_with_index do |val, i|
|
414
|
+
if val.is_a?(Alias)
|
415
|
+
anchor = val
|
416
|
+
if @anchors.key?(anchor.label)
|
417
|
+
#seq[i] = @anchors[anchor.label]
|
418
|
+
set_seq_at(seq, i, @anchors[anchor.label], anchor.linenum)
|
419
|
+
else
|
420
|
+
raise syntax_error("anchor '#{val.label}' not found", val.linenum)
|
421
|
+
end
|
422
|
+
elsif val.is_a?(Array) || val.is_a?(Hash)
|
423
|
+
resolve_aliases(val)
|
424
|
+
end
|
425
|
+
end
|
426
|
+
when Hash
|
427
|
+
map = data
|
428
|
+
map.each do |key, val|
|
429
|
+
if val.is_a?(Alias)
|
430
|
+
if @anchors.key?(val.label)
|
431
|
+
anchor = val
|
432
|
+
#map[key] = @anchors[anchor.label]
|
433
|
+
set_map_with(map, key, @anchors[anchor.label], anchor.linenum)
|
434
|
+
else
|
435
|
+
raise syntax_error("anchor '#{val.label}' not found", val.linenum)
|
436
|
+
end
|
437
|
+
elsif val.is_a?(Array) || val.is_a?(Hash)
|
438
|
+
resolve_aliases(val)
|
439
|
+
end
|
440
|
+
end
|
441
|
+
else
|
442
|
+
assert !data.is_a?(Alias)
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
|
447
|
+
def parse_block_text(column, value)
|
448
|
+
assert value =~ /^[>|\|]/
|
449
|
+
value =~ /^([>|\|])([-+]?)\s*(.*)$/
|
450
|
+
char = $1
|
451
|
+
indicator = $2
|
452
|
+
sep = char == "|" ? "\n" : " "
|
453
|
+
#text = $3.empty? ? '' : $3 + sep
|
454
|
+
text = $3
|
455
|
+
s = ''
|
456
|
+
empty = ''
|
457
|
+
min_indent = -1
|
458
|
+
while line = _getline()
|
459
|
+
line =~ /^( *)(.*)/
|
460
|
+
indent = $1.length
|
461
|
+
if $2.empty?
|
462
|
+
empty << "\n"
|
463
|
+
elsif indent < column
|
464
|
+
break
|
465
|
+
else
|
466
|
+
min_indent = indent if min_indent < 0 || min_indent > indent
|
467
|
+
s << empty << line
|
468
|
+
empty = ''
|
469
|
+
end
|
470
|
+
end
|
471
|
+
s << empty if indicator == '+'
|
472
|
+
s[-1] = "" if indicator == '-'
|
473
|
+
if min_indent > 0
|
474
|
+
sp = ' ' * min_indent
|
475
|
+
s.gsub!(/^#{sp}/, '')
|
476
|
+
end
|
477
|
+
if char == '>'
|
478
|
+
s.gsub!(/([^\n])\n([^\n])/, '\1 \2')
|
479
|
+
s.gsub!(/\n(\n+)/, '\1')
|
480
|
+
end
|
481
|
+
getline() if current_line() =~ /^\s*\#/
|
482
|
+
return create_scalar(text + s)
|
483
|
+
end
|
484
|
+
|
485
|
+
|
486
|
+
def parse_sequence(column, value)
|
487
|
+
assert value =~ /^-(( +)(.*))?$/
|
488
|
+
seq = create_sequence() # []
|
489
|
+
while true
|
490
|
+
unless value =~ /^-(( +)(.*))?$/
|
491
|
+
raise syntax_error("sequence item is expected.")
|
492
|
+
end
|
493
|
+
value2 = $3
|
494
|
+
space = $2
|
495
|
+
column2 = column + 1
|
496
|
+
linenum = current_linenum()
|
497
|
+
#
|
498
|
+
if !value2 || value2.empty?
|
499
|
+
elem = parse_child(column2)
|
500
|
+
else
|
501
|
+
value_start_column = column2 + space.length
|
502
|
+
elem = parse_value(column2, value2, value_start_column)
|
503
|
+
end
|
504
|
+
add_to_seq(seq, elem, linenum) #seq << elem
|
505
|
+
#
|
506
|
+
line = current_line()
|
507
|
+
break unless line
|
508
|
+
line =~ /^( *)(.*)/
|
509
|
+
indent = $1.length
|
510
|
+
if indent < column
|
511
|
+
break
|
512
|
+
elsif indent > column
|
513
|
+
raise syntax_error("invalid indent of sequence.")
|
514
|
+
end
|
515
|
+
value = $2
|
516
|
+
end
|
517
|
+
return seq
|
518
|
+
end
|
519
|
+
|
520
|
+
|
521
|
+
def parse_mapping(column, value)
|
522
|
+
#assert value =~ /^(:?["']?[-.\w]+["']? *):(( +)(.*))?$/ #'
|
523
|
+
assert value =~ /^((?::?[-.\w]+|'.*?'|".*?"|=|<<) *):(( +)(.*))?$/
|
524
|
+
map = create_mapping() # {}
|
525
|
+
while true
|
526
|
+
#unless value =~ /^(:?["']?[-.\w]+["']? *):(( +)(.*))?$/ #'
|
527
|
+
unless value =~ /^((?::?[-.\w]+|'.*?'|".*?"|=|<<) *):(( +)(.*))?$/
|
528
|
+
raise syntax_error("mapping item is expected.")
|
529
|
+
end
|
530
|
+
v = $1.strip
|
531
|
+
key = to_scalar(v)
|
532
|
+
value2 = $4
|
533
|
+
column2 = column + 1
|
534
|
+
linenum = current_linenum()
|
535
|
+
#
|
536
|
+
if !value2 || value2.empty?
|
537
|
+
elem = parse_child(column2)
|
538
|
+
else
|
539
|
+
value_start_column = column2 + $1.length + $3.length
|
540
|
+
elem = parse_value(column2, value2, value_start_column)
|
541
|
+
end
|
542
|
+
case v
|
543
|
+
when '='
|
544
|
+
set_default(map, elem, linenum)
|
545
|
+
when '<<'
|
546
|
+
merge_map(map, elem, linenum)
|
547
|
+
else
|
548
|
+
add_to_map(map, key, elem, linenum) # map[key] = elem
|
549
|
+
end
|
550
|
+
#
|
551
|
+
line = current_line()
|
552
|
+
break unless line
|
553
|
+
line =~ /^( *)(.*)/
|
554
|
+
indent = $1.length
|
555
|
+
if indent < column
|
556
|
+
break
|
557
|
+
elsif indent > column
|
558
|
+
raise syntax_error("invalid indent of mapping.")
|
559
|
+
end
|
560
|
+
value = $2
|
561
|
+
end
|
562
|
+
return map
|
563
|
+
end
|
564
|
+
|
565
|
+
|
566
|
+
def parse_scalar(indent, value)
|
567
|
+
data = create_scalar(to_scalar(value))
|
568
|
+
getline()
|
569
|
+
return data
|
570
|
+
end
|
571
|
+
|
572
|
+
|
573
|
+
def to_scalar(str)
|
574
|
+
case str
|
575
|
+
when /^"(.*)"([ \t]*\#.*$)?/ ; return $1
|
576
|
+
when /^'(.*)'([ \t]*\#.*$)?/ ; return $1
|
577
|
+
when /^(.*\S)[ \t]*\#/ ; str = $1
|
578
|
+
end
|
579
|
+
|
580
|
+
case str
|
581
|
+
when /^-?\d+$/ ; return str.to_i # integer
|
582
|
+
when /^-?\d+\.\d+$/ ; return str.to_f # float
|
583
|
+
when "true", "yes", "on" ; return true # true
|
584
|
+
when "false", "no", "off" ; return false # false
|
585
|
+
when "null", "~" ; return nil # nil
|
586
|
+
#when /^"(.*)"$/ ; return $1 # "string"
|
587
|
+
#when /^'(.*)'$/ ; return $1 # 'string'
|
588
|
+
when /^:(\w+)$/ ; return $1.intern # :symbol
|
589
|
+
when /^(\d\d\d\d)-(\d\d)-(\d\d)$/ # date
|
590
|
+
year, month, day = $1.to_i, $2.to_i, $3.to_i
|
591
|
+
return Date.new(year, month, day)
|
592
|
+
when /^(\d\d\d\d)-(\d\d)-(\d\d)(?:[Tt]|[ \t]+)(\d\d?):(\d\d):(\d\d)(\.\d*)?(?:Z|[ \t]*([-+]\d\d?)(?::(\d\d))?)?$/
|
593
|
+
year, mon, mday, hour, min, sec, usec, tzone_h, tzone_m = $1, $2, $3, $4, $5, $6, $7, $8, $9
|
594
|
+
#Time.utc(sec, min, hour, mday, mon, year, wday, yday, isdst, zone)
|
595
|
+
#t = Time.utc(sec, min, hour, mday, mon, year, nil, nil, nil, nil)
|
596
|
+
#Time.utc(year[, mon[, day[, hour[, min[, sec[, usec]]]]]])
|
597
|
+
time = Time.utc(year, mon, day, hour, min, sec, usec)
|
598
|
+
if tzone_h
|
599
|
+
diff_sec = tzone_h.to_i * 60 * 60
|
600
|
+
if tzone_m
|
601
|
+
if diff_sec > 0 ; diff_sec += tzone_m.to_i * 60
|
602
|
+
else ; diff_sec -= tzone_m.to_i * 60
|
603
|
+
end
|
604
|
+
end
|
605
|
+
p diff_sec
|
606
|
+
time -= diff_sec
|
607
|
+
end
|
608
|
+
return time
|
609
|
+
end
|
610
|
+
return str
|
611
|
+
end
|
612
|
+
|
613
|
+
|
614
|
+
def assert(bool_expr)
|
615
|
+
raise "*** assertion error" unless bool_expr
|
616
|
+
end
|
617
|
+
|
618
|
+
end
|
619
|
+
|
620
|
+
|
621
|
+
|
622
|
+
##
|
623
|
+
## ex.
|
624
|
+
## # load document with Parser
|
625
|
+
## str = ARGF.read()
|
626
|
+
## parser = Kwalify::Parser.new(str)
|
627
|
+
## document = parser.parse()
|
628
|
+
##
|
629
|
+
## # validate document
|
630
|
+
## schema = YAML.load(File.read('schema.yaml'))
|
631
|
+
## validator = Kwalify::Validator.new(schema)
|
632
|
+
## errors = validator.validate(document)
|
633
|
+
##
|
634
|
+
## # print validation result
|
635
|
+
## if errors && !errors.empty?
|
636
|
+
## parser.set_error_linenums(errors)
|
637
|
+
## errors.sort { |e1, e2| e1.linenum <=> e2.linenum }.each do |error|
|
638
|
+
## print "line %d: path %s: %s" % [error.linenum, error.path, error.message]
|
639
|
+
## end
|
640
|
+
## end
|
641
|
+
##
|
642
|
+
class Parser < PlainParser
|
643
|
+
|
644
|
+
def initialize(*args)
|
645
|
+
super
|
646
|
+
@linenums_table = {} # object_id -> hash or array
|
647
|
+
end
|
648
|
+
|
649
|
+
def parse()
|
650
|
+
@doc = super()
|
651
|
+
return @doc
|
652
|
+
end
|
653
|
+
|
654
|
+
def path_linenum(path)
|
655
|
+
return 1 if path.empty? || path == '/'
|
656
|
+
elems = path.split('/')
|
657
|
+
elems.shift if path[0] == ?/ # delete empty string on head
|
658
|
+
last_elem = elems.pop
|
659
|
+
c = @doc # collection
|
660
|
+
elems.each do |elem|
|
661
|
+
if c.is_a?(Array)
|
662
|
+
c = c[elem.to_i]
|
663
|
+
elsif c.is_a?(Hash)
|
664
|
+
c = c[elem]
|
665
|
+
else
|
666
|
+
assert false
|
667
|
+
end
|
668
|
+
end
|
669
|
+
linenums = @linenums_table[c.__id__]
|
670
|
+
if c.is_a?(Array)
|
671
|
+
linenum = linenums[last_elem.to_i]
|
672
|
+
elsif c.is_a?(Hash)
|
673
|
+
linenum = linenums[last_elem]
|
674
|
+
end
|
675
|
+
return linenum
|
676
|
+
end
|
677
|
+
|
678
|
+
def set_error_linenums(errors)
|
679
|
+
errors.each do |error|
|
680
|
+
error.linenum = path_linenum(error.path)
|
681
|
+
end
|
682
|
+
end
|
683
|
+
|
684
|
+
protected
|
685
|
+
|
686
|
+
def create_sequence(linenum=current_linenum())
|
687
|
+
seq = []
|
688
|
+
@linenums_table[seq.__id__] = []
|
689
|
+
return seq
|
690
|
+
end
|
691
|
+
|
692
|
+
def add_to_seq(seq, value, linenum)
|
693
|
+
seq << value
|
694
|
+
@linenums_table[seq.__id__] << linenum
|
695
|
+
end
|
696
|
+
|
697
|
+
def set_seq_at(seq, i, value, linenum)
|
698
|
+
seq[i] = value
|
699
|
+
@linenums_table[seq.__id__][i] = linenum
|
700
|
+
end
|
701
|
+
|
702
|
+
def create_mapping(linenum=current_linenum())
|
703
|
+
map = {}
|
704
|
+
@linenums_table[map.__id__] = {}
|
705
|
+
return map
|
706
|
+
end
|
707
|
+
|
708
|
+
def add_to_map(map, key, value, linenum)
|
709
|
+
map[key] = value
|
710
|
+
@linenums_table[map.__id__][key] = linenum
|
711
|
+
end
|
712
|
+
|
713
|
+
def set_map_with(map, key, value, linenum)
|
714
|
+
map[key] = value
|
715
|
+
@linenums_table[map.__id__][key] = linenum
|
716
|
+
end
|
717
|
+
|
718
|
+
def set_default(map, value, linenum)
|
719
|
+
map.default = value
|
720
|
+
@linenums_table[map.__id__][:'='] = linenum
|
721
|
+
end
|
722
|
+
|
723
|
+
def merge_map(map, collection, linenum)
|
724
|
+
t = @linenums_table[map.__id__]
|
725
|
+
list = collection.is_a?(Array) ? collection : [ collection ]
|
726
|
+
list.each do |m|
|
727
|
+
t2 = @linenums_table[m.__id__]
|
728
|
+
m.each do |key, val|
|
729
|
+
unless map.key?(key)
|
730
|
+
map[key] = val
|
731
|
+
t[key] = t2[key]
|
732
|
+
end
|
733
|
+
end
|
734
|
+
end
|
735
|
+
end
|
736
|
+
|
737
|
+
def create_scalar(value, linenum=current_linenum())
|
738
|
+
data = super(value)
|
739
|
+
#return Scalar.new(data, linenum)
|
740
|
+
return data
|
741
|
+
end
|
742
|
+
|
743
|
+
end
|
744
|
+
|
745
|
+
end
|
746
|
+
|
747
|
+
|
748
|
+
if __FILE__ == $0
|
749
|
+
require 'yaml'
|
750
|
+
require 'pp'
|
751
|
+
if $plain
|
752
|
+
parser = Kwalify::PlainParser.new(ARGF.read())
|
753
|
+
else
|
754
|
+
parser = Kwalify::Parser.new(ARGF.read())
|
755
|
+
end
|
756
|
+
doc = parser.parse()
|
757
|
+
pp doc
|
758
|
+
#y doc
|
759
|
+
end
|