dozuki-mapper 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +13 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/README.markdown +132 -0
- data/Rakefile +20 -0
- data/dozuki-mapper.gemspec +28 -0
- data/features/date_mapping.feature +25 -0
- data/features/each_mapping.feature +30 -0
- data/features/float_mapping.feature +25 -0
- data/features/int_mapping.feature +25 -0
- data/features/node_mapping.feature +26 -0
- data/features/step_definitions/date_steps.rb +20 -0
- data/features/step_definitions/each_steps.rb +38 -0
- data/features/step_definitions/float_steps.rb +19 -0
- data/features/step_definitions/int_steps.rb +20 -0
- data/features/step_definitions/node_steps.rb +37 -0
- data/features/step_definitions/string_steps.rb +23 -0
- data/features/string_mapping.feature +25 -0
- data/features/support/env.rb +1 -0
- data/lib/dozuki-mapper/proxy.rb +39 -0
- data/lib/dozuki-mapper/version.rb +5 -0
- data/lib/dozuki-mapper.rb +25 -0
- data/spec/dozuki-mapper/proxy_spec.rb +133 -0
- data/spec/dozuki-mapper_spec.rb +50 -0
- data/spec/spec_helper.rb +1 -0
- metadata +152 -0
data/.autotest
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Autotest.add_hook :initialize do |at|
|
2
|
+
root = File.dirname(__FILE__)
|
3
|
+
at.add_mapping(%r%^lib/(.*)\.rb$%) { |_, m|
|
4
|
+
["spec/#{m[1]}_spec.rb"]
|
5
|
+
}
|
6
|
+
at.add_mapping(%r%^spec/.*_spec\.rb$%) { |filename|
|
7
|
+
filename
|
8
|
+
}
|
9
|
+
at.add_mapping(%r%^spec/support/.*\.rb$%) { |_|
|
10
|
+
Dir[File.join(root, 'spec/**/*_spec.rb')]
|
11
|
+
}
|
12
|
+
nil
|
13
|
+
end
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
# Dozuki Mapper
|
2
|
+
|
3
|
+
A simple DSL for mapping xml documents directly to user-defined classes.
|
4
|
+
Ideal for API parsing.
|
5
|
+
|
6
|
+
## What does it do?
|
7
|
+
|
8
|
+
You've got an XML API and you want to model the output in Ruby objects,
|
9
|
+
parse values to primitives and build a structure. You don't want to write lots of code or XPaths or repeat yourself.
|
10
|
+
Dozuki-mapper, built on the Dozuki gem, lets you define the mapping
|
11
|
+
between the XML and the objects, but does little more, meaning you
|
12
|
+
have control over the definition of your class. It is currently only
|
13
|
+
suited to reasonably direct mappings as it's being used to model API
|
14
|
+
responses.
|
15
|
+
|
16
|
+
Features can be found in the [features
|
17
|
+
folder](https://github.com/jamesalmond/dozuki-mapper/tree/master/features).
|
18
|
+
|
19
|
+
It's built using the [dozuki gem](https://github.com/jamesalmond/dozuki)
|
20
|
+
|
21
|
+
# How do I use it?
|
22
|
+
|
23
|
+
xml = %Q{
|
24
|
+
<person>
|
25
|
+
<name>John Smith</name>
|
26
|
+
<address>1 Main Street</address>
|
27
|
+
<post_code>NW1 3ED</post_code>
|
28
|
+
</person>
|
29
|
+
}
|
30
|
+
|
31
|
+
class Person
|
32
|
+
include Dozuki::Mapper
|
33
|
+
|
34
|
+
attr_accessor :name, :address, :post_code
|
35
|
+
|
36
|
+
map_with do |map|
|
37
|
+
map.string :name
|
38
|
+
map.string :address
|
39
|
+
map.string :post_code
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
doc = Dozuki::XML.parse(xml)
|
45
|
+
|
46
|
+
person = Person.from_node(doc.get('./person')
|
47
|
+
|
48
|
+
Setting the relevant fields on the person object. More conversions (e.g.
|
49
|
+
integer, float, date) to come.
|
50
|
+
|
51
|
+
## Mapping data types
|
52
|
+
|
53
|
+
* [String](https://github.com/jamesalmond/dozuki-mapper/tree/master/features/string_mapping.feature)
|
54
|
+
* [Float](https://github.com/jamesalmond/dozuki-mapper/tree/master/features/float_mapping.feature)
|
55
|
+
* [Int](https://github.com/jamesalmond/dozuki-mapper/tree/master/features/int_mapping.feature)
|
56
|
+
* [Date](https://github.com/jamesalmond/dozuki-mapper/tree/master/features/date_mapping.feature)
|
57
|
+
|
58
|
+
## Mapping object hierarchies
|
59
|
+
|
60
|
+
|
61
|
+
* [Mapping nodes](https://github.com/jamesalmond/dozuki-mapper/tree/master/features/node_mapping.feature)
|
62
|
+
|
63
|
+
Example:
|
64
|
+
|
65
|
+
xml = %Q{
|
66
|
+
<plot>
|
67
|
+
<name>Tiny Plot</name>
|
68
|
+
<dimensions>
|
69
|
+
<width>100</width>
|
70
|
+
<depth>200</depth>
|
71
|
+
</dimensions>
|
72
|
+
</plot>
|
73
|
+
}
|
74
|
+
|
75
|
+
class Plot
|
76
|
+
include Dozuki::Mapper
|
77
|
+
attr_accessor :name, :dimensions
|
78
|
+
map_with do |map|
|
79
|
+
map.string :name
|
80
|
+
map.node :dimensions, :as => Dimensions
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class Dimensions
|
85
|
+
include Dozuki::Mapper
|
86
|
+
attr_accessor :width, :height
|
87
|
+
map_with do |map|
|
88
|
+
map.int :width
|
89
|
+
map.int :height
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
## Mapping collections
|
95
|
+
|
96
|
+
* [Mapping each](https://github.com/jamesalmond/dozuki-mapper/tree/master/features/each_mapping.feature)
|
97
|
+
|
98
|
+
|
99
|
+
## Contributing to Dozuki Mapper
|
100
|
+
|
101
|
+
* Fork the project.
|
102
|
+
* Add tests that cover the new feature or bug fix.
|
103
|
+
* Make your feature addition or bug fix.
|
104
|
+
* Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself in another branch so I can ignore when I pull)
|
105
|
+
* Send me a pull request. Bonus points for topic branches.
|
106
|
+
|
107
|
+
## LICENSE:
|
108
|
+
|
109
|
+
(The MIT License)
|
110
|
+
|
111
|
+
Copyright (c) 2011:
|
112
|
+
|
113
|
+
* [James Almond](http://jamesalmond.com)
|
114
|
+
|
115
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
116
|
+
a copy of this software and associated documentation files (the
|
117
|
+
'Software'), to deal in the Software without restriction, including
|
118
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
119
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
120
|
+
permit persons to whom the Software is furnished to do so, subject to
|
121
|
+
the following conditions:
|
122
|
+
|
123
|
+
The above copyright notice and this permission notice shall be
|
124
|
+
included in all copies or substantial portions of the Software.
|
125
|
+
|
126
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
127
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
128
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
129
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
130
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
131
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
132
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rspec'
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
require 'cucumber'
|
7
|
+
require 'cucumber/rake/task'
|
8
|
+
|
9
|
+
|
10
|
+
|
11
|
+
desc "Run specs"
|
12
|
+
RSpec::Core::RakeTask.new :spec
|
13
|
+
|
14
|
+
desc "Run integrations"
|
15
|
+
Cucumber::Rake::Task.new(:features) do |t|
|
16
|
+
t.cucumber_opts = "features --format pretty"
|
17
|
+
end
|
18
|
+
task :default => [:spec, :features]
|
19
|
+
|
20
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "dozuki-mapper/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "dozuki-mapper"
|
7
|
+
s.version = Dozuki::Mapper::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["James Almond"]
|
10
|
+
s.email = ["james@jamesalmond.com"]
|
11
|
+
s.homepage = "https://github.com/jamesalmond/dozuki-mapper"
|
12
|
+
s.summary = %q{A DSL for mapping API output to objects}
|
13
|
+
s.description = %q{A DSL for mapping API output to objects}
|
14
|
+
|
15
|
+
s.rubyforge_project = "dozuki-mapper"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {spec,features}/* .autotest`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.add_dependency("dozuki")
|
23
|
+
|
24
|
+
s.add_development_dependency("rspec")
|
25
|
+
s.add_development_dependency("cucumber")
|
26
|
+
s.add_development_dependency("autotest")
|
27
|
+
s.add_development_dependency("rake")
|
28
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
Feature: mapping a document with date elements to an object
|
2
|
+
So that I don't have to spend all my time typing in repetitive XPaths
|
3
|
+
As an API consumer
|
4
|
+
I want to map a document to an object
|
5
|
+
|
6
|
+
|
7
|
+
Scenario: I have a simple mapping
|
8
|
+
Given I have an event with start_date, end_date and sold_out
|
9
|
+
And I have the XML:
|
10
|
+
"""
|
11
|
+
<event>
|
12
|
+
<start_date>2011-02-01</start_date>
|
13
|
+
<end_date>2011-05-13</end_date>
|
14
|
+
<sold_out>2010-12-25</sold_out>
|
15
|
+
</event>
|
16
|
+
"""
|
17
|
+
When I map the event node to an event object with:
|
18
|
+
"""
|
19
|
+
map.date :start_date
|
20
|
+
map.date :end_date
|
21
|
+
map.date :sold_out
|
22
|
+
"""
|
23
|
+
Then the event should have a start_date of 01/2/2011
|
24
|
+
And the event should have a end_date of 13/5/2011
|
25
|
+
And the event should have a sold_out of 25/12/2010
|
@@ -0,0 +1,30 @@
|
|
1
|
+
Feature: mapping a node's children to to another object
|
2
|
+
So that I don't have to spend all my time typing in repetitive XPaths
|
3
|
+
As an API consumer
|
4
|
+
I want to map a node to a series of other objects
|
5
|
+
|
6
|
+
Scenario: I have a simple mapping
|
7
|
+
Given I have car with brand and colours
|
8
|
+
And I have a colour class with description and hex and a from_node method
|
9
|
+
And I have the XML:
|
10
|
+
"""
|
11
|
+
<car>
|
12
|
+
<brand>Fawd</brand>
|
13
|
+
<colour>
|
14
|
+
<description>Red</description>
|
15
|
+
<hex>#FF0000</hex>
|
16
|
+
</colour>
|
17
|
+
<colour>
|
18
|
+
<description>Blue</description>
|
19
|
+
<hex>#0000FF</hex>
|
20
|
+
</colour>
|
21
|
+
</car>
|
22
|
+
"""
|
23
|
+
When I map the car node to a car object with:
|
24
|
+
"""
|
25
|
+
map.string :brand
|
26
|
+
map.each :colour, :as => Colour, :to => :colours
|
27
|
+
"""
|
28
|
+
Then the car should have a brand of "Fawd"
|
29
|
+
And the car should have a colour with a description of "Red" and a hex of "#FF0000"
|
30
|
+
And the car should have a colour with a description of "Blue" and a hex of "#0000FF"
|
@@ -0,0 +1,25 @@
|
|
1
|
+
Feature: mapping a document with float elements to an object
|
2
|
+
So that I don't have to spend all my time typing in repetitive XPaths
|
3
|
+
As an API consumer
|
4
|
+
I want to map a document to an object
|
5
|
+
|
6
|
+
|
7
|
+
Scenario: I have a simple mapping
|
8
|
+
Given I have an item with max_price, min_price and average_price
|
9
|
+
And I have the XML:
|
10
|
+
"""
|
11
|
+
<item>
|
12
|
+
<max_price>20.43</max_price>
|
13
|
+
<min_price>9.50</min_price>
|
14
|
+
<average_price>99</average_price>
|
15
|
+
</item>
|
16
|
+
"""
|
17
|
+
When I map the item node to an item object with:
|
18
|
+
"""
|
19
|
+
map.float :max_price
|
20
|
+
map.float :min_price
|
21
|
+
map.float :average_price
|
22
|
+
"""
|
23
|
+
Then the event should have a max_price of 20.43
|
24
|
+
And the event should have a min_price of 9.50
|
25
|
+
And the event should have a average_price of 99.0
|
@@ -0,0 +1,25 @@
|
|
1
|
+
Feature: mapping a document with int elements to an object
|
2
|
+
So that I don't have to spend all my time typing in repetitive XPaths
|
3
|
+
As an API consumer
|
4
|
+
I want to map a document to an object
|
5
|
+
|
6
|
+
|
7
|
+
Scenario: I have a simple mapping
|
8
|
+
Given I have a shop with items, staff and suppliers
|
9
|
+
And I have the XML:
|
10
|
+
"""
|
11
|
+
<shop>
|
12
|
+
<items>300</items>
|
13
|
+
<staff>1</staff>
|
14
|
+
<suppliers>43</suppliers>
|
15
|
+
</shop>
|
16
|
+
"""
|
17
|
+
When I map the shop node to a shop object with:
|
18
|
+
"""
|
19
|
+
map.int :items
|
20
|
+
map.int :staff
|
21
|
+
map.int :suppliers
|
22
|
+
"""
|
23
|
+
Then the shop should have 1 staff
|
24
|
+
And the shop should have 300 items
|
25
|
+
And the shop should have 43 suppliers
|
@@ -0,0 +1,26 @@
|
|
1
|
+
Feature: mapping a node to to another object
|
2
|
+
So that I don't have to spend all my time typing in repetitive XPaths
|
3
|
+
As an API consumer
|
4
|
+
I want to map a node to another object
|
5
|
+
|
6
|
+
|
7
|
+
Scenario: I have a simple mapping
|
8
|
+
Given I have plot with name and dimensions
|
9
|
+
And I have a dimensions class with width, depth and a from_node class method
|
10
|
+
And I have the XML:
|
11
|
+
"""
|
12
|
+
<plot>
|
13
|
+
<name>Tiny Plot</name>
|
14
|
+
<dimensions>
|
15
|
+
<width>100</width>
|
16
|
+
<depth>200</depth>
|
17
|
+
</dimensions>
|
18
|
+
</plot>
|
19
|
+
"""
|
20
|
+
When I map the plot node to a plot object with:
|
21
|
+
"""
|
22
|
+
map.string :name
|
23
|
+
map.node :dimensions, :as => Dimensions
|
24
|
+
"""
|
25
|
+
Then the plot should have a name of "Tiny Plot"
|
26
|
+
And the plot should have dimensions with a width of 100 and a depth of 200
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Given /^I have an event with start_date, end_date and sold_out$/ do
|
2
|
+
class Event
|
3
|
+
include Dozuki::Mapper
|
4
|
+
attr_accessor :start_date, :end_date, :sold_out
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
When /^I map the event node to an event object with:$/ do |string|
|
9
|
+
Event.instance_eval %Q{
|
10
|
+
map_with do |map|
|
11
|
+
#{string}
|
12
|
+
end
|
13
|
+
}
|
14
|
+
@event = Event.from_node(@doc.get('/event'))
|
15
|
+
end
|
16
|
+
|
17
|
+
Then /^the event should have a (.*) of (\d+)\/(\d+)\/(\d+)$/ do |field, day, month, year|
|
18
|
+
@event.send(field).should == Date.civil(year.to_i, month.to_i, day.to_i)
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
Given /^I have car with brand and colours$/ do
|
2
|
+
class Car
|
3
|
+
include Dozuki::Mapper
|
4
|
+
attr_accessor :brand, :colours
|
5
|
+
def initialize
|
6
|
+
self.colours = []
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
Given /^I have a colour class with description and hex and a from_node method$/ do
|
12
|
+
class Colour
|
13
|
+
include Dozuki::Mapper
|
14
|
+
attr_accessor :description, :hex
|
15
|
+
map_with do |map|
|
16
|
+
map.string :description
|
17
|
+
map.string :hex
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
When /^I map the car node to a car object with:$/ do |string|
|
23
|
+
Car.instance_eval %Q{
|
24
|
+
map_with do |map|
|
25
|
+
#{string}
|
26
|
+
end
|
27
|
+
}
|
28
|
+
@car = Car.from_node(@doc.get('/car'))
|
29
|
+
end
|
30
|
+
|
31
|
+
Then /^the car should have a brand of "([^"]*)"$/ do |brand|
|
32
|
+
@car.brand.should == brand
|
33
|
+
end
|
34
|
+
|
35
|
+
Then /^the car should have a colour with a description of "([^"]*)" and a hex of "([^"]*)"$/ do |description, hex|
|
36
|
+
@car.colours.any?{|car| car.description == description && car.hex == hex}.should be_true
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
Given /^I have an item with max_price, min_price and average_price$/ do
|
2
|
+
class Item
|
3
|
+
include Dozuki::Mapper
|
4
|
+
attr_accessor :max_price, :min_price, :average_price
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
When /^I map the item node to an item object with:$/ do |string|
|
9
|
+
Item.instance_eval %Q{
|
10
|
+
map_with do |map|
|
11
|
+
#{string}
|
12
|
+
end
|
13
|
+
}
|
14
|
+
@item = Item.from_node(@doc.get('/item'))
|
15
|
+
end
|
16
|
+
|
17
|
+
Then /^the event should have a (.*) of (\d+\.\d+)$/ do |field, float|
|
18
|
+
@item.send(field).should == float.to_f
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Given /^I have a shop with items, staff and suppliers$/ do
|
2
|
+
class Shop
|
3
|
+
include Dozuki::Mapper
|
4
|
+
attr_accessor :staff, :items, :suppliers
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
Then /^the shop should have (\d+) (.*)$/ do |count, field|
|
9
|
+
@shop.send(field).should == count.to_i
|
10
|
+
end
|
11
|
+
|
12
|
+
When /^I map the shop node to a shop object with:$/ do |string|
|
13
|
+
Shop.instance_eval %Q{
|
14
|
+
map_with do |map|
|
15
|
+
#{string}
|
16
|
+
end
|
17
|
+
}
|
18
|
+
@shop = Shop.from_node(@doc.get('/shop'))
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
Given /^I have plot with name and dimensions$/ do
|
2
|
+
class Plot
|
3
|
+
include Dozuki::Mapper
|
4
|
+
attr_accessor :name, :dimensions
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
Given /^I have a dimensions class with width, depth and a from_node class method$/ do
|
9
|
+
class Dimensions
|
10
|
+
include Dozuki::Mapper
|
11
|
+
attr_accessor :width, :depth
|
12
|
+
map_with do |map|
|
13
|
+
map.int :width
|
14
|
+
map.int :depth
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
When /^I map the plot node to a plot object with:$/ do |string|
|
20
|
+
Plot.instance_eval %Q{
|
21
|
+
map_with do |map|
|
22
|
+
#{string}
|
23
|
+
end
|
24
|
+
}
|
25
|
+
@plot = Plot.from_node(@doc.get('/plot'))
|
26
|
+
end
|
27
|
+
|
28
|
+
Then /^the plot should have a name of "([^"]*)"$/ do |name|
|
29
|
+
@plot.name.should == name
|
30
|
+
end
|
31
|
+
|
32
|
+
Then /^the plot should have dimensions with a width of (\d+) and a depth of (\d+)$/ do |width, depth|
|
33
|
+
dimensions = @plot.dimensions
|
34
|
+
dimensions.width.should == width.to_i
|
35
|
+
dimensions.depth.should == depth.to_i
|
36
|
+
end
|
37
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
Given /^I have an person with name, address and post_code$/ do
|
2
|
+
class Person
|
3
|
+
include Dozuki::Mapper
|
4
|
+
attr_accessor :name, :address, :post_code
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
Given /^I have the XML:$/ do |string|
|
9
|
+
@doc = Dozuki::XML.parse(string)
|
10
|
+
end
|
11
|
+
|
12
|
+
When /^I map the person node to a person object with:$/ do |string|
|
13
|
+
Person.instance_eval %Q{
|
14
|
+
map_with do |map|
|
15
|
+
#{string}
|
16
|
+
end
|
17
|
+
}
|
18
|
+
@person = Person.from_node(@doc.get('/person'))
|
19
|
+
end
|
20
|
+
|
21
|
+
Then /^the person should have the (.*) "([^"]*)"$/ do |field, string|
|
22
|
+
@person.send(field).should == string
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
Feature: mapping a document with string elements to an object
|
2
|
+
So that I don't have to spend all my time typing in repetitive XPaths
|
3
|
+
As an API consumer
|
4
|
+
I want to map a document to an object
|
5
|
+
|
6
|
+
@string
|
7
|
+
Scenario: I have a simple mapping
|
8
|
+
Given I have an person with name, address and post_code
|
9
|
+
And I have the XML:
|
10
|
+
"""
|
11
|
+
<person>
|
12
|
+
<name>John Smith</name>
|
13
|
+
<address>1 Main Street</address>
|
14
|
+
<post_code>NW1 3ED</post_code>
|
15
|
+
</person>
|
16
|
+
"""
|
17
|
+
When I map the person node to a person object with:
|
18
|
+
"""
|
19
|
+
map.string :name
|
20
|
+
map.string :address
|
21
|
+
map.string :post_code
|
22
|
+
"""
|
23
|
+
Then the person should have the name "John Smith"
|
24
|
+
Then the person should have the address "1 Main Street"
|
25
|
+
Then the person should have the post_code "NW1 3ED"
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'dozuki-mapper'
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Dozuki
|
2
|
+
module Mapper
|
3
|
+
class Proxy
|
4
|
+
attr_accessor :receiver, :from_node
|
5
|
+
|
6
|
+
def initialize(receiver, node)
|
7
|
+
self.receiver = receiver
|
8
|
+
self.from_node = node
|
9
|
+
end
|
10
|
+
|
11
|
+
def string(attribute)
|
12
|
+
self.receiver.send("#{attribute}=", from_node.string("./#{attribute}"))
|
13
|
+
end
|
14
|
+
|
15
|
+
def int(attribute)
|
16
|
+
self.receiver.send("#{attribute}=", from_node.int("./#{attribute}"))
|
17
|
+
end
|
18
|
+
|
19
|
+
def date(attribute)
|
20
|
+
self.receiver.send("#{attribute}=", from_node.date("./#{attribute}"))
|
21
|
+
end
|
22
|
+
|
23
|
+
def float(attribute)
|
24
|
+
self.receiver.send("#{attribute}=", from_node.float("./#{attribute}"))
|
25
|
+
end
|
26
|
+
|
27
|
+
def node(attribute, opts={})
|
28
|
+
self.receiver.send("#{attribute}=", opts[:as].from_node(from_node.get("./#{attribute}")))
|
29
|
+
end
|
30
|
+
|
31
|
+
def each(attribute, opts={})
|
32
|
+
from_node.each("./#{attribute}") do |node|
|
33
|
+
receiver.send(opts[:to]) << opts[:as].from_node(node)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'dozuki'
|
2
|
+
require 'dozuki-mapper/proxy'
|
3
|
+
module Dozuki
|
4
|
+
module Mapper
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
def map_from(node)
|
9
|
+
self.class.mapper.call Proxy.new(self, node)
|
10
|
+
end
|
11
|
+
module ClassMethods
|
12
|
+
def map_with(&blk)
|
13
|
+
@mapper = blk
|
14
|
+
end
|
15
|
+
def mapper
|
16
|
+
@mapper
|
17
|
+
end
|
18
|
+
def from_node(node)
|
19
|
+
new.tap do |instance|
|
20
|
+
instance.map_from(node)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Dozuki
|
3
|
+
module Mapper
|
4
|
+
describe Proxy do
|
5
|
+
class TestClass < Struct.new(:field, :fields); end
|
6
|
+
|
7
|
+
describe "string" do
|
8
|
+
let(:receiver){ TestClass.new }
|
9
|
+
let(:node) { mock :node}
|
10
|
+
let(:method_name){:field}
|
11
|
+
let(:string) { "The Decemberists" }
|
12
|
+
|
13
|
+
before do
|
14
|
+
node.stub(:string).and_return(string)
|
15
|
+
end
|
16
|
+
|
17
|
+
subject { Proxy.new(receiver, node).string(method_name) }
|
18
|
+
it "should get the field from the node using the xpath ./field as a string" do
|
19
|
+
node.should_receive(:string).with('./field').and_return(string)
|
20
|
+
subject
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should set the field on the receiver with the string" do
|
24
|
+
subject
|
25
|
+
receiver.field.should == string
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "string" do
|
31
|
+
let(:receiver){ TestClass.new }
|
32
|
+
let(:node) { mock :node}
|
33
|
+
let(:method_name){:field}
|
34
|
+
let(:int) { 54 }
|
35
|
+
|
36
|
+
before do
|
37
|
+
node.stub(:int).and_return(int)
|
38
|
+
end
|
39
|
+
|
40
|
+
subject { Proxy.new(receiver, node).int(method_name) }
|
41
|
+
it "should get the field from the node using the xpath ./field as an int" do
|
42
|
+
node.should_receive(:int).with('./field').and_return(int)
|
43
|
+
subject
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should set the field on the receiver with the int" do
|
47
|
+
subject
|
48
|
+
receiver.field.should == int
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "float" do
|
53
|
+
let(:receiver){ TestClass.new }
|
54
|
+
let(:node) { mock :node}
|
55
|
+
let(:method_name){:field}
|
56
|
+
let(:float) { 503.0 }
|
57
|
+
|
58
|
+
before do
|
59
|
+
node.stub(:float).and_return(float)
|
60
|
+
end
|
61
|
+
|
62
|
+
subject { Proxy.new(receiver, node).float(method_name) }
|
63
|
+
it "should get the field from the node using the xpath ./field as an float" do
|
64
|
+
node.should_receive(:float).with('./field').and_return(float)
|
65
|
+
subject
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should set the field on the receiver with the float" do
|
69
|
+
subject
|
70
|
+
receiver.field.should == float
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
describe "node" do
|
76
|
+
let(:node) { mock :node }
|
77
|
+
let(:receiver){ TestClass.new }
|
78
|
+
let(:method_name){ :field }
|
79
|
+
let(:other_class) { mock :class }
|
80
|
+
let(:new_object) { mock :new_object }
|
81
|
+
|
82
|
+
before do
|
83
|
+
node.stub(:get).and_return(node)
|
84
|
+
other_class.stub(:from_node).and_return(new_object)
|
85
|
+
end
|
86
|
+
|
87
|
+
subject { Proxy.new(receiver, node).node(method_name, :as => other_class) }
|
88
|
+
|
89
|
+
it "should get the node from the receiver using the ./field xpath" do
|
90
|
+
node.should_receive(:get).with('./field').and_return(node)
|
91
|
+
subject
|
92
|
+
end
|
93
|
+
it "should create a new instance of the other class suing from_node" do
|
94
|
+
other_class.should_receive(:from_node).with(node).and_return(new_object)
|
95
|
+
subject
|
96
|
+
end
|
97
|
+
it "should set the new object to the field" do
|
98
|
+
subject
|
99
|
+
receiver.field.should == new_object
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "each" do
|
104
|
+
let(:node) { mock :node }
|
105
|
+
let(:receiver){ TestClass.new(nil, []) }
|
106
|
+
let(:other_class){ mock :other_class }
|
107
|
+
let(:new_object) { mock :new_object }
|
108
|
+
let(:field){ mock :field }
|
109
|
+
|
110
|
+
before do
|
111
|
+
node.stub(:each).and_yield(field)
|
112
|
+
other_class.stub(:from_node).and_return(new_object)
|
113
|
+
end
|
114
|
+
|
115
|
+
subject { Proxy.new(receiver, node).each(:field, :as => other_class, :to => :fields)}
|
116
|
+
|
117
|
+
it "should get each field from the node using the ./field xpath" do
|
118
|
+
node.should_receive(:each).with('./field').and_yield(field)
|
119
|
+
subject
|
120
|
+
end
|
121
|
+
it "should create a new instance of the other class using the from_node method" do
|
122
|
+
other_class.should_receive(:from_node).with(field).and_return(new_object)
|
123
|
+
subject
|
124
|
+
end
|
125
|
+
it "should append the object to the as field" do
|
126
|
+
subject
|
127
|
+
receiver.fields.should == [new_object]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Dozuki
|
4
|
+
describe Mapper do
|
5
|
+
let(:node) { mock :node }
|
6
|
+
let(:klass){ Class.new }
|
7
|
+
before do
|
8
|
+
klass.send(:include, Mapper)
|
9
|
+
end
|
10
|
+
describe "mappings" do
|
11
|
+
let(:instance) { klass.new }
|
12
|
+
let(:proxy) { mock :proxy }
|
13
|
+
before { Mapper::Proxy.stub(:new).and_return(proxy) }
|
14
|
+
subject {
|
15
|
+
klass.map_with do |arg|
|
16
|
+
@called_with = arg
|
17
|
+
end
|
18
|
+
instance.map_from(node)
|
19
|
+
}
|
20
|
+
it "should create a new proxy with the node and the mapper" do
|
21
|
+
Mapper::Proxy.should_receive(:new).with(instance, node).and_return(proxy)
|
22
|
+
subject
|
23
|
+
end
|
24
|
+
it "should yield the proxy to the block mapped block" do
|
25
|
+
subject
|
26
|
+
@called_with.should == proxy
|
27
|
+
end
|
28
|
+
end
|
29
|
+
describe "from_node" do
|
30
|
+
let(:node) { mock :node }
|
31
|
+
let(:instance) { mock :klass }
|
32
|
+
before do
|
33
|
+
klass.stub(:new).and_return(instance)
|
34
|
+
instance.stub(:map_from)
|
35
|
+
end
|
36
|
+
subject { klass.from_node(node) }
|
37
|
+
it "should create a new instance of the class" do
|
38
|
+
klass.should_receive(:new).and_return(instance)
|
39
|
+
subject
|
40
|
+
end
|
41
|
+
it "should map the instance from the node" do
|
42
|
+
instance.should_receive(:map_from).with(node)
|
43
|
+
subject
|
44
|
+
end
|
45
|
+
it "should return the instance of the class" do
|
46
|
+
subject.should == instance
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'dozuki-mapper'
|
metadata
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dozuki-mapper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- James Almond
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-05-16 00:00:00 +01:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: dozuki
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: "0"
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: "0"
|
36
|
+
type: :development
|
37
|
+
version_requirements: *id002
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: cucumber
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: "0"
|
47
|
+
type: :development
|
48
|
+
version_requirements: *id003
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: autotest
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
type: :development
|
59
|
+
version_requirements: *id004
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: rake
|
62
|
+
prerelease: false
|
63
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: "0"
|
69
|
+
type: :development
|
70
|
+
version_requirements: *id005
|
71
|
+
description: A DSL for mapping API output to objects
|
72
|
+
email:
|
73
|
+
- james@jamesalmond.com
|
74
|
+
executables: []
|
75
|
+
|
76
|
+
extensions: []
|
77
|
+
|
78
|
+
extra_rdoc_files: []
|
79
|
+
|
80
|
+
files:
|
81
|
+
- .autotest
|
82
|
+
- .gitignore
|
83
|
+
- .rspec
|
84
|
+
- Gemfile
|
85
|
+
- README.markdown
|
86
|
+
- Rakefile
|
87
|
+
- dozuki-mapper.gemspec
|
88
|
+
- features/date_mapping.feature
|
89
|
+
- features/each_mapping.feature
|
90
|
+
- features/float_mapping.feature
|
91
|
+
- features/int_mapping.feature
|
92
|
+
- features/node_mapping.feature
|
93
|
+
- features/step_definitions/date_steps.rb
|
94
|
+
- features/step_definitions/each_steps.rb
|
95
|
+
- features/step_definitions/float_steps.rb
|
96
|
+
- features/step_definitions/int_steps.rb
|
97
|
+
- features/step_definitions/node_steps.rb
|
98
|
+
- features/step_definitions/string_steps.rb
|
99
|
+
- features/string_mapping.feature
|
100
|
+
- features/support/env.rb
|
101
|
+
- lib/dozuki-mapper.rb
|
102
|
+
- lib/dozuki-mapper/proxy.rb
|
103
|
+
- lib/dozuki-mapper/version.rb
|
104
|
+
- spec/dozuki-mapper/proxy_spec.rb
|
105
|
+
- spec/dozuki-mapper_spec.rb
|
106
|
+
- spec/spec_helper.rb
|
107
|
+
has_rdoc: true
|
108
|
+
homepage: https://github.com/jamesalmond/dozuki-mapper
|
109
|
+
licenses: []
|
110
|
+
|
111
|
+
post_install_message:
|
112
|
+
rdoc_options: []
|
113
|
+
|
114
|
+
require_paths:
|
115
|
+
- lib
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
117
|
+
none: false
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: "0"
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
none: false
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: "0"
|
128
|
+
requirements: []
|
129
|
+
|
130
|
+
rubyforge_project: dozuki-mapper
|
131
|
+
rubygems_version: 1.6.2
|
132
|
+
signing_key:
|
133
|
+
specification_version: 3
|
134
|
+
summary: A DSL for mapping API output to objects
|
135
|
+
test_files:
|
136
|
+
- .autotest
|
137
|
+
- features/date_mapping.feature
|
138
|
+
- features/each_mapping.feature
|
139
|
+
- features/float_mapping.feature
|
140
|
+
- features/int_mapping.feature
|
141
|
+
- features/node_mapping.feature
|
142
|
+
- features/step_definitions/date_steps.rb
|
143
|
+
- features/step_definitions/each_steps.rb
|
144
|
+
- features/step_definitions/float_steps.rb
|
145
|
+
- features/step_definitions/int_steps.rb
|
146
|
+
- features/step_definitions/node_steps.rb
|
147
|
+
- features/step_definitions/string_steps.rb
|
148
|
+
- features/string_mapping.feature
|
149
|
+
- features/support/env.rb
|
150
|
+
- spec/dozuki-mapper/proxy_spec.rb
|
151
|
+
- spec/dozuki-mapper_spec.rb
|
152
|
+
- spec/spec_helper.rb
|