portable_model 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -13,20 +13,36 @@ module PortableModel
13
13
  # Export the record to a hash.
14
14
  #
15
15
  def export_to_hash
16
- # Export portable attributes.
17
- record_hash = self.class.portable_attributes.inject({}) do |hash, attr_name|
18
- hash[attr_name] = attributes[attr_name]
19
- hash
20
- end
16
+ self.class.start_exporting do |exported_records|
17
+ # If the record had already been exported during the current session, use
18
+ # the result of that previous export.
19
+ record_id = "#{self.class.table_name}_#{id}"
20
+ record_hash = exported_records[record_id]
21
+
22
+ unless record_hash
23
+ # Export portable attributes.
24
+ record_hash = self.class.portable_attributes.inject({}) do |hash, attr_name|
25
+ hash[attr_name] = if self.class.overridden_export_attrs.has_key?(attr_name)
26
+ overridden_value = self.class.overridden_export_attrs[attr_name]
27
+ overridden_value.is_a?(Proc) ? instance_eval(&overridden_value) : overridden_value
28
+ else
29
+ attributes[attr_name]
30
+ end
31
+ hash
32
+ end
21
33
 
22
- # Include the exported attributes of portable associations.
23
- self.class.portable_associations.inject(record_hash) do |hash, assoc_name|
24
- assoc = self.__send__(assoc_name)
25
- hash[assoc_name] = assoc.export_portable_association if assoc
26
- hash
27
- end
34
+ # Include the exported attributes of portable associations.
35
+ self.class.portable_associations.inject(record_hash) do |hash, assoc_name|
36
+ assoc = self.__send__(assoc_name)
37
+ hash[assoc_name] = assoc.export_portable_association if assoc
38
+ hash
39
+ end
28
40
 
29
- record_hash
41
+ exported_records[record_id] = record_hash
42
+ end
43
+
44
+ record_hash
45
+ end
30
46
  end
31
47
 
32
48
  # Export the record to a YAML file.
@@ -66,34 +82,44 @@ module PortableModel
66
82
  raise ArgumentError.new('specified argument is not a hash') unless record_hash.is_a?(Hash)
67
83
 
68
84
  # Override any necessary attributes before importing.
69
- record_hash = record_hash.merge(overridden_imported_attrs)
70
-
71
- transaction do
72
- if (columns_hash.include?(inheritance_column) &&
73
- (record_type_name = record_hash[inheritance_column.to_s]) &&
74
- !record_type_name.blank? &&
75
- record_type_name != sti_name)
76
- # The model implements STI and the record type points to a different
77
- # class; call the method in that class instead.
78
- return compute_type(record_type_name).import_from_hash(record_hash)
79
- end
80
-
81
- # First split out the attributes that correspond to portable
82
- # associations.
83
- assoc_attrs = portable_associations.inject({}) do |hash, assoc_name|
84
- hash[assoc_name] = record_hash.delete(assoc_name) if record_hash.has_key?(assoc_name)
85
- hash
86
- end
87
-
88
- # Create a new record.
89
- record = create!(record_hash)
90
-
91
- # Import each of the record's associations into the record.
92
- assoc_attrs.each do |assoc_name, assoc_value|
93
- record.import_into_association(assoc_name, assoc_value)
85
+ record_hash.merge!(overridden_import_attrs)
86
+
87
+ if (columns_hash.include?(inheritance_column) &&
88
+ (record_type_name = record_hash[inheritance_column.to_s]) &&
89
+ !record_type_name.blank? &&
90
+ record_type_name != sti_name)
91
+ # The model implements STI and the record type points to a different
92
+ # class; call the method in that class instead.
93
+ compute_type(record_type_name).import_from_hash(record_hash)
94
+ else
95
+ start_importing do |imported_records|
96
+ # If the hash had already been imported during the current session,
97
+ # use the result of that previous import.
98
+ record = imported_records[record_hash.object_id]
99
+
100
+ unless record
101
+ transaction do
102
+ # First split out the attributes that correspond to portable
103
+ # associations.
104
+ assoc_attrs = portable_associations.inject({}) do |hash, assoc_name|
105
+ hash[assoc_name] = record_hash.delete(assoc_name) if record_hash.has_key?(assoc_name)
106
+ hash
107
+ end
108
+
109
+ # Create a new record.
110
+ record = create!(record_hash)
111
+
112
+ # Import each of the record's associations into the record.
113
+ assoc_attrs.each do |assoc_name, assoc_value|
114
+ record.import_into_association(assoc_name, assoc_value)
115
+ end
116
+ end
117
+
118
+ imported_records[record_hash.object_id] = record
119
+ end
120
+
121
+ record
94
122
  end
