xml_mapper 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.3
1
+ 0.4.0
data/lib/xml_mapper.rb CHANGED
@@ -6,24 +6,10 @@ class XmlMapper
6
6
  class << self
7
7
  attr_accessor :mapper
8
8
 
9
- def text(*args)
10
- mapper.add_mapping(:text, *args)
11
- end
12
-
13
- def integer(*args)
14
- mapper.add_mapping(:integer, *args)
15
- end
16
-
17
- def boolean(*args)
18
- mapper.add_mapping(:boolean, *args)
19
- end
20
-
21
- def exists(*args)
22
- mapper.add_mapping(:exists, *args)
23
- end
24
-
25
- def node(*args)
26
- mapper.add_mapping(:node, *args)
9
+ [:text, :integer, :boolean, :exists, :not_exists, :node_name, :inner_text, :node, :attribute].each do |method_name|
10
+ define_method(method_name) do |*args|
11
+ mapper.add_mapping(method_name, *args)
12
+ end
27
13
  end
28
14
 
29
15
  def within(xpath, &block)
@@ -63,7 +49,7 @@ class XmlMapper
63
49
 
64
50
  def capture_submapping(&block)
65
51
  saved_mapper = self.mapper
66
- self.mapper = XmlMapper.new
52
+ self.mapper = self.new
67
53
  self.instance_eval(&block)
68
54
  captured_mapper = self.mapper
69
55
  self.mapper = saved_mapper
@@ -94,7 +80,10 @@ class XmlMapper
94
80
  def add_mapping(type, *args)
95
81
  options = extract_options_from_args(args)
96
82
  if args.first.is_a?(Hash)
97
- args.first.map { |xpath, key| add_single_mapping(type, xpath, key, options) }
83
+ if after_map_method = args.first.delete(:after_map)
84
+ options.merge!(:after_map => after_map_method)
85
+ end
86
+ args.first.map { |xpath, key| add_single_mapping(type, xpath, key, options) }
98
87
  else
99
88
  args.map { |arg| add_single_mapping(type, arg, arg, options) }
100
89
  end
@@ -104,8 +93,14 @@ class XmlMapper
104
93
  self.after_map_block = block
105
94
  end
106
95
 
107
- def add_single_mapping(type, xpath, key, options = {})
108
- self.mappings << { :type => type, :xpath => add_with_to_xpath(xpath), :key => key, :options => options }
96
+ def add_single_mapping(type, xpath_or_attribute, key, options = {})
97
+ mappings = { :type => type, :key => key, :options => options }
98
+ xpath = type == :attribute ? nil : xpath_or_attribute
99
+ if type == :attribute
100
+ mappings[:attribute] = xpath_or_attribute.to_s
101
+ end
102
+ mappings[:xpath] = add_with_to_xpath(xpath)
103
+ self.mappings << mappings
109
104
  end
110
105
 
111
106
  def add_with_to_xpath(xpath)
@@ -126,6 +121,7 @@ class XmlMapper
126
121
  xml_or_doc.map { |doc| attributes_from_xml(doc) }
127
122
  else
128
123
  doc = xml_or_doc.is_a?(Nokogiri::XML::Node) ? xml_or_doc : Nokogiri::XML(xml_or_doc)
124
+ doc = doc.root if doc.respond_to?(:root)
129
125
  atts = self.mappings.inject(xml_path.nil? ? {} : { :xml_path => xml_path }) do |hash, mapping|
130
126
  hash.merge(mapping[:key] => value_from_doc_and_mapping(doc, mapping))
131
127
  end
@@ -137,28 +133,44 @@ class XmlMapper
137
133
  def value_from_doc_and_mapping(doc, mapping)
138
134
  if mapping[:type] == :many
139
135
  mapping[:options][:mapper].attributes_from_xml(doc.search(mapping[:xpath]).to_a)
140
- elsif mapping[:type] == :exists
141
- !doc.at("//#{mapping[:xpath]}").nil?
142
136
  else
