hocon 0.0.7 → 0.1.0

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