datamappify 0.51.1 → 0.52.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/README.md +44 -17
  4. data/datamappify.gemspec +2 -2
  5. data/lib/datamappify/data/criteria/active_record/save.rb +5 -1
  6. data/lib/datamappify/data/criteria/common.rb +26 -20
  7. data/lib/datamappify/data/criteria/concerns/update_primary_record.rb +28 -0
  8. data/lib/datamappify/data/criteria/relational/concerns/set_criteria.rb +36 -0
  9. data/lib/datamappify/data/criteria/relational/find.rb +2 -0
  10. data/lib/datamappify/data/criteria/relational/find_by_key.rb +4 -3
  11. data/lib/datamappify/data/criteria/relational/save.rb +2 -0
  12. data/lib/datamappify/data/criteria/relational/save_by_key.rb +5 -3
  13. data/lib/datamappify/data/criteria/sequel/save.rb +5 -1
  14. data/lib/datamappify/data/mapper/attribute.rb +38 -17
  15. data/lib/datamappify/data/mapper.rb +7 -5
  16. data/lib/datamappify/data/provider/active_record.rb +7 -0
  17. data/lib/datamappify/data/provider/sequel.rb +8 -1
  18. data/lib/datamappify/data/record.rb +4 -0
  19. data/lib/datamappify/entity/composable/attribute.rb +16 -0
  20. data/lib/datamappify/entity/composable/attributes.rb +54 -0
  21. data/lib/datamappify/entity/composable/validators.rb +63 -0
  22. data/lib/datamappify/entity/composable.rb +6 -87
  23. data/lib/datamappify/entity.rb +2 -2
  24. data/lib/datamappify/repository/mapping_dsl.rb +2 -2
  25. data/lib/datamappify/repository/query_method/method.rb +15 -4
  26. data/lib/datamappify/version.rb +1 -1
  27. data/spec/entity/composable_spec.rb +3 -3
  28. data/spec/repository/reverse_mapped_spec.rb +56 -0
  29. data/spec/support/entities/author.rb +6 -0
  30. data/spec/support/entities/computer_hardware.rb +2 -2
  31. data/spec/support/entities/post.rb +9 -0
  32. data/spec/support/repositories/active_record/author_repository.rb +6 -0
  33. data/spec/support/repositories/active_record/post_repository.rb +9 -0
  34. data/spec/support/repositories/sequel/author_repository.rb +6 -0
  35. data/spec/support/repositories/sequel/post_repository.rb +9 -0
  36. data/spec/support/tables/active_record.rb +13 -0
  37. data/spec/support/tables/sequel.rb +17 -0
  38. data/spec/unit/entity/composable_spec.rb +3 -3
  39. metadata +35 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e2eb31e15b5957467c3d496b2ddaf6f584d4b652
4
- data.tar.gz: 7515f3c6935418a502e22213c75b7465a5faaf4e
3
+ metadata.gz: cd079889034877268b854c9c1402e747eb84348a
4
+ data.tar.gz: ea1988a5cea43748d9d55658e999b9c1a95a9de6
5
5
  SHA512:
6
- metadata.gz: 5f014a40766499d0f3e076d7ff82117a1e1bb5b5752741c917f14bcd5d5daceb1c5e080211f3bdd83cd8478425eb87d7ebee20582622fc0d23542f0150dcf91a
7
- data.tar.gz: c13d2de1a9596ccfe8951a7d447e749529f4ab5539b2f33bed0499a12e000dca871e36a040401d2f405f96037d3c440d585326bfbb94db880154b35bea06a149
6
+ metadata.gz: 18fde8f5ddccee08904440fdd706f0563130a78566a8860bc3a67017b35320444655db5226e6019d73e29e069750a80ddc267dd7e0bcc63d64a84daf05d263c8
7
+ data.tar.gz: 91fa0a11fd1af61e398b0b22e6dcad429c4123653dafabcb9c70bc39681ce0a8a9b1e61944996f688eac8221b771344a02679f9b137d3cc278a57a4b6b9114f6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## master
2
2
 
3
+ ## 0.52.0 [2013-06-26]
4
+
5
+ - Added support for reverse mapping attributes.
6
+
3
7
  ## 0.51.1 [2013-06-17]
4
8
 
5
9
  - Fixed an issue with attribute name and validation conflicts.
