kwalify 0.3.0 → 0.4.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/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
|