143
- value = mapping[:type] == :node ? doc.at(mapping[:xpath]) : inner_text_for_xpath(doc, mapping[:xpath])
144
- apply_after_map_to_value(value, mapping)
137
+ node = mapping[:xpath].length == 0 ? doc : doc.xpath(mapping[:xpath]).first
138
+ if mapping[:type] == :exists
139
+ !node.nil?
140
+ elsif mapping[:type] == :not_exists
141
+ node.nil?
142
+ else
143
+ value =
144
+ case mapping[:type]
145
+ when :node_name
146
+ doc.nil? ? nil : doc.name
147
+ when :inner_text
148
+ doc.nil? ? nil : doc.inner_text
149
+ when :node
150
+ node
151
+ when :attribute
152
+ node.nil? ? nil : (node.respond_to?(:root) ? node.root : node)[mapping[:attribute]]
153
+ else
154
+ inner_text_for_node(node)
155
+ end
156
+ apply_after_map_to_value(value, mapping)
157
+ end
145
158
  end
146
159
  end
147
160
 
148
161
  def apply_after_map_to_value(value, mapping)
149
- after_map = TYPE_TO_AFTER_CODE[mapping[:type]]
150
- after_map ||= mapping[:options][:after_map]
151
- if value && after_map
152
- value = value.send(after_map) if value.respond_to?(after_map)
153
- value = self.send(after_map, value) if self.respond_to?(after_map)
162
+ after_mappings = [TYPE_TO_AFTER_CODE[mapping[:type]], mapping[:options][:after_map]].compact
163
+ if value
164
+ after_mappings.each do |after_map|
165
+ value = value.send(after_map) if value.respond_to?(after_map)
166
+ value = self.send(after_map, value) if self.respond_to?(after_map)
167
+ end
154
168
  end
155
169
  value
156
170
  end
157
171
 
158
- def inner_text_for_xpath(doc, xpath)
159
- if node = doc.at(xpath)
160
- node.inner_text
161
- end
172
+ def inner_text_for_node(node)
173
+ node.inner_text if node
162
174
  end
163
175
 
164
176
  MAPPINGS = {
@@ -166,7 +178,9 @@ class XmlMapper
166
178
  "false" => false,
167
179
  "yes" => true,
168
180
  "y" => true,
169
- "n" => false
181
+ "n" => false,
182
+ "1" => true,
183
+ "0" => false
170
184
  }
171
185
 
172
186
  def string_to_boolean(value)
data/spec/example_spec.rb CHANGED
@@ -11,7 +11,10 @@ describe "ExampleSpec" do
11
11
  {
12
12
  :title => "Black on Both Sides", :version_title => "Extended Edition", :released_in => 1999,
13
13
  :artist_name => "Mos Def", :artist_id => 1212, :country => "DE", :allows_streaming => true,
14
- :tracks_count => 2, :released_on => Date.new(1999, 10, 12)
14
+ :tracks_count => 2, :released_on => Date.new(1999, 10, 12), :contributions => [
15
+ { :role => "artist", :name => "Mos Def" },
16
+ { :role => "producer", :name => "DJ Premier" },
17
+ ]
15
18
  }.each do |key, value|
16
19
  it "extracts #{value.inspect} as #{key}" do
17
20
  @attributes[key].should == value
@@ -19,8 +22,8 @@ describe "ExampleSpec" do
19
22
  end
20
23
 
21
24
  [
22
- { :track_title => "Fear Not of Man", :track_number => 1, :disk_number => 1, :explicit_lyrics => true },
23
- { :track_title => "Hip Hop", :track_number => 2, :disk_number => 1, :explicit_lyrics => true },
25
+ { :track_title => "Fear Not of Man", :track_number => 1, :disk_number => 1, :explicit_lyrics => true, :isrc => "1234" },
26
+ { :track_title => "Hip Hop", :track_number => 2, :disk_number => 1, :explicit_lyrics => false, :isrc => "2345" },
24
27
  ].each_with_index do |hash, offset|
25
28
  hash.each do |key, value|
26
29
  it "extracts #{value.inspect} for #{key} for track with offset #{offset}" do
@@ -9,17 +9,27 @@
9
9
  <name>Mos Def</name>
10
10
  <id>1212</id>
11
11
  </artist>
12
+ <contributions>
13
+ <artist>Mos Def</artist>
14
+ <producer>DJ Premier</producer>
15
+ </contributions>
12
16
  <tracks>
13
- <track>
17
+ <track code="1234">
14
18
  <title>Fear Not of Man</title>
