Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #8872 (new enhancement)

Opened 1 year ago

Last modified 10 months ago

Custom controllers can return hash of primitive types but not array

Reported by: candlerb Assigned to: core
Priority: low Milestone: 2.x
Component: ActiveSupport Version: edge
Severity: minor Keywords:
Cc:

Description

Tested with edge rails and active resource: r7161

Consider the following custom controller methods:

# app/controllers/foos_controller.rb
class FoosController < ApplicationController
  def gimme_hash
    @res = {'abc'=>'wibble'}
    render :xml => @res.to_xml
  end

  def gimme_array
    @res = ['abc']
    render :xml => @res.to_xml
  end
end

# config/routes.rb
ActionController::Routing::Routes.draw do |map|
  map.resources :foos, :collection => {
        :gimme_hash => :get,
        :gimme_array => :get,
  }
  # rest as usual
end

# run using:
script/server -p 3001

Now try to access it from an ActiveResource client:

script/console
>> class Foo < ActiveResource::Base; self.site="http://localhost:3001"; end
=> "http://localhost:3001"
>> Foo.get(:gimme_hash)
=> {"abc"=>"wibble"}
>> Foo.get(:gimme_array)
ActiveResource::ServerError: Failed with 500
        from /home/candlerb/local/edgetest/vendor/rails/activeresource/lib/active_resource/connection.rb:123:in `handle_response'
        from /home/candlerb/local/edgetest/vendor/rails/activeresource/lib/active_resource/connection.rb:104:in `request'
        from /home/candlerb/local/edgetest/vendor/rails/activeresource/lib/active_resource/connection.rb:72:in `get'
        from /home/candlerb/local/edgetest/vendor/rails/activeresource/lib/active_resource/custom_methods.rb:42:in `get'
        from (irb):3
>>

Fundamentally, the problem is that hashes with String keys and values can be serialized with to_xml, but not arrays of Strings:

