arcopy 0.0.1

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/replicate.rb ADDED
@@ -0,0 +1,24 @@
1
+ module Replicate
2
+ autoload :Emitter, 'replicate/emitter'
3
+ autoload :Dumper, 'replicate/dumper'
4
+ autoload :Loader, 'replicate/loader'
5
+ autoload :Object, 'replicate/object'
6
+ autoload :Status, 'replicate/status'
7
+ autoload :AR, 'replicate/active_record'
8
+
9
+ # Determine if this is a production looking environment. Used in bin/replicate
10
+ # to safeguard against loading in production.
11
+ def self.production_environment?
12
+ if defined?(Rails) && Rails.respond_to?(:env)
13
+ Rails.env.to_s == 'production'
14
+ elsif defined?(RAILS_ENV)
15
+ RAILS_ENV == 'production'
16
+ elsif ENV['RAILS_ENV']
17
+ ENV['RAILS_ENV'] == 'production'
18
+ elsif ENV['RACK_ENV']
19
+ ENV['RACK_ENV'] == 'production'
20
+ end
21
+ end
22
+
23
+ AR if defined?(::ActiveRecord::Base)
24
+ end
@@ -0,0 +1,347 @@
1
+ require 'active_record'
2
+
3
+ module Replicate
4
+ # ActiveRecord::Base instance methods used to dump replicant objects for the
5
+ # record and all 1:1 associations. This module implements the replicant_id
6
+ # and dump_replicant methods using AR's reflection API to determine
7
+ # relationships with other objects.
8
+ module AR
9
+ # Mixin for the ActiveRecord instance.
10
+ module InstanceMethods
11
+ # Replicate::Dumper calls this method on objects to trigger dumping a
12
+ # replicant object tuple. The default implementation dumps all belongs_to
13
+ # associations, then self, then all has_one associations, then any
14
+ # has_many or has_and_belongs_to_many associations declared with the
15
+ # replicate_associations macro.
16
+ #
17
+ # dumper - Dumper object whose #write method must be called with the
18
+ # type, id, and attributes hash.
19
+ #
20
+ # Returns nothing.
21
+ def dump_replicant(dumper, opts={})
22
+ @replicate_opts = opts
23
+ @replicate_opts[:associations] ||= []
24
+ @replicate_opts[:omit] ||= []
25
+ dump_all_association_replicants dumper, :belongs_to
26
+ dumper.write self.class.to_s, id, replicant_attributes, self
27
+ dump_all_association_replicants dumper, :has_one
28
+ included_associations.each do |association|
29
+ dump_association_replicants dumper, association
30
+ end
31
+ end
32
+
33
+ # List of associations to explicitly include when dumping this object.
34
+ def included_associations
35
+ (self.class.replicate_associations + @replicate_opts[:associations]).uniq
36
+ end
37
+
38
+ # List of attributes and associations to omit when dumping this object.
39
+ def omitted_attributes
40
+ (self.class.replicate_omit_attributes + @replicate_opts[:omit]).uniq
41
+ end
42
+
43
+ # Attributes hash used to persist this object. This consists of simply
44
+ # typed values (no complex types or objects) with the exception of special
45
+ # foreign key values. When an attribute value is [:id, "SomeClass:1234"],
46
+ # the loader will handle translating the id value to the local system's
47
+ # version of the same object.
48
+ def replicant_attributes
49
+ attributes = self.attributes.dup
50
+
51
+ omitted_attributes.each { |omit| attributes.delete(omit.to_s) }
52
+ self.class.reflect_on_all_associations(:belongs_to).each do |reflection|
53
+ next if omitted_attributes.include?(reflection.name)
54
+ if info = replicate_reflection_info(reflection)
55
+ if replicant_id = info[:replicant_id]
56
+ foreign_key = info[:foreign_key].to_s
57
+ attributes[foreign_key] = [:id, *replicant_id]
58
+ end
59
+ end
60
+ end
61
+
62
+ attributes
63
+ end
64
+
65
+ # Retrieve information on a reflection's associated class and various
66
+ # keys.
67
+ #
68
+ # Returns an info hash with these keys:
69
+ # :class - The class object the association points to.
70
+ # :primary_key - The string primary key column name.
71
+ # :foreign_key - The string foreign key column name.
72
+ # :replicant_id - The [classname, id] tuple identifying the record.
73
+ #
74
+ # Returns nil when the reflection can not be linked to a model.
75
+ def replicate_reflection_info(reflection)
76
+ options = reflection.options
77
+ if options[:polymorphic]
78
+ reference_class = attributes[reflection.foreign_type]
79
+ return if reference_class.nil?
80
+
81
+ klass = reference_class.constantize
82
+ primary_key = klass.primary_key
83
+ foreign_key = "#{reflection.name}_id"
84
+ else
85
+ klass = reflection.klass
86
+ primary_key = (options[:primary_key] || klass.primary_key).to_s
87
+ foreign_key = (options[:foreign_key] || "#{reflection.name}_id").to_s
88
+ end
89
+
90
+ info = {
91
+ :class => klass,
92
+ :primary_key => primary_key,
93
+ :foreign_key => foreign_key
94
+ }
95
+
96
+ if primary_key == klass.primary_key
97
+ if id = attributes[foreign_key]
98
+ info[:replicant_id] = [klass.to_s, id]
99
+ else
100
+ # nil value in association reference
101
+ end
102
+ else
103
+ # association uses non-primary-key foreign key. no special key
104
+ # conversion needed.
105
+ end
106
+
107
+ info
108
+ end
109
+
110
+ # The replicant id is a two tuple containing the class and object id. This
111
+ # is used by Replicant::Dumper to determine if the object has already been
112
+ # dumped or not.
113
+ def replicant_id
114
+ [self.class.name, id]
115
+ end
116
+
117
+ # Dump all associations of a given type.
118
+ #
119
+ # dumper - The Dumper object used to dump additional objects.
120
+ # association_type - :has_one, :belongs_to, :has_many
121
+ #
122
+ # Returns nothing.
123
+ def dump_all_association_replicants(dumper, association_type)
124
+ self.class.reflect_on_all_associations(association_type).each do |reflection|
125
+ next if omitted_attributes.include?(reflection.name)
126
+
127
+ # bail when this object has already been dumped
128
+ next if (info = replicate_reflection_info(reflection)) &&
129
+ (replicant_id = info[:replicant_id]) &&
130
+ dumper.dumped?(replicant_id)
131
+
132
+ next if (dependent = __send__(reflection.name)).nil?
133
+
134
+ case dependent
135
+ when ActiveRecord::Base, Array
136
+ dumper.dump(dependent)
137
+
138
+ # clear reference to allow GC
139
+ if respond_to?(:association)
140
+ association(reflection.name).reset
141
+ elsif respond_to?(:association_instance_set, true)
142
+ association_instance_set(reflection.name, nil)
143
+ end
144
+ else
145
+ warn "warn: #{self.class}##{reflection.name} #{association_type} association " \
146
+ "unexpectedly returned a #{dependent.class}. skipping."
147
+ end
148
+ end
149
+ end
150
+
151
+ # Dump objects associated with an AR object through an association name.
152
+ #
153
+ # object - AR object instance.
154
+ # association - Name of the association whose objects should be dumped.
155
+ #
156
+ # Returns nothing.
157
+ def dump_association_replicants(dumper, association)
158
+ if reflection = self.class.reflect_on_association(association)
159
+ objects = __send__(reflection.name)
160
+ dumper.dump(objects)
161
+ if reflection.macro == :has_and_belongs_to_many
162
+ dump_has_and_belongs_to_many_replicant(dumper, reflection)
163
+ end
164
+ object = __send__(reflection.name) # clear to allow GC
165
+ if object.respond_to?(:reset)
166
+ object.reset
167
+ end
168
+ else
169
+ warn "error: #{self.class}##{association} is invalid"
170
+ end
171
+ end
172
+
173
+ # Dump the special Habtm object used to establish many-to-many
174
+ # relationships between objects that have already been dumped. Note that
175
+ # this object and all objects referenced must have already been dumped
176
+ # before calling this method.
177
+ def dump_has_and_belongs_to_many_replicant(dumper, reflection)
178
+ dumper.dump Habtm.new(self, reflection)
179
+ end
180
+ end
181
+
182
+ # Mixin for the ActiveRecord class.
183
+ module ClassMethods
184
+ # Set and retrieve list of association names that should be dumped when
185
+ # objects of this class are dumped. This method may be called multiple
186
+ # times to add associations.
187
+ def replicate_associations(*names)
188
+ self.replicate_associations += names if names.any?
189
+ @replicate_associations || superclass.replicate_associations
190
+ end
191
+
192
+ # Set the list of association names to dump to the specific set of values.
193
+ def replicate_associations=(names)
194
+ @replicate_associations = names.uniq.map { |name| name.to_sym }
195
+ end
196
+
197
+ # Compound key used during load to locate existing objects for update.
198
+ # When no natural key is defined, objects are created new.
199
+ #
200
+ # attribute_names - Macro style setter.
201
+ def replicate_natural_key(*attribute_names)
202
+ self.replicate_natural_key = attribute_names if attribute_names.any?
203
+ @replicate_natural_key || superclass.replicate_natural_key
204
+ end
205
+
206
+ # Set the compound key used to locate existing objects for update when
207
+ # loading. When not set, loading will always create new records.
208
+ #
209
+ # attribute_names - Array of attribute name symbols
210
+ def replicate_natural_key=(attribute_names)
211
+ @replicate_natural_key = attribute_names
212
+ end
213
+
214
+ # Set or retrieve whether replicated object should keep its original id.
215
+ # When not set, replicated objects will be created with new id.
216
+ def replicate_id(boolean=nil)
217
+ self.replicate_id = boolean unless boolean.nil?
218
+ @replicate_id.nil? ? superclass.replicate_id : @replicate_id
219
+ end
220
+
221
+ # Set flag for replicating original id.
222
+ def replicate_id=(boolean)
223
+ self.replicate_natural_key = [self.primary_key.to_sym] if boolean
224
+ @replicate_id = boolean
225
+ end
226
+
227
+ # Set which, if any, attributes should not be dumped. Also works for
228
+ # associations.
229
+ #
230
+ # attribute_names - Macro style setter.
231
+ def replicate_omit_attributes(*attribute_names)
232
+ self.replicate_omit_attributes = attribute_names if attribute_names.any?
233
+ @replicate_omit_attributes || superclass.replicate_omit_attributes
234
+ end
235
+
236
+ # Set which, if any, attributes should not be dumped. Also works for
237
+ # associations.
238
+ #
239
+ # attribute_names - Array of attribute name symbols
240
+ def replicate_omit_attributes=(attribute_names)
241
+ @replicate_omit_attributes = attribute_names
242
+ end
243
+
244
+ # Load an individual record into the database. If the models defines a
245
+ # replicate_natural_key then an existing record will be updated if found
246
+ # instead of a new record being created.
247
+ #
248
+ # type - Model class name as a String.
249
+ # id - Primary key id of the record on the dump system. This must be
250
+ # translated to the local system and stored in the keymap.
251
+ # attrs - Hash of attributes to set on the new record.
252
+ #
253
+ # Returns the ActiveRecord object instance for the new record.
254
+ def load_replicant(type, id, attributes)
255
+ instance = replicate_find_existing_record(attributes) || new
256
+ create_or_update_replicant instance, attributes
257
+ end
258
+
259
+ # Locate an existing record using the replicate_natural_key attribute
260
+ # values.
261
+ #
262
+ # Returns the existing record if found, nil otherwise.
263
+ def replicate_find_existing_record(attributes)
264
+ return if replicate_natural_key.empty?
265
+ conditions = {}
266
+ replicate_natural_key.each do |attribute_name|
267
+ conditions[attribute_name] = attributes[attribute_name.to_s]
268
+ end
269
+ where(conditions).first
270
+ end
271
+
272
+ # Update an AR object's attributes and persist to the database without
273
+ # running validations or callbacks.
274
+ #
275
+ # Returns the [id, object] tuple for the newly replicated objected.
276
+ def create_or_update_replicant(instance, attributes)
277
+ # write replicated attributes to the instance
278
+ attributes.each do |key, value|
279
+ next if key == primary_key and not replicate_id
280
+ instance.send :write_attribute, key, value
281
+ end
282
+
283
+ # save the instance bypassing all callbacks and validations
284
+ replicate_disable_callbacks instance
285
+ instance.save :validate => false
286
+
287
+ [instance.send(instance.class.primary_key), instance]
288
+ end
289
+
290
+ # Disable all callbacks on an ActiveRecord::Base instance. Only the
291
+ # instance is effected. There is no way to re-enable callbacks once
292
+ # they've been disabled on an object.
293
+ def replicate_disable_callbacks(instance)
294
+ def instance.run_callbacks(*args)
295
+ yield if block_given?
296
+ end
297
+ end
298
+
299
+ end
300
+
301
+ # Special object used to dump the list of associated ids for a
302
+ # has_and_belongs_to_many association. The object includes attributes for
303
+ # locating the source object and writing the list of ids to the appropriate
304
+ # association method.
305
+ class Habtm
306
+ def initialize(object, reflection)
307
+ @object = object
308
+ @reflection = reflection
309
+ end
310
+
311
+ def id
312
+ end
313
+
314
+ def attributes
315
+ ids = @object.__send__("#{@reflection.name.to_s.singularize}_ids")
316
+ {
317
+ 'id' => [:id, @object.class.to_s, @object.id],
318
+ 'class' => @object.class.to_s,
319
+ 'ref_class' => @reflection.klass.to_s,
320
+ 'ref_name' => @reflection.name.to_s,
321
+ 'collection' => [:id, @reflection.klass.to_s, ids]
322
+ }
323
+ end
324
+
325
+ def dump_replicant(dumper, opts={})
326
+ type = self.class.name
327
+ id = "#{@object.class.to_s}:#{@reflection.name}:#{@object.id}"
328
+ dumper.write type, id, attributes, self
329
+ end
330
+
331
+ def self.load_replicant(type, id, attrs)
332
+ object = attrs['class'].constantize.find(attrs['id'])
333
+ ids = attrs['collection']
334
+ object.__send__("#{attrs['ref_name'].to_s.singularize}_ids=", ids)
335
+ [id, new(object, nil)]
336
+ end
337
+ end
338
+
339
+ # Load active record and install the extension methods.
340
+ ::ActiveRecord::Base.send :include, InstanceMethods
341
+ ::ActiveRecord::Base.send :extend, ClassMethods
342
+ ::ActiveRecord::Base.replicate_associations = []
343
+ ::ActiveRecord::Base.replicate_natural_key = []
344
+ ::ActiveRecord::Base.replicate_omit_attributes = []
345
+ ::ActiveRecord::Base.replicate_id = false
346
+ end
347
+ end
@@ -0,0 +1,142 @@
1
+ module Replicate
2
+ # Dump replicants in a streaming fashion.
3
+ #
4
+ # The Dumper takes objects and generates one or more replicant objects. A
5
+ # replicant has the form [type, id, attributes] and describes exactly one
6
+ # addressable record in a datastore. The type and id identify the model
7
+ # class name and model primary key id. The attributes Hash is a set of attribute
8
+ # name to primitively typed object value mappings.
9
+ #
10
+ # Example dump session:
11
+ #
12
+ # >> Replicate::Dumper.new do |dumper|
13
+ # >> dumper.marshal_to $stdout
14
+ # >> dumper.log_to $stderr
15
+ # >> dumper.dump User.find(1234)
16
+ # >> end
17
+ #
18
+ class Dumper < Emitter
19
+ # Create a new Dumper.
20
+ #
21
+ # io - IO object to write marshalled replicant objects to.
22
+ # block - Dump context block. If given, the end of the block's execution
23
+ # is assumed to be the end of the dump stream.
24
+ def initialize(io=nil)
25
+ @memo = Hash.new { |hash,k| hash[k] = {} }
26
+ super() do
27
+ marshal_to io if io
28
+ yield self if block_given?
29
+ end
30
+ end
31
+
32
+ # Register a filter to write marshalled data to the given IO object.
33
+ def marshal_to(io)
34
+ listen { |type, id, attrs, obj| Marshal.dump([type, id, attrs], io) }
35
+ end
36
+
37
+ # Register a filter to write status information to the given stream. By
38
+ # default, a single line is used to report object counts while the dump is
39
+ # in progress; dump counts for each class are written when complete. The
40
+ # verbose and quiet options can be used to increase or decrease
41
+ # verbosity.
42
+ #
43
+ # out - An IO object to write to, like stderr.
44
+ # verbose - Whether verbose output should be enabled.
45
+ # quiet - Whether quiet output should be enabled.
46
+ #
47
+ # Returns the Replicate::Status object.
48
+ def log_to(out=$stderr, verbose=false, quiet=false)
49
+ use Replicate::Status, 'dump', out, verbose, quiet
50
+ end
51
+
52
+ # Load a dump script. This evals the source of the file in the context
53
+ # of a special object with a #dump method that forwards to this instance.
54
+ # Dump scripts are useful when you want to dump a lot of stuff. Call the
55
+ # dump method as many times as necessary to dump all objects.
56
+ def load_script(path)
57
+ dumper = self
58
+ object = ::Object.new
59
+ meta = (class<<object;self;end)
60
+ [:dump, :load_script].each do |method|
61
+ meta.send(:define_method, method) { |*args| dumper.send(method, *args) }
62
+ end
63
+ file = find_file(path)
64
+ object.instance_eval File.read(file), file, 0
65
+ end
66
+
67
+ # Dump one or more objects to the internal array or provided dump
68
+ # stream. This method guarantees that the same object will not be dumped
69
+ # more than once.
70
+ #
71
+ # objects - ActiveRecord object instances.
72
+ #
73
+ # Returns nothing.
74
+ def dump(*objects)
75
+ opts = if objects.last.is_a? Hash
76
+ objects.pop
77
+ else
78
+ {}
79
+ end
80
+ objects = objects[0] if objects.size == 1 && objects[0].respond_to?(:to_ary)
81
+ objects.each do |object|
82
+ next if object.nil? || dumped?(object)
83
+ if object.respond_to?(:dump_replicant)
84
+ args = [self]
85
+ args << opts unless object.method(:dump_replicant).arity == 1
86
+ object.dump_replicant(*args)
87
+ else
88
+ raise NoMethodError, "#{object.class} must respond to #dump_replicant"
89
+ end
90
+ end
91
+ end
92
+
93
+ # Check if object has been written yet.
94
+ def dumped?(object)
95
+ if object.respond_to?(:replicant_id)
96
+ type, id = object.replicant_id
97
+ elsif object.is_a?(Array)
98
+ type, id = object
99
+ else
100
+ return false
101
+ end
102
+ @memo[type.to_s][id]
103
+ end
104
+
105
+ # Called exactly once per unique type and id. Emits to all listeners.
106
+ #
107
+ # type - The model class name as a String.
108
+ # id - The record's id. Usually an integer.
109
+ # attributes - All model attributes.
110
+ # object - The object this dump is generated for.
111
+ #
112
+ # Returns the object.
113
+ def write(type, id, attributes, object)
114
+ type = type.to_s
115
+ return if dumped?([type, id])
116
+ @memo[type][id] = true
117
+
118
+ emit type, id, attributes, object
119
+ end
120
+
121
+ # Retrieve dumped object counts for all classes.
122
+ #
123
+ # Returns a Hash of { class_name => count } where count is the number of
124
+ # objects dumped with a class of class_name.
125
+ def stats
126
+ stats = {}
127
+ @memo.each { |class_name, items| stats[class_name] = items.size }
128
+ stats
129
+ end
130
+
131
+ protected
132
+ def find_file(path)
133
+ path = "#{path}.rb" unless path =~ /\.rb$/
134
+ return path if File.exists? path
135
+ $LOAD_PATH.each do |prefix|
136
+ full_path = File.expand_path(path, prefix)
137
+ return full_path if File.exists? full_path
138
+ end
139
+ false
140
+ end
141
+ end
142
+ end