xml_mapper 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ -d
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use ree@xml_mapper
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ gem 'nokogiri'
7
+ # Add dependencies to develop your gem here.
8
+ # Include everything needed to run rake, tests, features, etc.
9
+ group :development do
10
+ gem 'autotest'
11
+ gem 'autotest-growl'
12
+ gem "rspec", "~> 2.1.0"
13
+ gem 'ruby-debug'
14
+ gem "cucumber", ">= 0"
15
+ gem "bundler", "~> 1.0.0"
16
+ gem "jeweler", "~> 1.5.1"
17
+ gem "rcov", ">= 0"
18
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,55 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ autotest (4.4.5)
5
+ autotest-growl (0.2.6)
6
+ builder (2.1.2)
7
+ columnize (0.3.1)
8
+ cucumber (0.9.2)
9
+ builder (~> 2.1.2)
10
+ diff-lcs (~> 1.1.2)
11
+ gherkin (~> 2.2.5)
12
+ json (~> 1.4.6)
13
+ term-ansicolor (~> 1.0.5)
14
+ diff-lcs (1.1.2)
15
+ gherkin (2.2.9)
16
+ json (~> 1.4.6)
17
+ term-ansicolor (~> 1.0.5)
18
+ git (1.2.5)
19
+ jeweler (1.5.1)
20
+ bundler (~> 1.0.0)
21
+ git (>= 1.2.5)
22
+ rake
23
+ json (1.4.6)
24
+ linecache (0.43)
25
+ nokogiri (1.4.3.1)
26
+ rake (0.8.7)
27
+ rcov (0.9.9)
28
+ rspec (2.1.0)
29
+ rspec-core (~> 2.1.0)
30
+ rspec-expectations (~> 2.1.0)
31
+ rspec-mocks (~> 2.1.0)
32
+ rspec-core (2.1.0)
33
+ rspec-expectations (2.1.0)
34
+ diff-lcs (~> 1.1.2)
35
+ rspec-mocks (2.1.0)
36
+ ruby-debug (0.10.3)
37
+ columnize (>= 0.1)
38
+ ruby-debug-base (~> 0.10.3.0)
39
+ ruby-debug-base (0.10.3)
40
+ linecache (>= 0.3)
41
+ term-ansicolor (1.0.5)
42
+
43
+ PLATFORMS
44
+ ruby
45
+
46
+ DEPENDENCIES
47
+ autotest
48
+ autotest-growl
49
+ bundler (~> 1.0.0)
50
+ cucumber
51
+ jeweler (~> 1.5.1)
52
+ nokogiri
53
+ rcov
54
+ rspec (~> 2.1.0)
55
+ ruby-debug
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Tobias Schwab
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,118 @@
1
+ = xml_mapper
2
+
3
+ Declarative mapping of XML files to ruby attributes
4
+
5
+ == Example XML
6
+
7
+ <album>
8
+ <title>Black on Both Sides</title>
9
+ <version_title>Extended Edition</version_title>
10
+ <released_in>1999</released_in>
11
+ <released_on>1999-10-12</released_on>
12
+ <country>de</country>
13
+ <allows_streaming>true</allows_streaming>
14
+ <artist>
15
+ <name>Mos Def</name>
16
+ <id>1212</id>
17
+ </artist>
18
+ <tracks>
19
+ <track>
20
+ <title>Fear Not of Man</title>
21
+ <number>1</number>
22
+ <disk>1</number>
23
+ <explicit_lyrics />
24
+ </track>
25
+ <track>
26
+ <title>Hip Hop</title>
27
+ <number>2</number>
28
+ <disk>1</number>
29
+ </track>
30
+ </tracks>
31
+ </album>
32
+
33
+ == Example Mapper
34
+
35
+ require "xml_mapper"
36
+ require "date"
37
+
38
+ class MyMapper < XmlMapper
39
+ text :title, :version_title # 1:1 - maps xpaths title and version_title to attribute keys
40
+ integer :released_in # converts value to an integer
41
+ text :country, :after_map => :upcase # calls after_map method on extracted value if value responds to the method
42
+ text :released_on, :after_map => :parse_date # calls after_map method defined in Mapper class when value does not respond
43
+ boolean :allows_streaming # maps Y, y, yes, true => true, N, n, no, false => false
44
+
45
+ within :artist do
46
+ text :name => :artist_name # adds mapping for xpath "artist/name" to :artist_name attribute
47
+ integer :id => :artist_id
48
+ end
49
+
50
+ many "tracks/track" => :tracks do # maps xpath "tracks/track" to array with key :tracks
51
+ text :title => :track_title
52
+ integer :number => :track_number
53
+ integer :disk => :disk_number
54
+ exists :explicit_lyrics # checks if a node with the xpath exists
55
+ end
56
+
57
+ after_map do # is called after attributes are extracted, self references the extracted attributes
58
+ self[:tracks_count] = self[:tracks].length
59
+ end
60
+
61
+ # to be used for after_map callbacks
62
+ def parse_date(date_string)
63
+ Date.parse(date_string)
64
+ end
65
+ end
66
+
67
+
68
+ == How to call
69
+
70
+ * MyMapper.attributes_from_xml(xml)
71
+ * MyMapper.attributes_from_xml_path(xml) (also sets :xml_path)
72
+
73
+ == Output
74
+
75
+ { :title=>"Black on Both Sides", :allows_streaming=>true, :version_title=>"Extended Edition", :tracks_count=>2,
76
+ :xml_path=>"/Users/tobias/Projects/xml_mapper/spec/fixtures/base.xml",
77
+ :tracks=>[
78
+ {:disk_number=>1, :explicit_lyrics=>true, :track_title=>"Fear Not of Man", :track_number=>1},
79
+ {:disk_number=>1, :explicit_lyrics=>true, :track_title=>"Hip Hop", :track_number=>2}
80
+ ],
81
+ :released_in=>1999, :released_on=>#<Date: 4902927/2,0,2299161>, :artist_name=>"Mos Def", :artist_id=>1212, :country=>"DE"
82
+ }
83
+
84
+ == Contributing to xml_mapper
85
+
86
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
87
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
88
+ * Fork the project
89
+ * Start a feature/bugfix branch
90
+ * Commit and push until you are happy with your contribution
91
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
92
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
93
+
94
+
95
+
96
+ == Copyright
97
+
98
+ Copyright (c) 2010 Tobias Schwab (Dynport GmbH).
99
+
100
+ Permission is hereby granted, free of charge, to any person obtaining
101
+ a copy of this software and associated documentation files (the
102
+ "Software"), to deal in the Software without restriction, including
103
+ without limitation the rights to use, copy, modify, merge, publish,
104
+ distribute, sublicense, and/or sell copies of the Software, and to
105
+ permit persons to whom the Software is furnished to do so, subject to
106
+ the following conditions:
107
+
108
+ The above copyright notice and this permission notice shall be
109
+ included in all copies or substantial portions of the Software.
110
+
111
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
112
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
113
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
114
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
115
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
116
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
117
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
118
+
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "xml_mapper"
16
+ gem.homepage = "http://github.com/tobstarr/xml_mapper"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{Declarative and clever XML to Ruby Mapping}
19
+ gem.description = %Q{Just check out the examples}
20
+ gem.email = "tobias.schwab@dynport.de"
21
+ gem.authors = ["Tobias Schwab"]
22
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
23
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
24
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
25
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
26
+ end
27
+ Jeweler::RubygemsDotOrgTasks.new
28
+
29
+ require 'rspec/core'
30
+ require 'rspec/core/rake_task'
31
+ RSpec::Core::RakeTask.new(:spec) do |spec|
32
+ spec.pattern = FileList['spec/**/*_spec.rb']
33
+ end
34
+
35
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
36
+ spec.pattern = 'spec/**/*_spec.rb'
37
+ spec.rcov = true
38
+ end
39
+
40
+ require 'cucumber/rake/task'
41
+ Cucumber::Rake::Task.new(:features)
42
+
43
+ task :default => :spec
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "xml_mapper #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.2
@@ -0,0 +1 @@
1
+ Autotest.add_discovery { "rspec2" }
File without changes
@@ -0,0 +1,13 @@
1
+ require 'bundler'
2
+ begin
3
+ Bundler.setup(:default, :development)
4
+ rescue Bundler::BundlerError => e
5
+ $stderr.puts e.message
6
+ $stderr.puts "Run `bundle install` to install missing gems"
7
+ exit e.status_code
8
+ end
9
+
10
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
11
+ require 'xml_mapper'
12
+
13
+ require 'rspec/expectations'
@@ -0,0 +1,9 @@
1
+ Feature: something something
2
+ In order to something something
3
+ A user something something
4
+ something something something
5
+
6
+ Scenario: something something
7
+ Given inspiration
8
+ When I create a sweet new gem
9
+ Then everyone should see how awesome I am
data/lib/xml_mapper.rb ADDED
@@ -0,0 +1,175 @@
1
+ require "nokogiri"
2
+
3
+ class XmlMapper
4
+ attr_accessor :mappings, :after_map_block, :within_xpath
5
+
6
+ class << self
7
+ attr_accessor :mapper
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)
27
+ end
28
+
29
+ def within(xpath, &block)
30
+ self.mapper.within_xpath ||= []
31
+ self.mapper.within_xpath << xpath
32
+ self.instance_eval(&block)
33
+ self.mapper.within_xpath.pop
34
+ end
35
+
36
+ def after_map(&block)
37
+ mapper.after_map(&block)
38
+ end
39
+
40
+ def many(*args, &block)
41
+ sub_mapper = block_given? ? capture_submapping(&block) : nil
42
+ add_mapper_to_args(args, sub_mapper)
43
+ mapper.add_mapping(:many, *args)
44
+ end
45
+
46
+ def attributes_from_xml(xml)
47
+ attributes_from_superclass(xml, :attributes_from_xml).merge(mapper.attributes_from_xml(xml))
48
+ end
49
+
50
+ def attributes_from_superclass(xml, method = :attributes_from_xml)
51
+ if self.superclass && self.superclass.respond_to?(:mapper)
52
+ attributes = self.superclass.mapper.send(method, xml)
53
+ attributes.delete(:xml_path)
54
+ attributes
55
+ else
56
+ {}
57
+ end
58
+ end
59
+
60
+ def attributes_from_xml_path(path)
61
+ attributes_from_superclass(path, :attributes_from_xml_path).merge(mapper.attributes_from_xml_path(path))
62
+ end
63
+
64
+ def capture_submapping(&block)
65
+ saved_mapper = self.mapper
66
+ self.mapper = XmlMapper.new
67
+ self.instance_eval(&block)
68
+ captured_mapper = self.mapper
69
+ self.mapper = saved_mapper
70
+ captured_mapper
71
+ end
72
+
73
+ def add_mapper_to_args(args, mapper)
74
+ if args.length > 1 && args.last.is_a?(Hash)
75
+ args.last[:mapper] = mapper
76
+ else
77
+ args << { :mapper => mapper }
78
+ end
79
+ end
80
+
81
+ def mapper
82
+ @mapper ||= self.new
83
+ end
84
+ end
85
+
86
+ def initialize
87
+ self.mappings = []
88
+ end
89
+
90
+ def extract_options_from_args(args)
91
+ args.length > 1 && args.last.is_a?(Hash) ? args.pop : {}
92
+ end
93
+
94
+ def add_mapping(type, *args)
95
+ options = extract_options_from_args(args)
96
+ if args.first.is_a?(Hash)
97
+ args.first.map { |xpath, key| add_single_mapping(type, xpath, key, options) }
98
+ else
99
+ args.map { |arg| add_single_mapping(type, arg, arg, options) }
100
+ end
101
+ end
102
+
103
+ def after_map(&block)
104
+ self.after_map_block = block
105
+ end
106
+
107
+ def add_single_mapping(type, xpath, key, options = {})
108
+ self.mappings << { :type => type, :xpath => add_with_to_xpath(xpath), :key => key, :options => options }
109
+ end
110
+
111
+ def add_with_to_xpath(xpath)
112
+ [self.within_xpath, xpath].flatten.compact.join("/")
113
+ end
114
+
115
+ def attributes_from_xml_path(path)
116
+ attributes_from_xml(File.read(path), path)
117
+ end
118
+
119
+ TYPE_TO_AFTER_CODE = {
120
+ :integer => :to_i,
121
+ :boolean => :string_to_boolean
122
+ }
123
+
124
+ def attributes_from_xml(xml_or_doc, xml_path = nil)
125
+ if xml_or_doc.is_a?(Array)
126
+ xml_or_doc.map { |doc| attributes_from_xml(doc) }
127
+ else
128
+ doc = xml_or_doc.is_a?(Nokogiri::XML::Node) ? xml_or_doc : Nokogiri::XML(xml_or_doc)
129
+ atts = self.mappings.inject(xml_path.nil? ? {} : { :xml_path => xml_path }) do |hash, mapping|
130
+ hash.merge(mapping[:key] => value_from_doc_and_mapping(doc, mapping))
131
+ end
132
+ atts.instance_eval(&self.after_map_block) if self.after_map_block
133
+ atts
134
+ end
135
+ end
136
+
137
+ def value_from_doc_and_mapping(doc, mapping)
138
+ if mapping[:type] == :many
139
+ mapping[:options][:mapper].attributes_from_xml(doc.search(mapping[:xpath]).to_a)
140
+ elsif mapping[:type] == :exists
141
+ !doc.at("//#{mapping[:xpath]}").nil?
142
+ 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)
145
+ end
146
+ end
147
+
148
+ 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)
154
+ end
155
+ value
156
+ end
157
+
158
+ def inner_text_for_xpath(doc, xpath)
159
+ if node = doc.at(xpath)
160
+ node.inner_text
161
+ end
162
+ end
163
+
164
+ MAPPINGS = {
165
+ "true" => true,
166
+ "false" => false,
167
+ "yes" => true,
168
+ "y" => true,
169
+ "n" => false
170
+ }
171
+
172
+ def string_to_boolean(value)
173
+ MAPPINGS[value.to_s.downcase]
174
+ end
175
+ end
@@ -0,0 +1,32 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + "/my_mapper")
4
+
5
+ describe "ExampleSpec" do
6
+ describe "mapping spec/fixtures/base.xml" do
7
+ before(:all) do
8
+ @attributes = MyMapper.attributes_from_xml_path(File.expand_path(File.dirname(__FILE__) + '/fixtures/base.xml'))
9
+ end
10
+
11
+ {
12
+ :title => "Black on Both Sides", :version_title => "Extended Edition", :released_in => 1999,
13
+ :artist_name => "Mos Def", :artist_id => 1212, :country => "DE", :allows_streaming => true,
14
+ :tracks_count => 2, :released_on => Date.new(1999, 10, 12)
15
+ }.each do |key, value|
16
+ it "extracts #{value.inspect} as #{key}" do
17
+ @attributes[key].should == value
18
+ end
19
+ end
20
+
21
+ [
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 },
24
+ ].each_with_index do |hash, offset|
25
+ hash.each do |key, value|
26
+ it "extracts #{value.inspect} for #{key} for track with offset #{offset}" do
27
+ @attributes[:tracks][offset][key].should == value
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ <album>
2
+ <title>Black on Both Sides</title>
3
+ <version_title>Extended Edition</version_title>
4
+ <released_in>1999</released_in>
5
+ <released_on>1999-10-12</released_on>
6
+ <country>de</country>
7
+ <allows_streaming>true</allows_streaming>
8
+ <artist>
9
+ <name>Mos Def</name>
10
+ <id>1212</id>
11
+ </artist>
12
+ <tracks>
13
+ <track>
14
+ <title>Fear Not of Man</title>
15
+ <number>1</number>
16
+ <disk>1</number>
17
+ <explicit_lyrics />
18
+ </track>
19
+ <track>
20
+ <title>Hip Hop</title>
21
+ <number>2</number>
22
+ <disk>1</number>
23
+ </track>
24
+ </tracks>
25
+ </album>
data/spec/my_mapper.rb ADDED
@@ -0,0 +1,31 @@
1
+ require "xml_mapper"
2
+ require "date"
3
+
4
+ class MyMapper < XmlMapper
5
+ text :title, :version_title # 1:1 - maps xpaths title and version_title to attribute keys
6
+ integer :released_in # converts value to an integer
7
+ text :country, :after_map => :upcase # calls after_map method on extracted value if value responds to the method
8
+ text :released_on, :after_map => :parse_date # calls after_map method defined in Mapper class when value does not respond
9
+ boolean :allows_streaming # maps Y, y, yes, true => true, N, n, no, false => false
10
+
11
+ within :artist do
12
+ text :name => :artist_name # adds mapping for xpath "artist/name"
13
+ integer :id => :artist_id
14
+ end
15
+
16
+ many "tracks/track" => :tracks do # maps xpath "tracks/track" to array with key :tracks
17
+ text :title => :track_title
18
+ integer :number => :track_number
19
+ integer :disk => :disk_number
20
+ exists :explicit_lyrics # checks if a node with the xpath exists
21
+ end
22
+
23
+ after_map do # is called after attributes are extracted, self references the extracted attributes
24
+ self[:tracks_count] = self[:tracks].length
25
+ end
26
+
27
+ # to be used for after_map callbacks
28
+ def parse_date(date_string)
29
+ Date.parse(date_string)
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'xml_mapper'
5
+
6
+ # Requires supporting files with custom matchers and macros, etc,
7
+ # in ./support/ and its subdirectories.
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
+
10
+ RSpec.configure do |config|
11
+
12
+ end
13
+
14
+ if defined?(Debugger) && Debugger.respond_to?(:settings)
15
+ Debugger.settings[:autolist] = 1
16
+ Debugger.settings[:autoeval] = true
17
+ end