require "spec_helper" describe Nori do Nori::PARSERS.each do |parser, class_name| context "using the :#{parser} parser" do let(:parser) { parser } it "should work with unnormalized characters" do xml = '&' expect(parse(xml)).to eq({ 'root' => "&" }) end it "should transform a simple tag with content" do xml = "This is the contents" expect(parse(xml)).to eq({ 'tag' => 'This is the contents' }) end it "should work with cdata tags" do xml = <<-END END expect(parse(xml)["tag"].strip).to eq("text inside cdata") end it "should transform a simple tag with attributes" do xml = "" hash = { 'tag' => { '@attr1' => '1', '@attr2' => '2' } } expect(parse(xml)).to eq(hash) end it "should transform repeating siblings into an array" do xml =<<-XML XML expect(parse(xml)['opt']['user'].class).to eq(Array) hash = { 'opt' => { 'user' => [{ '@login' => 'grep', '@fullname' => 'Gary R Epstein' },{ '@login' => 'stty', '@fullname' => 'Simon T Tyson' }] } } expect(parse(xml)).to eq(hash) end it "should not transform non-repeating siblings into an array" do xml =<<-XML XML expect(parse(xml)['opt']['user'].class).to eq(Hash) hash = { 'opt' => { 'user' => { '@login' => 'grep', '@fullname' => 'Gary R Epstein' } } } expect(parse(xml)).to eq(hash) end it "should prefix attributes with an @-sign to avoid problems with overwritten values" do xml =<<-XML grep 76737 XML expect(parse(xml)["multiRef"]).to eq({ "login" => "grep", "@id" => "id1", "id" => "76737" }) end context "without advanced typecasting" do it "should not transform 'true'" do hash = parse("true", :advanced_typecasting => false) expect(hash["value"]).to eq("true") end it "should not transform 'false'" do hash = parse("false", :advanced_typecasting => false) expect(hash["value"]).to eq("false") end it "should not transform Strings matching the xs:time format" do hash = parse("09:33:55Z", :advanced_typecasting => false) expect(hash["value"]).to eq("09:33:55Z") end it "should not transform Strings matching the xs:date format" do hash = parse("1955-04-18-05:00", :advanced_typecasting => false) expect(hash["value"]).to eq("1955-04-18-05:00") end it "should not transform Strings matching the xs:dateTime format" do hash = parse("1955-04-18T11:22:33-05:00", :advanced_typecasting => false) expect(hash["value"]).to eq("1955-04-18T11:22:33-05:00") end end context "with advanced typecasting" do it "should transform 'true' to TrueClass" do expect(parse("true")["value"]).to eq(true) end it "should transform 'false' to FalseClass" do expect(parse("false")["value"]).to eq(false) end it "should transform Strings matching the xs:time format to Time objects" do expect(parse("09:33:55.7Z")["value"]).to eq(Time.parse("09:33:55.7Z")) end it "should transform Strings matching the xs:time format ahead of utc to Time objects" do expect(parse("09:33:55+02:00")["value"]).to eq(Time.parse("09:33:55+02:00")) end it "should transform Strings matching the xs:date format to Date objects" do expect(parse("1955-04-18-05:00")["value"]).to eq(Date.parse("1955-04-18-05:00")) end it "should transform Strings matching the xs:dateTime format ahead of utc to Date objects" do expect(parse("1955-04-18+02:00")["value"]).to eq(Date.parse("1955-04-18+02:00")) end it "should transform Strings matching the xs:dateTime format to DateTime objects" do expect(parse("1955-04-18T11:22:33.5Z")["value"]).to eq( DateTime.parse("1955-04-18T11:22:33.5Z") ) end it "should transform Strings matching the xs:dateTime format ahead of utc to DateTime objects" do expect(parse("1955-04-18T11:22:33+02:00")["value"]).to eq( DateTime.parse("1955-04-18T11:22:33+02:00") ) end it "should transform Strings matching the xs:dateTime format with seconds and an offset to DateTime objects" do expect(parse("2004-04-12T13:20:15.5-05:00")["value"]).to eq( DateTime.parse("2004-04-12T13:20:15.5-05:00") ) end it "should not transform Strings containing an xs:time String and more" do expect(parse("09:33:55Z is a time")["value"]).to eq("09:33:55Z is a time") expect(parse("09:33:55Z_is_a_file_name")["value"]).to eq("09:33:55Z_is_a_file_name") end it "should not transform Strings containing an xs:date String and more" do expect(parse("1955-04-18-05:00 is a date")["value"]).to eq("1955-04-18-05:00 is a date") expect(parse("1955-04-18-05:00_is_a_file_name")["value"]).to eq("1955-04-18-05:00_is_a_file_name") end it "should not transform Strings containing an xs:dateTime String and more" do expect(parse("1955-04-18T11:22:33-05:00 is a dateTime")["value"]).to eq( "1955-04-18T11:22:33-05:00 is a dateTime" ) expect(parse("1955-04-18T11:22:33-05:00_is_a_file_name")["value"]).to eq( "1955-04-18T11:22:33-05:00_is_a_file_name" ) end ["00-00-00", "0000-00-00", "0000-00-00T00:00:00", "0569-23-0141", "DS2001-19-1312654773", "e6:53:01:00:ce:b4:06"].each do |date_string| it "should not transform a String like '#{date_string}' to date or time" do expect(parse("#{date_string}")["value"]).to eq(date_string) end end end context "Parsing xml with text and attributes" do before do xml =<<-XML Gary R Epstein Simon T Tyson XML @data = parse(xml) end it "correctly parse text nodes" do expect(@data).to eq({ 'opt' => { 'user' => [ 'Gary R Epstein', 'Simon T Tyson' ] } }) end it "parses attributes for text node if present" do expect(@data['opt']['user'][0].attributes).to eq({'login' => 'grep'}) end it "default attributes to empty hash if not present" do expect(@data['opt']['user'][1].attributes).to eq({}) end it "add 'attributes' accessor methods to parsed instances of String" do expect(@data['opt']['user'][0]).to respond_to(:attributes) expect(@data['opt']['user'][0]).to respond_to(:attributes=) end it "not add 'attributes' accessor methods to all instances of String" do expect("some-string").not_to respond_to(:attributes) expect("some-string").not_to respond_to(:attributes=) end end it "should typecast an integer" do xml = "10" expect(parse(xml)['tag']).to eq(10) end it "should typecast a true boolean" do xml = "true" expect(parse(xml)['tag']).to be(true) end it "should typecast a false boolean" do ["false"].each do |w| expect(parse("#{w}")['tag']).to be(false) end end it "should typecast a datetime" do xml = "2007-12-31 10:32" expect(parse(xml)['tag']).to eq(Time.parse( '2007-12-31 10:32' ).utc) end it "should typecast a date" do xml = "2007-12-31" expect(parse(xml)['tag']).to eq(Date.parse('2007-12-31')) end xml_entities = { "<" => "<", ">" => ">", '"' => """, "'" => "'", "&" => "&" } it "should unescape html entities" do xml_entities.each do |k,v| xml = "Some content #{v}" expect(parse(xml)['tag']).to match(Regexp.new(k)) end end it "should unescape XML entities in attributes" do xml_entities.each do |key, value| xml = "" expect(parse(xml)['tag']['@attr']).to match(Regexp.new(key)) end end it "should undasherize keys as tags" do xml = "Stuff" expect(parse(xml).keys).to include('tag_1') end it "should undasherize keys as attributes" do xml = "" expect(parse(xml)['tag1'].keys).to include('@attr_1') end it "should undasherize keys as tags and attributes" do xml = "" expect(parse(xml).keys).to include('tag_1') expect(parse(xml)['tag_1'].keys).to include('@attr_1') end it "should render nested content correctly" do xml = "Tag1 Content This is strong" expect(parse(xml)['root']['tag1']).to eq("Tag1 Content This is strong") end it "should render nested content with text nodes correctly" do xml = "Tag1 ContentStuff Hi There" expect(parse(xml)['root']).to eq("Tag1 ContentStuff Hi There") end it "should ignore attributes when a child is a text node" do xml = "Stuff" expect(parse(xml)).to eq({ "root" => "Stuff" }) end it "should ignore attributes when any child is a text node" do xml = "Stuff in italics" expect(parse(xml)).to eq({ "root" => "Stuff in italics" }) end it "should correctly transform multiple children" do xml = <<-XML 35 Home Simpson 1988-01-01 2000-04-28 23:01 true XML hash = { "user" => { "@gender" => "m", "age" => 35, "name" => "Home Simpson", "dob" => Date.parse('1988-01-01'), "joined_at" => Time.parse("2000-04-28 23:01"), "is_cool" => true } } expect(parse(xml)).to eq(hash) end it "should properly handle nil values (ActiveSupport Compatible)" do topic_xml = <<-EOT EOT expected_topic_hash = { 'title' => nil, 'id' => nil, 'approved' => nil, 'written_on' => nil, 'viewed_at' => nil, # don't execute arbitary YAML code 'content' => { "@type" => "yaml" }, 'parent_id' => nil, 'nil_true' => nil, 'namespaced' => nil } expect(parse(topic_xml)["topic"]).to eq(expected_topic_hash) end it "should handle a single record from xml (ActiveSupport Compatible)" do topic_xml = <<-EOT The First Topic David 1 true 0 2592000000 2003-07-16 2003-07-16T09:28:00+0000 --- \n1: should be an integer\n:message: Have a nice day\narray: \n- should-have-dashes: true\n should_have_underscores: true david@loudthinking.com 1.5 135 yes EOT expected_topic_hash = { 'title' => "The First Topic", 'author_name' => "David", 'id' => 1, 'approved' => true, 'replies_count' => 0, 'replies_close_in' => 2592000000, 'written_on' => Date.new(2003, 7, 16), 'viewed_at' => Time.utc(2003, 7, 16, 9, 28), # Changed this line where the key is :message. The yaml specifies this as a symbol, and who am I to change what you specify # The line in ActiveSupport is # 'content' => { 'message' => "Have a nice day", 1 => "should be an integer", "array" => [{ "should-have-dashes" => true, "should_have_underscores" => true }] }, 'content' => "--- \n1: should be an integer\n:message: Have a nice day\narray: \n- should-have-dashes: true\n should_have_underscores: true", 'author_email_address' => "david@loudthinking.com", 'parent_id' => nil, 'ad_revenue' => BigDecimal("1.50"), 'optimum_viewing_angle' => 135.0, # don't create symbols from arbitary remote code 'resident' => "yes" } parse(topic_xml)["topic"].each do |k,v| expect(v).to eq(expected_topic_hash[k]) end end it "should handle multiple records (ActiveSupport Compatible)" do topics_xml = <<-EOT The First Topic David 1 false 0 2592000000 2003-07-16 2003-07-16T09:28:00+0000 Have a nice day david@loudthinking.com The Second Topic Jason 1 false 0 2592000000 2003-07-16 2003-07-16T09:28:00+0000 Have a nice day david@loudthinking.com EOT expected_topic_hash = { 'title' => "The First Topic", 'author_name' => "David", 'id' => 1, 'approved' => false, 'replies_count' => 0, 'replies_close_in' => 2592000000, 'written_on' => Date.new(2003, 7, 16), 'viewed_at' => Time.utc(2003, 7, 16, 9, 28), 'content' => "Have a nice day", 'author_email_address' => "david@loudthinking.com", 'parent_id' => nil } # puts Nori.parse(topics_xml)['topics'].first.inspect parse(topics_xml)["topics"].first.each do |k,v| expect(v).to eq(expected_topic_hash[k]) end end context "with convert_attributes_to set to a custom formula" do it "alters attributes and values" do converter = lambda {|key, value| ["#{key}_k", "#{value}_v"] } xml = <<-XML 21 XML expect(parse(xml, :convert_attributes_to => converter)).to eq({'user' => {'@name_k' => 'value_v', 'age' => '21'}}) end end it "should handle a single record from_xml with attributes other than type (ActiveSupport Compatible)" do topic_xml = <<-EOT EOT expected_topic_hash = { '@id' => "175756086", '@owner' => "55569174@N00", '@secret' => "0279bf37a1", '@server' => "76", '@title' => "Colored Pencil PhotoBooth Fun", '@ispublic' => "1", '@isfriend' => "0", '@isfamily' => "0", } parse(topic_xml)["rsp"]["photos"]["photo"].each do |k, v| expect(v).to eq(expected_topic_hash[k]) end end it "should handle an emtpy array (ActiveSupport Compatible)" do blog_xml = <<-XML XML expected_blog_hash = {"blog" => {"posts" => []}} expect(parse(blog_xml)).to eq(expected_blog_hash) end it "should handle empty array with whitespace from xml (ActiveSupport Compatible)" do blog_xml = <<-XML XML expected_blog_hash = {"blog" => {"posts" => []}} expect(parse(blog_xml)).to eq(expected_blog_hash) end it "should handle array with one entry from_xml (ActiveSupport Compatible)" do blog_xml = <<-XML a post XML expected_blog_hash = {"blog" => {"posts" => ["a post"]}} expect(parse(blog_xml)).to eq(expected_blog_hash) end it "should handle array with multiple entries from xml (ActiveSupport Compatible)" do blog_xml = <<-XML a post another post XML expected_blog_hash = {"blog" => {"posts" => ["a post", "another post"]}} expect(parse(blog_xml)).to eq(expected_blog_hash) end it "should handle file types (ActiveSupport Compatible)" do blog_xml = <<-XML XML hash = parse(blog_xml) expect(hash.keys).to include('blog') expect(hash['blog'].keys).to include('logo') file = hash['blog']['logo'] expect(file.original_filename).to eq('logo.png') expect(file.content_type).to eq('image/png') end it "should handle file from xml with defaults (ActiveSupport Compatible)" do blog_xml = <<-XML XML file = parse(blog_xml)['blog']['logo'] expect(file.original_filename).to eq('untitled') expect(file.content_type).to eq('application/octet-stream') end it "should handle xsd like types from xml (ActiveSupport Compatible)" do bacon_xml = <<-EOT 0.5 12.50 1 2007-12-25T12:34:56+0000 YmFiZS5wbmc= EOT expected_bacon_hash = { 'weight' => 0.5, 'chunky' => true, 'price' => BigDecimal("12.50"), 'expires_at' => Time.utc(2007,12,25,12,34,56), 'notes' => "", 'illustration' => "babe.png" } expect(parse(bacon_xml)["bacon"]).to eq(expected_bacon_hash) end it "should let type trickle through when unknown (ActiveSupport Compatible)" do product_xml = <<-EOT 0.5 image.gif EOT expected_product_hash = { 'weight' => 0.5, 'image' => {'@type' => 'ProductImage', 'filename' => 'image.gif' }, } expect(parse(product_xml)["product"]).to eq(expected_product_hash) end it "should handle unescaping from xml (ActiveResource Compatible)" do xml_string = 'First & Last NameFirst &amp; Last Name' expected_hash = { 'bare_string' => 'First & Last Name', 'pre_escaped_string' => 'First & Last Name' } expect(parse(xml_string)['person']).to eq(expected_hash) end it "handle an empty xml string" do expect(parse('')).to eq({}) end # As returned in the response body by the unfuddle XML API when creating objects it "handle an xml string containing a single space" do expect(parse(' ')).to eq({}) end end end def parse(xml, options = {}) defaults = {:parser => parser} Nori.new(defaults.merge(options)).parse(xml) end end