Upgraded Rails and RSpec
[monkeycharger.git] / vendor / rails / activesupport / lib / active_support / core_ext / hash / conversions.rb
blobbbe35c25e45d631b4e0c2ac4790e5ffbd525791b
1 require 'date'
2 require 'cgi'
3 require 'base64'
4 require 'builder'
5 require 'xmlsimple'
7 # Extensions needed for Hash#to_query
8 class Object
9   def to_param #:nodoc:
10     to_s
11   end
13   def to_query(key) #:nodoc:
14     "#{CGI.escape(key.to_s)}=#{CGI.escape(to_param.to_s)}"
15   end
16 end
18 class Array
19   def to_query(key) #:nodoc:
20     collect { |value| value.to_query("#{key}[]") } * '&'
21   end
22 end
24 # Locked down XmlSimple#xml_in_string
25 class XmlSimple
26   # Same as xml_in but doesn't try to smartly shoot itself in the foot.
27   def xml_in_string(string, options = nil)
28     handle_options('in', options)
30     @doc = parse(string)
31     result = collapse(@doc.root)
33     if @options['keeproot']
34       merge({}, @doc.root.name, result)
35     else
36       result
37     end
38   end
40   def self.xml_in_string(string, options = nil)
41     new.xml_in_string(string, options)
42   end
43 end
45 module ActiveSupport #:nodoc:
46   module CoreExtensions #:nodoc:
47     module Hash #:nodoc:
48       module Conversions
49         XML_TYPE_NAMES = {
50           "Symbol"     => "symbol",
51           "Fixnum"     => "integer",
52           "Bignum"     => "integer",
53           "BigDecimal" => "decimal",
54           "Float"      => "float",
55           "Date"       => "date",
56           "DateTime"   => "datetime",
57           "Time"       => "datetime",
58           "TrueClass"  => "boolean",
59           "FalseClass" => "boolean"
60         } unless defined?(XML_TYPE_NAMES)
62         XML_FORMATTING = {
63           "symbol"   => Proc.new { |symbol| symbol.to_s },
64           "date"     => Proc.new { |date| date.to_s(:db) },
65           "datetime" => Proc.new { |time| time.xmlschema },
66           "binary"   => Proc.new { |binary| Base64.encode64(binary) },
67           "yaml"     => Proc.new { |yaml| yaml.to_yaml }
68         } unless defined?(XML_FORMATTING)
70         # TODO: use Time.xmlschema instead of Time.parse;
71         #       use regexp instead of Date.parse
72         unless defined?(XML_PARSING)
73           XML_PARSING = {
74             "symbol"       => Proc.new  { |symbol|  symbol.to_sym },
75             "date"         => Proc.new  { |date|    ::Date.parse(date) },
76             "datetime"     => Proc.new  { |time|    ::Time.parse(time).utc },
77             "integer"      => Proc.new  { |integer| integer.to_i },
78             "float"        => Proc.new  { |float|   float.to_f },
79             "decimal"      => Proc.new  { |number|  BigDecimal(number) },
80             "boolean"      => Proc.new  { |boolean| %w(1 true).include?(boolean.strip) },
81             "string"       => Proc.new  { |string|  string.to_s },
82             "yaml"         => Proc.new  { |yaml|    YAML::load(yaml) rescue yaml },
83             "base64Binary" => Proc.new  { |bin|     Base64.decode64(bin) },
84             # FIXME: Get rid of eval and institute a proper decorator here
85             "file"         => Proc.new do |file, entity|
86               f = StringIO.new(Base64.decode64(file))
87               eval "def f.original_filename() '#{entity["name"]}' || 'untitled' end"
88               eval "def f.content_type()      '#{entity["content_type"]}' || 'application/octet-stream' end"
89               f
90             end
91           }
93           XML_PARSING.update(
94             "double"   => XML_PARSING["float"],
95             "dateTime" => XML_PARSING["datetime"]
96           )
97         end
99         def self.included(klass)
100           klass.extend(ClassMethods)
101         end
103         def to_query(namespace = nil)
104           collect do |key, value|
105             value.to_query(namespace ? "#{namespace}[#{key}]" : key)
106           end.sort * '&'
107         end
109         def to_xml(options = {})
110           options[:indent] ||= 2
111           options.reverse_merge!({ :builder => Builder::XmlMarkup.new(:indent => options[:indent]),
112                                    :root => "hash" })
113           options[:builder].instruct! unless options.delete(:skip_instruct)
114           dasherize = !options.has_key?(:dasherize) || options[:dasherize]
115           root = dasherize ? options[:root].to_s.dasherize : options[:root].to_s
117           options[:builder].__send__(:method_missing, root) do
118             each do |key, value|
119               case value
120                 when ::Hash
121                   value.to_xml(options.merge({ :root => key, :skip_instruct => true }))
122                 when ::Array
123                   value.to_xml(options.merge({ :root => key, :children => key.to_s.singularize, :skip_instruct => true}))
124                 when ::Method, ::Proc
125                   # If the Method or Proc takes two arguments, then
126                   # pass the suggested child element name.  This is
127                   # used if the Method or Proc will be operating over
128                   # multiple records and needs to create an containing
129                   # element that will contain the objects being
130                   # serialized.
131                   if 1 == value.arity
132                     value.call(options.merge({ :root => key, :skip_instruct => true }))
133                   else
134                     value.call(options.merge({ :root => key, :skip_instruct => true }), key.to_s.singularize)
135                   end
136                 else
137                   if value.respond_to?(:to_xml)
138                     value.to_xml(options.merge({ :root => key, :skip_instruct => true }))
139                   else
140                     type_name = XML_TYPE_NAMES[value.class.name]
142                     key = dasherize ? key.to_s.dasherize : key.to_s
144                     attributes = options[:skip_types] || value.nil? || type_name.nil? ? { } : { :type => type_name }
145                     if value.nil?
146                       attributes[:nil] = true
147                     end
149                     options[:builder].tag!(key,
150                       XML_FORMATTING[type_name] ? XML_FORMATTING[type_name].call(value) : value,
151                       attributes
152                     )
153                 end
154               end
155             end
156             
157             yield options[:builder] if block_given?
158           end
160         end
162         module ClassMethods
163           def from_xml(xml)
164             # TODO: Refactor this into something much cleaner that doesn't rely on XmlSimple
165             typecast_xml_value(undasherize_keys(XmlSimple.xml_in_string(xml,
166               'forcearray'   => false,
167               'forcecontent' => true,
168               'keeproot'     => true,
169               'contentkey'   => '__content__')
170             ))
171           end
173           private
174             def typecast_xml_value(value)
175               case value.class.to_s
176                 when 'Hash'
177                   if value.has_key?("__content__")
178                     content = value["__content__"]
179                     if parser = XML_PARSING[value["type"]]
180                       if parser.arity == 2
181                         XML_PARSING[value["type"]].call(content, value)
182                       else
183                         XML_PARSING[value["type"]].call(content)
184                       end
185                     else
186                       content
187                     end
188                   elsif value['type'] == 'array'
189                     child_key, entries = value.detect { |k,v| k != 'type' }   # child_key is throwaway
190                     if entries.nil?
191                       []
192                     else
193                       case entries.class.to_s   # something weird with classes not matching here.  maybe singleton methods breaking is_a?
194                       when "Array"
195                         entries.collect { |v| typecast_xml_value(v) }
196                       when "Hash"
197                         [typecast_xml_value(entries)]
198                       else
199                         raise "can't typecast #{entries.inspect}"
200                       end
201                     end
202                   elsif value['type'] == 'string' && value['nil'] != 'true'
203                     ""
204                   # blank or nil parsed values are represented by nil
205                   elsif value.blank? || value['nil'] == 'true'
206                     nil
207                   # If the type is the only element which makes it then 
208                   # this still makes the value nil
209                   elsif value['type'] && value.size == 1
210                     nil
211                   else
212                     xml_value = value.inject({}) do |h,(k,v)|
213                       h[k] = typecast_xml_value(v)
214                       h
215                     end
216                     
217                     # Turn { :files => { :file => #<StringIO> } into { :files => #<StringIO> } so it is compatible with
218                     # how multipart uploaded files from HTML appear
219                     xml_value["file"].is_a?(StringIO) ? xml_value["file"] : xml_value
220                   end
221                 when 'Array'
222                   value.map! { |i| typecast_xml_value(i) }
223                   case value.length
224                     when 0 then nil
225                     when 1 then value.first
226                     else value
227                   end
228                 when 'String'
229                   value
230                 else
231                   raise "can't typecast #{value.class.name} - #{value.inspect}"
232               end
233             end
235             def undasherize_keys(params)
236               case params.class.to_s
237                 when "Hash"
238                   params.inject({}) do |h,(k,v)|
239                     h[k.to_s.tr("-", "_")] = undasherize_keys(v)
240                     h
241                   end
242                 when "Array"
243                   params.map { |v| undasherize_keys(v) }
244                 else
245                   params
246               end
247             end
248         end
249       end
250     end
251   end