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

create(name, opts, klass) click to toggle source

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
new(klass, opts={}) click to toggle source
# 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
register_source(name) click to toggle source

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() click to toggle source

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_associations_to(pk, record) click to toggle source

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_attributes_to(pk, record) click to toggle source

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_records() click to toggle source

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
ensure_indexed_source_data() click to toggle source

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
mapped_attributes_for(pk) click to toggle source

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
preload_association_data() click to toggle source

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
refresh() click to toggle source

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
refresh_affected_records() click to toggle source

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
resolve_attribute_for_datum(attribute, datum) click to toggle source

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
update_record(pk, record) click to toggle source

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