class Hash class << self # Converts valid XML into a Ruby Hash structure. # # Mixed content is treated as text and any tags in it are left unparsed # # Any attributes other than type on a node containing a text node will be # discarded # # [Typecasting is performed on elements that have a +type+ attribute:] # integer:: Returns an Integer # boolean:: Anything other than "true" evaluates to false. # datetime:: # Returns a Time object. See Time documentation for valid Time strings. # date:: # Returns a Date object. See Date documentation for valid Date strings. # # Keys are automatically converted to +snake_case+ # # [Simple] # # # 35 # Home Simpson # 1988-01-01 # 2000-04-28 23:01 # true # # # Becomes: # # { "user" => { # "gender" => "m", # "age" => 35, # "name" => "Home Simpson", # "dob" => DateObject( 1998-01-01 ), # "joined_at" => TimeObject( 2000-04-28 23:01), # "is_cool" => true # } # } # # [Mixed Content] # # # A Quick brown Fox # # # Evaluates to: # # { "story" => "A Quick brown Fox" } # # [Attributes other than type on a node containing text] # # # A Quick brown Fox # # # Are ignored: # # { "story" => "A Quick brown Fox" } # # [Other attributes in addition to +type+] # # 60 # # Evaluates with a typecast to an integer. But unit attribute is ignored: # # { "bicep" => 60 } # # @param [String] xml A string representation of valid XML. # # @return [Hash] A hash created by parsing +xml+ def from_xml( xml ) ToHashParser.from_xml(xml) end end # Convert to Mash. This class has semantics of ActiveSupport's # HashWithIndifferentAccess and we only have it so that people can write # params[:key] instead of params['key']. # # @return [Mash] This hash as a Mash for string or symbol key access. def to_mash hash = Mash.new(self) hash.default = default hash end ## # Convert to URL query param string # # { :name => "Bob", # :address => { # :street => '111 Ruby Ave.', # :city => 'Ruby Central', # :phones => ['111-111-1111', '222-222-2222'] # } # }.to_params # #=> "name=Bob&address[city]=Ruby Central&address[phones][]=111-111-1111&address[phones][]=222-222-2222&address[street]=111 Ruby Ave." # # @return [String] This hash as a query string # # @api public def to_params params = self.map { |k,v| normalize_param(k,v) }.join params.chop! # trailing & params end ## # Convert a key, value pair into a URL query param string # # normalize_param(:name, "Bob") #=> "name=Bob&" # # @param [Object] key The key for the param. # @param [Object] value The value for the param. # # @return [String] This key value pair as a param # # @api public def normalize_param(key, value) param = '' stack = [] if value.is_a?(Array) param << value.map { |element| normalize_param("#{key}[]", element) }.join elsif value.is_a?(Hash) stack << [key,value] else param << "#{key}=#{value}&" end stack.each do |parent, hash| hash.each do |k, v| if v.is_a?(Hash) stack << ["#{parent}[#{k}]", v] else param << normalize_param("#{parent}[#{k}]", v) end end end param end ## # Create a hash with *only* key/value pairs in receiver and +allowed+ # # { :one => 1, :two => 2, :three => 3 }.only(:one) #=> { :one => 1 } # # @param [Array[String, Symbol]] *allowed The hash keys to include. # # @return [Hash] A new hash with only the selected keys. # # @api public def only(*allowed) hash = {} allowed.each {|k| hash[k] = self[k] if self.has_key?(k) } hash end ## # Create a hash with all key/value pairs in receiver *except* +rejected+ # # { :one => 1, :two => 2, :three => 3 }.except(:one) # #=> { :two => 2, :three => 3 } # # @param [Array[String, Symbol]] *rejected The hash keys to exclude. # # @return [Hash] A new hash without the selected keys. # # @api public def except(*rejected) hash = self.dup rejected.each {|k| hash.delete(k) } hash end # @return [String] The hash as attributes for an XML tag. # # @example # { :one => 1, "two"=>"TWO" }.to_xml_attributes # #=> 'one="1" two="TWO"' def to_xml_attributes map do |k,v| %{#{k.to_s.snake_case.sub(/^(.{1,1})/) { |m| m.downcase }}="#{v}"} end.join(' ') end alias_method :to_html_attributes, :to_xml_attributes # @param html_class<#to_s> # The HTML class to add to the :class key. The html_class will be # concatenated to any existing classes. # # @example hash[:class] #=> nil # @example hash.add_html_class!(:selected) # @example hash[:class] #=> "selected" # @example hash.add_html_class!("class1 class2") # @example hash[:class] #=> "selected class1 class2" def add_html_class!(html_class) if self[:class] self[:class] = "#{self[:class]} #{html_class}" else self[:class] = html_class.to_s end end # Converts all keys into string values. This is used during reloading to # prevent problems when classes are no longer declared. # # @return [Array] An array of they hash's keys # # @example # hash = { One => 1, Two => 2 }.proctect_keys! # hash # => { "One" => 1, "Two" => 2 } def protect_keys! keys.each {|key| self[key.to_s] = delete(key) } end # Attempts to convert all string keys into Class keys. We run this after # reloading to convert protected hashes back into usable hashes. # # @example # # Provided that classes One and Two are declared in this scope: # hash = { "One" => 1, "Two" => 2 }.unproctect_keys! # hash # => { One => 1, Two => 2 } def unprotect_keys! keys.each do |key| (self[Object.full_const_get(key)] = delete(key)) rescue nil end end # Destructively and non-recursively convert each key to an uppercase string, # deleting nil values along the way. # # @return [Hash] The newly environmentized hash. # # @example # { :name => "Bob", :contact => { :email => "bob@bob.com" } }.environmentize_keys! # #=> { "NAME" => "Bob", "CONTACT" => { :email => "bob@bob.com" } } def environmentize_keys! keys.each do |key| val = delete(key) next if val.nil? self[key.to_s.upcase] = val end self end end 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 attr_accessor :name, :attributes, :children, :type cattr_accessor :typecasts, :available_typecasts 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| BigDecimal(v)} 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.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 [Integer, Boolean, Time, Date, Object] # 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 [String] 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 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 end end stack.pop.to_hash end end