Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Move several configuration values from Hash to ActiveSupport::XmlMini…

…, which both Hash and Array depends on.

Also, refactored ActiveModel serializers to just use ActiveSupport::XmlMini.to_tag. As consequence, if a serialized attribute is an array or a hash, it's not encoded as yaml, but as a hash or array.
  • Loading branch information...
commit 2e9af3638d950ef840e1287f99e323887ec6a4c4 1 parent 1bea5c7
José Valim josevalim authored
2  actionpack/CHANGELOG
View
@@ -1,5 +1,7 @@
*Rails 3.0.0 [beta 4/release candidate] (unreleased)*
+* Both :xml and :json renderers now forwards the given options to the model, allowing you to invoke them as render :xml => @projects, :include => :tasks [José Valim, Yehuda Katz]
+
* Renamed the field error CSS class from fieldWithErrors to field_with_errors for consistency. [Jeremy Kemper]
* Add support for shorthand routes like /projects/status(.:format) #4423 [Diego Carrion]
162 activemodel/lib/active_model/serializers/xml.rb
View
@@ -1,5 +1,6 @@
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/array/conversions'
require 'active_support/core_ext/hash/conversions'
require 'active_support/core_ext/hash/slice'
@@ -15,65 +16,29 @@ class Attribute #:nodoc:
def initialize(name, serializable, raw_value=nil)
@name, @serializable = name, serializable
- @raw_value = raw_value || @serializable.send(name)
-
+ @value = value || @serializable.send(name)
Andreas
andreas added a note

Shouldn't this line be

@value = raw_value || @serializable.send(name)

or change the name of the argument from "raw_value" to "value"?

José Valim Owner

Yes, patch with tests please!

Jakub Okoński
farnoy added a note

Should it look like:
http://gist.github.com/561115 ?
It's my first patch, and I'm not sure it's correct.

José Valim Owner

Yes, that is it! Do you think you can add a test as well? How did you come up with this error?

Jakub Okoński
farnoy added a note

I've just read andreas's note and wanted to try create a patch, I had no error with this. I'll try to create some test tommorow.

Andreas
andreas added a note

This is the minimal example I can find to reproduce the error:

require 'active_resource'
class Foo < ActiveResource::Base
  self.site = ""
end

Foo.new(:test => nil).to_xml

This will throw an ArgumentError. The problem is that :test is a private method on Object, which will be invoked since "value" is always nil.

José Valim Owner

Awesome! Farnoy, you can add the example above as a test case to ActiveResource and then add a fix to ensure it is going to work!

Andreas
andreas added a note

Here's an attempt at a patch with test: http://gist.github.com/562146

Jakub Okoński
farnoy added a note

For me this function is not needed, this issue has been undetected for months now, Serializer structure has overloaded constructor method, and this one that we are trying to fix is not used:
http://gist.github.com/562383
From documentation.

