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.
- data/lib/portable_model.rb +144 -43
- data/lib/portable_model/active_record.rb +16 -10
- data/lib/portable_model/version.rb +1 -1
- metadata +4 -4
data/lib/portable_model.rb
CHANGED
@@ -13,20 +13,36 @@ module PortableModel
|
|
13
13
|
# Export the record to a hash.
|
14
14
|
#
|
15
15
|
def export_to_hash
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
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
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
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(
|
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
|
146
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
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
|
|
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:
|
4
|
+
hash: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
|
-
- 0
|
8
7
|
- 1
|
9
8
|
- 0
|
10
|
-
|
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-
|
18
|
+
date: 2012-04-13 00:00:00 -07:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|