Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

use libxml-ruby if possible

  • Loading branch information...
commit 867c977c287fbfb95cdf6906eda6365badd0b830 1 parent f70bf90
Andy Shen authored
22 lib/campaign_monitor.rb
View
@@ -78,7 +78,7 @@ def initialize(api_key=CAMPAIGN_MONITOR_API_KEY)
# Takes a CampaignMonitor API method name and set of parameters;
# returns an XmlSimple object with the response
def request(method, params)
- response = XmlSimple.xml_in(http_get(request_url(method, params)), { 'keeproot' => false,
+ response = PARSER.xml_in(http_get(request_url(method, params)), { 'keeproot' => false,
'forcearray' => %w[List Campaign Subscriber Client SubscriberOpen SubscriberUnsubscribe SubscriberClick SubscriberBounce],
'noattr' => true })
response.delete('d1p1:type')
@@ -217,4 +217,22 @@ def initialize(email_address, list_id)
@list_id = list_id
end
end
-end
+end
+
+# If libxml is installed, we use the FasterXmlSimple library, that provides most of the functionality of XmlSimple
+# except it uses the xml/libxml library for xml parsing (rather than REXML).
+# If libxml isn't installed, we just fall back on XmlSimple.
+
+PARSER =
+ begin
+ require 'xml/libxml'
+ # Older version of libxml aren't stable (bus error when requesting attributes that don't exist) so we
+ # have to use a version greater than '0.3.8.2'.
+ raise LoadError unless XML::Parser::VERSION > '0.3.8.2'
+ $:.push(File.join(File.dirname(__FILE__), '..', 'support', 'faster-xml-simple', 'lib'))
+ require 'faster_xml_simple'
+ p 'Using libxml-ruby'
+ FasterXmlSimple
+ rescue LoadError
+ XmlSimple
+ end
187 support/faster-xml-simple/lib/faster_xml_simple.rb
View
@@ -0,0 +1,187 @@
+#
+# Copyright (c) 2006 Michael Koziarski
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy of
+# this software and associated documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is furnished to do so,
+# subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require 'rubygems'
+require 'xml/libxml'
+
+class FasterXmlSimple
+ Version = '0.5.0'
+ class << self
+ # Take an string containing XML, and returns a hash representing that
+ # XML document. For example:
+ #
+ # FasterXmlSimple.xml_in("<root><something>1</something></root>")
+ # {"root"=>{"something"=>{"__content__"=>"1"}}}
+ #
+ # Faster XML Simple is designed to be a drop in replacement for the xml_in
+ # functionality of http://xml-simple.rubyforge.org
+ #
+ # The following options are supported:
+ #
+ # * <tt>contentkey</tt>: The key to use for the content of text elements,
+ # defaults to '\_\_content__'
+ # * <tt>forcearray</tt>: The list of elements which should always be returned
+ # as arrays. Under normal circumstances single element arrays are inlined.
+ # * <tt>suppressempty</tt>: The value to return for empty elements, pass +true+
+ # to remove empty elements entirely.
+ # * <tt>keeproot</tt>: By default the hash returned has a single key with the
+ # name of the root element. If the name of the root element isn't
+ # interesting to you, pass +false+.
+ # * <tt>forcecontent</tt>: By default a text element with no attributes, will
+ # be collapsed to just a string instead of a hash with a single key.
+ # Pass +true+ to prevent this.
+ #
+ #
+ def xml_in(string, options={})
+ new(string, options).out
+ end
+ end
+
+ def initialize(string, options) #:nodoc:
+ @doc = parse(string)
+ @options = default_options.merge options
+ end
+
+ def out #:nodoc:
+ if @options['keeproot']
+ {@doc.root.name => collapse(@doc.root)}
+ else
+ collapse(@doc.root)
+ end
+ end
+
+ private
+ def default_options
+ {'contentkey' => '__content__', 'forcearray' => [], 'keeproot'=>true}
+ end
+
+ def collapse(element)
+ result = hash_of_attributes(element)
+ if text_node? element
+ text = collapse_text(element)
+ result[content_key] = text if text =~ /\S/
+ elsif element.children?
+ element.inject(result) do |hash, child|
+ unless child.text?
+ child_result = collapse(child)
+ (hash[child.name] ||= []) << child_result
+ end
+ hash
+ end
+ end
+ if result.empty?
+ return empty_element
+ end
+ # Compact them to ensure it complies with the user's requests
+ inline_single_element_arrays(result)
+ remove_empty_elements(result) if suppress_empty?
+ if content_only?(result) && !force_content?
+ result[content_key]
+ else
+ result
+ end
+ end
+
+ def content_only?(result)
+ result.keys == [content_key]
+ end
+
+ def content_key
+ @options['contentkey']
+ end
+
+ def force_array?(key_name)
+ Array(@options['forcearray']).include?(key_name)
+ end
+
+ def inline_single_element_arrays(result)
+ result.each do |key, value|
+ if value.size == 1 && value.is_a?(Array) && !force_array?(key)
+ result[key] = value.first
+ end
+ end
+ end
+
+ def remove_empty_elements(result)
+ result.each do |key, value|
+ if value == empty_element
+ result.delete key
+ end
+ end
+ end
+
+ def suppress_empty?
+ @options['suppressempty'] == true
+ end
+
+ def empty_element
+ if !@options.has_key? 'suppressempty'
+ {}
+ else
+ @options['suppressempty']
+ end
+ end
+
+ # removes the content if it's nothing but blanks, prevents
+ # the hash being polluted with lots of content like "\n\t\t\t"
+ def suppress_empty_content(result)
+ result.delete content_key if result[content_key] !~ /\S/
+ end
+
+ def force_content?
+ @options['forcecontent']
+ end
+
+ # a text node is one with 1 or more child nodes which are
+ # text nodes, and no non-text children, there's no sensible
+ # way to support nodes which are text and markup like:
+ # <p>Something <b>Bold</b> </p>
+ def text_node?(element)
+ !element.text? && element.all? {|c| c.text?}
+ end
+
+ # takes a text node, and collapses it into a string
+ def collapse_text(element)
+ element.map {|c| c.content } * ''
+ end
+
+ def hash_of_attributes(element)
+ result = {}
+ element.each_attr do |attribute|
+ name = attribute.name
+ name = [attribute.ns, attribute.name].join(':') if attribute.ns?
+ result[name] = attribute.value
+ end
+ result
+ end
+
+ def parse(string)
+ if string == ''
+ string = ' '
+ end
+ XML::Parser.string(string).parse
+ end
+end
+
+class XmlSimple # :nodoc:
+ def self.xml_in(*args)
+ FasterXmlSimple.xml_in *args
+ end
+end
47 support/faster-xml-simple/test/regression_test.rb
View
@@ -0,0 +1,47 @@
+require File.dirname(__FILE__) + '/test_helper'
+
+class RegressionTest < FasterXSTest
+ def test_content_nil_regressions
+ expected = {"asdf"=>{"jklsemicolon"=>{}}}
+ assert_equal expected, FasterXmlSimple.xml_in("<asdf><jklsemicolon /></asdf>")
+ assert_equal expected, FasterXmlSimple.xml_in("<asdf><jklsemicolon /></asdf>", 'forcearray'=>['asdf'])
+ end
+
+ def test_s3_regression
+ str = File.read("test/fixtures/test-7.xml")
+ assert_nil FasterXmlSimple.xml_in(str)["AccessControlPolicy"]["AccessControlList"]["__content__"]
+ end
+
+ def test_xml_simple_transparency
+ assert_equal XmlSimple.xml_in("<asdf />"), FasterXmlSimple.xml_in("<asdf />")
+ end
+
+ def test_suppress_empty_variations
+ str = "<asdf><fdsa /></asdf>"
+
+ assert_equal Hash.new, FasterXmlSimple.xml_in(str)["asdf"]["fdsa"]
+ assert_nil FasterXmlSimple.xml_in(str, 'suppressempty'=>nil)["asdf"]["fdsa"]
+ assert_equal '', FasterXmlSimple.xml_in(str, 'suppressempty'=>'')["asdf"]["fdsa"]
+ assert !FasterXmlSimple.xml_in(str, 'suppressempty'=>true)["asdf"].has_key?("fdsa")
+ end
+
+ def test_empty_string_doesnt_crash
+ assert_raise(XML::Parser::ParseError) do
+ silence_stderr do
+ FasterXmlSimple.xml_in('')
+ end
+ end
+ end
+
+ def test_keeproot_false
+ str = "<asdf><fdsa>1</fdsa></asdf>"
+ expected = {"fdsa"=>"1"}
+ assert_equal expected, FasterXmlSimple.xml_in(str, 'keeproot'=>false)
+ end
+
+ def test_keeproot_false_with_force_content
+ str = "<asdf><fdsa>1</fdsa></asdf>"
+ expected = {"fdsa"=>{"__content__"=>"1"}}
+ assert_equal expected, FasterXmlSimple.xml_in(str, 'keeproot'=>false, 'forcecontent'=>true)
+ end
+end
17 support/faster-xml-simple/test/test_helper.rb
View
@@ -0,0 +1,17 @@
+
+require 'test/unit'
+require 'faster_xml_simple'
+
+class FasterXSTest < Test::Unit::TestCase
+ def default_test
+ end
+
+ def silence_stderr
+ str = STDERR.dup
+ STDERR.reopen("/dev/null")
+ STDERR.sync=true
+ yield
+ ensure
+ STDERR.reopen(str)
+ end
+end
46 support/faster-xml-simple/test/xml_simple_comparison_test.rb
View
@@ -0,0 +1,46 @@
+require File.dirname(__FILE__) + '/test_helper'
+require 'yaml'
+
+class XmlSimpleComparisonTest < FasterXSTest
+
+ # Define test methods
+
+ Dir["test/fixtures/test-*.xml"].each do |file_name|
+ xml_file_name = file_name
+ method_name = File.basename(file_name, ".xml").gsub('-', '_')
+ yml_file_name = file_name.gsub('xml', 'yml')
+ rails_yml_file_name = file_name.gsub('xml', 'rails.yml')
+ class_eval <<-EOV, __FILE__, __LINE__
+ def #{method_name}
+ assert_equal YAML.load(File.read('#{yml_file_name}')),
+ FasterXmlSimple.xml_in(File.read('#{xml_file_name}'), default_options )
+ end
+
+ def #{method_name}_rails
+ assert_equal YAML.load(File.read('#{rails_yml_file_name}')),
+ FasterXmlSimple.xml_in(File.read('#{xml_file_name}'), rails_options)
+ end
+ EOV
+ end
+
+ def default_options
+ {
+ 'keeproot' => true,
+ 'contentkey' => '__content__',
+ 'forcecontent' => true,
+ 'suppressempty' => nil,
+ 'forcearray' => ['something-else']
+ }
+ end
+
+ def rails_options
+ {
+ 'forcearray' => false,
+ 'forcecontent' => true,
+ 'keeproot' => true,
+ 'contentkey' => '__content__'
+ }
+ end
+
+
+end
Please sign in to comment.
Something went wrong with that request. Please try again.