95
-
96
- record
97
123
  end
98
124
  end
99
125
 
@@ -104,13 +130,34 @@ module PortableModel
104
130
  import_from_hash(record_hash.merge(additional_attrs))
105
131
  end
106
132
 
133
+ # Starts an export session and yields a hash of currently exported records
134
+ # in the session to the specified block.
135
+ #
136
+ def start_exporting(&block)
137
+ start_porting(:exported_records, &block)
138
+ end
139
+
140
+ # Starts an import session and yields a hash of currently imported records
141
+ # in the session to the specified block.
142
+ #
143
+ def start_importing(&block)
144
+ start_porting(:imported_records, &block)
145
+ end
146
+
107
147
  # Returns the names of portable attributes, which are any attributes that
108
148
  # are not primary or foreign keys.
109
149
  #
110
150
  def portable_attributes
111
151
  columns.reject do |column|
112
152
  # TODO: Consider rejecting counter_cache columns as well; this will involve retrieving a has_many association's corresponding belongs_to association to retrieve its counter_cache_column.
113
- column.primary || column.name.in?(reflect_on_all_associations(:belongs_to).map(&:association_foreign_key))
153
+ (
154
+ column.primary ||
155
+ column.name.in?(excluded_export_attrs) && !overridden_export_attrs.has_key?(column.name) ||
156
+ (
157
+ column.name.in?(reflect_on_all_associations(:belongs_to).map(&:association_foreign_key)) &&
158
+ !column.name.in?(included_association_keys)
159
+ )
160
+ )
114
161
  end.map(&:name).map(&:to_s)
115
162
  end
116
163
 
@@ -129,12 +176,55 @@ module PortableModel
129
176
  end.map(&:name).map(&:to_s)
130
177
  end
131
178
 
179
+ def included_association_keys
180
+ @included_association_keys ||= Set.new
181
+ end
182
+
183
+ def excluded_export_attrs
184
+ @excluded_export_attrs ||= Set.new
185
+ end
186
+
187
+ def overridden_export_attrs
188
+ @overridden_export_attrs ||= {}
189
+ end
190
+
191
+ def overridden_import_attrs
192
+ @overridden_import_attrs ||= {}
193
+ end
194
+
132
195
  protected
133
196
 
197
+ # Includes the specified associations' foreign keys (which are normally
198
+ # excluded by default) whenever a record is exported.
199
+ #
200
+ def include_association_keys_on_export(*associations)
201
+ associations.inject(included_association_keys) do |included_keys, assoc|
202
+ assoc_reflection = reflect_on_association(assoc)
203
+ raise ArgumentError.new('can only include foreign keys of belongs_to associations') unless assoc_reflection.macro == :belongs_to
204
+ included_keys << assoc_reflection.association_foreign_key
205
+ end
206
+ end
207
+
208
+ # Excludes the specified attributes whenever a record is exported.
209
+ #
210
+ def exclude_attributes_on_export(*attrs)
211
+ excluded_export_attrs.merge(attrs.map(&:to_s))
212
+ end
213
+
214
+ # Overrides the specified attributes whenever a record is exported.
215
+ # Specified values can be procedures that dynamically generate the value.
216
+ #
217
+ def override_attributes_on_export(attrs)
218
+ attrs.inject(overridden_export_attrs) do |overridden_attrs, (attr_name, attr_value)|
219
+ overridden_attrs[attr_name.to_s] = attr_value
220
+ overridden_attrs
221
+ end
222
+ end
223
+
134
224
  # Overrides the specified attributes whenever a record is imported.
135
225
  #
136
226
  def override_attributes_on_import(attrs)
