require File.expand_path(File.dirname(__FILE__) + '/spec_helper') describe "XmlMapper" do before(:each) do @mapper = XmlMapper.new end describe "#add_mapping" do it "adds the correct mappings when only one symbol given" do @mapper.add_mapping(:text, :title) @mapper.mappings.should == [{ :type => :text, :xpath => "title", :key => :title, :options => {} }] end it "adds the correct mapping when type is not text" do @mapper.add_mapping(:length, :title) @mapper.mappings.should == [{ :type => :length, :xpath => "title", :key => :title, :options => {} }] end it "adds the correct mapping when type is node" do @mapper.add_mapping(:node, :title) @mapper.mappings.should == [{ :type => :node, :xpath => "title", :key => :title, :options => {} }] end it "adds multiple mappings when more symbols given" do @mapper.add_mapping(:text, :first_name, :last_name) @mapper.mappings.should == [ { :type => :text, :xpath => "first_name", :key => :first_name, :options => {} }, { :type => :text, :xpath => "last_name", :key => :last_name, :options => {} }, ] end it "sets attribute for attribute mapping when attribute is mapped to same name" do @mapper.add_mapping(:attribute, :first_name) @mapper.mappings.should == [ { :type => :attribute, :attribute => "first_name", :key => :first_name, :options => {}, :xpath => "" } ] end it "sets attribute for attribute mapping when attribute is mapped to different name" do @mapper.add_mapping(:attribute, :first_name => :firstname) @mapper.mappings.should == [ { :type => :attribute, :attribute => "first_name", :key => :firstname, :options => {}, :xpath => "" } ] end it "adds mappings when mapping given as hash" do @mapper.add_mapping(:text, :name => :first_name) @mapper.mappings.should == [{ :type => :text, :xpath => "name", :key => :first_name, :options => {} }] end it "adds mappings with options when mapping given as symbols and last arg is hash" do @mapper.add_mapping(:text, :name, :born, :after_map => :to_s) @mapper.mappings.should == [ { :type => :text, :xpath => "name", :key => :name, :options => { :after_map => :to_s } }, { :type => :text, :xpath => "born", :key => :born, :options => { :after_map => :to_s } }, ] end end describe "map attributes" do before(:each) do @xml = %( Black on Both Sides Mos Def 7 ) end it "maps the correct inner text when node found" do @mapper.add_mapping(:text, :artist_name, :title) @mapper.attributes_from_xml(@xml).should == { :title => "Black on Both Sides", :artist_name => "Mos Def" } end describe "#exists" do it "returns true when node exists" do xml = %(Black on Both SidesDE) root = Nokogiri::XML(xml).root @mapper.add_mapping(:exists, "rights[country='DE']" => :allows_streaming) @mapper.attributes_from_xml(xml).should == { :allows_streaming => true } end it "returns false when node does not exist" do xml = %(Black on Both SidesDE) @mapper.add_mapping(:exists, "rights[country='FR']" => :allows_streaming) @mapper.attributes_from_xml(xml).should == { :allows_streaming => false } end end describe "#node" do it "returns a nokogiri node" do @mapper.add_mapping(:node, :title) @mapper.attributes_from_xml(@xml)[:title].should be_an_instance_of(Nokogiri::XML::Element) end it "returns nil when node not found" do @mapper.add_mapping(:node, :rgne) @mapper.attributes_from_xml(@xml)[:rgne].should be_nil end it "returns the correct nokogiri node" do @mapper.add_mapping(:node, :title) node = @mapper.attributes_from_xml(@xml)[:title] node.inner_text.should == "Black on Both Sides" end it "can be combined with after_map" do @mapper.add_mapping(:node, :title, :after_map => :inner_text) @mapper.attributes_from_xml(@xml)[:title].should == "Black on Both Sides" end end it "maps not found nodes to nil" do @mapper.add_mapping(:text, :artist_name, :version_title, :long_title) @mapper.attributes_from_xml(@xml).should == { :version_title => nil, :long_title => nil, :artist_name => "Mos Def" } end it "converts integers to integer when found" do @mapper.add_mapping(:text, :artist_name) @mapper.add_mapping(:integer, :track_number) @mapper.attributes_from_xml(@xml).should == { :track_number => 7, :artist_name => "Mos Def" } end it "does not convert nil to integer for integer type" do @mapper.add_mapping(:text, :artist_name) @mapper.add_mapping(:integer, :track_number, :set_count) @mapper.attributes_from_xml(@xml).should == { :track_number => 7, :artist_name => "Mos Def", :set_count => nil } end it "calls method with name type on value when found and responding" do @mapper.add_mapping(:text, :artist_name, :after_map => :upcase) @mapper.attributes_from_xml(@xml).should == { :artist_name => "MOS DEF" } end it "uses mapper method defined in xml_mapper when value does not respond to :after_map" do class << @mapper def double(value) value.to_s * 2 end end @mapper.add_mapping(:text, :artist_name, :after_map => :double) @mapper.attributes_from_xml(@xml).should == { :artist_name => "Mos DefMos Def" } end it "allows multiple after_code mappings" do class << @mapper def double(value) value * 2 end end @mapper.add_mapping(:integer, :track_number, :after_map => :double) @mapper.attributes_from_xml(@xml).should == { :track_number => 14 } end it "allows defining a after_map mapping without ridicolous brackets" do @mapper.add_mapping(:string, :track_number => :num, :after_map => :to_i) @mapper.attributes_from_xml(@xml).should == { :num => 7 } end it "uses mapper method defined in xml_mapper when value does not respond to :after_map and given as hash" do class << @mapper def double(value) value * 2 end end # [{:type=>:text, :xpath=>"Graphic/ImgFormat", :key=>:image_format, :options=>{:after_map=>:double}}] @mapper.add_mapping(:text, { :artist_name => :name }, {:after_map => :double}) @mapper.attributes_from_xml(@xml).should == { :name => "Mos DefMos Def" } end it "takes a nokogiri node as argument" do @mapper.add_mapping(:text, :artist_name) @mapper.attributes_from_xml(Nokogiri::XML(@xml).root).should == { :artist_name => "Mos Def" } end it "takes a nokogiri document as argument" do @mapper.add_mapping(:text, :artist_name) @mapper.attributes_from_xml(Nokogiri::XML(@xml).root).should == { :artist_name => "Mos Def" } end it "should also takes an array of nodes as argument" do @mapper.add_mapping(:text, :artist_name) @mapper.attributes_from_xml([Nokogiri::XML(@xml).root, Nokogiri::XML(@xml).root]).should == [ { :artist_name => "Mos Def" }, { :artist_name => "Mos Def" } ] end it "also takes an array of nokogiri documents as argument" do @mapper.add_mapping(:text, :artist_name) @mapper.attributes_from_xml([Nokogiri::XML(@xml), Nokogiri::XML(@xml)]).should == [ { :artist_name => "Mos Def" }, { :artist_name => "Mos Def" } ] end describe "mapping embedded attributes" do before(:each) do @xml = %( Mos Def 1 Track 1 2 Track 2 ) end it "maps all embedded attributes" do submapper = XmlMapper.new submapper.add_mapping(:integer, :track_number) submapper.add_mapping(:text, :title) @mapper.add_mapping(:text, :artist_name) @mapper.add_mapping(:many, { "tracks/track" => :tracks }, :mapper => submapper) @mapper.attributes_from_xml(@xml).should == { :artist_name => "Mos Def", :tracks => [ { :title => "Track 1", :track_number => 1 }, { :title => "Track 2", :track_number => 2 }, ] } end end end describe "#attributes_from_xml_path" do before(:each) do @mapper.add_mapping(:text, :title) @xml = %( Black on Both Sides ) File.stub(:read).and_return @xml end it "sets the xml_path" do @mapper.attributes_from_xml_path("/some/path.xml").should == { :title => "Black on Both Sides", :xml_path => "/some/path.xml" } end it "calls File.read with correct parameters" do File.should_receive(:read).with("/some/path.xml").and_return @xml @mapper.attributes_from_xml_path("/some/path.xml") end it "allows using the xml_path in after_map block" do @mapper.after_map do self[:new_xml_path] = self[:xml_path] end @mapper.attributes_from_xml_path("/some/path.xml")[:new_xml_path].should == "/some/path.xml" end it "allows deleting the xml_path in after_map block" do @mapper.after_map do self.delete(:xml_path) end @mapper.attributes_from_xml_path("/some/path.xml").should_not have_key(:xml_path) end end describe "#after_map" do before(:each) do @mapper.after_map do self[:upc] = "1234" end end it "assigns after_map block" do @mapper.after_map_block.should_not be_nil end it "assigns a block to after_map_block" do @mapper.after_map_block.should be_an_instance_of(Proc) end it "should executes after_map block after mapping" do @mapper.attributes_from_xml("Some Titel").should == { :upc => "1234" } end end describe "converting strings" do describe "#string_to_boolean" do { "true" => true, "false" => false, "y" => true, "TRUE" => true, "" => nil, "YES" => true, "yes" => true, "n" => false, "1" => true, "0" => false }.each do |value, result| it "converts #{value.inspect} to #{result}" do @mapper.string_to_boolean(value).should == result end end end end describe "defining a DSL" do def create_class(base_class = "XmlMapper") class_name = "TestMapping#{Time.now.to_f.to_s.gsub(".", "")}" str = %( class #{class_name} < #{base_class} end ) eval(str) eval(class_name) end before(:each) do # so that we have a new class in each spec @clazz = create_class end it "initializes a mapper of the same class" do @clazz.mapper.class.name.should == @clazz.name end it "sets the correct mapping for text keyword" do @clazz.text(:title) @clazz.mapper.mappings.should == [{ :type => :text, :key => :title, :xpath => "title", :options => {} }] end it "sets the correct mapping for node keyword" do @clazz.node(:title) @clazz.mapper.mappings.should == [{ :type => :node, :key => :title, :xpath => "title", :options => {} }] end it "sets the correct mapping for text keyword" do @clazz.integer(:title) @clazz.mapper.mappings.should == [{ :type => :integer, :key => :title, :xpath => "title", :options => {} }] end it "allows getting attributes form xml_path" do File.stub(:read).and_return %(Test Title) @clazz.text(:title) @clazz.attributes_from_xml_path("/some/path.xml").should == { :title => "Test Title", :xml_path => "/some/path.xml" } end it "allows defining a after_map block" do @clazz.after_map do self[:upc] = "1234" end @clazz.text(:title) @clazz.attributes_from_xml(%(Test Title)).should == { :upc => "1234", :title => "Test Title" } end it "allows deleteing the xml path in after_block" do @clazz.after_map do self.delete(:xml_path) end File.stub(:read).and_return %(Test Title) @clazz.attributes_from_xml_path("/some/path.xml").should_not have_key(:xml_path) end it "allows using attributes from superclass in after_map block" it "allows using of instance methods of mapper for after_map" do @clazz.class_eval do def custom_mapper(txt) txt * 2 end end @clazz.text(:title, :after_map => :custom_mapper) @clazz.attributes_from_xml(%(Test)).should == { :title => "TestTest" } end it "allows after map when used in submapper and method exists in mapper" do xml = %( 11 22 ) @clazz.send(:define_method, "double") do |*args| args.first * 2 end @clazz.many "tracks/track" => :tracks do integer :track_number => :num, :after_map => :double end @clazz.attributes_from_xml(xml).should == { :tracks => [ { :num => 22 }, { :num => 44 }] } end it "accepts boolean as keyword" do @clazz.boolean(:allows_streaming) xml = %(Test Titletrue) @clazz.attributes_from_xml(xml).should == { :allows_streaming => true } end it "accepts exists as keyword" do @clazz.exists("rights[country='DE']" => :allows_streaming) xml = %(Black on Both SidesDE) @clazz.attributes_from_xml(xml).should == { :allows_streaming => true } end it "accepts not_exists as keyword" do @clazz.not_exists("forbidden_countries/country[text()='DE']" => :allows_streaming) xml = %(Black on Both SidesDE) @clazz.attributes_from_xml(xml).should == { :allows_streaming => false } end it "does keep scope for not_exists" do xml = %( Track 1 DE Track 2 AT ) @clazz.many "tracks/track" => :tracks do text :title not_exists "forbidden_countries/country[text()='DE']" => :allows_streaming end @clazz.attributes_from_xml(xml)[:tracks].should == [ { :title => "Track 1", :allows_streaming => false }, { :title => "Track 2", :allows_streaming => true }, ] end describe "#within" do it "adds the within xpath to all xpath mappings" do @clazz.within("artist") do text :name => :artist_name integer :id => :artist_id end @clazz.mapper.mappings.should == [ { :type => :text, :xpath => "artist/name", :key => :artist_name, :options => {} }, { :type => :integer, :xpath => "artist/id", :key => :artist_id, :options => {} }, ] end it "adds all nested within xpaths to xpath mappings" do @clazz.within("contributions") do within "artist" do text :name => :artist_name integer :id => :artist_id end end @clazz.mapper.mappings.should == [ { :type => :text, :xpath => "contributions/artist/name", :key => :artist_name, :options => {} }, { :type => :integer, :xpath => "contributions/artist/id", :key => :artist_id, :options => {} }, ] end it "allows multiple within blocks on same level" do @clazz.within "artist" do text :name => :artist_name end @clazz.within "file" do text :file_name end @clazz.mapper.mappings.should == [ { :type => :text, :xpath => "artist/name", :key => :artist_name, :options => {} }, { :type => :text, :xpath => "file/file_name", :key => :file_name, :options => {} }, ] end end describe "with mapper hierarchy" do it "attributes_from_xml includes superclass mapper as well" do @clazz.text(:artist_name) subclazz = create_class(@clazz.name) subclazz.text(:title) xml = %( Mos Def Black on Both Sides ) subclazz.attributes_from_xml(xml).should == { :artist_name => "Mos Def", :title => "Black on Both Sides" } end it "overwrites superclass mapper" do @clazz.text(:artist_name) subclazz = create_class(@clazz.name) subclazz.text(:title) subclazz.text(:artist_name, :after_map => :upcase) xml = %( Mos Def Black on Both Sides ) subclazz.attributes_from_xml(xml).should == { :artist_name => "MOS DEF", :title => "Black on Both Sides" } end it "attributes_from_xml_path includes superclass mapper as well" do @clazz.text(:artist_name) subclazz = create_class(@clazz.name) subclazz.text(:title) xml = %( Mos Def Black on Both Sides ) File.stub!(:read).and_return xml subclazz.attributes_from_xml_path("/some_path/album.xml").should == { :artist_name => "Mos Def", :title => "Black on Both Sides", :xml_path => "/some_path/album.xml" } end end describe "defining a submapper" do before(:each) do @clazz.many("tracks/track" => :tracks) do text :title integer :track_number end end it "sets the mapping type to many" do @clazz.mapper.mappings.first[:type].should == :many end it "sets the mapping key to track" do @clazz.mapper.mappings.first[:key].should == :tracks end it "sets the mapping xpath to tracks/track" do @clazz.mapper.mappings.first[:xpath].should == "tracks/track" end it "sets the correct submapper" do @clazz.mapper.mappings.first[:options][:mapper].mappings.should == [ { :type => :text, :key => :title, :xpath => "title", :options => {} }, { :type => :integer, :key => :track_number, :xpath => "track_number", :options => {} }, ] end it "attributes_from_xml returns the correct attributes" do @clazz.text(:artist_name) @clazz.text(:title) xml = %( Mos Def Black on Both Sides Track 1 1 Track 2 2 ) @clazz.attributes_from_xml(xml).should == { :artist_name => "Mos Def", :title => "Black on Both Sides", :tracks => [ { :title => "Track 1", :track_number => 1 }, { :title => "Track 2", :track_number => 2 } ] } end end describe "using node_name, inner_text and attribute" do it "extracts attributes for a many relationship when node_name and inner_text is used" do xml = %( Sexy sushi Sexi Sushi Sexi Sushi ) @clazz.many "Contributors/*" => :contributions do node_name :contribution_type inner_text :name end @clazz.attributes_from_xml(xml).should == { :contributions => [ { :contribution_type => "Performer", :name => "Sexy sushi" }, { :contribution_type => "Composer", :name => "Sexi Sushi" }, { :contribution_type => "Author", :name => "Sexi Sushi" }, ] } end it "extracts the correct attributes when attribute keyword is used in 'many'-mapping" do xml = %( Track 1 ) @clazz.many "tracks/track" => :tracks do attribute "some_code" => :isrc text :title end @clazz.attributes_from_xml(xml).should == { :tracks => [ { :isrc => "1234", :title => "Track 1" } ] } end it "extracts the correct attributes when attribute is used in within-mapping" do xml = %( Product Title ) @clazz.within "meta" do attribute "code" => :upc end @clazz.attributes_from_xml(xml)[:upc].should == "1234" end it "extracts the correct attributes when attribute is used in root mapping" do xml = %( Product Title ) @clazz.attribute :code => :upc @clazz.attributes_from_xml(xml)[:upc].should == "1234" end end end end