15
19
  <number>1</number>
16
20
  <disk>1</number>
17
21
  <explicit_lyrics />
22
+ <not_streamable_in>
23
+ <country>de</country>
24
+ </not_streamable_in>
18
25
  </track>
19
- <track>
26
+ <track code="2345">
20
27
  <title>Hip Hop</title>
21
28
  <number>2</number>
22
29
  <disk>1</number>
30
+ <not_streamable_in>
31
+ <country>at</country>
32
+ </not_streamable_in>
23
33
  </track>
24
34
  </tracks>
25
35
  </album>
data/spec/my_mapper.rb CHANGED
@@ -13,11 +13,18 @@ class MyMapper < XmlMapper
13
13
  integer :id => :artist_id
14
14
  end
15
15
 
16
+ many "contributions/*" => :contributions do # use the name of xml nodes for mappings
17
+ node_name :role # map name of xml node to :role
18
+ inner_text :name # map inner_text of xml node to :name
19
+ end
20
+
16
21
  many "tracks/track" => :tracks do # maps xpath "tracks/track" to array with key :tracks
22
+ attribute :code => :isrc # map xml attribute "code" to :isrc
17
23
  text :title => :track_title
18
24
  integer :number => :track_number
19
25
  integer :disk => :disk_number
20
- exists :explicit_lyrics # checks if a node with the xpath exists
26
+ exists :explicit_lyrics # checks if a node with the xpath exists
27
+ not_exists "not_streamable_in/country[text()='de']" => :allows_streaming
21
28
  end
22
29
 
23
30
  after_map do # is called after attributes are extracted, self references the extracted attributes
@@ -29,6 +29,20 @@ describe "XmlMapper" do
29
29
  ]
30
30
  end
31
31
 
32
+ it "sets attribute for attribute mapping when attribute is mapped to same name" do
33
+ @mapper.add_mapping(:attribute, :first_name)
34
+ @mapper.mappings.should == [
35
+ { :type => :attribute, :attribute => "first_name", :key => :first_name, :options => {}, :xpath => "" }
36
+ ]
37
+ end
38
+
39
+ it "sets attribute for attribute mapping when attribute is mapped to different name" do
40
+ @mapper.add_mapping(:attribute, :first_name => :firstname)
41
+ @mapper.mappings.should == [
42
+ { :type => :attribute, :attribute => "first_name", :key => :firstname, :options => {}, :xpath => "" }
43
+ ]
44
+ end
45
+
32
46
  it "adds mappings when mapping given as hash" do
33
47
  @mapper.add_mapping(:text, :name => :first_name)
34
48
  @mapper.mappings.should == [{ :type => :text, :xpath => "name", :key => :first_name, :options => {} }]
@@ -61,6 +75,7 @@ describe "XmlMapper" do
61
75
  describe "#exists" do
62
76
  it "returns true when node exists" do
63
77
  xml = %(<album><title>Black on Both Sides</title><rights><country>DE</country></rights></album>)
78
+ root = Nokogiri::XML(xml).root
64
79
  @mapper.add_mapping(:exists, "rights[country='DE']" => :allows_streaming)
65
80
  @mapper.attributes_from_xml(xml).should == { :allows_streaming => true }
66
81
  end
@@ -138,10 +153,29 @@ describe "XmlMapper" do
138
153
  }
139
154
  end
140
155
 
156
+ it "allows multiple after_code mappings" do
157
+ class << @mapper
158
+ def double(value)
159
+ value * 2
160
+ end
161
+ end
162
+ @mapper.add_mapping(:integer, :track_number, :after_map => :double)
163
+ @mapper.attributes_from_xml(@xml).should == {
164
+ :track_number => 14
165
+ }
166
+ end
167
+
168
+ it "allows defining a after_map mapping without ridicolous brackets" do
169
+ @mapper.add_mapping(:string, :track_number => :num, :after_map => :to_i)
170
+ @mapper.attributes_from_xml(@xml).should == {
171
+ :num => 7
172
+ }
173
+ end
174
+
141
175
  it "uses mapper method defined in xml_mapper when value does not respond to :after_map and given as hash" do
142
176
  class << @mapper
143
177
  def double(value)