137
- attrs.inject(overridden_imported_attrs) do |overridden_attrs, (attr_name, attr_value)|
227
+ attrs.inject(overridden_import_attrs) do |overridden_attrs, (attr_name, attr_value)|
138
228
  overridden_attrs[attr_name.to_s] = attr_value
139
229
  overridden_attrs
140
230
  end
@@ -142,8 +232,19 @@ module PortableModel
142
232
 
143
233
  private
144
234
 
145
- def overridden_imported_attrs
146
- @overridden_imported_attrs ||= {}
235
+ def start_porting(storage_identifier)
236
+ # Use thread-local storage to keep track of records that have been
237
+ # ported in the current session. This way, records that are encountered
238
+ # multiple times are represented using the same resulting object.
239
+ is_new_session = Thread.current[storage_identifier].nil?
240
+ Thread.current[storage_identifier] = {} if is_new_session
241
+
242
+ begin
243
+ # Yield the hash of records in the current session to the specified block.
244
+ yield(Thread.current[storage_identifier])
245
+ ensure
246
+ Thread.current[storage_identifier] = nil if is_new_session
247
+ end
147
248
  end
148
249
 
149
250
  end
@@ -55,7 +55,7 @@ module ActiveRecord::Associations
55
55
  #
56
56
  def export_portable_association
57
57
  NotPortableError.raise_on_not_portable(self)
58
- export_to_hash
58
+ proxy_reflection.klass.start_exporting { export_to_hash }
59
59
  end
60
60
 
61
61
  # Import the association from a hash.
@@ -63,13 +63,13 @@ module ActiveRecord::Associations
63
63
  def import_portable_association(record_hash)
64
64
  NotPortableError.raise_on_not_portable(self)
65
65
  raise ArgumentError.new('specified argument is not a hash') unless record_hash.is_a?(Hash)
66
+ raise 'cannot replace existing association record' unless target.nil?
66
67
 
67
- proxy_owner.transaction do
68
- if target.nil?
69
- assoc_record = proxy_reflection.klass.import_from_hash(record_hash.merge(primary_key_hash))
68
+ proxy_reflection.klass.start_importing do
69
+ proxy_owner.transaction do
70
+ record_hash.merge!(primary_key_hash)
71
+ assoc_record = proxy_reflection.klass.import_from_hash(record_hash)
70
72
  replace(assoc_record)
71
- else
72
- raise 'cannot replace existing association record'
73
73
  end
74
74
  end
75
75
  end
@@ -82,7 +82,7 @@ module ActiveRecord::Associations
82
82
  #
83
83
  def export_portable_association
84
84
  NotPortableError.raise_on_not_portable(self)
85
- map(&:export_to_hash)
85
+ proxy_reflection.klass.start_exporting { map(&:export_to_hash) }
86
86
  end
87
87
 
88
88
  # Import the association from an array of hashes.
@@ -91,9 +91,15 @@ module ActiveRecord::Associations
91
91
  NotPortableError.raise_on_not_portable(self)
92
92
  raise ArgumentError.new('specified argument is not an array of hashes') unless record_hashes.is_a?(Array) && record_hashes.all? { |record_hash| record_hash.is_a?(Hash) }
93
93
 
94
- proxy_owner.transaction do
95
- assoc_records = record_hashes.map { |record_hash| proxy_reflection.klass.import_from_hash(record_hash.merge(primary_key_hash)) }
96
- concat(*assoc_records)
94
+ proxy_reflection.klass.start_importing do
95
+ proxy_owner.transaction do
96
+ delete_all
97
+ assoc_records = record_hashes.map do |record_hash|
98
+ record_hash.merge!(primary_key_hash)
99
+ proxy_reflection.klass.import_from_hash(record_hash)
100
+ end
101
+ replace(assoc_records)
102
+ end
97
103
  end
98
104
  end
99
105
 
@@ -1,3 +1,3 @@
1
1
  module PortableModel
2
- VERSION = "0.1.0"
2
+ VERSION = "1.0.0"
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: portable_model
3
3
  version: !ruby/object:Gem::Version
4
- hash: 27
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
- - 0
8
7
  - 1
9
8
  - 0
10
- version: 0.1.0
9
+ - 0
10
+ version: 1.0.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Clyde Law
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-04-12 00:00:00 -07:00
18
+ date: 2012-04-13 00:00:00 -07:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency