typesafe_config 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/LICENSE +202 -0
  2. data/README.md +2 -0
  3. data/lib/typesafe/config/config_error.rb +12 -0
  4. data/lib/typesafe/config/config_factory.rb +9 -0
  5. data/lib/typesafe/config/config_object.rb +4 -0
  6. data/lib/typesafe/config/config_parse_options.rb +53 -0
  7. data/lib/typesafe/config/config_render_options.rb +46 -0
  8. data/lib/typesafe/config/config_syntax.rb +7 -0
  9. data/lib/typesafe/config/config_value_type.rb +26 -0
  10. data/lib/typesafe/config/impl/abstract_config_object.rb +64 -0
  11. data/lib/typesafe/config/impl/abstract_config_value.rb +130 -0
  12. data/lib/typesafe/config/impl/config_concatenation.rb +136 -0
  13. data/lib/typesafe/config/impl/config_float.rb +9 -0
  14. data/lib/typesafe/config/impl/config_impl.rb +10 -0
  15. data/lib/typesafe/config/impl/config_impl_util.rb +78 -0
  16. data/lib/typesafe/config/impl/config_int.rb +31 -0
  17. data/lib/typesafe/config/impl/config_number.rb +27 -0
  18. data/lib/typesafe/config/impl/config_string.rb +37 -0
  19. data/lib/typesafe/config/impl/full_includer.rb +4 -0
  20. data/lib/typesafe/config/impl/origin_type.rb +9 -0
  21. data/lib/typesafe/config/impl/parseable.rb +151 -0
  22. data/lib/typesafe/config/impl/parser.rb +882 -0
  23. data/lib/typesafe/config/impl/path.rb +59 -0
  24. data/lib/typesafe/config/impl/path_builder.rb +36 -0
  25. data/lib/typesafe/config/impl/resolve_status.rb +18 -0
  26. data/lib/typesafe/config/impl/simple_config.rb +11 -0
  27. data/lib/typesafe/config/impl/simple_config_list.rb +70 -0
  28. data/lib/typesafe/config/impl/simple_config_object.rb +178 -0
  29. data/lib/typesafe/config/impl/simple_config_origin.rb +174 -0
  30. data/lib/typesafe/config/impl/simple_include_context.rb +7 -0
  31. data/lib/typesafe/config/impl/simple_includer.rb +19 -0
  32. data/lib/typesafe/config/impl/token.rb +32 -0
  33. data/lib/typesafe/config/impl/token_type.rb +42 -0
  34. data/lib/typesafe/config/impl/tokenizer.rb +370 -0
  35. data/lib/typesafe/config/impl/tokens.rb +157 -0
  36. data/lib/typesafe/config/impl/unmergeable.rb +4 -0
  37. data/lib/typesafe/config/impl.rb +5 -0
  38. data/lib/typesafe/config.rb +4 -0
  39. data/lib/typesafe.rb +2 -0
  40. metadata +85 -0
@@ -0,0 +1,882 @@
1
+ require 'stringio'
2
+ require 'typesafe/config/impl'
3
+ require 'typesafe/config/impl/tokens'
4
+ require 'typesafe/config/impl/path_builder'
5
+ require 'typesafe/config/config_syntax'
6
+ require 'typesafe/config/config_value_type'
7
+ require 'typesafe/config/impl/config_string'
8
+ require 'typesafe/config/impl/config_concatenation'
9
+ require 'typesafe/config/config_error'
10
+ require 'typesafe/config/impl/simple_config_list'
11
+ require 'typesafe/config/impl/simple_config_object'
12
+
13
+ class Typesafe::Config::Impl::Parser
14
+
15
+ Tokens = Typesafe::Config::Impl::Tokens
16
+ ConfigSyntax = Typesafe::Config::ConfigSyntax
17
+ ConfigValueType = Typesafe::Config::ConfigValueType
18
+ ConfigConcatenation = Typesafe::Config::Impl::ConfigConcatenation
19
+ ConfigParseError = Typesafe::Config::ConfigError::ConfigParseError
20
+ SimpleConfigObject = Typesafe::Config::Impl::SimpleConfigObject
21
+ SimpleConfigList = Typesafe::Config::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 = Typesafe::Config::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 = Typesafe::Config::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 = Typesafe::Config::Impl::Parser::ParseContext.new(
877
+ options.syntax, origin, tokens,
878
+ Typesafe::Config::Impl::SimpleIncluder.make_full(options.includer),
879
+ include_context)
880
+ context.parse
881
+ end
882
+ end