File: parseable.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 (554 lines) | stat: -rw-r--r-- 17,005 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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
# encoding: utf-8

require 'stringio'
require 'pathname'
require_relative '../../hocon/impl'
require_relative '../../hocon/config_error'
require_relative '../../hocon/config_syntax'
require_relative '../../hocon/config_value_type'
require_relative '../../hocon/impl/config_impl'
require_relative '../../hocon/impl/simple_include_context'
require_relative '../../hocon/impl/simple_config_object'
require_relative '../../hocon/impl/simple_config_origin'
require_relative '../../hocon/impl/tokenizer'
require_relative '../../hocon/impl/config_parser'
require_relative '../../hocon/config_parseable'
require_relative '../../hocon/impl/config_document_parser'
require_relative '../../hocon/impl/simple_config_document'

#
# Internal implementation detail, not ABI stable, do not touch.
# For use only by the {@link com.typesafe.config} package.
# The point of this class is to avoid "propagating" each
# overload on "thing which can be parsed" through multiple
# interfaces. Most interfaces can have just one overload that
# takes a Parseable. Also it's used as an abstract "resource
# handle" in the ConfigIncluder interface.
#
class Hocon::Impl::Parseable
  include Hocon::ConfigParseable

  # Internal implementation detail, not ABI stable, do not touch
  module Relativizer
    def relative_to(filename)
      raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `Relativizer` must implement `relative_to` (#{self.class})"
    end
  end

  # The upstream library seems to use this as a global way of keeping track of
  # how many files have been included, to avoid cycles
  def self.parse_stack
    Thread.current[:hocon_parse_stack] ||= []
  end

  MAX_INCLUDE_DEPTH = 50

  def initialize

  end

  def fixup_options(base_options)
    syntax = base_options.syntax
    if !syntax
      syntax = guess_syntax
    end
    if !syntax
      syntax = Hocon::ConfigSyntax::CONF
    end
    modified = base_options.set_syntax(syntax)

    # make sure the app-provided includer falls back to default
    modified = modified.append_includer(Hocon::Impl::ConfigImpl.default_includer)
    # make sure the app-provided includer is complete
    modified = modified.set_includer(Hocon::Impl::SimpleIncluder.make_full(modified.includer))

    modified
  end

  def post_construct(base_options)
    @initial_options = fixup_options(base_options)
    @include_context = Hocon::Impl::SimpleIncludeContext.new(self)
    if @initial_options.origin_description
      @initial_origin = Hocon::Impl::SimpleConfigOrigin.new_simple(@initial_options.origin_description)
    else
      @initial_origin = create_origin
    end
  end

  # the general idea is that any work should be in here, not in the
  # constructor, so that exceptions are thrown from the public parse()
  # function and not from the creation of the Parseable.
  # Essentially this is a lazy field. The parser should close the
  # reader when it's done with it.
  #{//}# ALSO, IMPORTANT: if the file or URL is not found, this must throw.
  #{//}# to support the "allow missing" feature.
  def custom_reader
    raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `Parseable` must implement `custom_reader` (#{self.class})"
  end

  def reader(options)
    custom_reader
  end

  def self.trace(message)
    if Hocon::Impl::ConfigImpl.trace_loads_enabled
      Hocon::Impl::ConfigImpl.trace(message)
    end
  end

  def guess_syntax
    nil
  end

  def content_type
    nil
  end

  def relative_to(filename)
    # fall back to classpath; we treat the "filename" as absolute
    # (don't add a package name in front),
    # if it starts with "/" then remove the "/", for consistency
    # with ParseableResources.relativeTo
    resource = filename
    if filename.start_with?("/")
      resource = filename.slice(1)
    end
    self.class.new_resources(resource, options.set_origin_description(nil))
  end

  def include_context
    @include_context
  end

  def self.force_parsed_to_object(value)
    if value.is_a? Hocon::Impl::AbstractConfigObject
      value
    else
      raise Hocon::ConfigError::ConfigWrongTypeError.with_expected_actual(value.origin,
                                                         "",
                                                         "object at file root",
                                                         Hocon::ConfigValueType.value_type_name(value.value_type))
    end
  end

  def parse(base_options = nil)
    if (base_options.nil?)
      base_options = options
    end
    stack = self.class.parse_stack
    if stack.length >= MAX_INCLUDE_DEPTH
      raise Hocon::ConfigError::ConfigParseError.new(@initial_origin,
            "include statements nested more than #{MAX_INCLUDE_DEPTH} times, " +
            "you probably have a cycle in your includes.  Trace: #{stack}",
            nil)
    end

    # Push into beginning of stack
    stack.unshift(self)
    begin
      self.class.force_parsed_to_object(parse_value(base_options))
    ensure
      # Pop from beginning of stack
      stack.shift
    end
  end

  def parse_value(base_options = nil)
    if base_options.nil?
      base_options = options
    end

    # note that we are NOT using our "initialOptions",
    # but using the ones from the passed-in options. The idea is that
    # callers can get our original options and then parse with different
    # ones if they want.
    options = fixup_options(base_options)

    # passed-in options can override origin
    origin =
        if options.origin_description
          Hocon::Impl::SimpleConfigOrigin.new_simple(options.origin_description)
        else
          @initial_origin
        end
    parse_value_from_origin(origin, options)
  end

  def parse_value_from_origin(origin, final_options)
    begin
      raw_parse_value(origin, final_options)
    rescue IOError => e
      if final_options.allow_missing?
        Hocon::Impl::SimpleConfigObject.empty_missing(origin)
      else
        self.class.trace("exception loading #{origin.description}: #{e.class}: #{e.message}")
        raise Hocon::ConfigError::ConfigIOError.new(origin, "#{e.class.name}: #{e.message}", e)
      end
    end
  end

  def parse_document(base_options = nil)
    if base_options.nil?
      base_options = options
    end

    # note that we are NOT using our "initialOptions",
    # but using the ones from the passed-in options. The idea is that
    # callers can get our original options and then parse with different
    # ones if they want.
    options = fixup_options(base_options)

    # passed-in option can override origin
    origin = nil
    if ! options.origin_description.nil?
      origin = Hocon::Impl::SimpleConfigOrigin.new_simple(options.origin_description)
    else
      origin = @initial_origin
    end
    parse_document_from_origin(origin, options)
  end

  def parse_document_from_origin(origin, final_options)
    begin
      raw_parse_document(origin, final_options)
    rescue IOError => e
      if final_options.allow_missing?
        Hocon::Impl::SimpleConfigDocument.new(
          Hocon::Impl::ConfigNodeObject.new([]), final_options)
      else
        self.class.trace("exception loading #{origin.description}: #{e.class}: #{e.message}")
        raise ConfigIOError.new(origin, "#{e.class.name}: #{e.message}", e)
      end
    end
  end

  # this is parseValue without post-processing the IOException or handling
  # options.getAllowMissing()
  def raw_parse_value(origin, final_options)
    reader = reader(final_options)

    # after reader() we will have loaded the Content-Type
    content_type = content_type()

    options_with_content_type = nil
    if !(content_type.nil?)
      if Hocon::Impl::ConfigImpl.trace_loads_enabled && (! final_options.get_syntax.nil?)
        self.class.trace("Overriding syntax #{final_options.get_syntax} with Content-Type which specified #{content-type}")
      end
      options_with_content_type = final_options.set_syntax(content_type)
    else
      options_with_content_type = final_options
    end

    reader.open { |io|
      raw_parse_value_from_io(io, origin, options_with_content_type)
    }
  end

  def raw_parse_value_from_io(io, origin, final_options)
    tokens = Hocon::Impl::Tokenizer.tokenize(origin, io, final_options.syntax)
    document = Hocon::Impl::ConfigDocumentParser.parse(tokens, origin, final_options)
    Hocon::Impl::ConfigParser.parse(document, origin, final_options, include_context)
  end

  def raw_parse_document(origin, final_options)
    reader = reader(final_options)
    content_type = content_type()

    options_with_content_type = nil
    if !(content_type.nil?)
      if Hocon::Impl::ConfigImpl.trace_loads_enabled && (! final_options.get_syntax.nil?)
        self.class.trace("Overriding syntax #{final_options.get_syntax} with Content-Type which specified #{content-type}")
      end
      options_with_content_type = final_options.set_syntax(content_type)
    else
      options_with_content_type = final_options
    end

    reader.open { |io|
      raw_parse_document_from_io(io, origin, options_with_content_type)
    }
  end

  def raw_parse_document_from_io(reader, origin, final_options)
    tokens = Hocon::Impl::Tokenizer.tokenize(origin, reader, final_options.syntax)
    Hocon::Impl::SimpleConfigDocument.new(
                   Hocon::Impl::ConfigDocumentParser.parse(tokens, origin, final_options),
                   final_options)
  end

  def parse_config_document
    parse_document(options)
  end

  def origin
    @initial_origin
  end

  def create_origin
    raise Hocon::ConfigError::ConfigBugOrBrokenError, "subclasses of `Parseable` must implement `create_origin` (#{self.class})"
  end

  def options
    @initial_options
  end

  def to_s
    self.class.name.split('::').last
  end

  def self.syntax_from_extension(name)
    if name.end_with?(".json")
      Hocon::ConfigSyntax::JSON
    elsif name.end_with?(".conf")
      Hocon::ConfigSyntax::CONF
    else
      # Skipping PROPERTIES because we can't really support that in ruby
      nil
    end
  end

  # NOTE: skipping `readerFromStream` and `doNotClose` because they don't seem relevant in Ruby

  # NOTE: skipping `relativeTo(URL, String)` because we're not supporting URLs for now
  def self.relative_to(file, filename)
    child = Pathname.new(filename)
    file = Pathname.new(file)

    if child.absolute?
      nil
    end

    parent = file.parent

    if parent.nil?
      nil
    else
      File.join(parent, filename)
    end
  end

  # this is a parseable that doesn't exist and just throws when you try to parse it
  class ParseableNotFound < Hocon::Impl::Parseable
    def initialize(what, message, options)
      super()
      @what = what
      @message = message
      post_construct(options)
    end

    def custom_reader
      raise Hocon::ConfigError::ConfigBugOrBrokenError, @message
    end

    def create_origin
      Hocon::Impl::SimpleConfigOrigin.new_simple(@what)
    end
  end

  def self.new_not_found(what_not_found, message, options)
    ParseableNotFound.new(what_not_found, message, options)
  end

  # NOTE: skipping `ParseableReader` until we know we need it (probably should
  #  have done that with `ParseableNotFound`)

  class ParseableString < Hocon::Impl::Parseable
    def initialize(string, options)
      super()
      @input = string
      post_construct(options)
    end

    def custom_reader
      if Hocon::Impl::ConfigImpl.trace_loads_enabled
        self.class.trace("Loading config from a String: #{@input}")
      end
      # we return self here, which will cause `open` to be called on us, so
      # we can provide an implementation of that.
      self
    end

    def open
      if block_given?
        StringIO.open(@input) do |f|
          yield f
        end
      else
        StringIO.open(@input)
      end
    end

    def create_origin
      Hocon::Impl::SimpleConfigOrigin.new_simple("String")
    end

    def to_s
      "#{self.class.name.split('::').last} (#{@input})"
    end
  end

  def self.new_string(string, options)
    ParseableString.new(string, options)
  end

  # NOTE: Skipping `ParseableURL` for now as we probably won't support this right away

  class ParseableFile < Hocon::Impl::Parseable
    def initialize(input, options)
      super()
      @input = input
      post_construct(options)
    end

    def custom_reader
      if Hocon::Impl::ConfigImpl.trace_loads_enabled
        self.class.trace("Loading config from a String: #{@input}")
      end
      # we return self here, which will cause `open` to be called on us, so
      # we can provide an implementation of that.
      self
    end

    def open
      begin
        if block_given?
          File.open(@input, :encoding => 'bom|utf-8') do |f|
            yield f
          end
        else
          File.open(@input, :encoding => 'bom|utf-8')
        end
      rescue Errno::ENOENT
        if @initial_options.allow_missing?
          return Hocon::Impl::SimpleConfigObject.empty
        end

        raise Hocon::ConfigError::ConfigIOError.new(nil, "File not found. No file called #{@input}")
      end
    end

    def guess_syntax
      Hocon::Impl::Parseable.syntax_from_extension(File.basename(@input))
    end

    def relative_to(filename)
      sibling = nil
      if Pathname.new(filename).absolute?
        sibling = File.new(filename)
      else
        # this may return nil
        sibling = Hocon::Impl::Parseable.relative_to(@input, filename)
      end
      if sibling.nil?
        nil
      elsif File.exist?(sibling)
        self.class.trace("#{sibling} exists, so loading it as a file")
        Hocon::Impl::Parseable.new_file(sibling, options.set_origin_description(nil))
      else
        self.class.trace("#{sibling} does not exist, so trying it as a resource")
        super(filename)
      end
    end

    def create_origin
      Hocon::Impl::SimpleConfigOrigin.new_file(@input)
    end

    def to_s
      "#{self.class.name.split('::').last} (#{@input})"
    end

  end


  def self.new_file(file_path, options)
    ParseableFile.new(file_path, options)
  end

  # NOTE: skipping `ParseableResourceURL`, we probably won't support that

  # NOTE: this is not a faithful port of the `ParseableResources` class from the
  # upstream, because at least for now we're not going to try to do anything
  # crazy like look for files on the ruby load path.  However, there is a decent
  # chunk of logic elsewhere in the codebase that is written with the assumption
  # that this class will provide the 'last resort' attempt to find a config file
  # before giving up, so we're basically port just enough to have it provide
  # that last resort behavior
  class ParseableResources < Hocon::Impl::Parseable
    include Relativizer

    def initialize(resource, options)
      super()
      @resource = resource
      post_construct(options)
    end

    def reader
      raise Hocon::ConfigError::ConfigBugOrBrokenError, "reader() should not be called on resources"
    end

    def raw_parse_value(origin, final_options)
      # this is where the upstream code would go out and look for a file on the
      # classpath.  We're not going to do that, and instead we're just going to
      # raise the same exception that the upstream code would raise if it failed
      # to find the file.
      raise IOError, "resource not found: #{@resource}"
    end

    def guess_syntax
      Hocon::Impl::Parseable.syntax_from_extension(@resource)
    end

    def self.parent(resource)
      # the "resource" is not supposed to begin with a "/"
      # because it's supposed to be the raw resource
      # (ClassLoader#getResource), not the
      # resource "syntax" (Class#getResource)
      i = resource.rindex("/")
      if i < 0
        nil
      else
        resource.slice(0..i)
      end
    end

    def relative_to(sibling)
      if sibling.start_with?("/")
        # if it starts with "/" then don't make it relative to the
        # including resource
        Hocon::Impl::Parseable.new_resources(sibling.slice(1), options.set_origin_description(nil))
      else
        # here we want to build a new resource name and let
        # the class loader have it, rather than getting the
        # url with getResource() and relativizing to that url.
        # This is needed in case the class loader is going to
        # search a classpath.
        parent = self.class.parent(@resource)
        if parent.nil?
          Hocon::Impl::Parseable.new_resources(sibling, options.set_origin_description(nil))
        else
          Hocon::Impl::Parseable.new_resources("#{parent}/sibling", options.set_origin_description(nil))
        end
      end
    end

    def create_origin
      Hocon::Impl::SimpleConfigOrigin.new_resource(@resource)
    end

    def to_s
      "#{self.class.name.split('::').last}(#{@resource})"
    end
  end

  def self.new_resources(resource, options)
    ParseableResources.new(resource, options)
  end




  # NOTE: skipping `ParseableProperties`, we probably won't support that

end