1# frozen_string_literal: true
2require 'psych/tree_builder'
3require 'psych/scalar_scanner'
4require 'psych/class_loader'
5
6module Psych
7  module Visitors
8    ###
9    # YAMLTree builds a YAML ast given a Ruby object.  For example:
10    #
11    #   builder = Psych::Visitors::YAMLTree.new
12    #   builder << { :foo => 'bar' }
13    #   builder.tree # => #<Psych::Nodes::Stream .. }
14    #
15    class YAMLTree < Psych::Visitors::Visitor
16      class Registrar # :nodoc:
17        def initialize
18          @obj_to_id   = {}
19          @obj_to_node = {}
20          @targets     = []
21          @counter     = 0
22        end
23
24        def register target, node
25          return unless target.respond_to? :object_id
26          @targets << target
27          @obj_to_node[target.object_id] = node
28        end
29
30        def key? target
31          @obj_to_node.key? target.object_id
32        rescue NoMethodError
33          false
34        end
35
36        def id_for target
37          @obj_to_id[target.object_id] ||= (@counter += 1)
38        end
39
40        def node_for target
41          @obj_to_node[target.object_id]
42        end
43      end
44
45      attr_reader :started, :finished
46      alias :finished? :finished
47      alias :started? :started
48
49      def self.create options = {}, emitter = nil
50        emitter      ||= TreeBuilder.new
51        class_loader = ClassLoader.new
52        ss           = ScalarScanner.new class_loader
53        new(emitter, ss, options)
54      end
55
56      def initialize emitter, ss, options
57        super()
58        @started    = false
59        @finished   = false
60        @emitter    = emitter
61        @st         = Registrar.new
62        @ss         = ss
63        @options    = options
64        @line_width = options[:line_width]
65        if @line_width && @line_width < 0
66          if @line_width == -1
67            # Treat -1 as unlimited line-width, same as libyaml does.
68            @line_width = nil
69          else
70            fail(ArgumentError, "Invalid line_width #{@line_width}, must be non-negative or -1 for unlimited.")
71          end
72        end
73        @coders     = []
74
75        @dispatch_cache = Hash.new do |h,klass|
76          method = "visit_#{(klass.name || '').split('::').join('_')}"
77
78          method = respond_to?(method) ? method : h[klass.superclass]
79
80          raise(TypeError, "Can't dump #{target.class}") unless method
81
82          h[klass] = method
83        end
84      end
85
86      def start encoding = Nodes::Stream::UTF8
87        @emitter.start_stream(encoding).tap do
88          @started = true
89        end
90      end
91
92      def finish
93        @emitter.end_stream.tap do
94          @finished = true
95        end
96      end
97
98      def tree
99        finish unless finished?
100        @emitter.root
101      end
102
103      def push object
104        start unless started?
105        version = []
106        version = [1,1] if @options[:header]
107
108        case @options[:version]
109        when Array
110          version = @options[:version]
111        when String
112          version = @options[:version].split('.').map { |x| x.to_i }
113        else
114          version = [1,1]
115        end if @options.key? :version
116
117        @emitter.start_document version, [], false
118        accept object
119        @emitter.end_document !@emitter.streaming?
120      end
121      alias :<< :push
122
123      def accept target
124        # return any aliases we find
125        if @st.key? target
126          oid         = @st.id_for target
127          node        = @st.node_for target
128          anchor      = oid.to_s
129          node.anchor = anchor
130          return @emitter.alias anchor
131        end
132
133        if target.respond_to?(:encode_with)
134          dump_coder target
135        else
136          send(@dispatch_cache[target.class], target)
137        end
138      end
139
140      def visit_Psych_Omap o
141        seq = @emitter.start_sequence(nil, 'tag:yaml.org,2002:omap', false, Nodes::Sequence::BLOCK)
142        register(o, seq)
143
144        o.each { |k,v| visit_Hash k => v }
145        @emitter.end_sequence
146      end
147
148      def visit_Encoding o
149        tag = "!ruby/encoding"
150        @emitter.scalar o.name, nil, tag, false, false, Nodes::Scalar::ANY
151      end
152
153      def visit_Object o
154        tag = Psych.dump_tags[o.class]
155        unless tag
156          klass = o.class == Object ? nil : o.class.name
157          tag   = ['!ruby/object', klass].compact.join(':')
158        end
159
160        map = @emitter.start_mapping(nil, tag, false, Nodes::Mapping::BLOCK)
161        register(o, map)
162
163        dump_ivars o
164        @emitter.end_mapping
165      end
166
167      alias :visit_Delegator :visit_Object
168
169      def visit_Struct o
170        tag = ['!ruby/struct', o.class.name].compact.join(':')
171
172        register o, @emitter.start_mapping(nil, tag, false, Nodes::Mapping::BLOCK)
173        o.members.each do |member|
174          @emitter.scalar member.to_s, nil, nil, true, false, Nodes::Scalar::ANY
175          accept o[member]
176        end
177
178        dump_ivars o
179
180        @emitter.end_mapping
181      end
182
183      def visit_Exception o
184        tag = ['!ruby/exception', o.class.name].join ':'
185
186        @emitter.start_mapping nil, tag, false, Nodes::Mapping::BLOCK
187
188        {
189          'message'   => private_iv_get(o, 'mesg'),
190          'backtrace' => private_iv_get(o, 'backtrace'),
191        }.each do |k,v|
192          next unless v
193          @emitter.scalar k, nil, nil, true, false, Nodes::Scalar::ANY
194          accept v
195        end
196
197        dump_ivars o
198
199        @emitter.end_mapping
200      end
201
202      def visit_NameError o
203        tag = ['!ruby/exception', o.class.name].join ':'
204
205        @emitter.start_mapping nil, tag, false, Nodes::Mapping::BLOCK
206
207        {
208          'message'   => o.message.to_s,
209          'backtrace' => private_iv_get(o, 'backtrace'),
210        }.each do |k,v|
211          next unless v
212          @emitter.scalar k, nil, nil, true, false, Nodes::Scalar::ANY
213          accept v
214        end
215
216        dump_ivars o
217
218        @emitter.end_mapping
219      end
220
221      def visit_Regexp o
222        register o, @emitter.scalar(o.inspect, nil, '!ruby/regexp', false, false, Nodes::Scalar::ANY)
223      end
224
225      def visit_DateTime o
226        formatted = if o.offset.zero?
227                      o.strftime("%Y-%m-%d %H:%M:%S.%9N Z".freeze)
228                    else
229                      o.strftime("%Y-%m-%d %H:%M:%S.%9N %:z".freeze)
230                    end
231        tag = '!ruby/object:DateTime'
232        register o, @emitter.scalar(formatted, nil, tag, false, false, Nodes::Scalar::ANY)
233      end
234
235      def visit_Time o
236        formatted = format_time o
237        register o, @emitter.scalar(formatted, nil, nil, true, false, Nodes::Scalar::ANY)
238      end
239
240      def visit_Rational o
241        register o, @emitter.start_mapping(nil, '!ruby/object:Rational', false, Nodes::Mapping::BLOCK)
242
243        [
244          'denominator', o.denominator.to_s,
245          'numerator', o.numerator.to_s
246        ].each do |m|
247          @emitter.scalar m, nil, nil, true, false, Nodes::Scalar::ANY
248        end
249
250        @emitter.end_mapping
251      end
252
253      def visit_Complex o
254        register o, @emitter.start_mapping(nil, '!ruby/object:Complex', false, Nodes::Mapping::BLOCK)
255
256        ['real', o.real.to_s, 'image', o.imag.to_s].each do |m|
257          @emitter.scalar m, nil, nil, true, false, Nodes::Scalar::ANY
258        end
259
260        @emitter.end_mapping
261      end
262
263      def visit_Integer o
264        @emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
265      end
266      alias :visit_TrueClass :visit_Integer
267      alias :visit_FalseClass :visit_Integer
268      alias :visit_Date :visit_Integer
269
270      def visit_Float o
271        if o.nan?
272          @emitter.scalar '.nan', nil, nil, true, false, Nodes::Scalar::ANY
273        elsif o.infinite?
274          @emitter.scalar((o.infinite? > 0 ? '.inf' : '-.inf'),
275            nil, nil, true, false, Nodes::Scalar::ANY)
276        else
277          @emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
278        end
279      end
280
281      def visit_BigDecimal o
282        @emitter.scalar o._dump, nil, '!ruby/object:BigDecimal', false, false, Nodes::Scalar::ANY
283      end
284
285      def visit_String o
286        plain = true
287        quote = true
288        style = Nodes::Scalar::PLAIN
289        tag   = nil
290
291        if binary?(o)
292          o     = [o].pack('m0')
293          tag   = '!binary' # FIXME: change to below when syck is removed
294          #tag   = 'tag:yaml.org,2002:binary'
295          style = Nodes::Scalar::LITERAL
296          plain = false
297          quote = false
298        elsif o =~ /\n(?!\Z)/  # match \n except blank line at the end of string
299          style = Nodes::Scalar::LITERAL
300        elsif o == '<<'
301          style = Nodes::Scalar::SINGLE_QUOTED
302          tag   = 'tag:yaml.org,2002:str'
303          plain = false
304          quote = false
305        elsif @line_width && o.length > @line_width
306          style = Nodes::Scalar::FOLDED
307        elsif o =~ /^[^[:word:]][^"]*$/
308          style = Nodes::Scalar::DOUBLE_QUOTED
309        elsif not String === @ss.tokenize(o) or /\A0[0-7]*[89]/ =~ o
310          style = Nodes::Scalar::SINGLE_QUOTED
311        end
312
313        is_primitive = o.class == ::String
314        ivars = is_primitive ? [] : o.instance_variables
315
316        if ivars.empty?
317          unless is_primitive
318            tag = "!ruby/string:#{o.class}"
319            plain = false
320            quote = false
321          end
322          @emitter.scalar o, nil, tag, plain, quote, style
323        else
324          maptag = '!ruby/string'.dup
325          maptag << ":#{o.class}" unless o.class == ::String
326
327          register o, @emitter.start_mapping(nil, maptag, false, Nodes::Mapping::BLOCK)
328          @emitter.scalar 'str', nil, nil, true, false, Nodes::Scalar::ANY
329          @emitter.scalar o, nil, tag, plain, quote, style
330
331          dump_ivars o
332
333          @emitter.end_mapping
334        end
335      end
336
337      def visit_Module o
338        raise TypeError, "can't dump anonymous module: #{o}" unless o.name
339        register o, @emitter.scalar(o.name, nil, '!ruby/module', false, false, Nodes::Scalar::SINGLE_QUOTED)
340      end
341
342      def visit_Class o
343        raise TypeError, "can't dump anonymous class: #{o}" unless o.name
344        register o, @emitter.scalar(o.name, nil, '!ruby/class', false, false, Nodes::Scalar::SINGLE_QUOTED)
345      end
346
347      def visit_Range o
348        register o, @emitter.start_mapping(nil, '!ruby/range', false, Nodes::Mapping::BLOCK)
349        ['begin', o.begin, 'end', o.end, 'excl', o.exclude_end?].each do |m|
350          accept m
351        end
352        @emitter.end_mapping
353      end
354
355      def visit_Hash o
356        if o.class == ::Hash
357          register(o, @emitter.start_mapping(nil, nil, true, Psych::Nodes::Mapping::BLOCK))
358          o.each do |k,v|
359            accept k
360            accept v
361          end
362          @emitter.end_mapping
363        else
364          visit_hash_subclass o
365        end
366      end
367
368      def visit_Psych_Set o
369        register(o, @emitter.start_mapping(nil, '!set', false, Psych::Nodes::Mapping::BLOCK))
370
371        o.each do |k,v|
372          accept k
373          accept v
374        end
375
376        @emitter.end_mapping
377      end
378
379      def visit_Array o
380        if o.class == ::Array
381          visit_Enumerator o
382        else
383          visit_array_subclass o
384        end
385      end
386
387      def visit_Enumerator o
388        register o, @emitter.start_sequence(nil, nil, true, Nodes::Sequence::BLOCK)
389        o.each { |c| accept c }
390        @emitter.end_sequence
391      end
392
393      def visit_NilClass o
394        @emitter.scalar('', nil, 'tag:yaml.org,2002:null', true, false, Nodes::Scalar::ANY)
395      end
396
397      def visit_Symbol o
398        if o.empty?
399          @emitter.scalar "", nil, '!ruby/symbol', false, false, Nodes::Scalar::ANY
400        else
401          @emitter.scalar ":#{o}", nil, nil, true, false, Nodes::Scalar::ANY
402        end
403      end
404
405      def visit_BasicObject o
406        tag = Psych.dump_tags[o.class]
407        tag ||= "!ruby/marshalable:#{o.class.name}"
408
409        map = @emitter.start_mapping(nil, tag, false, Nodes::Mapping::BLOCK)
410        register(o, map)
411
412        o.marshal_dump.each(&method(:accept))
413
414        @emitter.end_mapping
415      end
416
417      private
418
419      def binary? string
420        string.encoding == Encoding::ASCII_8BIT && !string.ascii_only?
421      end
422
423      def visit_array_subclass o
424        tag = "!ruby/array:#{o.class}"
425        ivars = o.instance_variables
426        if ivars.empty?
427          node = @emitter.start_sequence(nil, tag, false, Nodes::Sequence::BLOCK)
428          register o, node
429          o.each { |c| accept c }
430          @emitter.end_sequence
431        else
432          node = @emitter.start_mapping(nil, tag, false, Nodes::Sequence::BLOCK)
433          register o, node
434
435          # Dump the internal list
436          accept 'internal'
437          @emitter.start_sequence(nil, nil, true, Nodes::Sequence::BLOCK)
438          o.each { |c| accept c }
439          @emitter.end_sequence
440
441          # Dump the ivars
442          accept 'ivars'
443          @emitter.start_mapping(nil, nil, true, Nodes::Sequence::BLOCK)
444          ivars.each do |ivar|
445            accept ivar
446            accept o.instance_variable_get ivar
447          end
448          @emitter.end_mapping
449
450          @emitter.end_mapping
451        end
452      end
453
454      def visit_hash_subclass o
455        ivars = o.instance_variables
456        if ivars.any?
457          tag = "!ruby/hash-with-ivars:#{o.class}"
458          node = @emitter.start_mapping(nil, tag, false, Psych::Nodes::Mapping::BLOCK)
459          register(o, node)
460
461          # Dump the elements
462          accept 'elements'
463          @emitter.start_mapping nil, nil, true, Nodes::Mapping::BLOCK
464          o.each do |k,v|
465            accept k
466            accept v
467          end
468          @emitter.end_mapping
469
470          # Dump the ivars
471          accept 'ivars'
472          @emitter.start_mapping nil, nil, true, Nodes::Mapping::BLOCK
473          o.instance_variables.each do |ivar|
474            accept ivar
475            accept o.instance_variable_get ivar
476          end
477          @emitter.end_mapping
478
479          @emitter.end_mapping
480        else
481          tag = "!ruby/hash:#{o.class}"
482          node = @emitter.start_mapping(nil, tag, false, Psych::Nodes::Mapping::BLOCK)
483          register(o, node)
484          o.each do |k,v|
485            accept k
486            accept v
487          end
488          @emitter.end_mapping
489        end
490      end
491
492      def dump_list o
493      end
494
495      def format_time time
496        if time.utc?
497          time.strftime("%Y-%m-%d %H:%M:%S.%9N Z")
498        else
499          time.strftime("%Y-%m-%d %H:%M:%S.%9N %:z")
500        end
501      end
502
503      def register target, yaml_obj
504        @st.register target, yaml_obj
505        yaml_obj
506      end
507
508      def dump_coder o
509        @coders << o
510        tag = Psych.dump_tags[o.class]
511        unless tag
512          klass = o.class == Object ? nil : o.class.name
513          tag   = ['!ruby/object', klass].compact.join(':')
514        end
515
516        c = Psych::Coder.new(tag)
517        o.encode_with(c)
518        emit_coder c, o
519      end
520
521      def emit_coder c, o
522        case c.type
523        when :scalar
524          @emitter.scalar c.scalar, nil, c.tag, c.tag.nil?, false, Nodes::Scalar::ANY
525        when :seq
526          @emitter.start_sequence nil, c.tag, c.tag.nil?, Nodes::Sequence::BLOCK
527          c.seq.each do |thing|
528            accept thing
529          end
530          @emitter.end_sequence
531        when :map
532          register o, @emitter.start_mapping(nil, c.tag, c.implicit, c.style)
533          c.map.each do |k,v|
534            accept k
535            accept v
536          end
537          @emitter.end_mapping
538        when :object
539          accept c.object
540        end
541      end
542
543      def dump_ivars target
544        target.instance_variables.each do |iv|
545          @emitter.scalar("#{iv.to_s.sub(/^@/, '')}", nil, nil, true, false, Nodes::Scalar::ANY)
546          accept target.instance_variable_get(iv)
547        end
548      end
549    end
550  end
551end
552