simple_attribute_mapper 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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
- ## TODO
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
- * configure the mappings, i.e.
58
+ ### Configuration
54
59
 
55
60
  ```
56
- # configure once
57
- SimpleAttributeMapper.from(User).to(Person).with({:user_name => :email}).with({:phone_number => :home_phone})
58
- # use later
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
- * nested attribute mappings
63
- * composite mappings through lambdas
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
@@ -0,0 +1,4 @@
1
+ module SimpleAttributeMapper
2
+ class ConfigurationError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module SimpleAttributeMapper
2
+ class DuplicateMappingError < StandardError
3
+ end
4
+ 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 = source.send(source_attribute)
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
- target
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,3 +1,3 @@
1
1
  module SimpleAttributeMapper
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  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
- raise "TODO: implement"
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.1
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-24 00:00:00.000000000 Z
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