typesafe_config 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/README.md +15 -0
  2. metadata +3 -40
  3. data/lib/typesafe/config/config_error.rb +0 -12
  4. data/lib/typesafe/config/config_factory.rb +0 -9
  5. data/lib/typesafe/config/config_object.rb +0 -4
  6. data/lib/typesafe/config/config_parse_options.rb +0 -53
  7. data/lib/typesafe/config/config_render_options.rb +0 -46
  8. data/lib/typesafe/config/config_syntax.rb +0 -7
  9. data/lib/typesafe/config/config_value_type.rb +0 -26
  10. data/lib/typesafe/config/impl/abstract_config_object.rb +0 -64
  11. data/lib/typesafe/config/impl/abstract_config_value.rb +0 -130
  12. data/lib/typesafe/config/impl/config_concatenation.rb +0 -136
  13. data/lib/typesafe/config/impl/config_float.rb +0 -9
  14. data/lib/typesafe/config/impl/config_impl.rb +0 -10
  15. data/lib/typesafe/config/impl/config_impl_util.rb +0 -78
  16. data/lib/typesafe/config/impl/config_int.rb +0 -31
  17. data/lib/typesafe/config/impl/config_number.rb +0 -27
  18. data/lib/typesafe/config/impl/config_string.rb +0 -37
  19. data/lib/typesafe/config/impl/full_includer.rb +0 -4
  20. data/lib/typesafe/config/impl/origin_type.rb +0 -9
  21. data/lib/typesafe/config/impl/parseable.rb +0 -151
  22. data/lib/typesafe/config/impl/parser.rb +0 -882
  23. data/lib/typesafe/config/impl/path.rb +0 -59
  24. data/lib/typesafe/config/impl/path_builder.rb +0 -36
  25. data/lib/typesafe/config/impl/resolve_status.rb +0 -18
  26. data/lib/typesafe/config/impl/simple_config.rb +0 -11
  27. data/lib/typesafe/config/impl/simple_config_list.rb +0 -70
  28. data/lib/typesafe/config/impl/simple_config_object.rb +0 -178
  29. data/lib/typesafe/config/impl/simple_config_origin.rb +0 -174
  30. data/lib/typesafe/config/impl/simple_include_context.rb +0 -7
  31. data/lib/typesafe/config/impl/simple_includer.rb +0 -19
  32. data/lib/typesafe/config/impl/token.rb +0 -32
  33. data/lib/typesafe/config/impl/token_type.rb +0 -42
  34. data/lib/typesafe/config/impl/tokenizer.rb +0 -370
  35. data/lib/typesafe/config/impl/tokens.rb +0 -157
  36. data/lib/typesafe/config/impl/unmergeable.rb +0 -4
  37. data/lib/typesafe/config/impl.rb +0 -5
  38. data/lib/typesafe/config.rb +0 -4
  39. data/lib/typesafe.rb +0 -2
@@ -1,882 +0,0 @@
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