File: config_concatenation.rb

package info (click to toggle)
ruby-hocon 1.4.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 768 kB
  • sloc: ruby: 7,903; makefile: 4
file content (285 lines) | stat: -rw-r--r-- 8,813 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# encoding: utf-8

require_relative '../../hocon/impl'
require_relative '../../hocon/impl/abstract_config_value'
require_relative '../../hocon/impl/abstract_config_object'
require_relative '../../hocon/impl/simple_config_list'
require_relative '../../hocon/config_object'
require_relative '../../hocon/impl/unmergeable'
require_relative '../../hocon/impl/simple_config_origin'
require_relative '../../hocon/impl/config_string'
require_relative '../../hocon/impl/container'

class Hocon::Impl::ConfigConcatenation
  include Hocon::Impl::Unmergeable
  include Hocon::Impl::Container
  include Hocon::Impl::AbstractConfigValue

  SimpleConfigList = Hocon::Impl::SimpleConfigList
  ConfigObject = Hocon::ConfigObject
  ConfigString = Hocon::Impl::ConfigString
  ResolveStatus = Hocon::Impl::ResolveStatus
  Unmergeable = Hocon::Impl::Unmergeable
  SimpleConfigOrigin = Hocon::Impl::SimpleConfigOrigin
  ConfigBugOrBrokenError = Hocon::ConfigError::ConfigBugOrBrokenError
  ConfigNotResolvedError = Hocon::ConfigError::ConfigNotResolvedError
  ConfigWrongTypeError = Hocon::ConfigError::ConfigWrongTypeError

  attr_reader :pieces

  def initialize(origin, pieces)
    super(origin)
    @pieces = pieces

    if pieces.size < 2
      raise ConfigBugOrBrokenError, "Created concatenation with less than 2 items: #{self}"
    end

    had_unmergeable = false
    pieces.each do |p|
      if p.is_a?(Hocon::Impl::ConfigConcatenation)
        raise ConfigBugOrBrokenError, "ConfigConcatenation should never be nested: #{self}"
      end
      if p.is_a?(Unmergeable)
        had_unmergeable = true
      end
    end

    unless had_unmergeable
      raise ConfigBugOrBrokenError, "Created concatenation without an unmergeable in it: #{self}"
    end
  end

  def value_type
    raise not_resolved
  end

  def unwrapped
    raise not_resolved
  end

  def new_copy(new_origin)
    self.class.new(new_origin, @pieces)
  end

  def ignores_fallbacks?
    # we can never ignore fallbacks because if a child ConfigReference
    # is self-referential we have to look lower in the merge stack
    # for its value.
    false
  end

  def unmerged_values
    [self]
  end

  #
  # Add left and right, or their merger, to builder
  #
  def self.join(builder, orig_right)
    left = builder[builder.size - 1]
    right = orig_right

    # check for an object which can be converted to a list
    # (this will be an object with numeric keys, like foo.0, foo.1)
    if (left.is_a?(ConfigObject)) && (right.is_a?(SimpleConfigList))
      left = Hocon::Impl::DefaultTransformer.transform(left, Hocon::ConfigValueType::LIST)
    elsif (left.is_a?(SimpleConfigList)) && (right.is_a?(ConfigObject))
      right = Hocon::Impl::DefaultTransformer.transform(right, Hocon::ConfigValueType::LIST)
    end

    # Since this depends on the type of two instances, I couldn't think
    # of much alternative to an instanceof chain. Visitors are sometimes
    # used for multiple dispatch but seems like overkill.
    joined = nil
    if (left.is_a?(ConfigObject)) && (right.is_a?(ConfigObject))
      joined = right.with_fallback(left)
    elsif (left.is_a?(SimpleConfigList)) && (right.is_a?(SimpleConfigList))
      joined = left.concatenate(right)
    elsif (left.is_a?(SimpleConfigList) || left.is_a?(ConfigObject)) &&
           is_ignored_whitespace(right)
      joined = left
      # it should be impossible that left is whitespace and right is a list or object
    elsif (left.is_a?(Hocon::Impl::ConfigConcatenation)) ||
        (right.is_a?(Hocon::Impl::ConfigConcatenation))
      raise ConfigBugOrBrokenError, "unflattened ConfigConcatenation"
    elsif (left.is_a?(Unmergeable)) || (right.is_a?(Unmergeable))
      # leave joined=null, cannot join
    else
      # handle primitive type or primitive type mixed with object or list
      s1 = left.transform_to_string
      s2 = right.transform_to_string
      if s1.nil? || s2.nil?
        raise ConfigWrongTypeError.new(left.origin,
                "Cannot concatenate object or list with a non-object-or-list, #{left} " +
                    "and #{right} are not compatible", nil)
      else
        joined_origin = SimpleConfigOrigin.merge_origins([left.origin, right.origin])
        joined = Hocon::Impl::ConfigString::Quoted.new(joined_origin, s1 + s2)
      end
    end

    if joined.nil?
      builder.push(right)
    else
      builder.pop
      builder.push(joined)
    end
  end

  def self.consolidate(pieces)
    if pieces.length < 2
      pieces
    else
      flattened = []
      pieces.each do |v|
        if v.is_a?(Hocon::Impl::ConfigConcatenation)
          flattened.concat(v.pieces)
        else
          flattened.push(v)
        end
      end

      consolidated = []
      flattened.each do |v|
        if consolidated.empty?
          consolidated.push(v)
        else
          join(consolidated, v)
        end
      end

      consolidated
    end
  end

  def self.concatenate(pieces)
    consolidated = consolidate(pieces)
    if consolidated.empty?
      nil
    elsif consolidated.length == 1
      consolidated[0]
    else
      merged_origin = SimpleConfigOrigin.merge_value_origins(consolidated)
      Hocon::Impl::ConfigConcatenation.new(merged_origin, consolidated)
    end
  end

  def resolve_substitutions(context, source)
    if Hocon::Impl::ConfigImpl.trace_substitution_enabled
      indent = context.depth + 2
      Hocon::Impl::ConfigImpl.trace("concatenation has #{@pieces.size} pieces",
                                    indent - 1)
      count = 0
      @pieces.each { |v|
        Hocon::Impl::ConfigImpl.trace("#{count}: #{v}", count)
        count += 1
      }
    end

    # Right now there's no reason to pushParent here because the
    # content of ConfigConcatenation should not need to replaceChild,
    # but if it did we'd have to do this.
    source_with_parent = source
    new_context = context

    resolved = []
    @pieces.each { |p|
      # to concat into a string we have to do a full resolve,
      # so unrestrict the context, then put restriction back afterward
      restriction = new_context.restrict_to_child
      result = new_context.unrestricted
                   .resolve(p, source_with_parent)
      r = result.value
      new_context = result.context.restrict(restriction)
      if Hocon::Impl::ConfigImpl.trace_substitution_enabled
        Hocon::Impl::ConfigImpl.trace("resolved concat piece to #{r}",
                                      context.depth)
      end

      if r
        resolved << r
      end
      # otherwise, it was optional ... omit
    }

    # now need to concat everything
    joined = self.class.consolidate(resolved)
    # if unresolved is allowed we can just become another
    # ConfigConcatenation
    if joined.size > 1 and context.options.allow_unresolved
      Hocon::Impl::ResolveResult.make(new_context, Hocon::Impl::ConfigConcatenation.new(origin, joined))
    elsif joined.empty?
      # we had just a list of optional references using ${?}
      Hocon::Impl::ResolveResult.make(new_context, nil)
    elsif joined.size == 1
      Hocon::Impl::ResolveResult.make(new_context, joined[0])
    else
      raise ConfigBugOrBrokenError.new(
                "Bug in the library; resolved list was joined to too many values: #{joined}")
    end
  end

  def resolve_status
    ResolveStatus::UNRESOLVED
  end

  def replace_child(child, replacement)
    new_pieces = replace_child_in_list(@pieces, child, replacement)
    if new_pieces == nil
      nil
    else
      self.class.new(origin, new_pieces)
    end
  end

  def has_descendant?(descendant)
    has_descendant_in_list?(@pieces, descendant)
  end

  # when you graft a substitution into another object,
  # you have to prefix it with the location in that object
  # where you grafted it; but save prefixLength so
  # system property and env variable lookups don 't get
  # broken.
  def relativized(prefix)
    new_pieces = []
    @pieces.each { |p|
      new_pieces << p.relativized(prefix)
    }
    self.class.new(origin, new_pieces)
  end

  def can_equal(other)
    other.is_a? Hocon::Impl::ConfigConcatenation
  end

  def ==(other)
    if other.is_a? Hocon::Impl::ConfigConcatenation
      can_equal(other) && @pieces == other.pieces
    else
      false
    end
  end

  def hash
    # note that "origin" is deliberately NOT part of equality
    @pieces.hash
  end

  def render_value_to_sb(sb, indent, at_root, options)
    @pieces.each do |piece|
      piece.render_value_to_sb(sb, indent, at_root, options)
    end
  end

  private

  def not_resolved
    ConfigNotResolvedError.new("need to Config#resolve(), see the API docs for Config#resolve(); substitution not resolved: #{self}")
  end

  def self.is_ignored_whitespace(value)
    return value.is_a?(ConfigString) && !value.was_quoted?
  end
end