jnunemaker / httparty

Makes http fun! Also, makes consuming restful web services dead easy.

This URL has Read+Write access

httparty / lib / core_extensions.rb
8a70a8ef » jnunemaker 2008-12-06 Removed active support. Add... 1 # Copyright (c) 2008 Sam Smoot.
2 #
3 # Permission is hereby granted, free of charge, to any person obtaining
4 # a copy of this software and associated documentation files (the
5 # "Software"), to deal in the Software without restriction, including
6 # without limitation the rights to use, copy, modify, merge, publish,
7 # distribute, sublicense, and/or sell copies of the Software, and to
8 # permit persons to whom the Software is furnished to do so, subject to
9 # the following conditions:
10 #
11 # The above copyright notice and this permission notice shall be
12 # included in all copies or substantial portions of the Software.
13 #
14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22 class Object
23 # @return <TrueClass, FalseClass>
24 #
25 # @example [].blank? #=> true
26 # @example [1].blank? #=> false
27 # @example [nil].blank? #=> false
28 #
29 # Returns true if the object is nil or empty (if applicable)
30 def blank?
31 nil? || (respond_to?(:empty?) && empty?)
32 end
33 end # class Object
34
35 class Numeric
36 # @return <TrueClass, FalseClass>
37 #
38 # Numerics can't be blank
39 def blank?
40 false
41 end
42 end # class Numeric
43
44 class NilClass
45 # @return <TrueClass, FalseClass>
46 #
47 # Nils are always blank
48 def blank?
49 true
50 end
51 end # class NilClass
52
53 class TrueClass
54 # @return <TrueClass, FalseClass>
55 #
56 # True is not blank.
57 def blank?
58 false
59 end
60 end # class TrueClass
61
62 class FalseClass
63 # False is always blank.
64 def blank?
65 true
66 end
67 end # class FalseClass
68
69 class String
70 # @example "".blank? #=> true
71 # @example " ".blank? #=> true
72 # @example " hey ho ".blank? #=> false
73 #
74 # @return <TrueClass, FalseClass>
75 #
76 # Strips out whitespace then tests if the string is empty.
77 def blank?
78 strip.empty?
79 end
80 end # class String
7fab301e » jnunemaker 2008-12-06 Moved xml stuff into core e... 81
82 require 'rexml/parsers/streamparser'
83 require 'rexml/parsers/baseparser'
84 require 'rexml/light/node'
85
86 # This is a slighly modified version of the XMLUtilityNode from
87 # http://merb.devjavu.com/projects/merb/ticket/95 (has.sox@gmail.com)
88 # It's mainly just adding vowels, as I ht cd wth n vwls :)
89 # This represents the hard part of the work, all I did was change the
90 # underlying parser.
91 class REXMLUtilityNode
92 attr_accessor :name, :attributes, :children, :type
93
94 def self.typecasts
95 @@typecasts
96 end
97
98 def self.typecasts=(obj)
99 @@typecasts = obj
100 end
101
102 def self.available_typecasts
103 @@typecasts
104 end
105
106 def self.available_typecasts=(obj)
107 @@typecasts = obj
108 end
109
110 self.typecasts = {}
111 self.typecasts["integer"] = lambda{|v| v.nil? ? nil : v.to_i}
112 self.typecasts["boolean"] = lambda{|v| v.nil? ? nil : (v.strip != "false")}
113 self.typecasts["datetime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
114 self.typecasts["date"] = lambda{|v| v.nil? ? nil : Date.parse(v)}
115 self.typecasts["dateTime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
116 self.typecasts["decimal"] = lambda{|v| BigDecimal(v)}
117 self.typecasts["double"] = lambda{|v| v.nil? ? nil : v.to_f}
118 self.typecasts["float"] = lambda{|v| v.nil? ? nil : v.to_f}
119 self.typecasts["symbol"] = lambda{|v| v.to_sym}
120 self.typecasts["string"] = lambda{|v| v.to_s}
121 self.typecasts["yaml"] = lambda{|v| v.nil? ? nil : YAML.load(v)}
122 self.typecasts["base64Binary"] = lambda{|v| v.unpack('m').first }
123
124 self.available_typecasts = self.typecasts.keys
125
126 def initialize(name, attributes = {})
127 @name = name.tr("-", "_")
128 # leave the type alone if we don't know what it is
129 @type = self.class.available_typecasts.include?(attributes["type"]) ? attributes.delete("type") : attributes["type"]
130
131 @nil_element = attributes.delete("nil") == "true"
132 @attributes = undasherize_keys(attributes)
133 @children = []
134 @text = false
135 end
136
137 def add_node(node)
138 @text = true if node.is_a? String
139 @children << node
140 end
141
142 def to_hash
143 if @type == "file"
144 f = StringIO.new((@children.first || '').unpack('m').first)
145 class << f
146 attr_accessor :original_filename, :content_type
147 end
148 f.original_filename = attributes['name'] || 'untitled'
149 f.content_type = attributes['content_type'] || 'application/octet-stream'
150 return {name => f}
151 end
152
153 if @text
154 return { name => typecast_value( translate_xml_entities( inner_html ) ) }
155 else
156 #change repeating groups into an array
157 groups = @children.inject({}) { |s,e| (s[e.name] ||= []) << e; s }
158
159 out = nil
160 if @type == "array"
161 out = []
162 groups.each do |k, v|
163 if v.size == 1
164 out << v.first.to_hash.entries.first.last
165 else
166 out << v.map{|e| e.to_hash[k]}
167 end
168 end
169 out = out.flatten
170
171 else # If Hash
172 out = {}
173 groups.each do |k,v|
174 if v.size == 1
175 out.merge!(v.first)
176 else
177 out.merge!( k => v.map{|e| e.to_hash[k]})
178 end
179 end
180 out.merge! attributes unless attributes.empty?
181 out = out.empty? ? nil : out
182 end
183
184 if @type && out.nil?
185 { name => typecast_value(out) }
186 else
187 { name => out }
188 end
189 end
190 end
191
192 # Typecasts a value based upon its type. For instance, if
193 # +node+ has #type == "integer",
194 # {{[node.typecast_value("12") #=> 12]}}
195 #
196 # @param value<String> The value that is being typecast.
197 #
198 # @details [:type options]
199 # "integer"::
200 # converts +value+ to an integer with #to_i
201 # "boolean"::
202 # checks whether +value+, after removing spaces, is the literal
203 # "true"
204 # "datetime"::
205 # Parses +value+ using Time.parse, and returns a UTC Time
206 # "date"::
207 # Parses +value+ using Date.parse
208 #
209 # @return <Integer, TrueClass, FalseClass, Time, Date, Object>
210 # The result of typecasting +value+.
211 #
212 # @note
213 # If +self+ does not have a "type" key, or if it's not one of the
214 # options specified above, the raw +value+ will be returned.
215 def typecast_value(value)
216 return value unless @type
217 proc = self.class.typecasts[@type]
218 proc.nil? ? value : proc.call(value)
219 end
220
221 # Convert basic XML entities into their literal values.
222 #
223 # @param value<#gsub> An XML fragment.
224 #
225 # @return <#gsub> The XML fragment after converting entities.
226 def translate_xml_entities(value)
227 value.gsub(/&lt;/, "<").
228 gsub(/&gt;/, ">").
229 gsub(/&quot;/, '"').
230 gsub(/&apos;/, "'").
231 gsub(/&amp;/, "&")
232 end
233
234 # Take keys of the form foo-bar and convert them to foo_bar
235 def undasherize_keys(params)
236 params.keys.each do |key, value|
237 params[key.tr("-", "_")] = params.delete(key)
238 end
239 params
240 end
241
242 # Get the inner_html of the REXML node.
243 def inner_html
244 @children.join
245 end
246
247 # Converts the node into a readable HTML node.
248 #
249 # @return <String> The HTML node in text form.
250 def to_html
251 attributes.merge!(:type => @type ) if @type
252 "<#{name}#{attributes.to_xml_attributes}>#{@nil_element ? '' : inner_html}</#{name}>"
253 end
254
255 # @alias #to_html #to_s
256 def to_s
257 to_html
258 end
259 end
260
261 class ToHashParser
262 def self.from_xml(xml)
263 stack = []
264 parser = REXML::Parsers::BaseParser.new(xml)
265
266 while true
267 event = parser.pull
268 case event[0]
269 when :end_document
270 break
271 when :end_doctype, :start_doctype
272 # do nothing
273 when :start_element
274 stack.push REXMLUtilityNode.new(event[1], event[2])
275 when :end_element
276 if stack.size > 1
277 temp = stack.pop
278 stack.last.add_node(temp)
279 end
280 when :text, :cdata
281 stack.last.add_node(event[1]) unless event[1].strip.length == 0
282 end
283 end
284 stack.pop.to_hash
285 end
286 end
287
288 class Hash
289 def self.from_xml(xml)
290 ToHashParser.from_xml(xml)
291 end
41134082 » jnunemaker 2008-12-06 Fixed weird uri normalizing... 292
293 # @return <String> This hash as a query string
294 #
295 # @example
296 # { :name => "Bob",
297 # :address => {
298 # :street => '111 Ruby Ave.',
299 # :city => 'Ruby Central',
300 # :phones => ['111-111-1111', '222-222-2222']
301 # }
302 # }.to_params
303 # #=> "name=Bob&address[city]=Ruby Central&address[phones][]=111-111-1111&address[phones][]=222-222-2222&address[street]=111 Ruby Ave."
304 def to_params
305 params = self.map { |k,v| normalize_param(k,v) }.join
306 params.chop! # trailing &
307 params
308 end
309
310 # @param key<Object> The key for the param.
311 # @param value<Object> The value for the param.
312 #
313 # @return <String> This key value pair as a param
314 #
315 # @example normalize_param(:name, "Bob") #=> "name=Bob&"
316 def normalize_param(key, value)
317 param = ''
318 stack = []
319
320 if value.is_a?(Array)
321 param << value.map { |element| normalize_param("#{key}[]", element) }.join
322 elsif value.is_a?(Hash)
323 stack << [key,value]
324 else
0b557978 » jnunemaker 2008-12-06 Added uri encode to Hash#to... 325 param << "#{key}=#{URI.encode(value)}&"
41134082 » jnunemaker 2008-12-06 Fixed weird uri normalizing... 326 end
327
328 stack.each do |parent, hash|
329 hash.each do |key, value|
330 if value.is_a?(Hash)
331 stack << ["#{parent}[#{key}]", value]
332 else
333 param << normalize_param("#{parent}[#{key}]", value)
334 end
335 end
336 end
337
338 param
339 end
7fab301e » jnunemaker 2008-12-06 Moved xml stuff into core e... 340 end