data/README.md CHANGED
@@ -1,18 +1,29 @@
1
1
  # Datamappify [![Gem Version](https://badge.fury.io/rb/datamappify.png)](http://badge.fury.io/rb/datamappify) [![Build Status](https://api.travis-ci.org/fredwu/datamappify.png?branch=master)](http://travis-ci.org/fredwu/datamappify) [![Coverage Status](https://coveralls.io/repos/fredwu/datamappify/badge.png)](https://coveralls.io/r/fredwu/datamappify) [![Code Climate](https://codeclimate.com/github/fredwu/datamappify.png)](https://codeclimate.com/github/fredwu/datamappify)
2
2
 
3
+ #### Compose, decouple and manage domain logic and data persistence separately. Works great with forms!
4
+
3
5
  ## Overview
4
6
 
5
- Compose and manage domain logic and data persistence separately and intelligently, Datamappify is loosely based on the [Repository Pattern](http://martinfowler.com/eaaCatalog/repository.html) and [Entity Aggregation](http://msdn.microsoft.com/en-au/library/ff649505.aspx).
7
+ The typical Rails (and ActiveRecord) way of building applications is great for small to medium sized projects, but when projects grow larger and more complex, your models too become larger and more complex - it is not uncommon to have god classes such as a User model.
8
+
9
+ Datamappify tries to solve two common problems in web applications:
10
+
11
+ 1. The coupling between domain logic and data persistence.
12
+ 2. The coupling between forms and models.
6
13
 
7
- Datamappify is built using [Virtus](https://github.com/solnic/virtus) and existing ORMs (ActiveRecord and Sequel, etc). The design goal is to utilise the powerfulness of existing ORMs as well as to separate domain logic (model behaviour) from data persistence.
14
+ Datamappify is loosely based on the [Repository Pattern](http://martinfowler.com/eaaCatalog/repository.html) and [Entity Aggregation](http://msdn.microsoft.com/en-au/library/ff649505.aspx), and is built on top of [Virtus](https://github.com/solnic/virtus) and existing ORMs (ActiveRecord and Sequel, etc).
8
15
 
9
- My motivation for creating Datamappify is to hide the complexity of dealing with data in different data sources including the ones from external web services. Features like lazy loading and dirty tracking are designed to enhance the usability of dealing with web services.
16
+ There are three main design goals:
17
+
18
+ 1. To utilise the powerfulness of existing ORMs so that using Datamappify doesn't interrupt too much of your current workflow. For example, [Devise](https://github.com/plataformatec/devise) would still work if you use it with a `UserAccount` ActiveRecord model that is attached to a `User` entity managed by Datamappify.
19
+ 2. To have a flexible entity model that works great with dealing with form data. For example, [SimpleForm](https://github.com/plataformatec/simple_form) would still work with nested attributes from different ORM models if you map entity attributes smartly in your repositories managed by Datamappify.
20
+ 3. To have a set of data providers to encapsulate the handling of how the data is persisted. This is especially useful for dealing with external data sources such as a web service. For example, by calling `UserRepository.save(user)`, certain attributes of the user entity are now persisted on a remote web service. Better yet, dirty tracking and lazy loading are supported out of the box!
10
21
 
11
22
  Datamappify consists of three components:
12
23
 
13
24
  - __Entity__ contains models behaviour, think an ActiveRecord model with the persistence specifics removed.
14
25
  - __Repository__ is responsible for data retrieval and persistence, e.g. `find`, `save` and `destroy`, etc.
15
- - __Data__ as the name suggests, holds your model data. It contains ORM objects (ActiveRecord and Sequel, etc).
26
+ - __Data__ as the name suggests, holds your model data. It contains ORM objects (e.g. ActiveRecord models).
16
27
 
17
28
  Below is a high level and somewhat simplified overview of Datamappify's architecture.
18
29
 
@@ -39,6 +50,10 @@ Add this line to your application's Gemfile:
39
50
 
40
51
  Entity uses [Virtus](https://github.com/solnic/virtus) DSL for defining attributes and [ActiveModel::Validations](http://api.rubyonrails.org/classes/ActiveModel/Validations.html) DSL for validations.
41
52
 
53
+ The cool thing about Virtus is that all your attributes get [coercion](https://github.com/solnic/virtus#collection-member-coercions) for free!
54
+
55
+ Below is an example of a User entity, with inline comments on how some of the DSLs work.
56
+
42
57
  ```ruby
43
58
  class User
44
59
  include Datamappify::Entity
@@ -142,11 +157,13 @@ class User
142
157
  end
143
158
  ```
144
159
 
145
- When an entity is lazy loaded, only attributes from the default source will be loaded. Other attributes will only be loaded once they are called. This is especially useful if some of your data sources are external services.
160
+ When an entity is lazy loaded, only attributes from the primary source (e.g. `User` entity's primary source would be `ActiveRecord::User` as specified in the corresponding repository) will be loaded. Other attributes will only be loaded once they are called. This is especially useful if some of your data sources are external web services.
146
161
 
147
162
  ### Repository
148
163
 
149
- Map entity attributes to DB columns - better yet, you can even map attributes to __different ORMs__!
164
+ Repository maps entity attributes to DB columns - better yet, you can even map attributes to __different ORMs__!
165
+
166
+ Below is an example of a repository for the User entity, you can have more than one repositories for the same entity.
150
167
 
151
168
  ```ruby
152
169
  class UserRepository
@@ -160,6 +177,8 @@ class UserRepository
160
177
 
161
178
  # specify any attributes that need to be mapped
162
179
  #
180
+ # for attributes mapped from a different source class, a foreign key on the source class is required
181
+ #
163
182
  # for example:
164
183
  # - 'last_name' is mapped to the 'User' ActiveRecord class and its 'surname' attribute
165
184
  # - 'driver_license' is mapped to the 'UserDriverLicense' ActiveRecord class and its 'number' attribute
@@ -169,6 +188,14 @@ class UserRepository
169
188
  map_attribute :driver_license, 'ActiveRecord::UserDriverLicense#number'
170
189
  map_attribute :passport, 'Sequel::UserPassport#number'
171
190
  map_attribute :health_care, 'Sequel::UserHealthCare#number'
191
+
192
+ # attributes can also be reverse mapped by specifying the `via` option
193
+ #
194
+ # for example, the below attribute will look for `hobby_id` on the user object,
195
+ # and map `hobby_name` from the `name` attribute of `ActiveRecord::Hobby`
196
+ #
197
+ # this is useful for mapping form fields (similar to ActiveRecord's nested attributes)
198
+ map_attribute :hobby_name, 'ActiveRecord::Hobby#name', :via => :hobby_id
172
199
  end
173
200
  ```
174
201
 
@@ -186,7 +213,7 @@ class GuestUserRepository < UserRepository
186
213
  end
187
214
  ```
188
215
 
189
- In the above example, both repositories deal with the `User` data model.
216
+ In the above example, both repositories deal with the `ActiveRecord::User` data model.
190
217
 
191
218
  ### Repository APIs
192
219
 
@@ -194,7 +221,7 @@ _More repository APIs are being added, below is a list of the currently implemen
194
221
 
195
222
  #### Retrieving an entity
196
223
 
197
- Pass in an id.
224
+ Accepts an id.
198
225
 
199
226
  ```ruby
200
227
  user = UserRepository.find(1)
@@ -202,7 +229,7 @@ user = UserRepository.find(1)
202
229
 
203
230
  #### Checking if an entity exists in the repository
204
231
 
205
- Pass in an entity.
232
+ Accepts an entity.
206
233
 
207
234
  ```ruby
208
235
  UserRepository.exists?(user)
@@ -228,7 +255,7 @@ _Note: it does not currently support searching attributes from different data pr
228
255
 
229
256
  #### Saving/updating entities
230
257
 
231
- Pass in an entity.
258
+ Accepts an entity.
232
259
 
233
260
  There is also `save!` that raises `Datamappify::Data::EntityNotSaved`.
234
261
 
@@ -240,10 +267,10 @@ Datamappify supports attribute dirty tracking - only dirty attributes will be sa
240
267
 
241
268
  ##### Mark attributes as dirty
242
269
 
243
- Sometimes it's useful to manually mark the whole entity, or some attributes in the entity to be dirty - i.e. when you are submitting a form and only want to update the changed attributes. In this case, you could:
270
+ Sometimes it's useful to manually mark the whole entity, or some attributes in the entity to be dirty. In this case, you could:
244
271
 
245
272
  ```ruby
246
- UserRepository.states.mark_as_dirty(user)
273
+ UserRepository.states.mark_as_dirty(user) # marks the whole entity as dirty
247
274
 
248
275
  UserRepository.states.find(user).changed? #=> true
249
276
  UserRepository.states.find(user).first_name_changed? #=> true
@@ -254,7 +281,7 @@ UserRepository.states.find(user).age_changed? #=> true
254
281
  Or:
255
282
 
256
283
  ```ruby
257
- UserRepository.states.mark_as_dirty(user, :first_name, :last_name)
284
+ UserRepository.states.mark_as_dirty(user, :first_name, :last_name) # marks only first_name and last_name as dirty
258
285
 
259
286
  UserRepository.states.find(user).changed? #=> true
260
287
  UserRepository.states.find(user).first_name_changed? #=> true
@@ -264,11 +291,11 @@ UserRepository.states.find(user).age_changed? #=> false
264
291
 
265
292
  #### Destroying an entity
266
293
 
267
- Pass in an entity.
294
+ Accepts an entity.
268
295
 
269
296
  There is also `destroy!` that raises `Datamappify::Data::EntityNotDestroyed`.
270
297
 
271
- Note that due to the attributes mapping, any data found in mapped ActiveRecord objects are not touched.
298
+ Note that due to the attributes mapping, any data found in mapped records are not touched. For example the corresponding `ActiveRecord::User` record will be destroyed, but `ActiveRecord::Hobby` that is associated will not.
272
299
 
273
300
  ```ruby
274
301
  UserRepository.destroy(user)
@@ -287,7 +314,7 @@ Datamappify supports the following callbacks via [Hooks](https://github.com/apot
287
314
  - after_save
288
315
  - after_destroy
289
316
 
290
- Callbacks are defined in repositories, and they have access to the entity. Example:
317
+ Callbacks are defined in repositories, and they have access to the entity. For example:
291
318
 
292
319
  ```ruby
293
320
  class UserRepository
@@ -315,7 +342,7 @@ class UserRepository
315
342
  end
316
343
  ```
317
344
 
318
- Note: Returning either `nil` or `false` from the callback will cancel all subsequent callbacks (and the action itself, it it's a `before_` callback).
345
+ Note: Returning either `nil` or `false` from the callback will cancel all subsequent callbacks (and the action itself, if it's a `before_` callback).
319
346
 
320
347
  ## Changelog
321
348
 
data/datamappify.gemspec CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Datamappify::VERSION
9
9
  spec.authors = ["Fred Wu"]
10
10
  spec.email = ["ifredwu@gmail.com"]
11
- spec.description = %q{Compose and manage domain logic and data persistence separately and intelligently.}
11
+ spec.description = %q{Compose, decouple and manage domain logic and data persistence separately. Works great with forms!}
12
12
  spec.summary = spec.description
13
13
  spec.homepage = "https://github.com/fredwu/datamappify"
14
14
  spec.license = "MIT"
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "virtus", "~> 0.5"
21
+ spec.add_dependency "virtus", ">= 1.0.0.beta0", "<= 2.0"
22
22
  spec.add_dependency "activemodel", ">= 4.0.0.rc1", "< 5"
23
23
  spec.add_dependency "hooks", "~> 0.3.0"
24
24
 
@@ -1,10 +1,13 @@
1
1
  require 'datamappify/data/criteria/relational/save'
2
+ require 'datamappify/data/criteria/concerns/update_primary_record'
2
3
 
3
4
  module Datamappify
4
5
  module Data
5
6
  module Criteria
6
7
  module ActiveRecord
7
8
  class Save < Relational::Save
9
+ include Concerns::UpdatePrimaryRecord
10
+
8
11
  private
9
12
 
10
13
  def save_record
@@ -14,7 +17,8 @@ module Datamappify
14
17
 
15
18
  def save(record)
16
19
  record.update_attributes attributes_and_values
17
- record
20
+
21
+ super
18
22
  end
19
23
  end
20
24
  end
@@ -11,11 +11,17 @@ module Datamappify
11
11
  attr_reader :entity
12
12
 
13
13
  # @return [void]
14
- attr_reader :criteria
14
+ attr_accessor :criteria
15
15
 
16
16
  # @return [Set<Mapper::Attribute>]
17
17
  attr_reader :attributes
18
18
 
19
+ # @return [Hash]
20
+ attr_accessor :attributes_and_values
21
+
22
+ # @return [Hash]
23
+ attr_reader :options
24
+
19
25
  # @param source_class [Class]
20
26
  #
21
27
  # @param args [any]
@@ -24,7 +30,7 @@ module Datamappify
24
30
  # an optional block
25
31
  def initialize(source_class, *args, &block)
26
32
  @source_class = source_class
27
- @entity, @criteria, @attributes = *args
33
+ @entity, @criteria, @attributes, @options = *args
28
34
  @block = block
29
35
  end
30
36
 
@@ -39,6 +45,23 @@ module Datamappify
39
45
  result
40
46
  end
41
47
 
48
+ # Attributes with their corresponding values
49
+ #
50
+ # @return [Hash]
51
+ def attributes_and_values
52
+ @attributes_and_values ||= begin
53
+ hash = {}
54
+
55
+ attributes.each do |attribute|
56
+ unless ignore_attribute?(attribute)
57
+ hash[attribute.source_attribute_name] = entity.send(attribute.name)
58
+ end
59
+ end
60
+
61
+ hash
62
+ end
63
+ end
64
+
42
65
  protected
43
66
 
44
67
  # Name of the default source class, e.g. +"User"+,
@@ -84,23 +107,6 @@ module Datamappify
84
107
  attributes_and_values.empty?
85
108
  end
86
109
 
87
- # Attributes with their corresponding values
88
- #
89
- # @return [Hash]
90
- def attributes_and_values
91
- @attributes_and_values ||= begin
92
- hash = {}
93
-
94
- attributes.each do |attribute|
95
- unless ignore_attribute?(attribute)
96
- hash[attribute.source_attribute_name] = entity.send(attribute.name)
97
- end
98
- end
99
-
100
- hash
101
- end
102
- end
103
-
104
110
  # Stores the attribute value in {Mapper::Attribute} for later use
105
111
  #
106
112
  # @return [void]
@@ -117,7 +123,7 @@ module Datamappify
117
123
 
118
124
  # @return [Attribute]
119
125
  def pk
120
- @pk ||= attributes.find { |attribute| attribute.key == :id }
126
+ @pk ||= attributes.find(&:primary_key?)
121
127
  end
122
128
 
123
129
  private
@@ -0,0 +1,28 @@
1
+ module Datamappify
2
+ module Data
3
+ module Criteria
4
+ module Concerns
5
+ module UpdatePrimaryRecord
6
+ private
7
+
8
+ def save(record)
9
+ if options && options[:via] && options[:primary_record]
10
+ update_primary_record_with(record)
11
+ end
12
+
13
+ record
14
+ end
15
+
16
+ def update_primary_record_with(record)
17
+ save = self.class.superclass.new(options[:primary_record].class, entity, {
18
+ :id => options[:primary_record].id
19
+ })
20
+
21
+ save.attributes_and_values = { options[:via] => record.id }
22
+ save.send(:save_record)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ module Datamappify
2
+ module Data
3
+ module Criteria
4
+ module Relational
5
+ module Concerns
6
+ module SetCriteria
7
+ def initialize(*args)
8
+ super
9
+
10
+ set_criteria if entity.id
11
+ end
12
+
13
+ private
14
+
15
+ def set_criteria
16
+ self.criteria = if options[:via]
17
+ criteria_for_reverse_mapping
18
+ else
19
+ criteria_for_normal_mapping
20
+ end
21
+ end
22
+
23
+ def criteria_for_reverse_mapping
24
+ reverse_id = options[:primary_record].send(options[:via])
25
+ reverse_id ? { :id => reverse_id } : {}
26
+ end
27
+
28
+ def criteria_for_normal_mapping
29
+ { key_name => entity.id }
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -7,6 +7,8 @@ module Datamappify
7
7
  record = source_class.where(criteria).first
8
8
 
9
9
  update_entity_with(record) if record
10
+
11
+ record
10
12
  end
11
13
 
12
14
  private
@@ -1,14 +1,15 @@
1
1
  require 'datamappify/data/criteria/relational/find'
2
+ require 'datamappify/data/criteria/relational/concerns/set_criteria'
2
3
 
3
4
  module Datamappify
4
5
  module Data
5
6
  module Criteria
6
7
  module Relational
7
8
  class FindByKey < Find
8
- def initialize(source_class, entity, attributes, &block)
9
- super(source_class, entity, nil, attributes, &block)
9
+ include Concerns::SetCriteria
10
10
 
11
- @criteria = { key_name => entity.id }
11
+ def initialize(source_class, entity, attributes, options = {}, &block)
12
+ super(source_class, entity, nil, attributes, options, &block)
12
13
  end
13
14
  end
14
15
  end
@@ -14,6 +14,8 @@ module Datamappify
14
14
  saved_record = save(record)
15
15
 
16
16
  update_entity_with(saved_record) if primary_record?
17
+
18
+ record
17
19
  end
18
20
 
19
21
  def update_entity_with(record)
@@ -1,12 +1,14 @@
1
+ require 'datamappify/data/criteria/relational/concerns/set_criteria'
2
+
1
3
  module Datamappify
2
4
  module Data
3
5
  module Criteria
4
6
  module Relational
5
7
  module SaveByKey
6
- def initialize(source_class, entity, attributes, &block)
7
- super(source_class, entity, {}, attributes, &block)
8
+ include Concerns::SetCriteria
8
9
 
9
- @criteria = { key_name => entity.id } unless entity.id.nil?
10
+ def initialize(source_class, entity, attributes, options = {}, &block)
11
+ super(source_class, entity, {}, attributes, options, &block)
10
12
  end
11
13
  end
12
14
  end
@@ -1,10 +1,13 @@
1
1
  require 'datamappify/data/criteria/relational/save'
2
+ require 'datamappify/data/criteria/concerns/update_primary_record'
2
3
 
3
4
  module Datamappify
4
5
  module Data
5
6
  module Criteria
6
7
  module Sequel
7
8
  class Save < Relational::Save
9
+ include Concerns::UpdatePrimaryRecord
10
+
8
11
  private
9
12
 
10
13
  def save_record
@@ -14,7 +17,8 @@ module Datamappify
14
17
 
15
18
  def save(record)
16
19
  record.update attributes_and_values
17
- record
20
+
21
+ super
18
22
  end
19
23
  end
20
24
  end
@@ -23,6 +23,9 @@ module Datamappify
23
23
  # @return [Class]
24
24
  attr_reader :primary_source_class
25
25
 
26
+ # @return [Hash]
27
+ attr_reader :options
28
+
26
29
  # @return [any]
27
30
  attr_accessor :value
28
31
 
@@ -33,16 +36,21 @@ module Datamappify
33
36
  # data provider, class and attribute,
34
37
  # e.g. "ActiveRecord::User#surname"
35
38
  #
36
- # @param primary_source_class [Class]
37
- def initialize(name, source, primary_source_class)
39
+ # @param options [Hash]
40
+ def initialize(name, source, options)
38
41
  @key = name
39
42
  @name = name.to_s
40
- @primary_source_class = primary_source_class
43
+ @options = options
44
+ @primary_source_class = options[:primary_source_class]
41
45
 
42
46
  @provider_name, @source_class_name, @source_attribute_name = parse_source(source)
43
47
 
44
- unless primary_attribute? || external_attribute?
45
- Record.build_association(self, primary_source_class)
48
+ if secondary_attribute?
49
+ if reverse_mapped?
50
+ Record.build_reversed_association(self, primary_source_class)
51
+ else
52
+ Record.build_association(self, primary_source_class)
53
+ end
46
54
  end
47
55
  end
48
56
 
@@ -96,18 +104,6 @@ module Datamappify
96
104
  source_attribute_name == 'id'
97
105
  end
98
106
 
99
- # @return [Boolean]
100
- def primary_attribute?
101
- provider_name == primary_provider_name && primary_source_class == source_class
102
- end
103
-
104
- # External attribute is from a different data provider than the primary data provider
105
- #
106
- # @return [Boolean]
107
- def external_attribute?
108
- provider_name != primary_provider_name
109
- end
110
-
111
107
  # @return [String]
112
108
  def primary_provider_name
113
109
  @primary_provider_name ||= primary_source_class.parent.to_s.demodulize
@@ -124,6 +120,31 @@ module Datamappify
124
120
  @primary_reference_key ||= :"#{primary_source_class.to_s.demodulize.underscore}_id"
125
121
  end
126
122
 
123
+ # Primary attribute is from the same data provider and the same source class
124
+ #
125
+ # @return [Boolean]
126
+ def primary_attribute?
127
+ provider_name == primary_provider_name && primary_source_class == source_class
128
+ end
129
+
130
+ # Secondary attribute is from the same data provider but a different source class
131
+ #
132
+ # @return [Boolean]
133
+ def secondary_attribute?
134
+ provider_name == primary_provider_name && primary_source_class != source_class
135
+ end
136
+
137
+ # External attribute is from a different data provider than the primary data provider
138
+ #
139
+ # @return [Boolean]
140
+ def external_attribute?
141
+ provider_name != primary_provider_name
142
+ end
143
+
144
+ def reverse_mapped?
145
+ @options[:via]
146
+ end
147
+
127
148
  private
128
149
 
129
150
  # @return [Array<String>]
@@ -78,24 +78,26 @@ module Datamappify
78
78
  # @return [Array<Attribute>]
79
79
  def default_attributes
80
80
  @default_attributes ||= default_attribute_names.collect do |attribute|
81
- Attribute.new(attribute, default_source_for(attribute), default_source_class)
81
+ Attribute.new(attribute, default_source_for(attribute), :primary_source_class => default_source_class)
82
82
  end
83
83
  end
84
84
 
85
85
  # @return [Array<Attribute>]
86
86
  def custom_attributes
87
- @custom_attributes ||= custom_mapping.collect do |attribute, source|
88
- map_custom_attribute(attribute, source)
87
+ @custom_attributes ||= custom_mapping.collect do |attribute, source_and_options|
88
+ map_custom_attribute(attribute, *source_and_options)
89
89
  end
90
90
  end
91
91
 
92
92
  # @param (see Data::Mapper::Attribute#initialize)
93
93
  #
94
94
  # @return [Attribute]
95
- def map_custom_attribute(name, source)
95
+ def map_custom_attribute(name, source, options)
96
96
  @custom_attribute_names << name
97
97
 
98
- Attribute.new(name, source, default_source_class)
98
+ options.merge!(:primary_source_class => default_source_class)
99
+
100
+ Attribute.new(name, source, options)
99
101
  end
100
102
 
101
103
  # @param attribute [Symbol]
@@ -24,6 +24,13 @@ module Datamappify
24
24
  belongs_to :#{default_source_class.model_name.element}
25
25
  CODE
26
26
  end
27
+
28
+ # @return [void]
29
+ def build_record_reversed_association(attribute, default_source_class)
30
+ default_source_class.class_eval <<-CODE, __FILE__, __LINE__ + 1
31
+ belongs_to :#{attribute.source_key}
32
+ CODE
33
+ end
27
34
  end
28
35
  end
29
36
  end
@@ -27,7 +27,14 @@ module Datamappify
27
27
  CODE
28
28
 
29
29
  attribute.source_class.class_eval <<-CODE, __FILE__, __LINE__ + 1
30
- one_to_one :#{default_source_class.table_name.to_s.singularize}
30
+ many_to_one :#{default_source_class.table_name.to_s.singularize}
31
+ CODE
32
+ end
33
+
34
+ # @return [void]
35
+ def build_record_reversed_association(attribute, default_source_class)
36
+ default_source_class.class_eval <<-CODE, __FILE__, __LINE__ + 1
37
+ many_to_one :#{attribute.source_key}
31
38
  CODE
32
39
  end
33
40
  end
@@ -20,6 +20,10 @@ module Datamappify
20
20
  def build_association(attribute, default_source_class)
21
21
  Provider.const_get(attribute.provider_name).build_record_association(attribute, default_source_class)
22
22
  end
23
+
24
+ def build_reversed_association(attribute, default_source_class)
25
+ Provider.const_get(attribute.provider_name).build_record_reversed_association(attribute, default_source_class)
26
+ end
23
27
  end
24
28
  end
25
29
  end
@@ -0,0 +1,16 @@
1
+ module Datamappify
2
+ module Entity
3
+ module Composable
4
+ class Attribute
5
+ # @param name [Virtus::Attribute]
6
+ #
7
+ # @param prefix [Symbol]
8
+ #
9
+ # @return [void]
10
+ def self.prefix(name, prefix)
11
+ :"#{prefix}_#{name}"
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end