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 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