diff --git a/lib/core_extensions.rb b/lib/core_extensions.rb index cf7a85e..61b292c 100644 --- a/lib/core_extensions.rb +++ b/lib/core_extensions.rb @@ -81,213 +81,6 @@ class String #:nodoc: end unless method_defined?(:blank?) end # class String -require 'rexml/parsers/streamparser' -require 'rexml/parsers/baseparser' -require 'rexml/light/node' -require "rexml/document" - -# This is a slighly modified version of the XMLUtilityNode from -# http://merb.devjavu.com/projects/merb/ticket/95 (has.sox@gmail.com) -# It's mainly just adding vowels, as I ht cd wth n vwls :) -# This represents the hard part of the work, all I did was change the -# underlying parser. -class REXMLUtilityNode #:nodoc: - attr_accessor :name, :attributes, :children, :type - - def self.typecasts - @@typecasts - end - - def self.typecasts=(obj) - @@typecasts = obj - end - - def self.available_typecasts - @@available_typecasts - end - - def self.available_typecasts=(obj) - @@available_typecasts = obj - end - - self.typecasts = {} - self.typecasts["integer"] = lambda{|v| v.nil? ? nil : v.to_i} - self.typecasts["boolean"] = lambda{|v| v.nil? ? nil : (v.strip != "false")} - self.typecasts["datetime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc} - self.typecasts["date"] = lambda{|v| v.nil? ? nil : Date.parse(v)} - self.typecasts["dateTime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc} - self.typecasts["decimal"] = lambda{|v| v.nil? ? nil : BigDecimal(v.to_s)} - self.typecasts["double"] = lambda{|v| v.nil? ? nil : v.to_f} - self.typecasts["float"] = lambda{|v| v.nil? ? nil : v.to_f} - self.typecasts["symbol"] = lambda{|v| v.nil? ? nil : v.to_sym} - self.typecasts["string"] = lambda{|v| v.to_s} - self.typecasts["yaml"] = lambda{|v| v.nil? ? nil : YAML.load(v)} - self.typecasts["base64Binary"] = lambda{|v| v.unpack('m').first } - - self.available_typecasts = self.typecasts.keys - - def initialize(name, attributes = {}) - @name = name.tr("-", "_") - # leave the type alone if we don't know what it is - @type = self.class.available_typecasts.include?(attributes["type"]) ? attributes.delete("type") : attributes["type"] - - @nil_element = attributes.delete("nil") == "true" - @attributes = undasherize_keys(attributes) - @children = [] - @text = false - end - - def add_node(node) - @text = true if node.is_a? String - @children << node - end - - def to_hash - if @type == "file" - f = StringIO.new((@children.first || '').unpack('m').first) - class << f - attr_accessor :original_filename, :content_type - end - f.original_filename = attributes['name'] || 'untitled' - f.content_type = attributes['content_type'] || 'application/octet-stream' - return {name => f} - end - - if @text - return { name => typecast_value( translate_xml_entities( inner_html ) ) } - else - #change repeating groups into an array - groups = @children.inject({}) { |s,e| (s[e.name] ||= []) << e; s } - - out = nil - if @type == "array" - out = [] - groups.each do |k, v| - if v.size == 1 - out << v.first.to_hash.entries.first.last - else - out << v.map{|e| e.to_hash[k]} - end - end - out = out.flatten - - else # If Hash - out = {} - groups.each do |k,v| - if v.size == 1 - out.merge!(v.first) - else - out.merge!( k => v.map{|e| e.to_hash[k]}) - end - end - out.merge! attributes unless attributes.empty? - out = out.empty? ? nil : out - end - - if @type && out.nil? - { name => typecast_value(out) } - else - { name => out } - end - end - end - - # Typecasts a value based upon its type. For instance, if - # +node+ has #type == "integer", - # {{[node.typecast_value("12") #=> 12]}} - # - # @param value The value that is being typecast. - # - # @details [:type options] - # "integer":: - # converts +value+ to an integer with #to_i - # "boolean":: - # checks whether +value+, after removing spaces, is the literal - # "true" - # "datetime":: - # Parses +value+ using Time.parse, and returns a UTC Time - # "date":: - # Parses +value+ using Date.parse - # - # @return - # The result of typecasting +value+. - # - # @note - # If +self+ does not have a "type" key, or if it's not one of the - # options specified above, the raw +value+ will be returned. - def typecast_value(value) - return value unless @type - proc = self.class.typecasts[@type] - proc.nil? ? value : proc.call(value) - end - - # Convert basic XML entities into their literal values. - # - # @param value<#gsub> An XML fragment. - # - # @return <#gsub> The XML fragment after converting entities. - def translate_xml_entities(value) - value.gsub(/</, "<"). - gsub(/>/, ">"). - gsub(/"/, '"'). - gsub(/'/, "'"). - gsub(/&/, "&") - end - - # Take keys of the form foo-bar and convert them to foo_bar - def undasherize_keys(params) - params.keys.each do |key, value| - params[key.tr("-", "_")] = params.delete(key) - end - params - end - - # Get the inner_html of the REXML node. - def inner_html - @children.join - end - - # Converts the node into a readable HTML node. - # - # @return The HTML node in text form. - def to_html - attributes.merge!(:type => @type ) if @type - "<#{name}#{attributes.to_xml_attributes}>#{@nil_element ? '' : inner_html}" - end - - # @alias #to_html #to_s - def to_s - to_html - end -end - -class ToHashParser #:nodoc: - def self.from_xml(xml) - stack = [] - parser = REXML::Parsers::BaseParser.new(xml) - - while true - event = parser.pull - case event[0] - when :end_document - break - when :end_doctype, :start_doctype - # do nothing - when :start_element - stack.push REXMLUtilityNode.new(event[1], event[2]) - when :end_element - if stack.size > 1 - temp = stack.pop - stack.last.add_node(temp) - end - when :text, :cdata - stack.last.add_node(event[1]) unless event[1].strip.length == 0 || stack.empty? - end - end - stack.pop.to_hash - end -end - class Hash #:nodoc: # @return This hash as a query string # @@ -354,6 +147,7 @@ class BlankSlate #:nodoc: end # 1.8.6 has mistyping of transitive in if statement +require "rexml/document" module REXML #:nodoc: class Document < Element #:nodoc: def write( output=$stdout, indent=-1, transitive=false, ie_hack=false ) diff --git a/lib/httparty.rb b/lib/httparty.rb index 0c4dbad..f269be9 100644 --- a/lib/httparty.rb +++ b/lib/httparty.rb @@ -133,4 +133,5 @@ require 'httparty/exceptions' require 'httparty/request' require 'httparty/response' require 'httparty/json' +require 'httparty/xml' require 'httparty/cookie_hash' diff --git a/lib/httparty/request.rb b/lib/httparty/request.rb index ca3bf7a..9930ba5 100644 --- a/lib/httparty/request.rb +++ b/lib/httparty/request.rb @@ -101,7 +101,7 @@ module HTTParty return nil if body.nil? or body.empty? case format when :xml - ToHashParser.from_xml(body) + HTTParty::XML.parse(body) when :json HTTParty::JSON.decode(body) else diff --git a/lib/httparty/xml.rb b/lib/httparty/xml.rb new file mode 100644 index 0000000..088b029 --- /dev/null +++ b/lib/httparty/xml.rb @@ -0,0 +1,207 @@ +require 'rexml/parsers/streamparser' +require 'rexml/parsers/baseparser' +require 'rexml/light/node' + +# This is a slighly modified version of the XMLUtilityNode from +# http://merb.devjavu.com/projects/merb/ticket/95 (has.sox@gmail.com) +# It's mainly just adding vowels, as I ht cd wth n vwls :) +# This represents the hard part of the work, all I did was change the +# underlying parser. +class REXMLUtilityNode #:nodoc: + attr_accessor :name, :attributes, :children, :type + + def self.typecasts + @@typecasts + end + + def self.typecasts=(obj) + @@typecasts = obj + end + + def self.available_typecasts + @@available_typecasts + end + + def self.available_typecasts=(obj) + @@available_typecasts = obj + end + + self.typecasts = {} + self.typecasts["integer"] = lambda{|v| v.nil? ? nil : v.to_i} + self.typecasts["boolean"] = lambda{|v| v.nil? ? nil : (v.strip != "false")} + self.typecasts["datetime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc} + self.typecasts["date"] = lambda{|v| v.nil? ? nil : Date.parse(v)} + self.typecasts["dateTime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc} + self.typecasts["decimal"] = lambda{|v| v.nil? ? nil : BigDecimal(v.to_s)} + self.typecasts["double"] = lambda{|v| v.nil? ? nil : v.to_f} + self.typecasts["float"] = lambda{|v| v.nil? ? nil : v.to_f} + self.typecasts["symbol"] = lambda{|v| v.nil? ? nil : v.to_sym} + self.typecasts["string"] = lambda{|v| v.to_s} + self.typecasts["yaml"] = lambda{|v| v.nil? ? nil : YAML.load(v)} + self.typecasts["base64Binary"] = lambda{|v| v.unpack('m').first } + + self.available_typecasts = self.typecasts.keys + + def initialize(name, attributes = {}) + @name = name.tr("-", "_") + # leave the type alone if we don't know what it is + @type = self.class.available_typecasts.include?(attributes["type"]) ? attributes.delete("type") : attributes["type"] + + @nil_element = attributes.delete("nil") == "true" + @attributes = undasherize_keys(attributes) + @children = [] + @text = false + end + + def add_node(node) + @text = true if node.is_a? String + @children << node + end + + def to_hash + if @type == "file" + f = StringIO.new((@children.first || '').unpack('m').first) + class << f + attr_accessor :original_filename, :content_type + end + f.original_filename = attributes['name'] || 'untitled' + f.content_type = attributes['content_type'] || 'application/octet-stream' + return {name => f} + end + + if @text + return { name => typecast_value( translate_xml_entities( inner_html ) ) } + else + #change repeating groups into an array + groups = @children.inject({}) { |s,e| (s[e.name] ||= []) << e; s } + + out = nil + if @type == "array" + out = [] + groups.each do |k, v| + if v.size == 1 + out << v.first.to_hash.entries.first.last + else + out << v.map{|e| e.to_hash[k]} + end + end + out = out.flatten + + else # If Hash + out = {} + groups.each do |k,v| + if v.size == 1 + out.merge!(v.first) + else + out.merge!( k => v.map{|e| e.to_hash[k]}) + end + end + out.merge! attributes unless attributes.empty? + out = out.empty? ? nil : out + end + + if @type && out.nil? + { name => typecast_value(out) } + else + { name => out } + end + end + end + + # Typecasts a value based upon its type. For instance, if + # +node+ has #type == "integer", + # {{[node.typecast_value("12") #=> 12]}} + # + # @param value The value that is being typecast. + # + # @details [:type options] + # "integer":: + # converts +value+ to an integer with #to_i + # "boolean":: + # checks whether +value+, after removing spaces, is the literal + # "true" + # "datetime":: + # Parses +value+ using Time.parse, and returns a UTC Time + # "date":: + # Parses +value+ using Date.parse + # + # @return + # The result of typecasting +value+. + # + # @note + # If +self+ does not have a "type" key, or if it's not one of the + # options specified above, the raw +value+ will be returned. + def typecast_value(value) + return value unless @type + proc = self.class.typecasts[@type] + proc.nil? ? value : proc.call(value) + end + + # Convert basic XML entities into their literal values. + # + # @param value<#gsub> An XML fragment. + # + # @return <#gsub> The XML fragment after converting entities. + def translate_xml_entities(value) + value.gsub(/</, "<"). + gsub(/>/, ">"). + gsub(/"/, '"'). + gsub(/'/, "'"). + gsub(/&/, "&") + end + + # Take keys of the form foo-bar and convert them to foo_bar + def undasherize_keys(params) + params.keys.each do |key, value| + params[key.tr("-", "_")] = params.delete(key) + end + params + end + + # Get the inner_html of the REXML node. + def inner_html + @children.join + end + + # Converts the node into a readable HTML node. + # + # @return The HTML node in text form. + def to_html + attributes.merge!(:type => @type ) if @type + "<#{name}#{attributes.to_xml_attributes}>#{@nil_element ? '' : inner_html}" + end + + # @alias #to_html #to_s + def to_s + to_html + end +end + +module HTTParty + module XML + def self.parse(xml) + stack = [] + parser = REXML::Parsers::BaseParser.new(xml) + + while true + event = parser.pull + case event[0] + when :end_document + break + when :end_doctype, :start_doctype + # do nothing + when :start_element + stack.push REXMLUtilityNode.new(event[1], event[2]) + when :end_element + if stack.size > 1 + temp = stack.pop + stack.last.add_node(temp) + end + when :text, :cdata + stack.last.add_node(event[1]) unless event[1].strip.length == 0 || stack.empty? + end + end + stack.pop.to_hash + end + end +end \ No newline at end of file