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.
Files changed (39) hide show
  1. data/LICENSE +202 -0
  2. data/README.md +19 -0
  3. data/lib/hocon.rb +2 -0
  4. data/lib/hocon/config_error.rb +12 -0
  5. data/lib/hocon/config_factory.rb +9 -0
  6. data/lib/hocon/config_object.rb +4 -0
  7. data/lib/hocon/config_parse_options.rb +53 -0
  8. data/lib/hocon/config_render_options.rb +46 -0
  9. data/lib/hocon/config_syntax.rb +7 -0
  10. data/lib/hocon/config_value_type.rb +26 -0
  11. data/lib/hocon/impl.rb +5 -0
  12. data/lib/hocon/impl/abstract_config_object.rb +64 -0
  13. data/lib/hocon/impl/abstract_config_value.rb +130 -0
  14. data/lib/hocon/impl/config_concatenation.rb +136 -0
  15. data/lib/hocon/impl/config_float.rb +9 -0
  16. data/lib/hocon/impl/config_impl.rb +10 -0
  17. data/lib/hocon/impl/config_impl_util.rb +78 -0
  18. data/lib/hocon/impl/config_int.rb +31 -0
  19. data/lib/hocon/impl/config_number.rb +27 -0
  20. data/lib/hocon/impl/config_string.rb +37 -0
  21. data/lib/hocon/impl/full_includer.rb +4 -0
  22. data/lib/hocon/impl/origin_type.rb +9 -0
  23. data/lib/hocon/impl/parseable.rb +151 -0
  24. data/lib/hocon/impl/parser.rb +882 -0
  25. data/lib/hocon/impl/path.rb +59 -0
  26. data/lib/hocon/impl/path_builder.rb +36 -0
  27. data/lib/hocon/impl/resolve_status.rb +18 -0
  28. data/lib/hocon/impl/simple_config.rb +11 -0
  29. data/lib/hocon/impl/simple_config_list.rb +70 -0
  30. data/lib/hocon/impl/simple_config_object.rb +178 -0
  31. data/lib/hocon/impl/simple_config_origin.rb +174 -0
  32. data/lib/hocon/impl/simple_include_context.rb +7 -0
  33. data/lib/hocon/impl/simple_includer.rb +19 -0
  34. data/lib/hocon/impl/token.rb +32 -0
  35. data/lib/hocon/impl/token_type.rb +42 -0
  36. data/lib/hocon/impl/tokenizer.rb +370 -0
  37. data/lib/hocon/impl/tokens.rb +157 -0
  38. data/lib/hocon/impl/unmergeable.rb +4 -0
  39. metadata +84 -0
@@ -0,0 +1,4 @@
1
+ require 'hocon/impl'
2
+
3
+ class Hocon::Impl::FullIncluder
4
+ end
@@ -0,0 +1,9 @@
1
+ require 'hocon/impl'
2
+
3
+ module Hocon::Impl::OriginType
4
+ ## for now, we only support a subset of these
5
+ GENERIC = 0
6
+ FILE = 1
7
+ #URL = 2
8
+ #RESOURCE = 3
9
+ end
@@ -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