144
- value.to_s * 2
178
+ value * 2
145
179
  end
146
180
  end
147
181
  # [{:type=>:text, :xpath=>"Graphic/ImgFormat", :key=>:image_format, :options=>{:after_map=>:double}}]
@@ -153,12 +187,27 @@ describe "XmlMapper" do
153
187
 
154
188
  it "takes a nokogiri node as argument" do
155
189
  @mapper.add_mapping(:text, :artist_name)
156
- @mapper.attributes_from_xml(Nokogiri::XML(@xml)).should == {
190
+ @mapper.attributes_from_xml(Nokogiri::XML(@xml).root).should == {
191
+ :artist_name => "Mos Def"
192
+ }
193
+ end
194
+
195
+ it "takes a nokogiri document as argument" do
196
+ @mapper.add_mapping(:text, :artist_name)
197
+ @mapper.attributes_from_xml(Nokogiri::XML(@xml).root).should == {
157
198
  :artist_name => "Mos Def"
158
199
  }
159
200
  end
160
201
 
161
202
  it "should also takes an array of nodes as argument" do
203
+ @mapper.add_mapping(:text, :artist_name)
204
+ @mapper.attributes_from_xml([Nokogiri::XML(@xml).root, Nokogiri::XML(@xml).root]).should == [
205
+ { :artist_name => "Mos Def" },
206
+ { :artist_name => "Mos Def" }
207
+ ]
208
+ end
209
+
210
+ it "also takes an array of nokogiri documents as argument" do
162
211
  @mapper.add_mapping(:text, :artist_name)
163
212
  @mapper.attributes_from_xml([Nokogiri::XML(@xml), Nokogiri::XML(@xml)]).should == [
164
213
  { :artist_name => "Mos Def" },
@@ -265,7 +314,7 @@ describe "XmlMapper" do
265
314
  describe "#string_to_boolean" do
266
315
  {
267
316
  "true" => true, "false" => false, "y" => true, "TRUE" => true, "" => nil, "YES" => true, "yes" => true,
268
- "n" => false
317
+ "n" => false, "1" => true, "0" => false
269
318
  }.each do |value, result|
270
319
  it "converts #{value.inspect} to #{result}" do
271
320
  @mapper.string_to_boolean(value).should == result
@@ -351,6 +400,31 @@ describe "XmlMapper" do
351
400
  }
352
401
  end
353
402
 
403
+ it "allows after map when used in submapper and method exists in mapper" do
404
+ xml = %(
405
+ <album>
406
+ <tracks>
407
+ <track>
408
+ <track_number>11</track_number>
409
+ </track>
410
+ <track>
411
+ <track_number>22</track_number>
412
+ </track>
413
+ </tracks>
414
+ </album>
415
+ )
416
+ @clazz.send(:define_method, "double") do |*args|
417
+ args.first * 2
418
+ end
419
+
420
+ @clazz.many "tracks/track" => :tracks do
421
+ integer :track_number => :num, :after_map => :double
422
+ end
423
+ @clazz.attributes_from_xml(xml).should == {
424
+ :tracks => [ { :num => 22 }, { :num => 44 }]
425
+ }
426
+ end
427
+
354
428
  it "accepts boolean as keyword" do
355
429
  @clazz.boolean(:allows_streaming)
356
430
  xml = %(<album><title>Test Title</title><allows_streaming>true</allows_streaming></album>)
@@ -363,6 +437,41 @@ describe "XmlMapper" do
363
437
  @clazz.attributes_from_xml(xml).should == { :allows_streaming => true }
364
438
  end
365
439
 
440
+ it "accepts not_exists as keyword" do
441
+ @clazz.not_exists("forbidden_countries/country[text()='DE']" => :allows_streaming)
442
+ xml = %(<album><title>Black on Both Sides</title><forbidden_countries><country>DE</country></rights></album>)
443
+ @clazz.attributes_from_xml(xml).should == { :allows_streaming => false }
444
+ end
445
+
446
+ it "does keep scope for not_exists" do
447
+ xml = %(
448
+ <album>
449
+ <tracks>
450
+ <track>
451
+ <title>Track 1</title>
452
+ <forbidden_countries>
453
+ <country>DE</country>
454
+ </forbidden_countries>
455
+ </track>
456
+ <track>
457
+ <title>Track 2</title>
458
+ <forbidden_countries>
459
+ <country>AT</country>
460
+ </forbidden_countries>
461
+ </track>
462
+ </tracks>
463
+ </album>
464
+ )
465
+ @clazz.many "tracks/track" => :tracks do
466
+ text :title
467
+ not_exists "forbidden_countries/country[text()='DE']" => :allows_streaming
468
+ end
469
+ @clazz.attributes_from_xml(xml)[:tracks].should == [
470
+ { :title => "Track 1", :allows_streaming => false },
471
+ { :title => "Track 2", :allows_streaming => true },
472
+ ]
473
+ end
474
+
366
475
  describe "#within" do
367
476
  it "adds the within xpath to all xpath mappings" do
368
477
  @clazz.within("artist") do
@@ -511,5 +620,75 @@ describe "XmlMapper" do
511
620
  end
512
621
  end
513
622
 
623
+
624
+ describe "using node_name, inner_text and attribute" do
625
+ it "extracts attributes for a many relationship when node_name and inner_text is used" do
626
+ xml = %(
627
+ <album>
628
+ <Contributors>
629
+ <Performer>Sexy sushi</Performer>
630
+ <Composer>Sexi Sushi</Composer>
631
+ <Author>Sexi Sushi</Author>
632
+ </Contributors>
633
+ </album>
634
+ )
635
+ @clazz.many "Contributors/*" => :contributions do
636
+ node_name :contribution_type
637
+ inner_text :name
638
+ end
639
+ @clazz.attributes_from_xml(xml).should == {
640
+ :contributions => [
641
+ { :contribution_type => "Performer", :name => "Sexy sushi" },
642
+ { :contribution_type => "Composer", :name => "Sexi Sushi" },
643
+ { :contribution_type => "Author", :name => "Sexi Sushi" },
644
+ ]
645
+ }
646
+ end
647
+
648
+ it "extracts the correct attributes when attribute keyword is used in 'many'-mapping" do
649
+ xml = %(
650
+ <album>
651
+ <tracks>
652
+ <track some_code="1234">
653
+ <title>Track 1</title>
654
+ </track>
655
+ </tracks>
656
+ </album>
657
+ )
658
+ @clazz.many "tracks/track" => :tracks do
659
+ attribute "some_code" => :isrc
660
+ text :title
661
+ end
662
+ @clazz.attributes_from_xml(xml).should == {
663
+ :tracks => [
664
+ { :isrc => "1234", :title => "Track 1" }
665
+ ]
666
+ }
667
+ end
668
+
669
+ it "extracts the correct attributes when attribute is used in within-mapping" do
670
+ xml = %(
671
+ <album>
672
+ <meta code="1234">
673
+ <title>Product Title</title>
674
+ </meta>
675
+ </></album>
676
+ )
677
+ @clazz.within "meta" do
678
+ attribute "code" => :upc
679
+ end
680
+ @clazz.attributes_from_xml(xml)[:upc].should == "1234"
681
+ end
682
+
683
+ it "extracts the correct attributes when attribute is used in root mapping" do
684
+ xml = %(
685
+ <album code="1234">
686
+ <title>Product Title</title>
687
+ </></album>
688
+ )
689
+ @clazz.attribute :code => :upc
690
+ @clazz.attributes_from_xml(xml)[:upc].should == "1234"
691
+ end
692
+ end
514
693
  end
515
694
  end
data/xml_mapper.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{xml_mapper}
8
- s.version = "0.3.3"
8
+ s.version = "0.4.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Tobias Schwab"]
12
- s.date = %q{2010-11-29}
12
+ s.date = %q{2010-12-01}
13
13
  s.description = %q{Declarative XML to Ruby mapping}
14
14
  s.email = %q{tobias.schwab@dynport.de}
15
15
  s.extra_rdoc_files = [
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xml_mapper
3
3
  version: !ruby/object:Gem::Version
4
- hash: 21
4
+ hash: 15
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 3
9
- - 3
10
- version: 0.3.3
8
+ - 4
9
+ - 0
10
+ version: 0.4.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Tobias Schwab
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-11-29 00:00:00 +01:00
18
+ date: 2010-12-01 00:00:00 +01:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency