010e246756
Some methods were added to public API in 5b14129d8d4ad302b4e11df6bd5c7891b75f393c and they should be not part of the public API.
208 lines
6.3 KiB
Ruby
208 lines
6.3 KiB
Ruby
require "time"
|
|
require "base64"
|
|
require "bigdecimal"
|
|
require "active_support/core_ext/module/delegation"
|
|
require "active_support/core_ext/string/inflections"
|
|
require "active_support/core_ext/date_time/calculations"
|
|
|
|
module ActiveSupport
|
|
# = XmlMini
|
|
#
|
|
# To use the much faster libxml parser:
|
|
# gem 'libxml-ruby', '=0.9.7'
|
|
# XmlMini.backend = 'LibXML'
|
|
module XmlMini
|
|
extend self
|
|
|
|
# This module decorates files deserialized using Hash.from_xml with
|
|
# the <tt>original_filename</tt> and <tt>content_type</tt> methods.
|
|
module FileLike #:nodoc:
|
|
attr_writer :original_filename, :content_type
|
|
|
|
def original_filename
|
|
@original_filename || "untitled"
|
|
end
|
|
|
|
def content_type
|
|
@content_type || "application/octet-stream"
|
|
end
|
|
end
|
|
|
|
DEFAULT_ENCODINGS = {
|
|
"binary" => "base64"
|
|
} unless defined?(DEFAULT_ENCODINGS)
|
|
|
|
unless defined?(TYPE_NAMES)
|
|
TYPE_NAMES = {
|
|
"Symbol" => "symbol",
|
|
"Integer" => "integer",
|
|
"BigDecimal" => "decimal",
|
|
"Float" => "float",
|
|
"TrueClass" => "boolean",
|
|
"FalseClass" => "boolean",
|
|
"Date" => "date",
|
|
"DateTime" => "dateTime",
|
|
"Time" => "dateTime",
|
|
"Array" => "array",
|
|
"Hash" => "hash"
|
|
}
|
|
|
|
# No need to map these on Ruby 2.4+
|
|
TYPE_NAMES["Fixnum"] = "integer" unless 0.class == Integer
|
|
TYPE_NAMES["Bignum"] = "integer" unless 0.class == Integer
|
|
end
|
|
|
|
FORMATTING = {
|
|
"symbol" => Proc.new { |symbol| symbol.to_s },
|
|
"date" => Proc.new { |date| date.to_s(:db) },
|
|
"dateTime" => Proc.new { |time| time.xmlschema },
|
|
"binary" => Proc.new { |binary| ::Base64.encode64(binary) },
|
|
"yaml" => Proc.new { |yaml| yaml.to_yaml }
|
|
} unless defined?(FORMATTING)
|
|
|
|
# TODO use regexp instead of Date.parse
|
|
unless defined?(PARSING)
|
|
PARSING = {
|
|
"symbol" => Proc.new { |symbol| symbol.to_s.to_sym },
|
|
"date" => Proc.new { |date| ::Date.parse(date) },
|
|
"datetime" => Proc.new { |time| Time.xmlschema(time).utc rescue ::DateTime.parse(time).utc },
|
|
"integer" => Proc.new { |integer| integer.to_i },
|
|
"float" => Proc.new { |float| float.to_f },
|
|
"decimal" => Proc.new do |number|
|
|
if String === number
|
|
begin
|
|
BigDecimal(number)
|
|
rescue ArgumentError
|
|
BigDecimal("0")
|
|
end
|
|
else
|
|
BigDecimal(number)
|
|
end
|
|
end,
|
|
"boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.to_s.strip) },
|
|
"string" => Proc.new { |string| string.to_s },
|
|
"yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml },
|
|
"base64Binary" => Proc.new { |bin| ::Base64.decode64(bin) },
|
|
"binary" => Proc.new { |bin, entity| _parse_binary(bin, entity) },
|
|
"file" => Proc.new { |file, entity| _parse_file(file, entity) }
|
|
}
|
|
|
|
PARSING.update(
|
|
"double" => PARSING["float"],
|
|
"dateTime" => PARSING["datetime"]
|
|
)
|
|
end
|
|
|
|
attr_accessor :depth
|
|
self.depth = 100
|
|
|
|
delegate :parse, to: :backend
|
|
|
|
def backend
|
|
current_thread_backend || @backend
|
|
end
|
|
|
|
def backend=(name)
|
|
backend = name && cast_backend_name_to_module(name)
|
|
self.current_thread_backend = backend if current_thread_backend
|
|
@backend = backend
|
|
end
|
|
|
|
def with_backend(name)
|
|
old_backend = current_thread_backend
|
|
self.current_thread_backend = name && cast_backend_name_to_module(name)
|
|
yield
|
|
ensure
|
|
self.current_thread_backend = old_backend
|
|
end
|
|
|
|
def to_tag(key, value, options)
|
|
type_name = options.delete(:type)
|
|
merged_options = options.merge(root: key, skip_instruct: true)
|
|
|
|
if value.is_a?(::Method) || value.is_a?(::Proc)
|
|
if value.arity == 1
|
|
value.call(merged_options)
|
|
else
|
|
value.call(merged_options, key.to_s.singularize)
|
|
end
|
|
elsif value.respond_to?(:to_xml)
|
|
value.to_xml(merged_options)
|
|
else
|
|
type_name ||= TYPE_NAMES[value.class.name]
|
|
type_name ||= value.class.name if value && !value.respond_to?(:to_str)
|
|
type_name = type_name.to_s if type_name
|
|
type_name = "dateTime" if type_name == "datetime"
|
|
|
|
key = rename_key(key.to_s, options)
|
|
|
|
attributes = options[:skip_types] || type_name.nil? ? {} : { type: type_name }
|
|
attributes[:nil] = true if value.nil?
|
|
|
|
encoding = options[:encoding] || DEFAULT_ENCODINGS[type_name]
|
|
attributes[:encoding] = encoding if encoding
|
|
|
|
formatted_value = FORMATTING[type_name] && !value.nil? ?
|
|
FORMATTING[type_name].call(value) : value
|
|
|
|
options[:builder].tag!(key, formatted_value, attributes)
|
|
end
|
|
end
|
|
|
|
def rename_key(key, options = {})
|
|
camelize = options[:camelize]
|
|
dasherize = !options.has_key?(:dasherize) || options[:dasherize]
|
|
if camelize
|
|
key = true == camelize ? key.camelize : key.camelize(camelize)
|
|
end
|
|
key = _dasherize(key) if dasherize
|
|
key
|
|
end
|
|
|
|
private
|
|
|
|
def _dasherize(key)
|
|
# $2 must be a non-greedy regex for this to work
|
|
left, middle, right = /\A(_*)(.*?)(_*)\Z/.match(key.strip)[1, 3]
|
|
"#{left}#{middle.tr('_ ', '--')}#{right}"
|
|
end
|
|
|
|
# TODO: Add support for other encodings
|
|
def _parse_binary(bin, entity)
|
|
case entity["encoding"]
|
|
when "base64"
|
|
::Base64.decode64(bin)
|
|
else
|
|
bin
|
|
end
|
|
end
|
|
|
|
def _parse_file(file, entity)
|
|
f = StringIO.new(::Base64.decode64(file))
|
|
f.extend(FileLike)
|
|
f.original_filename = entity["name"]
|
|
f.content_type = entity["content_type"]
|
|
f
|
|
end
|
|
|
|
def current_thread_backend
|
|
Thread.current[:xml_mini_backend]
|
|
end
|
|
|
|
def current_thread_backend=(name)
|
|
Thread.current[:xml_mini_backend] = name && cast_backend_name_to_module(name)
|
|
end
|
|
|
|
def cast_backend_name_to_module(name)
|
|
if name.is_a?(Module)
|
|
name
|
|
else
|
|
require "active_support/xml_mini/#{name.downcase}"
|
|
ActiveSupport.const_get("XmlMini_#{name}")
|
|
end
|
|
end
|
|
end
|
|
|
|
XmlMini.backend = "REXML"
|
|
end
|