mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00

quotes should not have changed. [ruby-core:59316] [Bug #9300] * ext/psych/lib/psych.rb: fixed missing require. * test/psych/test_string.rb: test git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@44531 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
516 lines
14 KiB
Ruby
516 lines
14 KiB
Ruby
require 'psych/tree_builder'
|
|
require 'psych/scalar_scanner'
|
|
require 'psych/class_loader'
|
|
|
|
module Psych
|
|
module Visitors
|
|
###
|
|
# YAMLTree builds a YAML ast given a Ruby object. For example:
|
|
#
|
|
# builder = Psych::Visitors::YAMLTree.new
|
|
# builder << { :foo => 'bar' }
|
|
# builder.tree # => #<Psych::Nodes::Stream .. }
|
|
#
|
|
class YAMLTree < Psych::Visitors::Visitor
|
|
class Registrar # :nodoc:
|
|
def initialize
|
|
@obj_to_id = {}
|
|
@obj_to_node = {}
|
|
@counter = 0
|
|
end
|
|
|
|
def register target, node
|
|
@obj_to_node[target.object_id] = node
|
|
end
|
|
|
|
def key? target
|
|
@obj_to_node.key? target.object_id
|
|
end
|
|
|
|
def id_for target
|
|
@obj_to_id[target.object_id] ||= (@counter += 1)
|
|
end
|
|
|
|
def node_for target
|
|
@obj_to_node[target.object_id]
|
|
end
|
|
end
|
|
|
|
attr_reader :started, :finished
|
|
alias :finished? :finished
|
|
alias :started? :started
|
|
|
|
def self.create options = {}, emitter = nil
|
|
emitter ||= TreeBuilder.new
|
|
class_loader = ClassLoader.new
|
|
ss = ScalarScanner.new class_loader
|
|
new(emitter, ss, options)
|
|
end
|
|
|
|
def self.new emitter = nil, ss = nil, options = nil
|
|
return super if emitter && ss && options
|
|
|
|
if $VERBOSE
|
|
warn "This API is deprecated, please pass an emitter, scalar scanner, and options or call #{self}.create() (#{caller.first})"
|
|
end
|
|
create emitter, ss
|
|
end
|
|
|
|
def initialize emitter, ss, options
|
|
super()
|
|
@started = false
|
|
@finished = false
|
|
@emitter = emitter
|
|
@st = Registrar.new
|
|
@ss = ss
|
|
@options = options
|
|
@coders = []
|
|
|
|
@dispatch_cache = Hash.new do |h,klass|
|
|
method = "visit_#{(klass.name || '').split('::').join('_')}"
|
|
|
|
method = respond_to?(method) ? method : h[klass.superclass]
|
|
|
|
raise(TypeError, "Can't dump #{target.class}") unless method
|
|
|
|
h[klass] = method
|
|
end
|
|
end
|
|
|
|
def start encoding = Nodes::Stream::UTF8
|
|
@emitter.start_stream(encoding).tap do
|
|
@started = true
|
|
end
|
|
end
|
|
|
|
def finish
|
|
@emitter.end_stream.tap do
|
|
@finished = true
|
|
end
|
|
end
|
|
|
|
def tree
|
|
finish unless finished?
|
|
@emitter.root
|
|
end
|
|
|
|
def push object
|
|
start unless started?
|
|
version = []
|
|
version = [1,1] if @options[:header]
|
|
|
|
case @options[:version]
|
|
when Array
|
|
version = @options[:version]
|
|
when String
|
|
version = @options[:version].split('.').map { |x| x.to_i }
|
|
else
|
|
version = [1,1]
|
|
end if @options.key? :version
|
|
|
|
@emitter.start_document version, [], false
|
|
accept object
|
|
@emitter.end_document !@emitter.streaming?
|
|
end
|
|
alias :<< :push
|
|
|
|
def accept target
|
|
# return any aliases we find
|
|
if @st.key? target
|
|
oid = @st.id_for target
|
|
node = @st.node_for target
|
|
anchor = oid.to_s
|
|
node.anchor = anchor
|
|
return @emitter.alias anchor
|
|
end
|
|
|
|
if target.respond_to?(:to_yaml)
|
|
begin
|
|
loc = target.method(:to_yaml).source_location.first
|
|
if loc !~ /(syck\/rubytypes.rb|psych\/core_ext.rb)/
|
|
unless target.respond_to?(:encode_with)
|
|
if $VERBOSE
|
|
warn "implementing to_yaml is deprecated, please implement \"encode_with\""
|
|
end
|
|
|
|
target.to_yaml(:nodump => true)
|
|
end
|
|
end
|
|
rescue
|
|
# public_method or source_location might be overridden,
|
|
# and it's OK to skip it since it's only to emit a warning
|
|
end
|
|
end
|
|
|
|
if target.respond_to?(:encode_with)
|
|
dump_coder target
|
|
else
|
|
send(@dispatch_cache[target.class], target)
|
|
end
|
|
end
|
|
|
|
def visit_Psych_Omap o
|
|
seq = @emitter.start_sequence(nil, '!omap', false, Nodes::Sequence::BLOCK)
|
|
register(o, seq)
|
|
|
|
o.each { |k,v| visit_Hash k => v }
|
|
@emitter.end_sequence
|
|
end
|
|
|
|
def visit_Object o
|
|
tag = Psych.dump_tags[o.class]
|
|
unless tag
|
|
klass = o.class == Object ? nil : o.class.name
|
|
tag = ['!ruby/object', klass].compact.join(':')
|
|
end
|
|
|
|
map = @emitter.start_mapping(nil, tag, false, Nodes::Mapping::BLOCK)
|
|
register(o, map)
|
|
|
|
dump_ivars o
|
|
@emitter.end_mapping
|
|
end
|
|
|
|
def visit_Struct o
|
|
tag = ['!ruby/struct', o.class.name].compact.join(':')
|
|
|
|
register o, @emitter.start_mapping(nil, tag, false, Nodes::Mapping::BLOCK)
|
|
o.members.each do |member|
|
|
@emitter.scalar member.to_s, nil, nil, true, false, Nodes::Scalar::ANY
|
|
accept o[member]
|
|
end
|
|
|
|
dump_ivars o
|
|
|
|
@emitter.end_mapping
|
|
end
|
|
|
|
def visit_Exception o
|
|
tag = ['!ruby/exception', o.class.name].join ':'
|
|
|
|
@emitter.start_mapping nil, tag, false, Nodes::Mapping::BLOCK
|
|
|
|
{
|
|
'message' => private_iv_get(o, 'mesg'),
|
|
'backtrace' => private_iv_get(o, 'backtrace'),
|
|
}.each do |k,v|
|
|
next unless v
|
|
@emitter.scalar k, nil, nil, true, false, Nodes::Scalar::ANY
|
|
accept v
|
|
end
|
|
|
|
dump_ivars o
|
|
|
|
@emitter.end_mapping
|
|
end
|
|
|
|
def visit_Regexp o
|
|
register o, @emitter.scalar(o.inspect, nil, '!ruby/regexp', false, false, Nodes::Scalar::ANY)
|
|
end
|
|
|
|
def visit_DateTime o
|
|
formatted = if o.offset.zero?
|
|
o.strftime("%Y-%m-%d %H:%M:%S.%9N Z".freeze)
|
|
else
|
|
o.strftime("%Y-%m-%d %H:%M:%S.%9N %:z".freeze)
|
|
end
|
|
tag = '!ruby/object:DateTime'
|
|
register o, @emitter.scalar(formatted, nil, tag, false, false, Nodes::Scalar::ANY)
|
|
end
|
|
|
|
def visit_Time o
|
|
formatted = format_time o
|
|
register o, @emitter.scalar(formatted, nil, nil, true, false, Nodes::Scalar::ANY)
|
|
end
|
|
|
|
def visit_Rational o
|
|
register o, @emitter.start_mapping(nil, '!ruby/object:Rational', false, Nodes::Mapping::BLOCK)
|
|
|
|
[
|
|
'denominator', o.denominator.to_s,
|
|
'numerator', o.numerator.to_s
|
|
].each do |m|
|
|
@emitter.scalar m, nil, nil, true, false, Nodes::Scalar::ANY
|
|
end
|
|
|
|
@emitter.end_mapping
|
|
end
|
|
|
|
def visit_Complex o
|
|
register o, @emitter.start_mapping(nil, '!ruby/object:Complex', false, Nodes::Mapping::BLOCK)
|
|
|
|
['real', o.real.to_s, 'image', o.imag.to_s].each do |m|
|
|
@emitter.scalar m, nil, nil, true, false, Nodes::Scalar::ANY
|
|
end
|
|
|
|
@emitter.end_mapping
|
|
end
|
|
|
|
def visit_Integer o
|
|
@emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
|
|
end
|
|
alias :visit_TrueClass :visit_Integer
|
|
alias :visit_FalseClass :visit_Integer
|
|
alias :visit_Date :visit_Integer
|
|
|
|
def visit_Float o
|
|
if o.nan?
|
|
@emitter.scalar '.nan', nil, nil, true, false, Nodes::Scalar::ANY
|
|
elsif o.infinite?
|
|
@emitter.scalar((o.infinite? > 0 ? '.inf' : '-.inf'),
|
|
nil, nil, true, false, Nodes::Scalar::ANY)
|
|
else
|
|
@emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
|
|
end
|
|
end
|
|
|
|
def visit_BigDecimal o
|
|
@emitter.scalar o._dump, nil, '!ruby/object:BigDecimal', false, false, Nodes::Scalar::ANY
|
|
end
|
|
|
|
def visit_String o
|
|
plain = true
|
|
quote = true
|
|
style = Nodes::Scalar::PLAIN
|
|
tag = nil
|
|
str = o
|
|
|
|
if binary?(o)
|
|
str = [o].pack('m').chomp
|
|
tag = '!binary' # FIXME: change to below when syck is removed
|
|
#tag = 'tag:yaml.org,2002:binary'
|
|
style = Nodes::Scalar::LITERAL
|
|
plain = false
|
|
quote = false
|
|
elsif o =~ /\n/
|
|
style = Nodes::Scalar::LITERAL
|
|
elsif o =~ /^\W[^"]*$/
|
|
style = Nodes::Scalar::DOUBLE_QUOTED
|
|
else
|
|
unless String === @ss.tokenize(o)
|
|
style = Nodes::Scalar::SINGLE_QUOTED
|
|
end
|
|
end
|
|
|
|
ivars = find_ivars o
|
|
|
|
if ivars.empty?
|
|
unless o.class == ::String
|
|
tag = "!ruby/string:#{o.class}"
|
|
plain = false
|
|
quote = false
|
|
end
|
|
@emitter.scalar str, nil, tag, plain, quote, style
|
|
else
|
|
maptag = '!ruby/string'
|
|
maptag << ":#{o.class}" unless o.class == ::String
|
|
|
|
register o, @emitter.start_mapping(nil, maptag, false, Nodes::Mapping::BLOCK)
|
|
@emitter.scalar 'str', nil, nil, true, false, Nodes::Scalar::ANY
|
|
@emitter.scalar str, nil, tag, plain, quote, style
|
|
|
|
dump_ivars o
|
|
|
|
@emitter.end_mapping
|
|
end
|
|
end
|
|
|
|
def visit_Module o
|
|
raise TypeError, "can't dump anonymous module: #{o}" unless o.name
|
|
register o, @emitter.scalar(o.name, nil, '!ruby/module', false, false, Nodes::Scalar::SINGLE_QUOTED)
|
|
end
|
|
|
|
def visit_Class o
|
|
raise TypeError, "can't dump anonymous class: #{o}" unless o.name
|
|
register o, @emitter.scalar(o.name, nil, '!ruby/class', false, false, Nodes::Scalar::SINGLE_QUOTED)
|
|
end
|
|
|
|
def visit_Range o
|
|
register o, @emitter.start_mapping(nil, '!ruby/range', false, Nodes::Mapping::BLOCK)
|
|
['begin', o.begin, 'end', o.end, 'excl', o.exclude_end?].each do |m|
|
|
accept m
|
|
end
|
|
@emitter.end_mapping
|
|
end
|
|
|
|
def visit_Hash o
|
|
tag = o.class == ::Hash ? nil : "!ruby/hash:#{o.class}"
|
|
implicit = !tag
|
|
|
|
register(o, @emitter.start_mapping(nil, tag, implicit, Psych::Nodes::Mapping::BLOCK))
|
|
|
|
o.each do |k,v|
|
|
accept k
|
|
accept v
|
|
end
|
|
|
|
@emitter.end_mapping
|
|
end
|
|
|
|
def visit_Psych_Set o
|
|
register(o, @emitter.start_mapping(nil, '!set', false, Psych::Nodes::Mapping::BLOCK))
|
|
|
|
o.each do |k,v|
|
|
accept k
|
|
accept v
|
|
end
|
|
|
|
@emitter.end_mapping
|
|
end
|
|
|
|
def visit_Array o
|
|
if o.class == ::Array
|
|
register o, @emitter.start_sequence(nil, nil, true, Nodes::Sequence::BLOCK)
|
|
o.each { |c| accept c }
|
|
@emitter.end_sequence
|
|
else
|
|
visit_array_subclass o
|
|
end
|
|
end
|
|
|
|
def visit_NilClass o
|
|
@emitter.scalar('', nil, 'tag:yaml.org,2002:null', true, false, Nodes::Scalar::ANY)
|
|
end
|
|
|
|
def visit_Symbol o
|
|
@emitter.scalar ":#{o}", nil, nil, true, false, Nodes::Scalar::ANY
|
|
end
|
|
|
|
private
|
|
# FIXME: Remove the index and count checks in Psych 3.0
|
|
NULL = "\x00"
|
|
BINARY_RANGE = "\x00-\x7F"
|
|
WS_RANGE = "^ -~\t\r\n"
|
|
|
|
def binary? string
|
|
(string.encoding == Encoding::ASCII_8BIT && !string.ascii_only?) ||
|
|
string.index(NULL) ||
|
|
string.count(BINARY_RANGE, WS_RANGE).fdiv(string.length) > 0.3
|
|
end
|
|
|
|
def visit_array_subclass o
|
|
tag = "!ruby/array:#{o.class}"
|
|
if o.instance_variables.empty?
|
|
node = @emitter.start_sequence(nil, tag, false, Nodes::Sequence::BLOCK)
|
|
register o, node
|
|
o.each { |c| accept c }
|
|
@emitter.end_sequence
|
|
else
|
|
node = @emitter.start_mapping(nil, tag, false, Nodes::Sequence::BLOCK)
|
|
register o, node
|
|
|
|
# Dump the internal list
|
|
accept 'internal'
|
|
@emitter.start_sequence(nil, nil, true, Nodes::Sequence::BLOCK)
|
|
o.each { |c| accept c }
|
|
@emitter.end_sequence
|
|
|
|
# Dump the ivars
|
|
accept 'ivars'
|
|
@emitter.start_mapping(nil, nil, true, Nodes::Sequence::BLOCK)
|
|
o.instance_variables.each do |ivar|
|
|
accept ivar
|
|
accept o.instance_variable_get ivar
|
|
end
|
|
@emitter.end_mapping
|
|
|
|
@emitter.end_mapping
|
|
end
|
|
end
|
|
|
|
def dump_list o
|
|
end
|
|
|
|
# '%:z' was no defined until 1.9.3
|
|
if RUBY_VERSION < '1.9.3'
|
|
def format_time time
|
|
formatted = time.strftime("%Y-%m-%d %H:%M:%S.%9N")
|
|
|
|
if time.utc?
|
|
formatted += " Z"
|
|
else
|
|
zone = time.strftime('%z')
|
|
formatted += " #{zone[0,3]}:#{zone[3,5]}"
|
|
end
|
|
|
|
formatted
|
|
end
|
|
else
|
|
def format_time time
|
|
if time.utc?
|
|
time.strftime("%Y-%m-%d %H:%M:%S.%9N Z")
|
|
else
|
|
time.strftime("%Y-%m-%d %H:%M:%S.%9N %:z")
|
|
end
|
|
end
|
|
end
|
|
|
|
# FIXME: remove this method once "to_yaml_properties" is removed
|
|
def find_ivars target
|
|
begin
|
|
loc = target.method(:to_yaml_properties).source_location.first
|
|
unless loc.start_with?(Psych::DEPRECATED) || loc.end_with?('rubytypes.rb')
|
|
if $VERBOSE
|
|
warn "#{loc}: to_yaml_properties is deprecated, please implement \"encode_with(coder)\""
|
|
end
|
|
return target.to_yaml_properties
|
|
end
|
|
rescue
|
|
# public_method or source_location might be overridden,
|
|
# and it's OK to skip it since it's only to emit a warning.
|
|
end
|
|
|
|
target.instance_variables
|
|
end
|
|
|
|
def register target, yaml_obj
|
|
@st.register target, yaml_obj
|
|
yaml_obj
|
|
end
|
|
|
|
def dump_coder o
|
|
@coders << o
|
|
tag = Psych.dump_tags[o.class]
|
|
unless tag
|
|
klass = o.class == Object ? nil : o.class.name
|
|
tag = ['!ruby/object', klass].compact.join(':')
|
|
end
|
|
|
|
c = Psych::Coder.new(tag)
|
|
o.encode_with(c)
|
|
emit_coder c
|
|
end
|
|
|
|
def emit_coder c
|
|
case c.type
|
|
when :scalar
|
|
@emitter.scalar c.scalar, nil, c.tag, c.tag.nil?, false, Nodes::Scalar::ANY
|
|
when :seq
|
|
@emitter.start_sequence nil, c.tag, c.tag.nil?, Nodes::Sequence::BLOCK
|
|
c.seq.each do |thing|
|
|
accept thing
|
|
end
|
|
@emitter.end_sequence
|
|
when :map
|
|
@emitter.start_mapping nil, c.tag, c.implicit, c.style
|
|
c.map.each do |k,v|
|
|
accept k
|
|
accept v
|
|
end
|
|
@emitter.end_mapping
|
|
when :object
|
|
accept c.object
|
|
end
|
|
end
|
|
|
|
def dump_ivars target
|
|
ivars = find_ivars target
|
|
|
|
ivars.each do |iv|
|
|
@emitter.scalar("#{iv.to_s.sub(/^@/, '')}", nil, nil, true, false, Nodes::Scalar::ANY)
|
|
accept target.instance_variable_get(iv)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|