class SourcedAttributes::Source
Constants
- DEFAULT_OPTIONS
The default values for source-agnostic options that can be overridden by including new values in the Hash argument to `sources_attributes_from`.
Public Class Methods
A factory for creating instances of Source
subclasses based on the parameterized name passed in and the list of registered subclasses.
# File lib/sourced_attributes/source.rb, line 12 def create name, opts, klass (@@subclasses[name] or Source).new klass, opts end
# File lib/sourced_attributes/source.rb, line 31 def initialize klass, opts={} # The model this source is working on. @klass = klass # A configuration hash for source-agnostic options, passed in as a Hash # from the arguments given to the `sources_attributes_from` helper. @options = DEFAULT_OPTIONS.merge(opts) # A generic configuration hash for source-specific options, managed # through the `configure` helper. @config = {} # The primary key that this source will use to find records to update. # `local` is the alias of the primary key in the locally, while `source` # is the alias of the primary key in the source data. @primary_key = { local: :id, source: :id } # A mapping of attributes on the model to field names from the source. @attribute_map = {} # A mapping of attributes which require special preparation to Procs # which can perform that preparation, provided by the configuration. @complex_attributes = {} # A mapping of attributes which are only to be updated when a given # condition is met to Procs which represent that condition. @conditional_attributes = {} # A list of associations that this source will update. Each entry is a # hash, containing the keys :name, :primary_key, :preload. @associations = [] # The most recent set of data from the source, formatted as a Hash using # the :primary_key values as keys. Updated by `refresh` and used by # `apply` to update records. @source_data = [] # The last-retrieved set of data from the source, formatted the same way # as @source_data. @previous_data = [] # The records that that the source data will affect. Updated by # `refresh_affected_records`. @affected_records = {} end
Subclasses of Source
should register aliases with the factory through this method.
# File lib/sourced_attributes/source.rb, line 18 def register_source name @@subclasses[name] = self end
Public Instance Methods
Apply the current set of source data to the records it affects
# File lib/sourced_attributes/source.rb, line 175 def apply # Make sure the source data is up-to-date refresh # Make sure the source data is indexed by the primary key ensure_indexed_source_data # Make sure that all of the affected records are loaded refresh_affected_records # Preload all key-value pairs needed to fully associate this record. preload_association_data # Wrap all of the updates in a single transaction @klass.transaction do if @options[:batch_size] @affected_records.each_slice(@options[:batch_size]) do |batch| @klass.import batch.map{ |pk, record| update_record(pk, record); record } end else @affected_records.each do |pk, record| update_record pk, record end end end end
Apply the current set of source data to the associations for the given primary key.
# File lib/sourced_attributes/source.rb, line 152 def apply_associations_to pk, record @associations.each do |config| # Get the model that this association references reflection = @klass.reflect_on_association(config[:name]) # The associated records are already loaded, but need to be plucked out # of their containing hash associated_records = config[:data][@source_data[pk][config[:source_key]]] # Apply the updated association to the record record.assign_attributes(config[:name] => associated_records) end end
Apply the current set of source data to the attributes for the given primary key.
# File lib/sourced_attributes/source.rb, line 144 def apply_attributes_to pk, record # Map the attributes from the source data to their local counterparts # and apply it to the record record.assign_attributes(mapped_attributes_for(pk)) end
Create new instances of @klass for every key that does not already have an instance associated with it.
# File lib/sourced_attributes/source.rb, line 93 def create_new_records @source_data.keys.each do |pk| # TODO: Add an option for creating/not creating new records @affected_records[pk] ||= @klass.new(@primary_key[:local] => pk) end end
Create a Hash from the @source_data array, keyed by @primary_key. If @source_data is already a Hash, assume it has already been indexed and do nothing.
# File lib/sourced_attributes/source.rb, line 83 def ensure_indexed_source_data return if @source_data.is_a?(Hash) @source_data = @source_data.inject({}) do |hash, datum| hash[datum[@primary_key[:source]]] = datum hash end end
Apply the attribute map to the source data for the given record
# File lib/sourced_attributes/source.rb, line 114 def mapped_attributes_for pk source = @source_data[pk] @attribute_map.inject({}) do |hash, (attribute,_)| # Only apply conditional attributes if they're condition is met if @conditional_attributes.has_key?(attribute) next hash unless @conditional_attributes[attribute].call(source) end hash[attribute] = resolve_attribute_for_datum(attribute, source) hash end end
Load into memory all key-value pairs needed to fully associate all records that a source handles.
# File lib/sourced_attributes/source.rb, line 128 def preload_association_data @associations.each do |assoc| # Get the model that this association references. reflection = @klass.reflect_on_association(assoc[:name]) # Determine which key-value pairs should be preloaded. values = @source_data.map{ |key, datum| datum[assoc[:source_key]] } # Load the data, keyed by the local primary key assoc[:data] = reflection.klass \ .where(assoc[:primary_key] => values) \ .select(assoc[:primary_key], reflection.klass.primary_key) \ .index_by(&assoc[:primary_key]) end end
Talk to the data source to refresh the contents of @source_data.
# File lib/sourced_attributes/source.rb, line 204 def refresh; raise :subclass_responsiblity; end
Fill @affected_records with all records affected by the current set of source data, creating new records for any keys which do not yet exist.
# File lib/sourced_attributes/source.rb, line 102 def refresh_affected_records # Ensure that the source data is indexed by primary key... ensure_indexed_source_data # ...so that it can be skimmed to find existing records. @affected_records = @klass \ .where(@primary_key[:local] => @source_data.keys) \ .index_by(&@primary_key[:local]) # Then create new objects for the remaining data create_new_records if @options[:create_new] end
Given an attribute name and a primary key, resolve the value to be given to that attribute using the configuration supplied through the DSL
.
# File lib/sourced_attributes/source.rb, line 69 def resolve_attribute_for_datum attribute, datum # The alias of this attribute in the source data source_name = @attribute_map[attribute] # Complex Attributes are evaluated with the datum as a parameter if @complex_attributes.has_key?(attribute) @complex_attributes[attribute].call(datum) else datum[source_name] end end
Perform all of the operations related to updating a sourced record
# File lib/sourced_attributes/source.rb, line 165 def update_record pk, record # Update attributes apply_attributes_to(pk, record) # Update associations apply_associations_to(pk, record) # Save the record if it should be record.save if (@options[:save] && !@options[:batch_size]) end