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