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
@@ -0,0 +1,274 @@
1
+ require 'hocon/impl'
2
+ require 'hocon/impl/unmergeable'
3
+ require 'hocon/impl/replaceable_merge_stack'
4
+
5
+ # This is just like ConfigDelayedMerge except we know statically
6
+ # that it will turn out to be an object.
7
+ class Hocon::Impl::ConfigDelayedMergeObject
8
+ include Hocon::Impl::Unmergeable
9
+ include Hocon::Impl::ReplaceableMergeStack
10
+ include Hocon::Impl::AbstractConfigObject
11
+
12
+ def initialize(origin, stack)
13
+ super(origin)
14
+
15
+ @stack = stack
16
+
17
+ if stack.empty?
18
+ raise Hocon::ConfigError::ConfigBugOrBrokenError.new("creating empty delayed merge value", nil)
19
+ end
20
+
21
+ if !@stack[0].is_a? Hocon::Impl::AbstractConfigObject
22
+ error_message = "created a delayed merge object not guaranteed to be an object"
23
+ raise Hocon::ConfigError::ConfigBugOrBrokenError.new(error_message, nil)
24
+ end
25
+
26
+ stack.each do |v|
27
+ if v.is_a?(Hocon::Impl::ConfigDelayedMergeObject) || v.is_a?(Hocon::Impl::ConfigDelayedMergeObject)
28
+ error_message = "placed nested DelayedMerge in a ConfigDelayedMerge, should have consolidated stack"
29
+ raise Hocon::ConfigError::ConfigBugOrBrokenError.new(error_message, nil)
30
+ end
31
+ end
32
+ end
33
+
34
+ attr_reader :stack
35
+
36
+ def new_copy(status, origin)
37
+ if status != resolve_status
38
+ raise Hocon::ConfigError::ConfigBugOrBrokenError.new(
39
+ "attempt to create resolved ConfigDelayedMergeObject")
40
+ end
41
+ Hocon::Impl::ConfigDelayedMergeObject.new(origin, @stack)
42
+ end
43
+
44
+ def resolve_substitutions(context, source)
45
+ merged = Hocon::Impl::ConfigDelayedMerge.resolve_substitutions(self, @stack, context, source)
46
+ merged.as_object_result
47
+ end
48
+
49
+ def make_replacement(context, skipping)
50
+ Hocon::Impl::ConfigDelayedMerge.make_replacement(context, @stack, skipping)
51
+ end
52
+
53
+ def resolve_status
54
+ Hocon::Impl::ResolveStatus::UNRESOLVED
55
+ end
56
+
57
+ def replace_child(child, replacement)
58
+ new_stack = Hocon::Impl::AbstractConfigValue.replace_child_in_list(@stack, child, replacement)
59
+ if new_stack == nil
60
+ nil
61
+ else
62
+ self.class.new(origin, new_stack)
63
+ end
64
+ end
65
+
66
+ def has_descendant?(descendant)
67
+ Hocon::Impl::AbstractConfigValue.has_descendant_in_list?(@stack, descendant)
68
+ end
69
+
70
+ def relativized(prefix)
71
+ new_stack = []
72
+ @stack.each { |o|
73
+ new_stack << o.relativized(prefix)
74
+ }
75
+ self.class.new(origin, new_stack)
76
+ end
77
+
78
+ def ignores_fallbacks?
79
+ Hocon::Impl::ConfigDelayedMerge.stack_ignores_fallbacks?(@stack)
80
+ end
81
+
82
+ def merged_with_the_unmergeable(fallback)
83
+ require_not_ignoring_fallbacks
84
+
85
+ merged_stack_with_the_unmergeable(@stack, fallback)
86
+ end
87
+
88
+ def merged_with_object(fallback)
89
+ merged_with_non_object(fallback)
90
+ end
91
+
92
+ def merged_with_non_object(fallback)
93
+ require_not_ignoring_fallbacks
94
+
95
+ merged_stack_with_non_object(@stack, fallback)
96
+ end
97
+
98
+ # No implementation of withFallback here,
99
+ # just use the implementation in the super-class
100
+
101
+ def with_only_key(key)
102
+ raise self.class.not_resolved
103
+ end
104
+
105
+ def without_key(key)
106
+ raise self.class.not_resolved
107
+ end
108
+
109
+ def with_only_path_or_nil(key)
110
+ raise self.class.not_resolved
111
+ end
112
+
113
+ def with_only_path(key)
114
+ raise self.class.not_resolved
115
+ end
116
+
117
+ def without_path(key)
118
+ raise self.class.not_resolved
119
+ end
120
+
121
+ def with_value(key_or_path, value = nil)
122
+ raise self.class.not_resolved
123
+ end
124
+
125
+ def unmerged_values
126
+ @stack
127
+ end
128
+
129
+ def can_equal(other)
130
+ other.is_a? Hocon::Impl::ConfigDelayedMergeObject
131
+ end
132
+
133
+ def ==(other)
134
+ # note that "origin" is deliberately NOT part of equality
135
+ if other.is_a? Hocon::Impl::ConfigDelayedMergeObject
136
+ can_equal(other) && (@stack == other.stack || @stack.equal?(other.stack))
137
+ else
138
+ false
139
+ end
140
+ end
141
+
142
+ def hash
143
+ # note that "origin" is deliberately NOT part of equality
144
+ @stack.hash
145
+ end
146
+
147
+ def render_to_sb(sb, indent, at_root, at_key, options)
148
+ Hocon::Impl::ConfigDelayedMerge.render_value_to_sb_from_stack(@stack, sb, indent, at_root, at_key, options)
149
+ end
150
+
151
+ def self.not_resolved
152
+ error_message = "need to Config#resolve() before using this object, see the API docs for Config#resolve()"
153
+ Hocon::ConfigError::ConfigNotResolvedError.new(error_message, nil)
154
+ end
155
+
156
+ def unwrapped
157
+ raise self.class.not_resolved
158
+ end
159
+
160
+ def [](key)
161
+ raise self.class.not_resolved
162
+ end
163
+
164
+ def has_key?(key)
165
+ raise self.class.not_resolved
166
+ end
167
+
168
+ def has_value?(value)
169
+ raise self.class.not_resolved
170
+ end
171
+
172
+ def each
173
+ raise self.class.not_resolved
174
+ end
175
+
176
+ def empty?
177
+ raise self.class.not_resolved
178
+ end
179
+
180
+ def keys
181
+ raise self.class.not_resolved
182
+ end
183
+
184
+ def values
185
+ raise self.class.not_resolved
186
+ end
187
+
188
+ def size
189
+ raise self.class.not_resolved
190
+ end
191
+
192
+ def self.unmergeable?(object)
193
+ # Ruby note: This is the best way I could find to simulate
194
+ # else if (layer instanceof Unmergeable) in java since we're including
195
+ # the Unmergeable module instead of extending an Unmergeable class
196
+ object.class.included_modules.include?(Hocon::Impl::Unmergeable)
197
+ end
198
+
199
+ def attempt_peek_with_partial_resolve(key)
200
+ # a partial resolve of a ConfigDelayedMergeObject always results in a
201
+ # SimpleConfigObject because all the substitutions in the stack get
202
+ # resolved in order to look up the partial.
203
+ # So we know here that we have not been resolved at all even
204
+ # partially.
205
+ # Given that, all this code is probably gratuitous, since the app code
206
+ # is likely broken. But in general we only throw NotResolved if you try
207
+ # to touch the exact key that isn't resolved, so this is in that
208
+ # spirit.
209
+
210
+ # we'll be able to return a key if we have a value that ignores
211
+ # fallbacks, prior to any unmergeable values.
212
+ @stack.each do |layer|
213
+ if layer.is_a?(Hocon::Impl::AbstractConfigObject)
214
+ v = layer.attempt_peek_with_partial_resolve(key)
215
+
216
+ if !v.nil?
217
+ if v.ignores_fallbacks?
218
+ # we know we won't need to merge anything in to this
219
+ # value
220
+ return v
221
+ else
222
+ # we can't return this value because we know there are
223
+ # unmergeable values later in the stack that may
224
+ # contain values that need to be merged with this
225
+ # value. we'll throw the exception when we get to those
226
+ # unmergeable values, so continue here.
227
+ next
228
+ end
229
+ elsif self.class.unmergeable?(layer)
230
+ error_message = "should not be reached: unmergeable object returned null value"
231
+ raise Hocon::ConfigError::ConfigBugOrBrokenError.new(error_message, nil)
232
+ else
233
+ # a non-unmergeable AbstractConfigObject that returned null
234
+ # for the key in question is not relevant, we can keep
235
+ # looking for a value.
236
+ next
237
+ end
238
+ elsif self.class.unmergeable?(layer)
239
+ error_message = "Key '#{key}' is not available at '#{origin.description}'" +
240
+ "because value at '#{layer.origin.description}' has not been resolved" +
241
+ " and may turn out to contain or hide '#{key}'. Be sure to Config#resolve()" +
242
+ " before using a config object"
243
+ raise Hocon::ConfigError::ConfigNotResolvedError.new(error_message, nil)
244
+ elsif layer.resolved_status == ResolveStatus::UNRESOLVED
245
+ # if the layer is not an object, and not a substitution or
246
+ # merge,
247
+ # then it's something that's unresolved because it _contains_
248
+ # an unresolved object... i.e. it's an array
249
+ if !layer.is_a?(Hocon::Impl::ConfigList)
250
+ error_message = "Expecting a list here, not #{layer}"
251
+ raise Hocon::ConfigError::ConfigBugOrBrokenError.new(error_message, nil)
252
+ end
253
+ return nil
254
+ else
255
+ # non-object, but resolved, like an integer or something.
256
+ # has no children so the one we're after won't be in it.
257
+ # we would only have this in the stack in case something
258
+ # else "looks back" to it due to a cycle.
259
+ # anyway at this point we know we can't find the key anymore.
260
+ if !layer.ignores_fallbacks?
261
+ error_message = "resolved non-object should ignore fallbacks"
262
+ raise Hocon::ConfigError::ConfigBugOrBrokenError.new(error_message, nil)
263
+ end
264
+ return nil
265
+ end
266
+ end
267
+
268
+ # If we get here, then we never found anything unresolved which means
269
+ # the ConfigDelayedMergeObject should not have existed. some
270
+ # invariant was violated.
271
+ error_message = "Delayed merge stack does not contain any unmergeable values"
272
+ raise Hocon::ConfigError::ConfigBugOrBrokenError.new(error_message, nil)
273
+ end
274
+ end
@@ -0,0 +1,647 @@
1
+ # encoding: utf-8
2
+
3
+ require 'stringio'
4
+ require 'hocon/impl'
5
+ require 'hocon/config_error'
6
+ require 'hocon/impl/tokens'
7
+ require 'hocon/impl/config_node_single_token'
8
+ require 'hocon/impl/config_node_comment'
9
+ require 'hocon/impl/abstract_config_node_value'
10
+ require 'hocon/impl/config_node_concatenation'
11
+ require 'hocon/impl/config_include_kind'
12
+ require 'hocon/impl/config_node_object'
13
+ require 'hocon/impl/config_node_array'
14
+ require 'hocon/impl/config_node_root'
15
+
16
+ class Hocon::Impl::ConfigDocumentParser
17
+
18
+ ConfigSyntax = Hocon::ConfigSyntax
19
+ ConfigParseError = Hocon::ConfigError::ConfigParseError
20
+ ConfigBugOrBrokenError = Hocon::ConfigError::ConfigBugOrBrokenError
21
+ ConfigValueType = Hocon::ConfigValueType
22
+ Tokens = Hocon::Impl::Tokens
23
+ PathParser = Hocon::Impl::PathParser
24
+ ArrayIterator = Hocon::Impl::ArrayIterator
25
+ ConfigImplUtil = Hocon::Impl::ConfigImplUtil
26
+ ConfigIncludeKind = Hocon::Impl::ConfigIncludeKind
27
+ ConfigNodeSingleToken = Hocon::Impl::ConfigNodeSingleToken
28
+ ConfigNodeSimpleValue = Hocon::Impl::ConfigNodeSimpleValue
29
+ ConfigNodeInclude = Hocon::Impl::ConfigNodeInclude
30
+ ConfigNodeField = Hocon::Impl::ConfigNodeField
31
+ ConfigNodeObject = Hocon::Impl::ConfigNodeObject
32
+ ConfigNodeArray = Hocon::Impl::ConfigNodeArray
33
+ ConfigNodeRoot = Hocon::Impl::ConfigNodeRoot
34
+
35
+ def self.parse(tokens, origin, options)
36
+ syntax = options.syntax.nil? ? ConfigSyntax::CONF : options.syntax
37
+ context = Hocon::Impl::ConfigDocumentParser::ParseContext.new(syntax, origin, tokens)
38
+ context.parse
39
+ end
40
+
41
+ def self.parse_value(tokens, origin, options)
42
+ syntax = options.syntax.nil? ? ConfigSyntax::CONF : options.syntax
43
+ context = Hocon::Impl::ConfigDocumentParser::ParseContext.new(syntax, origin, tokens)
44
+ context.parse_single_value
45
+ end
46
+
47
+ class ParseContext
48
+ def initialize(flavor, origin, tokens)
49
+ @line_number = 1
50
+ @buffer = []
51
+ @tokens = tokens
52
+ @flavor = flavor
53
+ @equals_count = 0
54
+ @base_origin = origin
55
+ end
56
+
57
+ def pop_token
58
+ if @buffer.empty?
59
+ return @tokens.next
60
+ end
61
+ @buffer.pop
62
+ end
63
+
64
+ def next_token
65
+ t = pop_token
66
+ if @flavor.equal?(ConfigSyntax::JSON)
67
+ if Tokens.unquoted_text?(t) && !unquoted_whitespace?(t)
68
+ raise parse_error("Token not allowed in valid JSON: '#{Tokens.unquoted_text(t)}'")
69
+ elsif Tokens.substitution?(t)
70
+ raise parse_error("Substitutions (${} syntax) not allowed in JSON")
71
+ end
72
+ end
73
+ t
74
+ end
75
+
76
+ def next_token_collecting_whitespace(nodes)
77
+ while true
78
+ t = next_token
79
+ if Tokens.ignored_whitespace?(t) || Tokens.newline?(t) || unquoted_whitespace?(t)
80
+ nodes.push(ConfigNodeSingleToken.new(t))
81
+ if Tokens.newline?(t)
82
+ @line_number = t.line_number + 1
83
+ end
84
+ elsif Tokens.comment?(t)
85
+ nodes.push(Hocon::Impl::ConfigNodeComment.new(t))
86
+ else
87
+ new_number = t.line_number
88
+ if new_number >= 0
89
+ @line_number = new_number
90
+ end
91
+ return t
92
+ end
93
+ end
94
+ end
95
+
96
+ def put_back(token)
97
+ @buffer.push(token)
98
+ end
99
+
100
+ # In arrays and objects, comma can be omitted
101
+ # as long as there's at least one newline instead.
102
+ # this skips any newlines in front of a comma,
103
+ # skips the comma, and returns true if it found
104
+ # either a newline or a comma. The iterator
105
+ # is left just after the comma or the newline.
106
+ def check_element_separator(nodes)
107
+ if @flavor.equal?(ConfigSyntax::JSON)
108
+ t = next_token_collecting_whitespace(nodes)
109
+ if t.equal?(Tokens::COMMA)
110
+ nodes.push(ConfigNodeSingleToken.new(t))
111
+ return true
112
+ else
113
+ put_back(t)
114
+ return false
115
+ end
116
+ else
117
+ saw_separator_or_new_line = false
118
+ t = next_token
119
+ while true
120
+ if Tokens.ignored_whitespace?(t) || unquoted_whitespace?(t)
121
+ nodes.push(ConfigNodeSingleToken.new(t))
122
+ elsif Tokens.comment?(t)
123
+ nodes.push(Hocon::Impl::ConfigNodeComment.new(t))
124
+ elsif Tokens.newline?(t)
125
+ saw_separator_or_new_line = true
126
+ @line_number += 1
127
+ nodes.push(ConfigNodeSingleToken.new(t))
128
+ # we want to continue to also eat
129
+ # a comma if there is one.
130
+ elsif t.equal?(Tokens::COMMA)
131
+ nodes.push(ConfigNodeSingleToken.new(t))
132
+ return true
133
+ else
134
+ # non-newline-or-comma
135
+ put_back(t)
136
+ return saw_separator_or_new_line
137
+ end
138
+ t = next_token
139
+ end
140
+ end
141
+ end
142
+
143
+ # parse a concatenation. If there is no concatenation, return the next value
144
+ def consolidate_values(nodes)
145
+ # this trick is not done in JSON
146
+ if @flavor.equal?(ConfigSyntax::JSON)
147
+ return nil
148
+ end
149
+
150
+ # create only if we have value tokens
151
+ values = []
152
+ value_count = 0
153
+
154
+ # ignore a newline up front
155
+ t = next_token_collecting_whitespace(nodes)
156
+ while true
157
+ v = nil
158
+ if Tokens.ignored_whitespace?(t)
159
+ values.push(ConfigNodeSingleToken.new(t))
160
+ t = next_token
161
+ next
162
+ elsif Tokens.value?(t) || Tokens.unquoted_text?(t) || Tokens.substitution?(t) || t == Tokens::OPEN_CURLY || t == Tokens::OPEN_SQUARE
163
+ # there may be newlines _within_ the objects and arrays
164
+ v = parse_value(t)
165
+ value_count += 1
166
+ else
167
+ break
168
+ end
169
+
170
+ if v.nil?
171
+ raise ConfigBugOrBrokenError, "no value"
172
+ end
173
+
174
+ values.push(v)
175
+ t = next_token # but don't consolidate across a newline
176
+ end
177
+
178
+ put_back(t)
179
+
180
+ # No concatenation was seen, but a single value may have been parsed, so return it, and put back
181
+ # all succeeding tokens
182
+ if value_count < 2
183
+ value = nil
184
+ values.each do |node|
185
+ if node.is_a?(Hocon::Impl::AbstractConfigNodeValue)
186
+ value = node
187
+ elsif value.nil?
188
+ nodes.add(node)
189
+ else
190
+ put_back(node.tokens[0])
191
+ end
192
+ end
193
+ return value
194
+ end
195
+
196
+ # Put back any trailing whitespace, as the parent object is responsible for tracking
197
+ # any leading/trailing whitespace
198
+ for i in (0..values.size - 1).reverse_each
199
+ if values[i].is_a?(ConfigNodeSingleToken)
200
+ put_back(values[i].token)
201
+ values.delete_at(i)
202
+ else
203
+ break
204
+ end
205
+ end
206
+ Hocon::Impl::ConfigNodeConcatenation.new(values)
207
+ end
208
+
209
+ def parse_error(message, cause = nil)
210
+ ConfigParseError.new(@base_origin.with_line_number(@line_number), message, cause)
211
+ end
212
+
213
+ def add_quote_suggestion(bad_token, message, last_path = nil, inside_equals = nil)
214
+ if inside_equals.nil?
215
+ inside_equals = @equals_count > 0
216
+ end
217
+
218
+ previous_field_name = last_path != nil ? last_path.render : nil
219
+
220
+ if bad_token == Tokens::EOF.to_s
221
+ # EOF requires special handling for the error to make sense.
222
+ if previous_field_name != nil
223
+ part = "#{message} (if you intended '#{previous_field_name}'" +
224
+ "' to be part of a value, instead of a key, " +
225
+ "try adding double quotes around the whole value"
226
+ else
227
+ return message
228
+ end
229
+ else
230
+ if previous_field_name != nil
231
+ part = "#{message} (if you intended #{bad_token}" +
232
+ " to be part of the value for '#{previous_field_name}', " +
233
+ "try enclosing the value in double quotes"
234
+ else
235
+ part = "#{message} (if you intended #{bad_token}" +
236
+ " to be part of a key or string value, " +
237
+ "try enclosing the key or value in double quotes"
238
+ end
239
+ end
240
+
241
+ # Don't have a special case to throw a message about changing the file to .properties, since
242
+ # we don't support that format
243
+ part
244
+ end
245
+
246
+ def parse_value(t)
247
+ v = nil
248
+ starting_equals_count = @equals_count
249
+
250
+ if Tokens.value?(t) || Tokens.unquoted_text?(t) || Tokens.substitution?(t)
251
+ v = Hocon::Impl::ConfigNodeSimpleValue.new(t)
252
+ elsif t.equal?(Tokens::OPEN_CURLY)
253
+ v = parse_object(true)
254
+ elsif t.equal?(Tokens::OPEN_SQUARE)
255
+ v = parse_array
256
+ else
257
+ raise parse_error(add_quote_suggestion(t.to_s, "Expecting a value but got wrong token: #{t}"))
258
+ end
259
+
260
+ if @equals_count != starting_equals_count
261
+ raise ConfigBugOrBrokenError, "Bug in config parser: unbalanced equals count"
262
+ end
263
+
264
+ v
265
+ end
266
+
267
+ def parse_key(token)
268
+ if @flavor.equal?(ConfigSyntax::JSON)
269
+ if Tokens.value_with_type?(token, ConfigValueType::STRING)
270
+ return PathParser.parse_path_node_expression(Hocon::Impl::ArrayIterator.new([token]), nil)
271
+ else
272
+ raise ConfigParseError, "Expecting close brace } or a field name here, got #{token}"
273
+ end
274
+ else
275
+ expression = []
276
+ t = token
277
+ while Tokens.value?(t) || Tokens.unquoted_text?(t)
278
+ expression.push(t)
279
+ t = next_token # note: don't cross a newline
280
+ end
281
+
282
+ if expression.empty?
283
+ raise parse_error("expecting a close brace or a field name here, got #{t}")
284
+ end
285
+
286
+ put_back(t) # put back the token we ended with
287
+ PathParser.parse_path_node_expression(ArrayIterator.new(expression), nil)
288
+ end
289
+ end
290
+
291
+ def include_keyword?(t)
292
+ Tokens.unquoted_text?(t) && Tokens.unquoted_text(t) == "include"
293
+ end
294
+
295
+ def unquoted_whitespace?(t)
296
+ unless Tokens.unquoted_text?(t)
297
+ return false
298
+ end
299
+
300
+ s = Tokens.unquoted_text(t)
301
+
302
+ s.each_char do |c|
303
+ unless ConfigImplUtil.whitespace?(c)
304
+ return false
305
+ end
306
+ end
307
+ true
308
+ end
309
+
310
+ def key_value_separator?(t)
311
+ if @flavor.equal?(ConfigSyntax::JSON)
312
+ t.equal?(Tokens::COLON)
313
+ else
314
+ t.equal?(Tokens::COLON) || t.equal?(Tokens::EQUALS) || t.equal?(Tokens::PLUS_EQUALS)
315
+ end
316
+ end
317
+
318
+ def parse_include(children)
319
+ t = next_token_collecting_whitespace(children)
320
+
321
+ # we either have a quoted string or the "file()" syntax
322
+ if Tokens.unquoted_text?(t)
323
+ # get foo(
324
+ kind_text = Tokens.unquoted_text(t)
325
+
326
+ if kind_text == "url("
327
+ kind = ConfigIncludeKind::URL
328
+ elsif kind_text == "file("
329
+ kind = ConfigIncludeKind::FILE
330
+ elsif kind_text == "classpath("
331
+ kind = ConfigIncludeKind::CLASSPATH
332
+ else
333
+ raise parse_error("expecting include parameter to be quoted filename, file(), classpath(), or url(). No spaces are allowed before the open paren. Not expecting: #{t}")
334
+ end
335
+
336
+ children.push(ConfigNodeSingleToken.new(t))
337
+
338
+ # skip space inside parens
339
+ t = next_token_collecting_whitespace(children)
340
+
341
+ # quoted string
342
+ unless Tokens.value_with_type?(t, ConfigValueType::STRING)
343
+ raise parse_error("expecting a quoted string inside file(), classpath(), or url(), rather than: #{t}")
344
+ end
345
+ children.push(ConfigNodeSimpleValue.new(t))
346
+ # skip space after string, inside parens
347
+ t = next_token_collecting_whitespace(children)
348
+
349
+ if Tokens.unquoted_text?(t) && Tokens.unquoted_text(t) == ")"
350
+ # OK, close paren
351
+ else
352
+ raise parse_error("expecting a close parentheses ')' here, not: #{t}")
353
+ end
354
+ ConfigNodeInclude.new(children, kind)
355
+ elsif Tokens.value_with_type?(t, ConfigValueType::STRING)
356
+ children.push(ConfigNodeSimpleValue.new(t))
357
+ ConfigNodeInclude.new(children, ConfigIncludeKind::HEURISTIC)
358
+ else
359
+ raise parse_error("include keyword is not followed by a quoted string, but by: #{t}")
360
+ end
361
+ end
362
+
363
+ def parse_object(had_open_curly)
364
+ # invoked just after the OPEN_CURLY (or START, if !hadOpenCurly)
365
+ after_comma = false
366
+ last_path = nil
367
+ last_inside_equals = false
368
+ object_nodes = []
369
+ keys = Hash.new
370
+ if had_open_curly
371
+ object_nodes.push(ConfigNodeSingleToken.new(Tokens::OPEN_CURLY))
372
+ end
373
+
374
+ while true
375
+ t = next_token_collecting_whitespace(object_nodes)
376
+ if t.equal?(Tokens::CLOSE_CURLY)
377
+ if @flavor.equal?(ConfigSyntax::JSON) && after_comma
378
+ raise parse_error(add_quote_suggestion(t.to_s,
379
+ "expecting a field name after a comma, got a close brace } instead"))
380
+ elsif !had_open_curly
381
+ raise parse_error(add_quote_suggestion(t.to_s,
382
+ "unbalanced close brace '}' with no open brace"))
383
+ end
384
+ object_nodes.push(ConfigNodeSingleToken.new(Tokens::CLOSE_CURLY))
385
+ break
386
+ elsif t.equal?(Tokens::EOF) && !had_open_curly
387
+ put_back(t)
388
+ break
389
+ elsif !@flavor.equal?(ConfigSyntax::JSON) && include_keyword?(t)
390
+ include_nodes = []
391
+ include_nodes.push(ConfigNodeSingleToken.new(t))
392
+ object_nodes.push(parse_include(include_nodes))
393
+ after_comma = false
394
+ else
395
+ key_value_nodes = []
396
+ key_token = t
397
+ path = parse_key(key_token)
398
+ key_value_nodes.push(path)
399
+ after_key = next_token_collecting_whitespace(key_value_nodes)
400
+ inside_equals = false
401
+
402
+ if @flavor.equal?(ConfigSyntax::CONF) && after_key.equal?(Tokens::OPEN_CURLY)
403
+ # can omit the ':' or '=' before an object value
404
+ next_value = parse_value(after_key)
405
+ else
406
+ unless key_value_separator?(after_key)
407
+ raise parse_error(add_quote_suggestion(after_key.to_s,
408
+ "Key '#{path.render()}' may not be followed by token: #{after_key}"))
409
+ end
410
+
411
+ key_value_nodes.push(ConfigNodeSingleToken.new(after_key))
412
+
413
+ if after_key.equal?(Tokens::EQUALS)
414
+ inside_equals = true
415
+ @equals_count += 1
416
+ end
417
+
418
+ next_value = consolidate_values(key_value_nodes)
419
+ if next_value.nil?
420
+ next_value = parse_value(next_token_collecting_whitespace(key_value_nodes))
421
+ end
422
+ end
423
+
424
+ key_value_nodes.push(next_value)
425
+ if inside_equals
426
+ @equals_count -= 1
427
+ end
428
+ last_inside_equals = inside_equals
429
+
430
+ key = path.value.first
431
+ remaining = path.value.remainder
432
+
433
+ if remaining.nil?
434
+ existing = keys[key]
435
+ unless existing.nil?
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.equal?(ConfigSyntax::JSON)
442
+ raise parse_error("JSON does not allow duplicate fields: '#{key}' was already seen")
443
+ end
444
+ end
445
+ keys[key] = true
446
+ else
447
+ if @flavor.equal?(ConfigSyntax::JSON)
448
+ raise ConfigBugOrBrokenError, "somehow got multi-element path in JSON mode"
449
+ end
450
+ keys[key] = true
451
+ end
452
+
453
+ after_comma = false
454
+ object_nodes.push(ConfigNodeField.new(key_value_nodes))
455
+ end
456
+
457
+ if check_element_separator(object_nodes)
458
+ # continue looping
459
+ after_comma = true
460
+ else
461
+ t = next_token_collecting_whitespace(object_nodes)
462
+ if t.equal?(Tokens::CLOSE_CURLY)
463
+ unless had_open_curly
464
+ raise parse_error(add_quote_suggestion(t.to_s,
465
+ "unbalanced close brace '}' with no open brace",
466
+ last_path,
467
+ last_inside_equals,))
468
+ end
469
+ object_nodes.push(ConfigNodeSingleToken.new(t))
470
+ break
471
+ elsif had_open_curly
472
+ raise parse_error(add_quote_suggestion(t.to_s,
473
+ "Expecting close brace } or a comma, got #{t}",
474
+ last_path,
475
+ last_inside_equals,))
476
+ else
477
+ if t.equal?(Tokens::EOF)
478
+ put_back(t)
479
+ break
480
+ else
481
+ raise parse_error(add_quote_suggestion(t.to_s,
482
+ "Expecting close brace } or a comma, got #{t}",
483
+ last_path,
484
+ last_inside_equals,))
485
+ end
486
+ end
487
+ end
488
+ end
489
+
490
+ ConfigNodeObject.new(object_nodes)
491
+ end
492
+
493
+ def parse_array
494
+ children = []
495
+ children.push(ConfigNodeSingleToken.new(Tokens::OPEN_SQUARE))
496
+ # invoked just after the OPEN_SQUARE
497
+ t = nil
498
+
499
+ next_value = consolidate_values(children)
500
+ unless next_value.nil?
501
+ children.push(next_value)
502
+ else
503
+ t = next_token_collecting_whitespace(children)
504
+
505
+ # special-case the first element
506
+ if t.equal?(Tokens::CLOSE_SQUARE)
507
+ children.push(ConfigNodeSingleToken.new(t))
508
+ return ConfigNodeArray.new(children)
509
+ elsif Tokens.value?(t) || t.equal?(Tokens::OPEN_CURLY) ||
510
+ t.equal?(Tokens::OPEN_SQUARE) || Tokens.unquoted_text?(t) ||
511
+ Tokens.substitution?(t)
512
+ next_value = parse_value(t)
513
+ children.push(next_value)
514
+ else
515
+ raise parse_error("List should have ] or a first element after the open [, instead had token: #{t}" +
516
+ " (if you want #{t} to be part of a string value, then double-quote it)")
517
+ end
518
+ end
519
+
520
+ # now remaining elements
521
+ while true
522
+ # just after a value
523
+ if check_element_separator(children)
524
+ # comma (or newline equivalent) consumed
525
+ else
526
+ t = next_token_collecting_whitespace(children)
527
+ if t.equal?(Tokens::CLOSE_SQUARE)
528
+ children.push(ConfigNodeSingleToken.new(t))
529
+ return ConfigNodeArray.new(children)
530
+ else
531
+ raise parse_error("List should have ended with ] or had a comma, instead had token: #{t}" +
532
+ " (if you want #{t} to be part of a string value, then double-quote it)")
533
+ end
534
+ end
535
+
536
+ # now just after a comma
537
+ next_value = consolidate_values(children)
538
+ unless next_value.nil?
539
+ children.push(next_value)
540
+ else
541
+ t = next_token_collecting_whitespace(children)
542
+ if Tokens.value?(t) || t.equal?(Tokens::OPEN_CURLY) ||
543
+ t.equal?(Tokens::OPEN_SQUARE) || Tokens.unquoted_text?(t) ||
544
+ Tokens.substitution?(t)
545
+ next_value = parse_value(t)
546
+ children.push(next_value)
547
+ elsif !@flavor.equal?(ConfigSyntax::JSON) && t.equal?(Tokens::CLOSE_SQUARE)
548
+ # we allow one trailing comma
549
+ put_back(t)
550
+ else
551
+ raise parse_error("List should have had new element after a comma, instead had token: #{t}" +
552
+ " (if you want the comma or #{t} to be part of a string value, then double-quote it)")
553
+ end
554
+ end
555
+ end
556
+ end
557
+
558
+ def parse
559
+ children = []
560
+ t = next_token
561
+ if t.equal?(Tokens::START)
562
+ # OK
563
+ else
564
+ raise ConfigBugOrBrokenException, "token stream did not begin with START, had #{t}"
565
+ end
566
+
567
+ t = next_token_collecting_whitespace(children)
568
+ result = nil
569
+ missing_curly = false
570
+ if t.equal?(Tokens::OPEN_CURLY) || t.equal?(Tokens::OPEN_SQUARE)
571
+ result = parse_value(t)
572
+ else
573
+ if @flavor.equal?(ConfigSyntax::JSON)
574
+ if t.equal?(Tokens::EOF)
575
+ raise parse_error("Empty document")
576
+ else
577
+ raise parse_error("Document must have an object or array at root, unexpected token: #{t}")
578
+ end
579
+ else
580
+ # the root object can omit the surrounding braces.
581
+ # this token should be the first field's key, or part
582
+ # of it, so put it back.
583
+ put_back(t)
584
+ missing_curly = true
585
+ result = parse_object(false)
586
+ end
587
+ end
588
+
589
+ # Need to pull the children out of the resulting node so we can keep leading
590
+ # and trailing whitespace if this was a no-brace object. Otherwise, we need to add
591
+ # the result into the list of children.
592
+ if result.is_a?(ConfigNodeObject) && missing_curly
593
+ children += result.children
594
+ else
595
+ children.push(result)
596
+ end
597
+ t = next_token_collecting_whitespace(children)
598
+ if t.equal?(Tokens::EOF)
599
+ if missing_curly
600
+ # If there were no braces, the entire document should be treated as a single object
601
+ ConfigNodeRoot.new([ConfigNodeObject.new(children)], @base_origin)
602
+ else
603
+ ConfigNodeRoot.new(children, @base_origin)
604
+ end
605
+ else
606
+ raise parse_error("Document has trailing tokens after first object or array: #{t}")
607
+ end
608
+ end
609
+
610
+ # Parse a given input stream into a single value node. Used when doing a replace inside a ConfigDocument.
611
+ def parse_single_value
612
+ t = next_token
613
+ if t.equal?(Tokens::START)
614
+ # OK
615
+ else
616
+ raise ConfigBugOrBrokenError, "token stream did not begin with START, had #{t}"
617
+ end
618
+
619
+ t = next_token
620
+ if Tokens.ignored_whitespace?(t) || Tokens.newline?(t) || unquoted_whitespace?(t) || Tokens.comment?(t)
621
+ raise parse_error("The value from setValue cannot have leading or trailing newlines, whitespace, or comments")
622
+ end
623
+ if t.equal?(Tokens::EOF)
624
+ raise parse_error("Empty value")
625
+ end
626
+ if @flavor.equal?(ConfigSyntax::JSON)
627
+ node = parse_value(t)
628
+ t = next_token
629
+ if t.equal?(Tokens::EOF)
630
+ return node
631
+ else
632
+ raise parse_error("Parsing JSON and the value set in setValue was either a concatenation or had trailing whitespace, newlines, or comments")
633
+ end
634
+ else
635
+ put_back(t)
636
+ nodes = []
637
+ node = consolidate_values(nodes)
638
+ t = next_token
639
+ if t.equal?(Tokens::EOF)
640
+ node
641
+ else
642
+ raise parse_error("The value from setValue cannot have leading or trailing newlines, whitespace, or comments")
643
+ end
644
+ end
645
+ end
646
+ end
647
+ end