hocon 0.0.1
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/LICENSE +202 -0
- data/README.md +19 -0
- data/lib/hocon.rb +2 -0
- data/lib/hocon/config_error.rb +12 -0
- data/lib/hocon/config_factory.rb +9 -0
- data/lib/hocon/config_object.rb +4 -0
- data/lib/hocon/config_parse_options.rb +53 -0
- data/lib/hocon/config_render_options.rb +46 -0
- data/lib/hocon/config_syntax.rb +7 -0
- data/lib/hocon/config_value_type.rb +26 -0
- data/lib/hocon/impl.rb +5 -0
- data/lib/hocon/impl/abstract_config_object.rb +64 -0
- data/lib/hocon/impl/abstract_config_value.rb +130 -0
- data/lib/hocon/impl/config_concatenation.rb +136 -0
- data/lib/hocon/impl/config_float.rb +9 -0
- data/lib/hocon/impl/config_impl.rb +10 -0
- data/lib/hocon/impl/config_impl_util.rb +78 -0
- data/lib/hocon/impl/config_int.rb +31 -0
- data/lib/hocon/impl/config_number.rb +27 -0
- data/lib/hocon/impl/config_string.rb +37 -0
- data/lib/hocon/impl/full_includer.rb +4 -0
- data/lib/hocon/impl/origin_type.rb +9 -0
- data/lib/hocon/impl/parseable.rb +151 -0
- data/lib/hocon/impl/parser.rb +882 -0
- data/lib/hocon/impl/path.rb +59 -0
- data/lib/hocon/impl/path_builder.rb +36 -0
- data/lib/hocon/impl/resolve_status.rb +18 -0
- data/lib/hocon/impl/simple_config.rb +11 -0
- data/lib/hocon/impl/simple_config_list.rb +70 -0
- data/lib/hocon/impl/simple_config_object.rb +178 -0
- data/lib/hocon/impl/simple_config_origin.rb +174 -0
- data/lib/hocon/impl/simple_include_context.rb +7 -0
- data/lib/hocon/impl/simple_includer.rb +19 -0
- data/lib/hocon/impl/token.rb +32 -0
- data/lib/hocon/impl/token_type.rb +42 -0
- data/lib/hocon/impl/tokenizer.rb +370 -0
- data/lib/hocon/impl/tokens.rb +157 -0
- data/lib/hocon/impl/unmergeable.rb +4 -0
- metadata +84 -0
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'hocon/impl'
|
2
|
+
require 'hocon/config_syntax'
|
3
|
+
require 'hocon/impl/config_impl'
|
4
|
+
require 'hocon/impl/simple_include_context'
|
5
|
+
require 'hocon/impl/simple_config_object'
|
6
|
+
require 'hocon/impl/simple_config_origin'
|
7
|
+
require 'hocon/impl/tokenizer'
|
8
|
+
require 'hocon/impl/parser'
|
9
|
+
|
10
|
+
class Hocon::Impl::Parseable
|
11
|
+
class ParseableFile < Hocon::Impl::Parseable
|
12
|
+
def initialize(file_path, options)
|
13
|
+
@input = file_path
|
14
|
+
post_construct(options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def guess_syntax
|
18
|
+
Hocon::Impl::Parseable.syntax_from_extension(File.basename(@input))
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_origin
|
22
|
+
Hocon::Impl::SimpleConfigOrigin.new_file(@input)
|
23
|
+
end
|
24
|
+
|
25
|
+
def reader
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def open
|
30
|
+
if block_given?
|
31
|
+
File.open(@input) do |f|
|
32
|
+
yield f
|
33
|
+
end
|
34
|
+
else
|
35
|
+
File.open(@input)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.new_file(file_path, options)
|
41
|
+
ParseableFile.new(file_path, options)
|
42
|
+
end
|
43
|
+
|
44
|
+
def options
|
45
|
+
@initial_options
|
46
|
+
end
|
47
|
+
|
48
|
+
def include_context
|
49
|
+
@include_context
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.force_parsed_to_object(value)
|
53
|
+
if value.is_a? Hocon::Impl::AbstractConfigObject
|
54
|
+
value
|
55
|
+
else
|
56
|
+
raise ConfigWrongTypeError.new(value.origin, "", "object at file root",
|
57
|
+
value.value_type.name)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def parse
|
62
|
+
self.class.force_parsed_to_object(parse_value(options))
|
63
|
+
end
|
64
|
+
|
65
|
+
def parse_value(base_options)
|
66
|
+
# note that we are NOT using our "initialOptions",
|
67
|
+
# but using the ones from the passed-in options. The idea is that
|
68
|
+
# callers can get our original options and then parse with different
|
69
|
+
# ones if they want.
|
70
|
+
options = fixup_options(base_options)
|
71
|
+
|
72
|
+
# passed-in options can override origin
|
73
|
+
origin =
|
74
|
+
if options.origin_description
|
75
|
+
Hocon::Impl::SimpleConfigOrigin.new_simple(options.origin_description)
|
76
|
+
else
|
77
|
+
@initial_origin
|
78
|
+
end
|
79
|
+
parse_value_from_origin(origin, options)
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def self.syntax_from_extension(filename)
|
86
|
+
case File.extname(filename)
|
87
|
+
when ".json"
|
88
|
+
Hocon::ConfigSyntax::JSON
|
89
|
+
when ".conf"
|
90
|
+
Hocon::ConfigSyntax::CONF
|
91
|
+
when ".properties"
|
92
|
+
Hocon::ConfigSyntax::PROPERTIES
|
93
|
+
else
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def post_construct(base_options)
|
99
|
+
@initial_options = fixup_options(base_options)
|
100
|
+
@include_context = Hocon::Impl::SimpleIncludeContext.new(self)
|
101
|
+
if @initial_options.origin_description
|
102
|
+
@initial_origin = SimpleConfigOrigin.new_simple(@initial_options.origin_description)
|
103
|
+
else
|
104
|
+
@initial_origin = create_origin
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def fixup_options(base_options)
|
109
|
+
syntax = base_options.syntax
|
110
|
+
if !syntax
|
111
|
+
syntax = guess_syntax
|
112
|
+
end
|
113
|
+
if !syntax
|
114
|
+
syntax = Hocon::ConfigSyntax.CONF
|
115
|
+
end
|
116
|
+
|
117
|
+
modified = base_options.with_syntax(syntax)
|
118
|
+
modified = modified.append_includer(Hocon::Impl::ConfigImpl.default_includer)
|
119
|
+
modified = modified.with_includer(Hocon::Impl::SimpleIncluder.make_full(modified.includer))
|
120
|
+
|
121
|
+
modified
|
122
|
+
end
|
123
|
+
|
124
|
+
def parse_value_from_origin(origin, final_options)
|
125
|
+
begin
|
126
|
+
raw_parse_value(origin, final_options)
|
127
|
+
rescue IOError => e
|
128
|
+
if final_options.allow_missing?
|
129
|
+
Hocon::Impl::SimpleConfigObject.empty_missing(origin)
|
130
|
+
else
|
131
|
+
raise ConfigIOError.new(origin, "#{e.class.name}: #{e.message}", e)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# this is parseValue without post-processing the IOException or handling
|
137
|
+
# options.getAllowMissing()
|
138
|
+
def raw_parse_value(origin, final_options)
|
139
|
+
## TODO: if we were going to support loading from URLs, this
|
140
|
+
## method would need to deal with the content-type.
|
141
|
+
|
142
|
+
reader.open { |io|
|
143
|
+
raw_parse_value_from_io(io, origin, final_options)
|
144
|
+
}
|
145
|
+
end
|
146
|
+
|
147
|
+
def raw_parse_value_from_io(io, origin, final_options)
|
148
|
+
tokens = Hocon::Impl::Tokenizer.tokenize(origin, io, final_options.syntax)
|
149
|
+
Hocon::Impl::Parser.parse(tokens, origin, final_options, include_context)
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,882 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'hocon/impl'
|
3
|
+
require 'hocon/impl/tokens'
|
4
|
+
require 'hocon/impl/path_builder'
|
5
|
+
require 'hocon/config_syntax'
|
6
|
+
require 'hocon/config_value_type'
|
7
|
+
require 'hocon/impl/config_string'
|
8
|
+
require 'hocon/impl/config_concatenation'
|
9
|
+
require 'hocon/config_error'
|
10
|
+
require 'hocon/impl/simple_config_list'
|
11
|
+
require 'hocon/impl/simple_config_object'
|
12
|
+
|
13
|
+
class Hocon::Impl::Parser
|
14
|
+
|
15
|
+
Tokens = Hocon::Impl::Tokens
|
16
|
+
ConfigSyntax = Hocon::ConfigSyntax
|
17
|
+
ConfigValueType = Hocon::ConfigValueType
|
18
|
+
ConfigConcatenation = Hocon::Impl::ConfigConcatenation
|
19
|
+
ConfigParseError = Hocon::ConfigError::ConfigParseError
|
20
|
+
SimpleConfigObject = Hocon::Impl::SimpleConfigObject
|
21
|
+
SimpleConfigList = Hocon::Impl::SimpleConfigList
|
22
|
+
|
23
|
+
class TokenWithComments
|
24
|
+
def initialize(token, comments = [])
|
25
|
+
@token = token
|
26
|
+
@comments = comments
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader :token, :comments
|
30
|
+
|
31
|
+
def remove_all
|
32
|
+
if @comments.empty?
|
33
|
+
self
|
34
|
+
else
|
35
|
+
TokenWithComments.new(@token)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def prepend(earlier)
|
40
|
+
if earlier.empty?
|
41
|
+
self
|
42
|
+
elsif @comments.empty?
|
43
|
+
TokenWithComments.new(@token, earlier)
|
44
|
+
else
|
45
|
+
merged = []
|
46
|
+
merged.concat(earlier)
|
47
|
+
merged.concat(@comments)
|
48
|
+
TokenWithComments.new(@token, merged)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def prepend_comments(origin)
|
53
|
+
if @comments.empty?
|
54
|
+
origin
|
55
|
+
else
|
56
|
+
new_comments = @comments.map { |c| Tokens.comment_text(c) }
|
57
|
+
origin.prepend_comments(new_comments)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def append_comments(origin)
|
62
|
+
if @comments.empty?
|
63
|
+
origin
|
64
|
+
else
|
65
|
+
new_comments = @comments.map { |c| Tokens.comment_text(c) }
|
66
|
+
origin.append_comments(new_comments)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_s
|
71
|
+
# this ends up in user-visible error messages, so we don't want the
|
72
|
+
# comments
|
73
|
+
@token.to_s
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class ParseContext
|
78
|
+
class Element
|
79
|
+
def initialize(initial, can_be_empty)
|
80
|
+
@can_be_empty = can_be_empty
|
81
|
+
@sb = StringIO.new(initial)
|
82
|
+
end
|
83
|
+
|
84
|
+
attr_reader :sb
|
85
|
+
|
86
|
+
def to_s
|
87
|
+
"Element(#{sb.string}, #{@can_be_empty})"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
def self.attracts_trailing_comments?(token)
|
93
|
+
# EOF can't have a trailing comment; START, OPEN_CURLY, and
|
94
|
+
# OPEN_SQUARE followed by a comment should behave as if the comment
|
95
|
+
# went with the following field or element. Associating a comment
|
96
|
+
# with a newline would mess up all the logic for comment tracking,
|
97
|
+
# so don't do that either.
|
98
|
+
!(Tokens.newline?(token) ||
|
99
|
+
token == Tokens::START ||
|
100
|
+
token == Tokens::OPEN_CURLY)
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.attracts_leading_comments?(token)
|
104
|
+
# a comment just before a close } generally doesn't go with the
|
105
|
+
# value before it, unless it's on the same line as that value
|
106
|
+
!(Tokens.newline?(token) ||
|
107
|
+
token == Tokens::START ||
|
108
|
+
token == Tokens::CLOSE_CURLY ||
|
109
|
+
token == Tokens::CLOSE_SQUARE ||
|
110
|
+
token == Tokens::EOF)
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.include_keyword?(token)
|
114
|
+
Tokens.unquoted_text?(token) &&
|
115
|
+
(Tokens.unquoted_text(token) == "include")
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.add_path_text(buf, was_quoted, new_text)
|
119
|
+
i = if was_quoted
|
120
|
+
-1
|
121
|
+
else
|
122
|
+
new_text.index('.') || -1
|
123
|
+
end
|
124
|
+
current = buf.last
|
125
|
+
if i < 0
|
126
|
+
# add to current path element
|
127
|
+
current.sb << new_text
|
128
|
+
# any empty quoted string means this element can
|
129
|
+
# now be empty.
|
130
|
+
if was_quoted && (current.sb.length == 0)
|
131
|
+
current.can_be_empty = true
|
132
|
+
end
|
133
|
+
else
|
134
|
+
# "buf" plus up to the period is an element
|
135
|
+
current.sb << new_text[0, i]
|
136
|
+
# then start a new element
|
137
|
+
buf.push(Element.new("", false))
|
138
|
+
# recurse to consume remainder of new_text
|
139
|
+
add_path_text(buf, false, new_text[i + 1, new_text.length - 1])
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.parse_path_expression(expression, origin, original_text = nil)
|
144
|
+
buf = []
|
145
|
+
buf.push(Element.new("", false))
|
146
|
+
|
147
|
+
if expression.empty?
|
148
|
+
raise ConfigBadPathError.new(
|
149
|
+
origin,
|
150
|
+
original_text,
|
151
|
+
"Expecting a field name or path here, but got nothing")
|
152
|
+
end
|
153
|
+
|
154
|
+
expression.each do |t|
|
155
|
+
if Tokens.value_with_type?(t, ConfigValueType::STRING)
|
156
|
+
v = Tokens.value(t)
|
157
|
+
# this is a quoted string; so any periods
|
158
|
+
# in here don't count as path separators
|
159
|
+
s = v.transform_to_string
|
160
|
+
add_path_text(buf, true, s)
|
161
|
+
elsif t == Tokens::EOF
|
162
|
+
# ignore this; when parsing a file, it should not happen
|
163
|
+
# since we're parsing a token list rather than the main
|
164
|
+
# token iterator, and when parsing a path expression from the
|
165
|
+
# API, it's expected to have an EOF.
|
166
|
+
else
|
167
|
+
# any periods outside of a quoted string count as
|
168
|
+
# separators
|
169
|
+
text = nil
|
170
|
+
if Tokens.value?(t)
|
171
|
+
# appending a number here may add
|
172
|
+
# a period, but we _do_ count those as path
|
173
|
+
# separators, because we basically want
|
174
|
+
# "foo 3.0bar" to parse as a string even
|
175
|
+
# though there's a number in it. The fact that
|
176
|
+
# we tokenize non-string values is largely an
|
177
|
+
# implementation detail.
|
178
|
+
v = Tokens.value(t)
|
179
|
+
text = v.transform_to_string
|
180
|
+
elsif Tokens.unquoted_text?(t)
|
181
|
+
text = Tokens.unquoted_text(t)
|
182
|
+
else
|
183
|
+
raise ConfigBadPathError.new(
|
184
|
+
origin,
|
185
|
+
original_text,
|
186
|
+
"Token not allowed in path expression: #{t}" +
|
187
|
+
" (you can double-quote this token if you really want it here)")
|
188
|
+
end
|
189
|
+
|
190
|
+
add_path_text(buf, false, text)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
pb = Hocon::Impl::PathBuilder.new
|
195
|
+
buf.each do |e|
|
196
|
+
if (e.sb.length == 0) && !e.can_be_empty?
|
197
|
+
raise ConfigBadPathError.new(
|
198
|
+
origin,
|
199
|
+
original_text,
|
200
|
+
"path has a leading, trailing, or two adjacent period '.' (use quoted \"\" empty string if you want an empty element)")
|
201
|
+
else
|
202
|
+
pb.append_key(e.sb.string)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
pb.result
|
207
|
+
end
|
208
|
+
|
209
|
+
def initialize(flavor, origin, tokens, includer, include_context)
|
210
|
+
@line_number = 1
|
211
|
+
@flavor = flavor
|
212
|
+
@base_origin = origin
|
213
|
+
@buffer = []
|
214
|
+
@tokens = tokens
|
215
|
+
@includer = includer
|
216
|
+
@include_context = include_context
|
217
|
+
@path_stack = []
|
218
|
+
# this is the number of "equals" we are inside,
|
219
|
+
# used to modify the error message to reflect that
|
220
|
+
# someone may think this is .properties format.
|
221
|
+
@equals_count = 0
|
222
|
+
end
|
223
|
+
|
224
|
+
def key_value_separator_token?(t)
|
225
|
+
if @flavor == ConfigSyntax::JSON
|
226
|
+
t == Tokens::COLON
|
227
|
+
else
|
228
|
+
[Tokens::COLON, Tokens::EQUALS, Tokens::PLUS_EQUALS].any? { |sep| sep == t }
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def consolidate_comment_block(comment_token)
|
233
|
+
# a comment block "goes with" the following token
|
234
|
+
# unless it's separated from it by a blank line.
|
235
|
+
# we want to build a list of newline tokens followed
|
236
|
+
# by a non-newline non-comment token; with all comments
|
237
|
+
# associated with that final non-newline non-comment token.
|
238
|
+
# a comment AFTER a token, without an intervening newline,
|
239
|
+
# also goes with that token, but isn't handled in this method,
|
240
|
+
# instead we handle it later by peeking ahead.
|
241
|
+
new_lines = []
|
242
|
+
comments = []
|
243
|
+
|
244
|
+
previous_token = nil
|
245
|
+
next_token = comment_token
|
246
|
+
while true
|
247
|
+
if Tokens.newline?(next_token)
|
248
|
+
if (previous_token != nil) && Tokens.newline?(previous_token)
|
249
|
+
# blank line; drop all comments to this point and
|
250
|
+
# start a new comment block
|
251
|
+
comments.clear
|
252
|
+
end
|
253
|
+
new_lines.push(next_token)
|
254
|
+
elsif Tokens.comment?(next_token)
|
255
|
+
comments.push(next_token)
|
256
|
+
else
|
257
|
+
# a non-newline non-comment token
|
258
|
+
|
259
|
+
# comments before a close brace or bracket just get dumped
|
260
|
+
unless self.class.attracts_leading_comments?(next_token)
|
261
|
+
comments.clear
|
262
|
+
end
|
263
|
+
break
|
264
|
+
end
|
265
|
+
|
266
|
+
previous_token = next_token
|
267
|
+
next_token = @tokens.next
|
268
|
+
end
|
269
|
+
|
270
|
+
# put our concluding token in the queue with all the comments
|
271
|
+
# attached
|
272
|
+
@buffer.push(TokenWithComments.new(next_token, comments))
|
273
|
+
|
274
|
+
# now put all the newlines back in front of it
|
275
|
+
new_lines.reverse.each do |nl|
|
276
|
+
@buffer.push(TokenWithComments.new(nl))
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
# merge a bunch of adjacent values into one
|
281
|
+
# value; change unquoted text into a string
|
282
|
+
# value.
|
283
|
+
def consolidate_value_tokens
|
284
|
+
# this trick is not done in JSON
|
285
|
+
return if @flavor == ConfigSyntax::JSON
|
286
|
+
|
287
|
+
# create only if we have value tokens
|
288
|
+
values = nil
|
289
|
+
|
290
|
+
# ignore a newline up front
|
291
|
+
t = next_token_ignoring_newline
|
292
|
+
while true
|
293
|
+
v = nil
|
294
|
+
if (Tokens.value?(t.token)) || (Tokens.unquoted_text?(t.token)) ||
|
295
|
+
(Tokens.substitution?(t.token)) || (t.token == Tokens::OPEN_CURLY) ||
|
296
|
+
(t.token == Tokens::OPEN_SQUARE)
|
297
|
+
# there may be newlines _within_ the objects and arrays
|
298
|
+
v = parse_value(t)
|
299
|
+
else
|
300
|
+
break
|
301
|
+
end
|
302
|
+
|
303
|
+
if v.nil?
|
304
|
+
raise ConfigBugError("no value")
|
305
|
+
end
|
306
|
+
|
307
|
+
if values.nil?
|
308
|
+
values = []
|
309
|
+
end
|
310
|
+
values.push(v)
|
311
|
+
|
312
|
+
t = next_token # but don't consolidate across a newline
|
313
|
+
end
|
314
|
+
# the last one wasn't a value token
|
315
|
+
put_back(t)
|
316
|
+
|
317
|
+
return if values.nil?
|
318
|
+
|
319
|
+
consolidated = ConfigConcatenation.concatenate(values)
|
320
|
+
|
321
|
+
put_back(TokenWithComments.new(Tokens.new_value(consolidated)))
|
322
|
+
end
|
323
|
+
|
324
|
+
def line_origin
|
325
|
+
@base_origin.set_line_number(@line_number)
|
326
|
+
end
|
327
|
+
|
328
|
+
def parse_value(t)
|
329
|
+
v = nil
|
330
|
+
|
331
|
+
if Tokens.value?(t.token)
|
332
|
+
# if we consolidateValueTokens() multiple times then
|
333
|
+
# this value could be a concatenation, object, array,
|
334
|
+
# or substitution already.
|
335
|
+
v = Tokens.value(t.token)
|
336
|
+
elsif Tokens.unquoted_text?(t.token)
|
337
|
+
v = Hocon::Impl::ConfigString.new(t.token.origin, Tokens.unquoted_text(t.token))
|
338
|
+
elsif Tokens.substitution?(t.token)
|
339
|
+
v = ConfigReference.new(t.token.origin, token_to_substitution_expression(t.token))
|
340
|
+
elsif t.token == Tokens::OPEN_CURLY
|
341
|
+
v = parse_object(true)
|
342
|
+
elsif t.token == Tokens::OPEN_SQUARE
|
343
|
+
v = parse_array
|
344
|
+
else
|
345
|
+
raise parse_error(
|
346
|
+
add_quote_suggestion(t.token.to_s,
|
347
|
+
"Expecting a value but got wrong token: #{t.token}"))
|
348
|
+
end
|
349
|
+
|
350
|
+
v.with_origin(t.prepend_comments(v.origin))
|
351
|
+
end
|
352
|
+
|
353
|
+
def create_value_under_path(path, value)
|
354
|
+
# for path foo.bar, we are creating
|
355
|
+
# { "foo" : { "bar" : value } }
|
356
|
+
keys = []
|
357
|
+
|
358
|
+
key = path.first
|
359
|
+
remaining = path.remainder
|
360
|
+
while !key.nil?
|
361
|
+
# for ruby: convert string keys to symbols
|
362
|
+
if key.is_a?(String)
|
363
|
+
key = key.to_sym
|
364
|
+
end
|
365
|
+
keys.push(key)
|
366
|
+
if remaining.nil?
|
367
|
+
break
|
368
|
+
else
|
369
|
+
key = remaining.first
|
370
|
+
remaining = remaining.remainder
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
# the setComments(null) is to ensure comments are only
|
375
|
+
# on the exact leaf node they apply to.
|
376
|
+
# a comment before "foo.bar" applies to the full setting
|
377
|
+
# "foo.bar" not also to "foo"
|
378
|
+
keys = keys.reverse
|
379
|
+
# this is just a ruby means for doing first/rest
|
380
|
+
deepest, *rest = *keys
|
381
|
+
o = SimpleConfigObject.new(value.origin.set_comments(nil),
|
382
|
+
{deepest => value})
|
383
|
+
while !rest.empty?
|
384
|
+
deepest, *rest = *rest
|
385
|
+
o = SimpleConfigObject.new(value.origin.set_comments(nil),
|
386
|
+
{deepest => o})
|
387
|
+
end
|
388
|
+
|
389
|
+
o
|
390
|
+
end
|
391
|
+
|
392
|
+
def parse_key(token)
|
393
|
+
if @flavor == ConfigSyntax::JSON
|
394
|
+
if Tokens.value_with_type?(token.token, ConfigValueType::STRING)
|
395
|
+
key = Tokens.value(token.token).unwrapped
|
396
|
+
Path.new_key(key)
|
397
|
+
else
|
398
|
+
raise parse_error(add_key_name("Expecting close brace } or a field name here, got #{token}"))
|
399
|
+
end
|
400
|
+
else
|
401
|
+
expression = []
|
402
|
+
t = token
|
403
|
+
while Tokens.value?(t.token) || Tokens.unquoted_text?(t.token)
|
404
|
+
expression.push(t.token)
|
405
|
+
t = next_token # note: don't cross a newline
|
406
|
+
end
|
407
|
+
|
408
|
+
if expression.empty?
|
409
|
+
raise parse_error(add_key_name("expecting a close brace or a field name here, got #{t}"))
|
410
|
+
end
|
411
|
+
|
412
|
+
put_back(t)
|
413
|
+
self.class.parse_path_expression(expression, line_origin)
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
def parse_object(had_open_curly)
|
418
|
+
# invoked just after the OPEN_CURLY (or START, if !hadOpenCurly)
|
419
|
+
values = {}
|
420
|
+
object_origin = line_origin
|
421
|
+
after_comma = false
|
422
|
+
last_path = nil
|
423
|
+
last_inside_equals = false
|
424
|
+
|
425
|
+
while true
|
426
|
+
t = next_token_ignoring_newline
|
427
|
+
if t.token == Tokens::CLOSE_CURLY
|
428
|
+
if (@flavor == ConfigSyntax::JSON) && after_comma
|
429
|
+
raise parse_error(
|
430
|
+
add_quote_suggestion(t,
|
431
|
+
"unbalanced close brace '}' with no open brace"))
|
432
|
+
end
|
433
|
+
|
434
|
+
object_origin = t.append_comments(object_origin)
|
435
|
+
break
|
436
|
+
elsif (t.token == Tokens::EOF) && !had_open_curly
|
437
|
+
put_back(t)
|
438
|
+
break
|
439
|
+
elsif (@flavor != ConfigSyntax::JSON) &&
|
440
|
+
self.class.include_keyword?(t.token)
|
441
|
+
parse_include(values)
|
442
|
+
after_comma = false
|
443
|
+
else
|
444
|
+
key_token = t
|
445
|
+
path = parse_key(key_token)
|
446
|
+
after_key = next_token_ignoring_newline
|
447
|
+
inside_equals = false
|
448
|
+
|
449
|
+
# path must be on-stack while we parse the value
|
450
|
+
@path_stack.push(path)
|
451
|
+
value_token = nil
|
452
|
+
new_value = nil
|
453
|
+
if (@flavor == ConfigSyntax::CONF) &&
|
454
|
+
(after_key.token == Tokens::OPEN_CURLY)
|
455
|
+
# can omit the ':' or '=' before an object value
|
456
|
+
value_token = after_key
|
457
|
+
else
|
458
|
+
if !key_value_separator_token?(after_key.token)
|
459
|
+
raise parse_error(
|
460
|
+
add_quote_suggestion(after_key,
|
461
|
+
"Key '#{path.render}' may not be followed by token: #{after_key}"))
|
462
|
+
end
|
463
|
+
|
464
|
+
if after_key.token == Tokens::EQUALS
|
465
|
+
inside_equals = true
|
466
|
+
@equals_count += 1
|
467
|
+
end
|
468
|
+
|
469
|
+
consolidate_value_tokens
|
470
|
+
value_token = next_token_ignoring_newline
|
471
|
+
|
472
|
+
# put comments from separator token on the value token
|
473
|
+
value_token = value_token.prepend(after_key.comments)
|
474
|
+
end
|
475
|
+
|
476
|
+
# comments from the key token go to the value token
|
477
|
+
new_value = parse_value(value_token.prepend(key_token.comments))
|
478
|
+
|
479
|
+
if after_key.token == Tokens::PLUS_EQUALS
|
480
|
+
previous_ref = ConfigReference.new(
|
481
|
+
new_value.origin,
|
482
|
+
SubstitutionExpression.new(full_current_path, true))
|
483
|
+
list = SimpleConfigList.new(new_value.origin, [new_value])
|
484
|
+
new_value = ConfigConcatenation.concatenate([previous_ref, list])
|
485
|
+
end
|
486
|
+
|
487
|
+
new_value = add_any_comments_after_any_comma(new_value)
|
488
|
+
|
489
|
+
last_path = @path_stack.pop
|
490
|
+
if inside_equals
|
491
|
+
@equals_count -= 1
|
492
|
+
end
|
493
|
+
last_inside_equals = inside_equals
|
494
|
+
|
495
|
+
key = path.first
|
496
|
+
|
497
|
+
# for ruby: convert string keys to symbols
|
498
|
+
if key.is_a?(String)
|
499
|
+
key = key.to_sym
|
500
|
+
end
|
501
|
+
|
502
|
+
remaining = path.remainder
|
503
|
+
|
504
|
+
if !remaining
|
505
|
+
existing = values[key]
|
506
|
+
if existing
|
507
|
+
# In strict JSON, dups should be an error; while in
|
508
|
+
# our custom config language, they should be merged
|
509
|
+
# if the value is an object (or substitution that
|
510
|
+
# could become an object).
|
511
|
+
|
512
|
+
if @flavor == ConfigSyntax::JSON
|
513
|
+
raise parse_error("JSON does not allow duplicate fields: '#{key}'" +
|
514
|
+
" was already seen at #{existing.origin().description()}")
|
515
|
+
else
|
516
|
+
new_value = new_value.with_fallback(existing)
|
517
|
+
end
|
518
|
+
end
|
519
|
+
values[key] = new_value
|
520
|
+
else
|
521
|
+
if @flavor == ConfigSyntax::JSON
|
522
|
+
raise ConfigBugError, "somehow got multi-element path in JSON mode"
|
523
|
+
end
|
524
|
+
|
525
|
+
obj = create_value_under_path(remaining, new_value)
|
526
|
+
existing = values[key]
|
527
|
+
if !existing.nil?
|
528
|
+
obj = obj.with_fallback(existing)
|
529
|
+
end
|
530
|
+
values[key] = obj
|
531
|
+
end
|
532
|
+
|
533
|
+
after_comma = false
|
534
|
+
end
|
535
|
+
|
536
|
+
if check_element_separator
|
537
|
+
# continue looping
|
538
|
+
after_comma = true
|
539
|
+
else
|
540
|
+
t = next_token_ignoring_newline
|
541
|
+
if t.token == Tokens::CLOSE_CURLY
|
542
|
+
if !had_open_curly
|
543
|
+
raise parse_error(
|
544
|
+
add_quote_suggestion(last_path, last_inside_equals,
|
545
|
+
t, "unbalanced close brace '}' with no open brace"))
|
546
|
+
end
|
547
|
+
|
548
|
+
object_origin = t.append_comments(object_origin)
|
549
|
+
break
|
550
|
+
elsif had_open_curly
|
551
|
+
raise parse_error(
|
552
|
+
add_quote_suggestion(t, "Expecting close brace } or a comma, got #{t}",
|
553
|
+
last_path, last_inside_equals))
|
554
|
+
else
|
555
|
+
if t.token == Tokens::END
|
556
|
+
put_back(t)
|
557
|
+
break
|
558
|
+
else
|
559
|
+
raise parse_error(
|
560
|
+
add_quote_suggestion(t, "Expecting end of input or a comma, got #{t}",
|
561
|
+
last_path, last_inside_equals))
|
562
|
+
end
|
563
|
+
end
|
564
|
+
end
|
565
|
+
end
|
566
|
+
|
567
|
+
SimpleConfigObject.new(object_origin, values)
|
568
|
+
|
569
|
+
end
|
570
|
+
|
571
|
+
def parse_array
|
572
|
+
# invoked just after the OPEN_SQUARE
|
573
|
+
array_origin = line_origin
|
574
|
+
values = []
|
575
|
+
|
576
|
+
consolidate_value_tokens
|
577
|
+
|
578
|
+
t = next_token_ignoring_newline
|
579
|
+
|
580
|
+
# special-case the first element
|
581
|
+
if t.token == Tokens::CLOSE_SQUARE
|
582
|
+
SimpleConfigList.new(t.append_comments(array_origin), [])
|
583
|
+
elsif (Tokens.value?(t.token)) ||
|
584
|
+
(t.token == Tokens::OPEN_CURLY) ||
|
585
|
+
(to.token == Tokens::OPEN_SQUARE)
|
586
|
+
v = parse_value(t)
|
587
|
+
v = add_any_comments_after_any_comma(v)
|
588
|
+
values.push(v)
|
589
|
+
else
|
590
|
+
raise parse_error(add_key_name("List should have ] or a first element after the open [, instead had token: " +
|
591
|
+
"#{t} (if you want #{t} to be part of a string value, then double-quote it)"))
|
592
|
+
end
|
593
|
+
|
594
|
+
# now remaining elements
|
595
|
+
while true
|
596
|
+
# just after a value
|
597
|
+
if check_element_separator
|
598
|
+
# comma (or newline equivalent) consumed
|
599
|
+
else
|
600
|
+
t = next_token_ignoring_newline
|
601
|
+
if t.token == Tokens::CLOSE_SQUARE
|
602
|
+
return SimpleConfigList.new(t.append_comments(array_origin), values)
|
603
|
+
else
|
604
|
+
raise parse_error(add_key_name("List should have ended with ] or had a comma, instead had token: " +
|
605
|
+
"#{t} (if you want #{t} to be part of a string value, then double-quote it)"))
|
606
|
+
end
|
607
|
+
end
|
608
|
+
|
609
|
+
# now just after a comma
|
610
|
+
consolidate_value_tokens
|
611
|
+
|
612
|
+
t = next_token_ignoring_newline
|
613
|
+
|
614
|
+
if (Tokens.value?(t.token)) ||
|
615
|
+
(t.token == Tokens::OPEN_CURLY) ||
|
616
|
+
(t.token == Tokens::OPEN_SQUARE)
|
617
|
+
v = parse_value(t)
|
618
|
+
v = add_any_comments_after_any_comma(v)
|
619
|
+
values.push(v)
|
620
|
+
elsif (@flavor != ConfigSyntax::JSON) &&
|
621
|
+
(t.token == Tokens::CLOSE_SQUARE)
|
622
|
+
# we allow one trailing comma
|
623
|
+
put_back(t)
|
624
|
+
else
|
625
|
+
raise parse_error(add_key_name("List should have had new element after a comma, instead had token: " +
|
626
|
+
"#{t} (if you want the comma or #{t} to be part of a string value, then double-quote it)"))
|
627
|
+
end
|
628
|
+
end
|
629
|
+
end
|
630
|
+
|
631
|
+
def parse
|
632
|
+
t = next_token_ignoring_newline
|
633
|
+
if t.token != Tokens::START
|
634
|
+
raise ConfigBugError, "token stream did not begin with START, had #{t}"
|
635
|
+
end
|
636
|
+
|
637
|
+
t = next_token_ignoring_newline
|
638
|
+
result = nil
|
639
|
+
if (t.token == Tokens::OPEN_CURLY) or
|
640
|
+
(t.token == Tokens::OPEN_SQUARE)
|
641
|
+
result = parse_value(t)
|
642
|
+
else
|
643
|
+
if @syntax == ConfigSyntax::JSON
|
644
|
+
if t.token == Tokens::END
|
645
|
+
raise parse_error("Empty document")
|
646
|
+
else
|
647
|
+
raise parse_error("Document must have an object or array at root, unexpected token: #{t}")
|
648
|
+
end
|
649
|
+
else
|
650
|
+
## the root object can omit the surrounding braces.
|
651
|
+
## this token should be the first field's key, or part
|
652
|
+
## of it, so put it back.
|
653
|
+
put_back(t)
|
654
|
+
result = parse_object(false)
|
655
|
+
## in this case we don't try to use commentsStack comments
|
656
|
+
## since they would all presumably apply to fields not the
|
657
|
+
## root object
|
658
|
+
end
|
659
|
+
end
|
660
|
+
|
661
|
+
t = next_token_ignoring_newline
|
662
|
+
if t.token == Tokens::EOF
|
663
|
+
result
|
664
|
+
else
|
665
|
+
raise parse_error("Document has trailing tokens after first object or array: #{t}")
|
666
|
+
end
|
667
|
+
end
|
668
|
+
|
669
|
+
def put_back(token)
|
670
|
+
if Tokens.comment?(token.token)
|
671
|
+
raise ConfigBugError, "comment token should have been stripped before it was available to put back"
|
672
|
+
end
|
673
|
+
@buffer.push(token)
|
674
|
+
end
|
675
|
+
|
676
|
+
def next_token_ignoring_newline
|
677
|
+
t = next_token
|
678
|
+
while Tokens.newline?(t.token)
|
679
|
+
# line number tokens have the line that was _ended_ by the
|
680
|
+
# newline, so we have to add one. We have to update lineNumber
|
681
|
+
# here and also below, because not all tokens store a line
|
682
|
+
# number, but newline tokens always do.
|
683
|
+
@line_number = t.token.line_number + 1
|
684
|
+
|
685
|
+
t = next_token
|
686
|
+
end
|
687
|
+
|
688
|
+
# update line number again, iff we have one
|
689
|
+
new_number = t.token.line_number
|
690
|
+
if new_number >= 0
|
691
|
+
@line_number = new_number
|
692
|
+
end
|
693
|
+
|
694
|
+
t
|
695
|
+
end
|
696
|
+
|
697
|
+
def next_token
|
698
|
+
with_comments = pop_token
|
699
|
+
t = with_comments.token
|
700
|
+
|
701
|
+
if Tokens.problem?(t)
|
702
|
+
origin = t.origin
|
703
|
+
message = Tokens.get_problem_message(t)
|
704
|
+
cause = Tokens.get_problem_cause(t)
|
705
|
+
suggest_quotes = Tokens.get_problem_suggest_quotes(t)
|
706
|
+
if suggest_quotes
|
707
|
+
message = add_quote_suggestion(t.to_s, message)
|
708
|
+
else
|
709
|
+
message = add_key_name(message)
|
710
|
+
end
|
711
|
+
raise ConfigParseError.new(origin, message, cause)
|
712
|
+
else
|
713
|
+
if @syntax == ConfigSyntax::JSON
|
714
|
+
if Tokens.unquoted_text?(t)
|
715
|
+
raise parse_error(add_key_name("Token not allowed in valid JSON: '#{Tokens.get_unquoted_text(t)}'"))
|
716
|
+
elsif Tokens.substitution?(t)
|
717
|
+
raise parse_error(add_key_name("Substitutions (${} syntax) not allowed in JSON"))
|
718
|
+
end
|
719
|
+
end
|
720
|
+
|
721
|
+
with_comments
|
722
|
+
end
|
723
|
+
end
|
724
|
+
|
725
|
+
def add_any_comments_after_any_comma(v)
|
726
|
+
t = next_token # do NOT skip newlines, we only
|
727
|
+
# want same-line comments
|
728
|
+
if t.token == Tokens::COMMA
|
729
|
+
# steal the comments from after the comma
|
730
|
+
put_back(t.remove_all)
|
731
|
+
v.with_origin(t.append_comments(v.origin))
|
732
|
+
else
|
733
|
+
put_back(t)
|
734
|
+
v
|
735
|
+
end
|
736
|
+
end
|
737
|
+
|
738
|
+
# In arrays and objects, comma can be omitted
|
739
|
+
# as long as there's at least one newline instead.
|
740
|
+
# this skips any newlines in front of a comma,
|
741
|
+
# skips the comma, and returns true if it found
|
742
|
+
# either a newline or a comma. The iterator
|
743
|
+
# is left just after the comma or the newline.
|
744
|
+
def check_element_separator
|
745
|
+
if @flavor == ConfigSyntax::JSON
|
746
|
+
t = next_token_ignoring_newline
|
747
|
+
if (t.token == Tokens::COMMA)
|
748
|
+
true
|
749
|
+
else
|
750
|
+
put_back(t)
|
751
|
+
false
|
752
|
+
end
|
753
|
+
else
|
754
|
+
saw_separator_or_newline = false
|
755
|
+
t = next_token
|
756
|
+
while true
|
757
|
+
if Tokens.newline?(t.token)
|
758
|
+
# newline number is the line just ended, so add one
|
759
|
+
@line_number = t.token.line_number + 1
|
760
|
+
saw_separator_or_newline = true
|
761
|
+
|
762
|
+
# we want to continue to also eat a comma if there is one
|
763
|
+
elsif t.token == Tokens::COMMA
|
764
|
+
return true
|
765
|
+
else
|
766
|
+
# non-newline-or-comma
|
767
|
+
put_back(t)
|
768
|
+
return saw_separator_or_newline
|
769
|
+
end
|
770
|
+
t = next_token
|
771
|
+
end
|
772
|
+
end
|
773
|
+
end
|
774
|
+
|
775
|
+
def parse_error(message, cause = nil)
|
776
|
+
ConfigParseError.new(line_origin, message, cause)
|
777
|
+
end
|
778
|
+
|
779
|
+
def previous_field_name(last_path = nil)
|
780
|
+
if !last_path.nil?
|
781
|
+
last_path.render
|
782
|
+
elsif @path_stack.empty?
|
783
|
+
nil
|
784
|
+
else
|
785
|
+
@path_stack[0].render
|
786
|
+
end
|
787
|
+
end
|
788
|
+
|
789
|
+
def add_key_name(message)
|
790
|
+
prev_field_name = previous_field_name
|
791
|
+
if !prev_field_name.nil?
|
792
|
+
"in value for key '#{prev_field_name}': #{message}"
|
793
|
+
else
|
794
|
+
message
|
795
|
+
end
|
796
|
+
end
|
797
|
+
|
798
|
+
def add_quote_suggestion(bad_token, message, last_path = nil, inside_equals = (@equals_count > 0))
|
799
|
+
prev_field_name = previous_field_name(last_path)
|
800
|
+
part =
|
801
|
+
if bad_token == Tokens::EOF.to_s
|
802
|
+
# EOF requires special handling for the error to make sense.
|
803
|
+
if !prev_field_name.nil?
|
804
|
+
"#{message} (if you intended '#{prev_field_name}' " +
|
805
|
+
"to be part of a value, instead of a key, " +
|
806
|
+
"try adding double quotes around the whole value"
|
807
|
+
else
|
808
|
+
message
|
809
|
+
end
|
810
|
+
else
|
811
|
+
if !prev_field_name.nil?
|
812
|
+
"#{message} (if you intended #{bad_token} " +
|
813
|
+
"to be part of the value for '#{prev_field_name}', " +
|
814
|
+
"try enclosing the value in double quotes"
|
815
|
+
else
|
816
|
+
"#{message} (if you intended #{bad_token} " +
|
817
|
+
"to be part of a key or string value, " +
|
818
|
+
"try enclosing the key or value in double quotes"
|
819
|
+
end
|
820
|
+
end
|
821
|
+
|
822
|
+
if inside_equals
|
823
|
+
"#{part}, or you may be able to rename the file .properties rather than .conf)"
|
824
|
+
else
|
825
|
+
"#{part})"
|
826
|
+
end
|
827
|
+
end
|
828
|
+
|
829
|
+
|
830
|
+
def pop_token
|
831
|
+
with_preceding_comments = pop_token_without_trailing_comment
|
832
|
+
# handle a comment AFTER the other token,
|
833
|
+
# but before a newline. If the next token is not
|
834
|
+
# a comment, then any comment later on the line is irrelevant
|
835
|
+
# since it would end up going with that later token, not
|
836
|
+
# this token. Comments are supposed to be processed prior
|
837
|
+
# to adding stuff to the buffer, so they can only be found
|
838
|
+
# in "tokens" not in "buffer" in theory.
|
839
|
+
if !self.class.attracts_trailing_comments?(with_preceding_comments.token)
|
840
|
+
with_preceding_comments
|
841
|
+
elsif @buffer.empty?
|
842
|
+
after = @tokens.next
|
843
|
+
if Tokens.comment?(after)
|
844
|
+
with_preceding_comments.add(after)
|
845
|
+
else
|
846
|
+
@buffer << TokenWithComments.new(after)
|
847
|
+
with_preceding_comments
|
848
|
+
end
|
849
|
+
else
|
850
|
+
# comments are supposed to get attached to a token,
|
851
|
+
# not put back in the buffer. Assert this as an invariant.
|
852
|
+
if Tokens.comment?(@buffer.last.token)
|
853
|
+
raise ConfigBugError, "comment token should not have been in buffer: #{@buffer}"
|
854
|
+
end
|
855
|
+
with_preceding_comments
|
856
|
+
end
|
857
|
+
end
|
858
|
+
|
859
|
+
def pop_token_without_trailing_comment
|
860
|
+
if @buffer.empty?
|
861
|
+
t = @tokens.next
|
862
|
+
if Tokens.comment?(t)
|
863
|
+
consolidate_comment_block(t)
|
864
|
+
@buffer.pop
|
865
|
+
else
|
866
|
+
TokenWithComments.new(t)
|
867
|
+
end
|
868
|
+
else
|
869
|
+
@buffer.pop
|
870
|
+
end
|
871
|
+
end
|
872
|
+
|
873
|
+
end
|
874
|
+
|
875
|
+
def self.parse(tokens, origin, options, include_context)
|
876
|
+
context = Hocon::Impl::Parser::ParseContext.new(
|
877
|
+
options.syntax, origin, tokens,
|
878
|
+
Hocon::Impl::SimpleIncluder.make_full(options.includer),
|
879
|
+
include_context)
|
880
|
+
context.parse
|
881
|
+
end
|
882
|
+
end
|