>> {'abc'=>'wibble'}.to_xml
=> "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<hash>\n  <abc>wibble</abc>\n</hash>\n"
>> [].to_xml
=> "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<nil-classes type=\"array\"/>\n"
>> [Foo.new].to_xml
=> "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<foos type=\"array\">\n  <foo>\n  </foo>\n</foos>\n"
>> ['abc'].to_xml
RuntimeError: Not all elements respond to to_xml
        from /home/candlerb/local/edgetest/vendor/rails/activerecord/lib/../../activesupport/lib/active_support/core_ext/array/conversions.rb:48:in `to_xml'
        from (irb):6

I think that being able to serialize an array of Strings (or integers etc) is a reasonable thing to want to do. Perhaps serialize them as <string>....</string>, <integer>...</integer> etc?

Attachments

primitive_types_in_to_xml.diff (3.3 kB) - added by Assaf on 10/23/07 00:06:15.

Change History

07/04/07 08:58:27 changed by candlerb

  • type changed from defect to enhancement.
  • summary changed from Custom controllers can return hash but not array to Custom controllers can return hash of primitive types but not array.

I note that hash values can already be strings, integers, floats etc. So perhaps the result of serializing an Array should be something like this:

# a = ["A string", 123, 4.5, nil]

<records type="array">
  <record>A string</record>
  <record type="integer">123</record>
  <record type="float">4.5</record>
  <record nil="true"></record>
</records>

This is roughly consistent with current behaviour of a heterogeneous array:

>> class Foo < ActiveResource::Base; self.site="http://localhost:3001"; end
=> "http://localhost:3001"
>> class Bar < ActiveResource::Base; self.site="http://localhost:3001"; end
=> "http://localhost:3001"
>> puts [Foo.new(:abc=>"def"), Foo.new(:abc=>"xyz")].to_xml
<?xml version="1.0" encoding="UTF-8"?>
<foos type="array">
  <foo>
    <abc>def</abc>
  </foo>
  <foo>
    <abc>xyz</abc>
  </foo>
</foos>
=> nil
>> puts [Foo.new(:abc=>"def"), Bar.new(:abc=>"xyz")].to_xml
<?xml version="1.0" encoding="UTF-8"?>
<records type="array">
  <record>
    <abc>def</abc>
  </record>
  <record>
    <abc>xyz</abc>
  </record>
</records>
=> nil

This does beg the question as to whether the rules for XML serialization and deserialization which Rails performs should be documented somewhere, allowing all possible nestings to be worked out, such as:

  • hashes containing primitive types
  • hashes containing hashes
  • hashes containing objects
  • objects containing primitive types
  • objects containing hashes
  • objects containing objects
  • arrays containing primitive types
  • arrays containing hashes
  • arrays containing objects
  • arrays containing arrays
  • hashes containing arrays
  • objects containing arrays
  • ...

At the moment this doesn't seem to be particularly well defined. For example the heterogeneous array shown above doesn't indicate that one record is a 'Foo' while another is a 'Bar'. Shouldn't it be something like this?

<records type="array">
  <foo>
    <abc>def</abc>
  </foo>
  <bar>
    <abc>xyz</abc>
  </bar>
</records>

Also, how about nested objects:

>> puts [Foo.new(:abc=>"def", :xxx=>Foo.new(:abc=>"xyz"))].to_xml
<?xml version="1.0" encoding="UTF-8"?>
<foos type="array">
  <foo>
    <xxx>
      <abc>xyz</abc>
    </xxx>
    <abc>def</abc>
  </foo>
</foos>
=> nil

It's not clear from the encoding that <xxx> is a Foo, not a plain hash.

Perhaps we end up with SOAP if we follow this too far :-( But I do think it should be clear what it, and is not, possible or allowed. I'm not opposed to there being clearly documented restrictions - e.g. that hash keys can only be Strings.

10/23/07 00:06:15 changed by Assaf

  • attachment primitive_types_in_to_xml.diff added.

10/23/07 00:09:23 changed by Assaf

Attached, patched and test case that allows to_xml on arrays of primitive types, using the same formatting logic as Hash.to_xml:

# a = ["A string", 123, 4.5, nil].to_xml

<records type="array">
 <record>A string</record>
 <record type="integer">123</record>
 <record type="float">4.5</record>
 <record nil="true"></record>
</records>

12/21/07 00:04:01 changed by munkyboy

here's another way to add support for this use case:

module BasicDataTypeToXMLSupport
  def to_xml(options = {})
    options[:indent]   ||= 2
    options[:builder]  ||= Builder::XmlMarkup.new(:indent => options[:indent])
    tagname = self.class.to_s.underscore
    options[:builder].instruct! unless options.delete(:skip_instruct)
    xml = options[:builder]
    xml.tag!(tagname, {}) do |tag|
      tag.text!(self.to_s)
    end
  end
end
class String
  include BasicDataTypeToXMLSupport
end
class Fixnum
  include BasicDataTypeToXMLSupport
end

01/02/08 01:41:00 changed by Assaf

munkyboy, that would, except for element names being 'string' instead of 'foo'. Alterantive:

module ActiveSupport #:nodoc:
  module CoreExtensions #:nodoc:
    module SimpleType
      def to_xml(options = {})
        tag_name = options[:root] ? options[:root].to_s : self.class.to_s.underscore
        tag_name = tag_name.dasherize  if !options.has_key?(:dasherize) || options[:dasherize]
        type_name = Hash::Conversions::XML_TYPE_NAMES[self.class.name]
        attributes = options[:skip_types] || self.nil? || type_name.nil? ? { } : { :type => type_name }
        options[:builder].instruct! unless options.delete(:skip_instruct)
        if self.nil?
          options[:builder].tag! tag_name, :nil=>true
        else
          options[:builder].tag! tag_name,
            Hash::Conversions::XML_FORMATTING[type_name] ? Hash::Conversions::XML_FORMATTING[type_name].call(self).to_s : self.to_s,
            attributes
        end
      end
    end
  end
end

[NilClass, String, Numeric, Date, Time, TrueClass, FalseClass].each do |cls|
  cls.class_eval do
    include ActiveSupport::CoreExtensions::SimpleType
  end
end