hocon 0.0.7 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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