#EDIT:
andreas test won't work, as the second constructor is triggered by .to_xml

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@type = compute_type
- @value = compute_value
- end
-
- # There is a significant speed improvement if the value
- # does not need to be escaped, as <tt>tag!</tt> escapes all values
- # to ensure that valid XML is generated. For known binary
- # values, it is at least an order of magnitude faster to
- # Base64 encode binary values and directly put them in the
- # output XML than to pass the original value or the Base64
- # encoded value to the <tt>tag!</tt> method. It definitely makes
- # no sense to Base64 encode the value and then give it to
- # <tt>tag!</tt>, since that just adds additional overhead.
- def needs_encoding?
- ![ :binary, :date, :datetime, :boolean, :float, :integer ].include?(type)
end
- def decorations(include_types = true)
+ def decorations
decorations = {}
-
- if type == :binary
- decorations[:encoding] = 'base64'
- end
-
- if include_types && type != :string
- decorations[:type] = type
- end
-
- if value.nil?
- decorations[:nil] = true
- end
-
+ decorations[:encoding] = 'base64' if type == :binary
+ decorations[:type] = type unless type == :string
+ decorations[:nil] = true if value.nil?
decorations
end
- protected
- def compute_type
- type = Hash::XML_TYPE_NAMES[@raw_value.class.name]
- type ||= :string if @raw_value.respond_to?(:to_str)
- type ||= :yaml
- type
- end
+ protected
- def compute_value
- if formatter = Hash::XML_FORMATTING[type.to_s]
- @raw_value ? formatter.call(@raw_value) : nil
- else
- @raw_value
- end
- end
+ def compute_type
+ type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name]
+ type ||= :string if value.respond_to?(:to_str)
+ type ||= :yaml
+ type
+ end
end
class MethodAttribute < Attribute #:nodoc:
- protected
- def compute_type
- Hash::XML_TYPE_NAMES[@raw_value.class.name] || :string
- end
end
attr_reader :options
@@ -92,7 +57,7 @@ def initialize(serializable, options = nil)
# then because <tt>:except</tt> is set to a default value, the second
# level model can have both <tt>:except</tt> and <tt>:only</tt> set. So if
# <tt>:only</tt> is set, always delete <tt>:except</tt>.
- def serializable_attributes_hash
+ def attributes_hash
attributes = @serializable.attributes
if options[:only].any?
attributes.slice(*options[:only])
@@ -104,10 +69,12 @@ def serializable_attributes_hash
end
def serializable_attributes
- serializable_attributes_hash.map { |name, value| self.class::Attribute.new(name, @serializable, value) }
+ attributes_hash.map do |name, value|
+ self.class::Attribute.new(name, @serializable, value)
+ end
end
- def serializable_method_attributes
+ def serializable_methods
Array.wrap(options[:methods]).inject([]) do |methods, name|
methods << self.class::MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s)
methods
@@ -115,80 +82,53 @@ def serializable_method_attributes
end
def serialize
- args = [root]
-
- if options[:namespace]
- args << {:xmlns => options[:namespace]}
- end
+ require 'builder' unless defined? ::Builder
- if options[:type]
- args << {:type => options[:type]}
- end
-
- builder.tag!(*args) do
- add_attributes
- procs = options.delete(:procs)
- options[:procs] = procs
- add_procs
- yield builder if block_given?
- end
- end
+ options[:indent] ||= 2
+ options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent])
- private
- def builder
- @builder ||= begin
- require 'builder' unless defined? ::Builder
- options[:indent] ||= 2
- builder = options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent])
+ @builder = options[:builder]
+ @builder.instruct! unless options[:skip_instruct]
- unless options[:skip_instruct]
- builder.instruct!
- options[:skip_instruct] = true
- end
+ root = (options[:root] || @serializable.class.model_name.singular).to_s
+ root = ActiveSupport::XmlMini.rename_key(root, options)
- builder
- end
- end
-
- def root
- root = (options[:root] || @serializable.class.model_name.singular).to_s
- reformat_name(root)
- end
+ args = [root]
+ args << {:xmlns => options[:namespace]} if options[:namespace]
+ args << {:type => options[:type]} if options[:type] && !options[:skip_types]
- def dasherize?
- !options.has_key?(:dasherize) || options[:dasherize]
+ @builder.tag!(*args) do
+ add_attributes_and_methods
+ add_extra_behavior
+ add_procs
+ yield @builder if block_given?
end
+ end
- def camelize?
- options.has_key?(:camelize) && options[:camelize]
- end
+ private
- def reformat_name(name)
- name = name.camelize if camelize?
- dasherize? ? name.dasherize : name
- end
+ def add_extra_behavior
+ end
- def add_attributes
- (serializable_attributes + serializable_method_attributes).each do |attribute|
- builder.tag!(
- reformat_name(attribute.name),
- attribute.value.to_s,
- attribute.decorations(!options[:skip_types])
- )
- end
+ def add_attributes_and_methods
+ (serializable_attributes + serializable_methods).each do |attribute|
+ key = ActiveSupport::XmlMini.rename_key(attribute.name, options)
+ ActiveSupport::XmlMini.to_tag(key, attribute.value,
+ options.merge(attribute.decorations))
end
+ end
- def add_procs
- if procs = options.delete(:procs)
- [ *procs ].each do |proc|
- if proc.arity > 1
- proc.call(options, @serializable)
- else
- proc.call(options)
- end
+ def add_procs
+ if procs = options.delete(:procs)
+ Array.wrap(procs).each do |proc|
+ if proc.arity == 1
+ proc.call(options)
+ else
+ proc.call(options, @serializable)
end
end
end
+ end
end
def to_xml(options = {}, &block)
2  activerecord/CHANGELOG
View
@@ -1,5 +1,7 @@
*Rails 3.0.0 [beta 4/release candidate] (unreleased)*
+* Serialized attributes are not converted to YAML if they are any of the formats that can be serialized to XML (like Hash, Array and Strings). [José Valim]
+
* Destroy uses optimistic locking. If lock_version on the record you're destroying doesn't match lock_version in the database, a StaleObjectError is raised. #1966 [Curtis Hawthorne]
* PostgreSQL: drop support for old postgres driver. Use pg 0.9.0 or later. [Jeremy Kemper]
85 activerecord/lib/active_record/serializers/xml_serializer.rb
View
@@ -182,16 +182,31 @@ def initialize(*args)
options[:except] |= Array.wrap(@serializable.class.inheritance_column)
end
+ def add_extra_behavior
+ add_includes
+ end
+
+ def add_includes
+ procs = options.delete(:procs)
+ @serializable.send(:serializable_add_includes, options) do |association, records, opts|
+ add_associations(association, records, opts)
+ end
+ options[:procs] = procs
+ end
+
+ # TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well.
def add_associations(association, records, opts)
+ association_name = association.to_s.singularize
+ merged_options = options.merge(opts).merge!(:root => association_name)
+
if records.is_a?(Enumerable)
- tag = reformat_name(association.to_s)
- type = options[:skip_types] ? {} : {:type => "array"}
+ tag = ActiveSupport::XmlMini.rename_key(association.to_s, options)
+ type = options[:skip_types] ? { } : {:type => "array"}
if records.empty?
- builder.tag!(tag, type)
+ @builder.tag!(tag, type)
else
- builder.tag!(tag, type) do
- association_name = association.to_s.singularize
+ @builder.tag!(tag, type) do
records.each do |record|
if options[:skip_types]
record_type = {}
@@ -200,60 +215,30 @@ def add_associations(association, records, opts)
record_type = {:type => record_class}
end
- record.to_xml opts.merge(:root => association_name).merge(record_type)
+ record.to_xml merged_options.merge(record_type)
end
end
end
- else
- if record = @serializable.send(association)
- record.to_xml(opts.merge(:root => association))
- end
- end
- end
-
- def serialize
- args = [root]
- if options[:namespace]
- args << {:xmlns=>options[:namespace]}
- end
-
- if options[:type]
- args << {:type=>options[:type]}
- end
-
- builder.tag!(*args) do
- add_attributes
- procs = options.delete(:procs)
- @serializable.send(:serializable_add_includes, options) { |association, records, opts|
- add_associations(association, records, opts)
- }
- options[:procs] = procs
- add_procs
- yield builder if block_given?
+ elsif record = @serializable.send(association)
+ record.to_xml(merged_options)
end
end
class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
- protected
- def compute_type
- type = @serializable.class.serialized_attributes.has_key?(name) ? :yaml : @serializable.class.columns_hash[name].type
-
- case type
- when :text
- :string
- when :time
- :datetime
- else
- type
- end
- end
- end
+ def compute_type
+ type = @serializable.class.serialized_attributes.has_key?(name) ?
+ super : @serializable.class.columns_hash[name].type
Víctor Martínez
knoopx added a note

