simple_attribute_mapper 0.0.1 → 0.0.2
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/README.md +33 -8
- data/lib/simple_attribute_mapper/configuration.rb +34 -0
- data/lib/simple_attribute_mapper/configuration_error.rb +4 -0
- data/lib/simple_attribute_mapper/duplicate_mapping_error.rb +4 -0
- data/lib/simple_attribute_mapper/mapper.rb +35 -4
- data/lib/simple_attribute_mapper/version.rb +1 -1
- data/lib/simple_attribute_mapper.rb +23 -1
- data/simple_attribute_mapper.gemspec +1 -1
- data/spec/simple_attribute_mapper_spec.rb +136 -8
- metadata +7 -3
data/README.md
CHANGED
@@ -26,6 +26,7 @@ class Person
|
|
26
26
|
atrribute :last_name, String
|
27
27
|
attribute :email, String
|
28
28
|
attribute :home_phone, String
|
29
|
+
attribute :mailing_address, String
|
29
30
|
end
|
30
31
|
|
31
32
|
class User < ActiveRecord::Base
|
@@ -33,6 +34,10 @@ class User < ActiveRecord::Base
|
|
33
34
|
# last_name
|
34
35
|
# user_name
|
35
36
|
# phone_number
|
37
|
+
# street
|
38
|
+
# city
|
39
|
+
# state
|
40
|
+
# zip
|
36
41
|
end
|
37
42
|
```
|
38
43
|
|
@@ -44,23 +49,43 @@ specify additional mappings, i.e. user_name -> email
|
|
44
49
|
user = User.find(0000)
|
45
50
|
user.user_name # => "test@example.com"
|
46
51
|
mapper = SimpleAttributeMapper::Mapper.new({:user_name => :email})
|
47
|
-
person = mapper.map(user, Person) # returns a new instance of Person
|
52
|
+
person = mapper.map(user, Person) # returns a new instance of Person
|
48
53
|
person.email # => "test@example.com"
|
49
54
|
```
|
50
55
|
|
51
|
-
|
56
|
+
Typically you won't create instances of SimpleAttributeMapper::Mapper in your code, instead you will configure all mappings and use `SimpleAttributeMapper.map`, see below
|
52
57
|
|
53
|
-
|
58
|
+
### Configuration
|
54
59
|
|
55
60
|
```
|
56
|
-
#
|
57
|
-
SimpleAttributeMapper.
|
58
|
-
|
61
|
+
# in rails: config/initializers/simple_attribute_mapper_config.rb
|
62
|
+
SimpleAttributeMapper.configure do |config|
|
63
|
+
config << SimpleAttributeMapper.from(User).to(Person).with({:user_name => :email}).with({:phone_number => :home_phone})
|
64
|
+
end
|
65
|
+
|
66
|
+
# use later in application to map
|
67
|
+
user = User.find(0000)
|
59
68
|
person = SimpleAttributeMapper.map(user, Person)
|
69
|
+
|
70
|
+
```
|
71
|
+
|
72
|
+
### Mapping options
|
73
|
+
|
60
74
|
```
|
75
|
+
# default, maps all matching attributes
|
76
|
+
SimpleAttributeMapper.from(User).to(Person)
|
77
|
+
|
78
|
+
# map source to target
|
79
|
+
SimpleAttributeMapper.from(User).to(Person).with({:user_name => :email})
|
61
80
|
|
62
|
-
|
63
|
-
|
81
|
+
# map nested source to target
|
82
|
+
# use array; i.e. User#mailing_address -> Address#country -> Country#name
|
83
|
+
SimpleAttributeMapper.from(User).to(Person).with({[:mailing_address, :country, :name] => :country_name})
|
84
|
+
|
85
|
+
# map composite source to target
|
86
|
+
# use lambda
|
87
|
+
SimpleAttributeMapper.from(User).to(Person).with({ lambda { |source| "#{source.street}\n#{source.city}, #{source.state}\n#{source.zip}" } => :mailing_address})
|
88
|
+
```
|
64
89
|
|
65
90
|
## Contributing
|
66
91
|
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module SimpleAttributeMapper
|
2
|
+
class Configuration
|
3
|
+
def initialize
|
4
|
+
@maps = []
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :maps
|
8
|
+
|
9
|
+
def add_mapping(map)
|
10
|
+
maps.each do |existing_map|
|
11
|
+
if existing_map.source_class == map.source_class && existing_map.target_class == map.target_class
|
12
|
+
raise DuplicateMappingError.new("Map is already configured for '#{map.source_class}' -> '#{map.target_class}'")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
maps << map
|
17
|
+
end
|
18
|
+
|
19
|
+
def << map
|
20
|
+
add_mapping(map)
|
21
|
+
end
|
22
|
+
|
23
|
+
def find_mapping(source_class, target_class)
|
24
|
+
maps.select do |map|
|
25
|
+
map.source_class == source_class && map.target_class == target_class
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# for testing only
|
30
|
+
def clear
|
31
|
+
@maps = []
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -10,7 +10,13 @@ module SimpleAttributeMapper
|
|
10
10
|
raise UnMappableError.new("source has no attributes") unless source.respond_to?(:attributes)
|
11
11
|
|
12
12
|
target = target_class.new
|
13
|
+
map_matching_attributes(source, target)
|
14
|
+
map_specified_attributes(source, target)
|
13
15
|
|
16
|
+
target
|
17
|
+
end
|
18
|
+
|
19
|
+
def map_matching_attributes(source, target)
|
14
20
|
source.attributes.each do |attribute|
|
15
21
|
attribute_writer = "#{attribute[0]}=".to_sym
|
16
22
|
attribute_value = attribute[1]
|
@@ -19,16 +25,41 @@ module SimpleAttributeMapper
|
|
19
25
|
target.send(attribute_writer, attribute_value)
|
20
26
|
end
|
21
27
|
end
|
28
|
+
end
|
22
29
|
|
30
|
+
def map_specified_attributes(source, target)
|
23
31
|
mappings.each do |source_attribute, target_attribute|
|
24
32
|
attribute_writer = "#{target_attribute}=".to_sym
|
25
|
-
attribute_value =
|
26
|
-
# puts attribute_writer
|
27
|
-
# puts attribute_value
|
33
|
+
attribute_value = resolve_attribute_value(source_attribute, source)
|
34
|
+
# puts "#{attribute_writer} - #{attribute_value}"
|
28
35
|
target.send(attribute_writer, attribute_value)
|
29
36
|
end
|
37
|
+
end
|
30
38
|
|
31
|
-
|
39
|
+
def resolve_attribute_value(mapping_key, source)
|
40
|
+
if mapping_key.is_a?(Symbol)
|
41
|
+
source.send(mapping_key)
|
42
|
+
elsif mapping_key.is_a?(Array)
|
43
|
+
resolve_nested_value(mapping_key, source)
|
44
|
+
elsif mapping_key.is_a?(Proc)
|
45
|
+
mapping_key.call(source)
|
46
|
+
else
|
47
|
+
raise "Fatal, mapping for '#{mapping_key}' is unknown"
|
48
|
+
end
|
32
49
|
end
|
50
|
+
|
51
|
+
def resolve_nested_value(mapping_key, source)
|
52
|
+
root_key = mapping_key.shift
|
53
|
+
current = source.send(root_key)
|
54
|
+
|
55
|
+
mapping_key.each do |nested|
|
56
|
+
value = current.send(nested)
|
57
|
+
if nested == mapping_key.last
|
58
|
+
return value
|
59
|
+
end
|
60
|
+
current = value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
33
64
|
end
|
34
65
|
end
|
@@ -1,14 +1,36 @@
|
|
1
1
|
require "simple_attribute_mapper/version"
|
2
2
|
require "simple_attribute_mapper/mapper"
|
3
|
+
require "simple_attribute_mapper/configuration_error"
|
4
|
+
require "simple_attribute_mapper/duplicate_mapping_error"
|
3
5
|
require "simple_attribute_mapper/un_mappable_error"
|
4
6
|
require "simple_attribute_mapper/map"
|
7
|
+
require "simple_attribute_mapper/configuration"
|
5
8
|
|
6
9
|
module SimpleAttributeMapper
|
10
|
+
class << self
|
11
|
+
attr_accessor :configuration
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.configure
|
15
|
+
self.configuration ||= Configuration.new
|
16
|
+
yield(configuration)
|
17
|
+
end
|
18
|
+
|
7
19
|
def self.from(source_class)
|
8
20
|
Map.new(source_class)
|
9
21
|
end
|
10
22
|
|
11
23
|
def self.map(source_instance, target_class)
|
12
|
-
|
24
|
+
if self.configuration.maps.length == 0
|
25
|
+
raise ConfigurationError.new("There are no mappings configured, check the documentation for configuration steps")
|
26
|
+
end
|
27
|
+
|
28
|
+
mappings = configuration.find_mapping(source_instance.class, target_class)
|
29
|
+
if mappings.length == 0
|
30
|
+
raise ConfigurationError.new("There are no mappings configured for '#{source_instance.class}' -> '#{target_class}'")
|
31
|
+
end
|
32
|
+
|
33
|
+
mapper = Mapper.new(mappings[0].mappings)
|
34
|
+
mapper.map(source_instance, target_class)
|
13
35
|
end
|
14
36
|
end
|
@@ -8,7 +8,7 @@ Gem::Specification.new do |gem|
|
|
8
8
|
gem.version = SimpleAttributeMapper::VERSION
|
9
9
|
gem.authors = ["Jesse House"]
|
10
10
|
gem.email = ["jesse.house@gmail.com"]
|
11
|
-
gem.description = %q{Maps attribute values from one object to another}
|
11
|
+
gem.description = %q{Maps attribute values from one object to another. See the README on github for more information}
|
12
12
|
gem.summary = %q{See the README for more information}
|
13
13
|
gem.homepage = "https://github.com/house9/simple_attribute_mapper"
|
14
14
|
|
@@ -1,6 +1,95 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe SimpleAttributeMapper do
|
4
|
+
before(:each) { SimpleAttributeMapper.configure { |config| config.clear } }
|
5
|
+
|
6
|
+
describe "map" do
|
7
|
+
class Foo
|
8
|
+
include Virtus
|
9
|
+
|
10
|
+
attribute :foo, String
|
11
|
+
attribute :abc, String
|
12
|
+
attribute :source_baz, String
|
13
|
+
end
|
14
|
+
|
15
|
+
class Bar
|
16
|
+
include Virtus
|
17
|
+
|
18
|
+
attribute :bar, String
|
19
|
+
attribute :abc, String
|
20
|
+
attribute :target_baz, String
|
21
|
+
end
|
22
|
+
|
23
|
+
let(:source) { Foo }
|
24
|
+
let(:target) { Bar }
|
25
|
+
|
26
|
+
it "raises when no mappings" do
|
27
|
+
maps = []
|
28
|
+
SimpleAttributeMapper.configuration.stub(:maps).and_return(maps)
|
29
|
+
instance = source.new(foo: "FOO", abc: "ABC")
|
30
|
+
expect { SimpleAttributeMapper.map(instance, target) }.to raise_error(SimpleAttributeMapper::ConfigurationError)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "raises when no mappings for specified mapping" do
|
34
|
+
maps = [ SimpleAttributeMapper.from(source).to(target).with({source_baz: :target_baz}) ]
|
35
|
+
SimpleAttributeMapper.configuration.stub(:maps).and_return(maps)
|
36
|
+
instance = source.new(foo: "FOO", abc: "ABC")
|
37
|
+
new_target = Struct.new(:abc)
|
38
|
+
expect { SimpleAttributeMapper.map(instance, new_target) }.to raise_error(SimpleAttributeMapper::ConfigurationError)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "maps" do
|
42
|
+
maps = [ SimpleAttributeMapper.from(source).to(target).with({source_baz: :target_baz}) ]
|
43
|
+
SimpleAttributeMapper.configuration.stub(:maps).and_return(maps)
|
44
|
+
source_instance = source.new(foo: "FOO", abc: "ABC", source_baz: "BAZ")
|
45
|
+
target_instance = SimpleAttributeMapper.map(source_instance, target)
|
46
|
+
target_instance.bar.should be_nil
|
47
|
+
target_instance.abc.should == "ABC"
|
48
|
+
target_instance.target_baz.should == "BAZ"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "configure" do
|
53
|
+
describe "add_mapping" do
|
54
|
+
let(:source) { Struct.new(:foo, :xyz) }
|
55
|
+
let(:target) { Struct.new(:bar, :abc) }
|
56
|
+
|
57
|
+
it "adds to the configured maps" do
|
58
|
+
SimpleAttributeMapper.configure do |config|
|
59
|
+
config.add_mapping(SimpleAttributeMapper.from(source).to(target))
|
60
|
+
end
|
61
|
+
|
62
|
+
SimpleAttributeMapper.configuration.maps.length.should == 1
|
63
|
+
end
|
64
|
+
|
65
|
+
it "raises when adding duplicates" do
|
66
|
+
expect do
|
67
|
+
SimpleAttributeMapper.configure do |config|
|
68
|
+
config.add_mapping(SimpleAttributeMapper.from(source).to(target))
|
69
|
+
config.add_mapping(SimpleAttributeMapper.from(source).to(target))
|
70
|
+
end
|
71
|
+
end.to raise_error(SimpleAttributeMapper::DuplicateMappingError)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context "builds the mappings" do
|
76
|
+
let(:source) { Struct.new(:foo, :xyz) }
|
77
|
+
let(:target1) { Struct.new(:bar, :abc) }
|
78
|
+
let(:target2) { Struct.new(:baz) }
|
79
|
+
let(:target3) { Struct.new(:foo, :xyz) }
|
80
|
+
|
81
|
+
it "is built" do
|
82
|
+
SimpleAttributeMapper.configure do |config|
|
83
|
+
config << SimpleAttributeMapper.from(source).to(target1).with({foo: :bar}).with(xyz: :abc)
|
84
|
+
config << SimpleAttributeMapper.from(source).to(target2).with({foo: :baz})
|
85
|
+
config << SimpleAttributeMapper.from(source).to(target3)
|
86
|
+
end
|
87
|
+
SimpleAttributeMapper.configuration.maps.length.should == 3
|
88
|
+
SimpleAttributeMapper.configuration.maps[0].should be_an_instance_of(SimpleAttributeMapper::Map)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
4
93
|
describe "from" do
|
5
94
|
it "returns map" do
|
6
95
|
source = Struct.new(:foo)
|
@@ -46,12 +135,31 @@ describe SimpleAttributeMapper::Mapper do
|
|
46
135
|
end
|
47
136
|
|
48
137
|
describe "map" do
|
138
|
+
class Country
|
139
|
+
include Virtus
|
140
|
+
|
141
|
+
attribute :name, String
|
142
|
+
end
|
143
|
+
|
144
|
+
class Address
|
145
|
+
include Virtus
|
146
|
+
|
147
|
+
attribute :street, String
|
148
|
+
attribute :city, String
|
149
|
+
attribute :state, String
|
150
|
+
attribute :zip, String
|
151
|
+
attribute :country, Country
|
152
|
+
end
|
153
|
+
|
49
154
|
class Foo
|
50
155
|
include Virtus
|
51
156
|
|
52
157
|
attribute :baz, String
|
53
158
|
attribute :foo, String
|
54
159
|
attribute :abc, String
|
160
|
+
attribute :address, Address
|
161
|
+
attribute :first_name, String
|
162
|
+
attribute :last_name, String
|
55
163
|
end
|
56
164
|
|
57
165
|
class Bar
|
@@ -59,6 +167,9 @@ describe SimpleAttributeMapper::Mapper do
|
|
59
167
|
|
60
168
|
attribute :baz, String
|
61
169
|
attribute :xyz, String
|
170
|
+
attribute :city, String
|
171
|
+
attribute :country_name, String
|
172
|
+
attribute :full_name, String
|
62
173
|
end
|
63
174
|
|
64
175
|
it "raises error when no source has no attributes" do
|
@@ -72,6 +183,31 @@ describe SimpleAttributeMapper::Mapper do
|
|
72
183
|
bar.baz.should == "BAZ"
|
73
184
|
end
|
74
185
|
|
186
|
+
context "mapping nested attributes" do
|
187
|
+
it "maps 1 level deep" do
|
188
|
+
mapper = SimpleAttributeMapper::Mapper.new({[:address, :city] => :city})
|
189
|
+
foo = Foo.new({baz: "BAZ", address: Address.new(city: "Foo City")})
|
190
|
+
bar = mapper.map(foo, Bar)
|
191
|
+
bar.city.should == "Foo City"
|
192
|
+
end
|
193
|
+
|
194
|
+
it "goes deeper" do
|
195
|
+
mapper = SimpleAttributeMapper::Mapper.new({[:address, :country, :name] => :country_name})
|
196
|
+
foo = Foo.new(baz: "BAZ", address: Address.new(country: Country.new(name: "Foo Country")))
|
197
|
+
bar = mapper.map(foo, Bar)
|
198
|
+
bar.country_name.should == "Foo Country"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
context "composite mapping" do
|
203
|
+
it "maps first and last to full name" do
|
204
|
+
mapper = SimpleAttributeMapper::Mapper.new({ lambda { |source| "#{source.first_name} #{source.last_name}" } => :full_name})
|
205
|
+
foo = Foo.new(first_name: "Frank", last_name: "Risso")
|
206
|
+
bar = mapper.map(foo, Bar)
|
207
|
+
bar.full_name.should == "Frank Risso"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
75
211
|
it "does not map when no target attribute" do
|
76
212
|
mapper = SimpleAttributeMapper::Mapper.new
|
77
213
|
foo = Foo.new({baz: "BAZ", foo: "FOO"})
|
@@ -88,12 +224,4 @@ describe SimpleAttributeMapper::Mapper do
|
|
88
224
|
end
|
89
225
|
end
|
90
226
|
|
91
|
-
describe "Building a mapping" do
|
92
|
-
let(:source) { Struct.new(:foo) }
|
93
|
-
let(:target) { Struct.new(:bar) }
|
94
227
|
|
95
|
-
it "is built" do
|
96
|
-
map = SimpleAttributeMapper.from(source).to(target).with({foo: :bar}).with(xyz: :abc)
|
97
|
-
map.should be_an_instance_of(SimpleAttributeMapper::Map)
|
98
|
-
end
|
99
|
-
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simple_attribute_mapper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-12-
|
12
|
+
date: 2012-12-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
@@ -43,7 +43,8 @@ dependencies:
|
|
43
43
|
- - ! '>='
|
44
44
|
- !ruby/object:Gem::Version
|
45
45
|
version: '0'
|
46
|
-
description: Maps attribute values from one object to another
|
46
|
+
description: Maps attribute values from one object to another. See the README on github
|
47
|
+
for more information
|
47
48
|
email:
|
48
49
|
- jesse.house@gmail.com
|
49
50
|
executables: []
|
@@ -56,6 +57,9 @@ files:
|
|
56
57
|
- README.md
|
57
58
|
- Rakefile
|
58
59
|
- lib/simple_attribute_mapper.rb
|
60
|
+
- lib/simple_attribute_mapper/configuration.rb
|
61
|
+
- lib/simple_attribute_mapper/configuration_error.rb
|
62
|
+
- lib/simple_attribute_mapper/duplicate_mapping_error.rb
|
59
63
|
- lib/simple_attribute_mapper/map.rb
|
60
64
|
- lib/simple_attribute_mapper/mapper.rb
|
61
65
|
- lib/simple_attribute_mapper/un_mappable_error.rb
|