1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
|
require 'date'
require 'cgi'
require 'base64'
require 'builder'
require 'xmlsimple'
# Locked down XmlSimple#xml_in_string
class XmlSimple
# Same as xml_in but doesn't try to smartly shoot itself in the foot.
def xml_in_string(string, options = nil)
handle_options('in', options)
@doc = parse(string)
result = collapse(@doc.root)
if @options['keeproot']
merge({}, @doc.root.name, result)
else
result
end
end
def self.xml_in_string(string, options = nil)
new.xml_in_string(string, options)
end
end
module ActiveSupport #:nodoc:
module CoreExtensions #:nodoc:
module Hash #:nodoc:
module Conversions
XML_TYPE_NAMES = {
"Symbol" => "symbol",
"Fixnum" => "integer",
"Bignum" => "integer",
"BigDecimal" => "decimal",
"Float" => "float",
"Date" => "date",
"DateTime" => "datetime",
"Time" => "datetime",
"TrueClass" => "boolean",
"FalseClass" => "boolean"
} 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| 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 },
"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| Base64.decode64(bin) },
# FIXME: Get rid of eval and institute a proper decorator here
"file" => Proc.new do |file, entity|
f = StringIO.new(Base64.decode64(file))
eval "def f.original_filename() '#{entity["name"]}' || 'untitled' end"
eval "def f.content_type() '#{entity["content_type"]}' || 'application/octet-stream' end"
f
end
}
XML_PARSING.update(
"double" => XML_PARSING["float"],
"dateTime" => XML_PARSING["datetime"]
)
end
def self.included(klass)
klass.extend(ClassMethods)
end
# Converts a hash into a string suitable for use as a URL query string. An optional <tt>namespace</tt> can be
# passed to enclose the param names (see example below).
#
# ==== Example:
# { :name => 'David', :nationality => 'Danish' }.to_query # => "name=David&nationality=Danish"
#
# { :name => 'David', :nationality => 'Danish' }.to_query('user') # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish"
def to_query(namespace = nil)
collect do |key, value|
value.to_query(namespace ? "#{namespace}[#{key}]" : key)
end.sort * '&'
end
def to_xml(options = {})
options[:indent] ||= 2
options.reverse_merge!({ :builder => Builder::XmlMarkup.new(:indent => options[:indent]),
:root => "hash" })
options[:builder].instruct! unless options.delete(:skip_instruct)
dasherize = !options.has_key?(:dasherize) || options[:dasherize]
root = dasherize ? options[:root].to_s.dasherize : options[:root].to_s
options[:builder].__send__(:method_missing, root) do
each do |key, value|
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 = dasherize ? key.to_s.dasherize : key.to_s
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
end
yield options[:builder] if block_given?
end
end
module ClassMethods
def from_xml(xml)
# TODO: Refactor this into something much cleaner that doesn't rely on XmlSimple
typecast_xml_value(undasherize_keys(XmlSimple.xml_in_string(xml,
'forcearray' => false,
'forcecontent' => true,
'keeproot' => true,
'contentkey' => '__content__')
))
end
private
def typecast_xml_value(value)
case value.class.to_s
when 'Hash'
if value['type'] == 'array'
child_key, entries = value.detect { |k,v| k != 'type' } # child_key is throwaway
if entries.nil? || (c = value['__content__'] && c.blank?)
[]
else
case entries.class.to_s # something weird with classes not matching here. maybe singleton methods breaking is_a?
when "Array"
entries.collect { |v| typecast_xml_value(v) }
when "Hash"
[typecast_xml_value(entries)]
else
raise "can't typecast #{entries.inspect}"
end
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
else
content
end
elsif value['type'] == 'string' && value['nil'] != 'true'
""
# blank or nil parsed values are represented by nil
elsif value.blank? || value['nil'] == 'true'
nil
# If the type is the only element which makes it then
# this still makes the value nil
elsif value['type'] && value.size == 1
nil
else
xml_value = value.inject({}) do |h,(k,v)|
h[k] = typecast_xml_value(v)
h
end
# Turn { :files => { :file => #<StringIO> } into { :files => #<StringIO> } so it is compatible with
# how multipart uploaded files from HTML appear
xml_value["file"].is_a?(StringIO) ? xml_value["file"] : xml_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
when 'String'
value
else
raise "can't typecast #{value.class.name} - #{value.inspect}"
end
end
def undasherize_keys(params)
case params.class.to_s
when "Hash"
params.inject({}) do |h,(k,v)|
h[k.to_s.tr("-", "_")] = undasherize_keys(v)
h
end
when "Array"
params.map { |v| undasherize_keys(v) }
else
params
end
end
end
end
end
end
end
|