This will miserably fail if the attribute was not part of the model but was dynamically generated by the resulting SQL. There's a bug on this:
https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/4840-to_xml-doesnt-work-in-such-case-eventselecttitle-as-tto_xml#ticket-4840-5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
- class MethodAttribute < Attribute #:nodoc:
- protected
- def compute_type
- Hash::XML_TYPE_NAMES[@serializable.send(name).class.name] || :string
+ case type
+ when :text
+ :string
+ when :time
+ :datetime
+ else
+ type
end
+ end
+ protected :compute_type
end
end
end
5 activerecord/test/cases/base_test.rb
View
@@ -2085,6 +2085,7 @@ def test_to_xml
assert_equal "topic", xml.root.name
assert_equal "The First Topic" , xml.elements["//title"].text
assert_equal "David" , xml.elements["//author-name"].text
+ assert_match "Have a nice day", xml.elements["//content"].text
assert_equal "1", xml.elements["//id"].text
assert_equal "integer" , xml.elements["//id"].attributes['type']
@@ -2095,10 +2096,6 @@ def test_to_xml
assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text
assert_equal "datetime" , xml.elements["//written-on"].attributes['type']
- assert_match(/^--- Have a nice day\n/ , xml.elements["//content"].text)
- assert_equal 'Have a nice day' , YAML.load(xml.elements["//content"].text)
- assert_equal "yaml" , xml.elements["//content"].attributes['type']
-
assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text
assert_equal nil, xml.elements["//parent-id"].text
4 activerecord/test/cases/xml_serialization_test.rb
View
@@ -79,8 +79,8 @@ def test_should_serialize_boolean
assert_match %r{<awesome type=\"boolean\">false</awesome>}, @xml
end
- def test_should_serialize_yaml
- assert_match %r{<preferences type=\"yaml\">---\s?\n:gem: ruby\n</preferences>}, @xml
+ def test_should_serialize_hash
+ assert_match %r{<preferences>\s*<gem>ruby</gem>\s*</preferences>}m, @xml
end
end
2  activesupport/CHANGELOG
View
@@ -1,5 +1,7 @@
*Rails 3.0.0 [beta 4/release candidate] (unreleased)*
+* Array#to_xml is more powerful and able to handle the same types as Hash#to_xml #4490 [Neeraj Singh]
+
* Harmonize the caching API and refactor the backends. #4452 [Brian Durand]
All caches:
* Add default options to initializer that will be sent to all read, write, fetch, exist?, increment, and decrement
1  activesupport/lib/active_support/core_ext/array.rb
View
@@ -5,4 +5,3 @@
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/array/grouping'
require 'active_support/core_ext/array/random_access'
-require 'active_support/core_ext/hash/conversions_xml_value'
33 activesupport/lib/active_support/core_ext/array/conversions.rb
View
@@ -1,7 +1,7 @@
+require 'active_support/xml_mini'
require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/hash/conversions_xml_value'
require 'active_support/core_ext/hash/reverse_merge'
-require 'active_support/inflector'
+require 'active_support/core_ext/string/inflections'
class Array
# Converts the array to a comma-separated sentence where the last element is joined by the connector word. Options:
@@ -52,8 +52,6 @@ def to_formatted_s(format = :default)
alias_method :to_default_s, :to_s
alias_method :to_s, :to_formatted_s
- include Hash::XmlValue
-
# Returns a string that represents this array in XML by sending +to_xml+
# to each element. Active Record collections delegate their representation
# in XML to this method.
@@ -133,22 +131,27 @@ def to_xml(options = {})
require 'builder' unless defined?(Builder)
options = options.dup
- options[:indent] ||= 2
- options.reverse_merge!({ :builder => Builder::XmlMarkup.new(:indent => options[:indent]) })
-
- options[:root] ||= all? { |e| e.is_a?(first.class) && first.class.to_s != "Hash" } ? ActiveSupport::Inflector.pluralize(ActiveSupport::Inflector.underscore(first.class.name)).tr('/', '_') : "objects"
+ options[:indent] ||= 2
+ options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
+ options[:root] ||= if first.class.to_s != "Hash" && all? { |e| e.is_a?(first.class) }
+ underscored = ActiveSupport::Inflector.underscore(first.class.name)
+ ActiveSupport::Inflector.pluralize(underscored).tr('/', '_')
+ else
+ "objects"
+ end
+ builder = options[:builder]
+ builder.instruct! unless options.delete(:skip_instruct)
- options[:builder].instruct! unless options.delete(:skip_instruct)
- root = rename_key(options[:root].to_s, options)
+ root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options)
+ children = options.delete(:children) || root.singularize
- options[:children] ||= options[:root].singularize
attributes = options[:skip_types] ? {} : {:type => "array"}
- return options[:builder].tag!(root, attributes) if empty?
+ return builder.tag!(root, attributes) if empty?
- options[:builder].__send__(:method_missing, root, attributes) do
- each { |value| xml_value(options[:children], value, options) }
- yield options[:builder] if block_given?
+ builder.__send__(:method_missing, root, attributes) do
+ each { |value| ActiveSupport::XmlMini.to_tag(children, value, options) }
+ yield builder if block_given?
end
end
1  activesupport/lib/active_support/core_ext/hash.rb
View
@@ -1,5 +1,4 @@
require 'active_support/core_ext/hash/conversions'
-require 'active_support/core_ext/hash/conversions_xml_value'
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/hash/diff'
require 'active_support/core_ext/hash/except'
116 activesupport/lib/active_support/core_ext/hash/conversions.rb
View
@@ -1,88 +1,11 @@
+require 'active_support/xml_mini'
require 'active_support/time'
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/hash/reverse_merge'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/string/inflections'
-require 'active_support/core_ext/hash/conversions_xml_value'
class Hash
- # This module exists to decorate 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
-
- include XmlValue
-
- XML_TYPE_NAMES = {
- "Symbol" => "symbol",
- "Fixnum" => "integer",
- "Bignum" => "integer",
- "BigDecimal" => "decimal",
- "Float" => "float",
- "TrueClass" => "boolean",
- "FalseClass" => "boolean",
- "Date" => "date",
- "DateTime" => "datetime",
- "Time" => "datetime",
- "Array" => "array",
- "Hash" => "hash"
- } unless defined?(XML_TYPE_NAMES)
-
- XML_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| ActiveSupport::Base64.encode64(binary) },
- "yaml" => Proc.new { |yaml| yaml.to_yaml }
- } unless defined?(XML_FORMATTING)
-
- # TODO: use Time.xmlschema instead of Time.parse;
- # use regexp instead of Date.parse
- unless defined?(XML_PARSING)
- XML_PARSING = {
- "symbol" => Proc.new { |symbol| symbol.to_sym },
- "date" => Proc.new { |date| ::Date.parse(date) },
- "datetime" => Proc.new { |time| ::Time.parse(time).utc rescue ::DateTime.parse(time).utc },
- "integer" => Proc.new { |integer| integer.to_i },
- "float" => Proc.new { |float| float.to_f },
- "decimal" => Proc.new { |number| BigDecimal(number) },
- "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.strip) },
- "string" => Proc.new { |string| string.to_s },
- "yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml },
- "base64Binary" => Proc.new { |bin| ActiveSupport::Base64.decode64(bin) },
- "binary" => Proc.new do |bin, entity|
- case entity['encoding']
- when 'base64'
- ActiveSupport::Base64.decode64(bin)
- # TODO: Add support for other encodings
- else
- bin
- end
- end,
- "file" => Proc.new do |file, entity|
- f = StringIO.new(ActiveSupport::Base64.decode64(file))
- f.extend(FileLike)
- f.original_filename = entity['name']
- f.content_type = entity['content_type']
- f
- end
- }
-
- XML_PARSING.update(
- "double" => XML_PARSING["float"],
- "dateTime" => XML_PARSING["datetime"]
- )
- end
-
# Returns a string containing an XML representation of its receiver:
#
# {"foo" => 1, "bar" => 2}.to_xml
@@ -135,17 +58,18 @@ def to_xml(options = {})
require 'builder' unless defined?(Builder)
options = options.dup
- options[:indent] ||= 2
- options.reverse_merge!({ :builder => Builder::XmlMarkup.new(:indent => options[:indent]),
- :root => "hash" })
- options[:builder].instruct! unless options.delete(:skip_instruct)
- root = rename_key(options[:root].to_s, options)
- # common upto this point
- options[:builder].__send__(:method_missing, root) do
- each do |key, value|
- xml_value(key, value, options)
- end
- yield options[:builder] if block_given?
+ options[:indent] ||= 2
+ options[:root] ||= "hash"
+ options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
+
+ builder = options[:builder]
+ builder.instruct! unless options.delete(:skip_instruct)
+
+ root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options)
+
+ builder.__send__(:method_missing, root) do
+ each { |key, value| ActiveSupport::XmlMini.to_tag(key, value, options) }
+ yield builder if block_given?
end
end
@@ -174,12 +98,8 @@ def typecast_xml_value(value)
end
elsif value.has_key?("__content__")
content = value["__content__"]
- if parser = XML_PARSING[value["type"]]
- if parser.arity == 2
- XML_PARSING[value["type"]].call(content, value)
- else
- XML_PARSING[value["type"]].call(content)
- end
+ if parser = ActiveSupport::XmlMini::PARSING[value["type"]]
+ parser.arity == 1 ? parser.call(content) : parser.call(content, value)
else
content
end
@@ -205,11 +125,7 @@ def typecast_xml_value(value)
end
when 'Array'
value.map! { |i| typecast_xml_value(i) }
- case value.length
- when 0 then nil
- when 1 then value.first
- else value
- end
+ value.length > 1 ? value : value.first
when 'String'
value
else
51 activesupport/lib/active_support/core_ext/hash/conversions_xml_value.rb
View
@@ -1,51 +0,0 @@
-class Hash
- module XmlValue
- def xml_value(key, value, options)
- case value
- when ::Hash
- value.to_xml(options.merge({ :root => key, :skip_instruct => true }))
- when ::Array
- value.to_xml(options.merge({ :root => key, :children => key.to_s.singularize, :skip_instruct => true}))
- when ::Method, ::Proc
- # If the Method or Proc takes two arguments, then
- # pass the suggested child element name. This is
- # used if the Method or Proc will be operating over
- # multiple records and needs to create an containing
- # element that will contain the objects being
- # serialized.
- if 1 == value.arity
- value.call(options.merge({ :root => key, :skip_instruct => true }))
- else
- value.call(options.merge({ :root => key, :skip_instruct => true }), key.to_s.singularize)
- end
- else
- if value.respond_to?(:to_xml)
- value.to_xml(options.merge({ :root => key, :skip_instruct => true }))
- else
- type_name = XML_TYPE_NAMES[value.class.name]
-
- key = rename_key(key.to_s, options)
-
- attributes = options[:skip_types] || value.nil? || type_name.nil? ? { } : { :type => type_name }
- if value.nil?
- attributes[:nil] = true
- end
-
- options[:builder].tag!(key,
- XML_FORMATTING[type_name] ? XML_FORMATTING[type_name].call(value) : value,
- attributes
- )
- end
- end
- #yield options[:builder] if block_given?
- end
-
- def rename_key(key, options = {})
- camelize = options.has_key?(:camelize) && options[:camelize]
- dasherize = !options.has_key?(:dasherize) || options[:dasherize]
- key = key.camelize if camelize
- dasherize ? key.dasherize : key
- end
- end
-end
-
127 activesupport/lib/active_support/xml_mini.rb
View
@@ -9,6 +9,71 @@ module ActiveSupport
module XmlMini
extend self
+ # This module exists to decorate 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?(TYPE_NAMES)
+
+ TYPE_NAMES = {
+ "Symbol" => "symbol",
+ "Fixnum" => "integer",
+ "Bignum" => "integer",
+ "BigDecimal" => "decimal",
+ "Float" => "float",
+ "TrueClass" => "boolean",
+ "FalseClass" => "boolean",
+ "Date" => "date",
+ "DateTime" => "datetime",
+ "Time" => "datetime",
+ "Array" => "array",
+ "Hash" => "hash"
+ } unless defined?(TYPE_NAMES)
+
+ 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| ActiveSupport::Base64.encode64(binary) },
+ "yaml" => Proc.new { |yaml| yaml.to_yaml }
+ } unless defined?(FORMATTING)
+
+ # TODO: use Time.xmlschema instead of Time.parse;
+ # use regexp instead of Date.parse
+ unless defined?(PARSING)
+ PARSING = {
+ "symbol" => Proc.new { |symbol| symbol.to_sym },
+ "date" => Proc.new { |date| ::Date.parse(date) },
+ "datetime" => Proc.new { |time| ::Time.parse(time).utc rescue ::DateTime.parse(time).utc },
+ "integer" => Proc.new { |integer| integer.to_i },
+ "float" => Proc.new { |float| float.to_f },
+ "decimal" => Proc.new { |number| BigDecimal(number) },
+ "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.strip) },
+ "string" => Proc.new { |string| string.to_s },
+ "yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml },
+ "base64Binary" => Proc.new { |bin| ActiveSupport::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_reader :backend
delegate :parse, :to => :backend
@@ -16,7 +81,7 @@ def backend=(name)
if name.is_a?(Module)
@backend = name
else
- require "active_support/xml_mini/#{name.to_s.downcase}.rb"
+ require "active_support/xml_mini/#{name.to_s.downcase}"
@backend = ActiveSupport.const_get("XmlMini_#{name}")
end
end
@@ -27,6 +92,66 @@ def with_backend(name)
ensure
self.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
+
+ 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.has_key?(:camelize) && options[:camelize]
+ dasherize = !options.has_key?(:dasherize) || options[:dasherize]
+ key = key.camelize if camelize
+ key = key.dasherize if dasherize
+ key
+ end
+
+ protected
+
+ # TODO: Add support for other encodings
+ def _parse_binary(bin, entity) #:nodoc:
+ case entity['encoding']
+ when 'base64'
+ ActiveSupport::Base64.decode64(bin)
+ else
+ bin
+ end
+ end
+
+ def _parse_file(file, entity)
+ f = StringIO.new(ActiveSupport::Base64.decode64(file))
+ f.extend(FileLike)
+ f.original_filename = entity['name']
+ f.content_type = entity['content_type']
+ f
+ end
end
XmlMini.backend = 'REXML'
Andreas

Shouldn't this line be

@value = raw_value || @serializable.send(name)

or change the name of the argument from "raw_value" to "value"?

José Valim

Yes, patch with tests please!

Jakub Okoński

Should it look like:
http://gist.github.com/561115 ?
It's my first patch, and I'm not sure it's correct.

José Valim

Yes, that is it! Do you think you can add a test as well? How did you come up with this error?

Jakub Okoński

I've just read andreas's note and wanted to try create a patch, I had no error with this. I'll try to create some test tommorow.

Andreas

This is the minimal example I can find to reproduce the error:

require 'active_resource'
class Foo < ActiveResource::Base
  self.site = ""
end

Foo.new(:test => nil).to_xml

This will throw an ArgumentError. The problem is that :test is a private method on Object, which will be invoked since "value" is always nil.

José Valim

Awesome! Farnoy, you can add the example above as a test case to ActiveResource and then add a fix to ensure it is going to work!

Jakub Okoński

For me this function is not needed, this issue has been undetected for months now, Serializer structure has overloaded constructor method, and this one that we are trying to fix is not used:
http://gist.github.com/562383
From documentation.

#EDIT:
andreas test won't work, as the second constructor is triggered by .to_xml

Víctor Martínez

This will miserably fail if the attribute was not part of the model but was dynamically generated by the resulting SQL. There's a bug on this:
https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/4840-to_xml-doesnt-work-in-such-case-eventselecttitle-as-tto_xml#ticket-4840-5

Please sign in to comment.
Something went wrong with that request. Please try again.