portable_model 0.1.0 → 1.0.0
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/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
|