class Sequel::Packer

Constants

ARBITRARY_MODIFICATION_FIELD

field(&block)

ASSOCIATION_FIELD

field(:association, subpacker)

BLOCK_FIELD

field(:foo, &block)

METHOD_FIELD

field(:foo)

VERSION

Public Class Methods

eager(*associations) click to toggle source

Specify additional eager loading that should take place when fetching data to be packed. Commonly used to add filters to association datasets via eager procs.

Users should not assume when using eager procs that the proc actually gets executed. If models with their associations already loaded are passed to pack then the proc will never get processed. Any filtering logic should be duplicated within a field block.

# File lib/sequel/packer.rb, line 145
def self.eager(*associations)
  @class_eager_hash = EagerHash.merge!(
    @class_eager_hash,
    EagerHash.normalize_eager_args(*associations),
  )
end
field(field_name=nil, subpacker=nil, *traits, &block) click to toggle source

Declare a field to be packed in the output hash. This method can be called in multiple ways:

field(:field_name)

  • Calls the method :field_name on a model and stores the result under the key :field_name in the packed hash.

field(:field_name, &block)

  • Yields the model to the block and stores the result under the key :field_name in the packed hash.

field(:association, subpacker, *traits)

  • Packs model.association using the designated subpacker with the specified traits.

field(&block)

  • Yields the model and the partially packed hash to the block, allowing for arbitrary modification of the output hash.

# File lib/sequel/packer.rb, line 79
def self.field(field_name=nil, subpacker=nil, *traits, &block)
  Validation.check_field_arguments(
    @model, field_name, subpacker, traits, &block)
  field_type = determine_field_type(field_name, subpacker, block)

  if field_type == ASSOCIATION_FIELD
    set_association_packer(field_name, subpacker, *traits)
  end

  @class_fields << {
    type: field_type,
    name: field_name,
    block: block,
  }
end
inherited(subclass) click to toggle source

Think of this method as the “initialize” method for a Packer class. Every Packer class keeps track of the fields, traits, and other various operations defined using the DSL internally.

# File lib/sequel/packer.rb, line 18
def self.inherited(subclass)
  subclass.instance_variable_set(:@model, @model)
  subclass.instance_variable_set(:@class_fields, @class_fields&.dup || [])
  subclass.instance_variable_set(:@class_traits, @class_traits&.dup || {})
  subclass.instance_variable_set(:@class_packers, @class_packers&.dup || {})
  subclass.instance_variable_set(
    :@class_eager_hash,
    EagerHash.deep_dup(@class_eager_hash),
  )
  subclass.instance_variable_set(
    :@class_precomputations,
    @class_precomputations&.dup || [],
  )
  subclass.instance_variable_set(
    :@class_with_contexts,
    @class_with_contexts&.dup || [],
  )
end
model(klass) click to toggle source

Declare the type of Sequel::Model this Packer will be used for. Used to validate associations at declaration time.

# File lib/sequel/packer.rb, line 39
def self.model(klass)
  if !(klass < Sequel::Model)
    fail(
      ArgumentError,
      'model declaration must be a subclass of Sequel::Model',
    )
  end

  fail ArgumentError, 'model already declared' if @model

  @model = klass
end
new(*traits, **context) click to toggle source

Initialize a Packer instance with the given traits and additional context. This Packer can then pack multiple datasets or models via the pack method.

# File lib/sequel/packer.rb, line 193
def initialize(*traits, **context)
  @context = context

  @subpackers = {}

  # Technically we only need to duplicate these fields if we modify any of
  # them, but manually implementing some sort of copy-on-write functionality
  # is messy and error prone.
  @instance_fields = class_fields.dup
  @instance_packers = class_packers.dup
  @instance_eager_hash = EagerHash.deep_dup(class_eager_hash)
  @instance_precomputations = class_precomputations.dup

  class_with_contexts.each do |with_context_block|
    self.instance_exec(&with_context_block)
  end

  # Evaluate trait blocks, which might add new fields to @instance_fields,
  # new packers to @instance_packers, new associations to
  # @instance_eager_hash, and/or new precomputations to
  # @instance_precomputations.
  traits.each do |trait|
    trait_block = class_traits[trait]
    if !trait_block
      raise UnknownTraitError, "Unknown trait for #{self.class}: :#{trait}"
    end

    self.instance_exec(&trait_block)
  end

  # Create all the subpackers, and merge in their eager hashes.
  @instance_packers.each do |association, (subpacker, traits)|
    association_packer = subpacker.new(*traits, **@context)

    @subpackers[association] = association_packer

    @instance_eager_hash = EagerHash.merge!(
      @instance_eager_hash,
      {association => association_packer.send(:eager_hash)},
    )
  end
end
pack(data, *traits, **context) click to toggle source

Pack the given data with the specified traits and additional context. Context is automatically passed down to any subpackers.

Data can be provided as a Sequel::Dataset, an array of Sequel::Models, a single Sequel::Model, or nil. Even when passing models that have already been materialized, eager loading will be used to efficiently fetch associations.

Returns an array of packed hashes, or a single packed hash if a single model was passed in. Returns nil if nil was passed in.

# File lib/sequel/packer.rb, line 186
def self.pack(data, *traits, **context)
  return nil if !data
  new(*traits, **context).pack(data)
end
precompute(&block) click to toggle source

Declare an arbitrary operation to be performed one all the data has been fetched. The block will be executed once and be passed all of the models that will be packed by this Packer, even if this Packer is nested as a subpacker of other packers. The block can save the result of the computation in an instance variable which can then be accessed in the blocks passed to field.

# File lib/sequel/packer.rb, line 158
def self.precompute(&block)
  if !block
    raise ArgumentError, 'Sequel::Packer.precompute must be passed a block'
  end
  @class_precomputations << block
end
set_association_packer(association, subpacker, *traits) click to toggle source

Register that nested models related to the packed model by association should be packed using the given subpacker with the specified traits.

# File lib/sequel/packer.rb, line 118
def self.set_association_packer(association, subpacker, *traits)
  Validation.check_association_packer(
    @model, association, subpacker, traits)
  @class_packers[association] = [subpacker, traits]
end
trait(name, &block) click to toggle source

Define a trait, a set of optional fields that can be packed in certain situations. The block can call main Packer DSL methods: field, set_association_packer, eager, or precompute.

# File lib/sequel/packer.rb, line 127
def self.trait(name, &block)
  if @class_traits.key?(name)
    raise ArgumentError, "Trait :#{name} already defined"
  end
  if !block_given?
    raise ArgumentError, 'Must give a block when defining a trait'
  end
  @class_traits[name] = block
end
with_context(&block) click to toggle source

Declare a block to be called after a Packer has been initialized with context. The block can call the common Packer DSL methods. It is most commonly used to pass eager procs that depend on the Packer context to eager.

# File lib/sequel/packer.rb, line 169
def self.with_context(&block)
  if !block
    raise ArgumentError, 'Sequel::Packer.with_context must be passed a block'
  end
  @class_with_contexts << block
end

Private Class Methods

determine_field_type( field_name, subpacker, block ) click to toggle source

Helper for determing a field type from the arguments to field.

# File lib/sequel/packer.rb, line 96
                     def self.determine_field_type(
  field_name,
  subpacker,
  block
)
  if block
    if field_name
      BLOCK_FIELD
    else
      ARBITRARY_MODIFICATION_FIELD
    end
  else
    if subpacker
      ASSOCIATION_FIELD
    else
      METHOD_FIELD
    end
  end
end

Public Instance Methods

pack(data) click to toggle source

Pack the given data with the traits and additional context specified when the Packer instance was created.

Data can be provided as a Sequel::Dataset, an array of Sequel::Models, a single Sequel::Model, or nil. Even when passing models that have already been materialized, eager loading will be used to efficiently fetch associations.

Returns an array of packed hashes, or a single packed hash if a single model was passed in. Returns nil if nil was passed in.

# File lib/sequel/packer.rb, line 246
def pack(data)
  case data
  when Sequel::Dataset
    data = data.eager(@instance_eager_hash) if @instance_eager_hash
    models = data.all

    run_precomputations(models)
    pack_models(models)
  when Sequel::Model
    if @instance_eager_hash
      EagerLoading.eager_load(class_model, [data], @instance_eager_hash)
    end

    run_precomputations([data])
    pack_model(data)
  when Array
    if @instance_eager_hash
      EagerLoading.eager_load(class_model, data, @instance_eager_hash)
    end

    run_precomputations(data)
    pack_models(data)
  when NilClass
    nil
  end
end

Private Instance Methods

class_eager_hash() click to toggle source
# File lib/sequel/packer.rb, line 434
def class_eager_hash
  self.class.instance_variable_get(:@class_eager_hash)
end
class_fields() click to toggle source
# File lib/sequel/packer.rb, line 430
def class_fields
  self.class.instance_variable_get(:@class_fields)
end
class_model() click to toggle source

The following methods expose the class instance variables containing the core definition of the Packer.

# File lib/sequel/packer.rb, line 426
def class_model
  self.class.instance_variable_get(:@model)
end
class_packers() click to toggle source
# File lib/sequel/packer.rb, line 438
def class_packers
  self.class.instance_variable_get(:@class_packers)
end
class_precomputations() click to toggle source
# File lib/sequel/packer.rb, line 446
def class_precomputations
  self.class.instance_variable_get(:@class_precomputations)
end
class_traits() click to toggle source
# File lib/sequel/packer.rb, line 442
def class_traits
  self.class.instance_variable_get(:@class_traits)
end
class_with_contexts() click to toggle source
# File lib/sequel/packer.rb, line 450
def class_with_contexts
  self.class.instance_variable_get(:@class_with_contexts)
end
eager(*associations) click to toggle source

See the definition of self.eager. This method accepts the exact same arguments. When used within a trait block, this method is called rather than the class method.

# File lib/sequel/packer.rb, line 390
def eager(*associations)
  @instance_eager_hash = EagerHash.merge!(
    @instance_eager_hash,
    EagerHash.normalize_eager_args(*associations),
  )
end
eager_hash() click to toggle source

Access the internal eager hash.

# File lib/sequel/packer.rb, line 419
def eager_hash
  @instance_eager_hash
end
field(field_name=nil, subpacker=nil, *traits, &block) click to toggle source

See the definition of self.field. This method accepts the exact same arguments. When fields are declared within trait blocks, this method is called rather than the class method.

# File lib/sequel/packer.rb, line 358
def field(field_name=nil, subpacker=nil, *traits, &block)
  klass = self.class

  Validation.check_field_arguments(
    class_model, field_name, subpacker, traits, &block)
  field_type =
    klass.send(:determine_field_type, field_name, subpacker, block)

  if field_type == ASSOCIATION_FIELD
    set_association_packer(field_name, subpacker, *traits)
  end

  @instance_fields << {
    type: field_type,
    name: field_name,
    block: block,
  }
end
has_precomputations?() click to toggle source

Check if a Packer has any precompute blocks declared, to avoid the overhead of flattening the child associations.

# File lib/sequel/packer.rb, line 300
def has_precomputations?
  return true if @instance_precomputations.any?
  return false if !@subpackers
  @subpackers.values.any? {|sp| sp.send(:has_precomputations?)}
end
pack_association(association, associated_models) click to toggle source

Pack models from an association using the designated subpacker.

# File lib/sequel/packer.rb, line 335
def pack_association(association, associated_models)
  return nil if !associated_models

  packer = @subpackers[association]

  if !packer
    raise(
      NoAssociationSubpackerDefinedError,
      "pack_association called for the #{class_model}.#{association} " +
        'association, but no Packer has been set for that association.',
    )
  end

  if associated_models.is_a?(Array)
    packer.send(:pack_models, associated_models)
  else
    packer.send(:pack_model, associated_models)
  end
end
pack_model(model) click to toggle source

Pack a single model by processing all of the Packer's declared fields.

# File lib/sequel/packer.rb, line 307
def pack_model(model)
  h = {}

  @instance_fields.each do |field_options|
    field_name = field_options[:name]

    case field_options[:type]
    when METHOD_FIELD
      h[field_name] = model.send(field_name)
    when BLOCK_FIELD
      h[field_name] = instance_exec(model, &field_options[:block])
    when ASSOCIATION_FIELD
      associated_objects = model.send(field_name)
      h[field_name] = pack_association(field_name, associated_objects)
    when ARBITRARY_MODIFICATION_FIELD
      instance_exec(model, h, &field_options[:block])
    end
  end

  h
end
pack_models(models) click to toggle source

Pack an array of models by processing all of the Packer's declared fields.

# File lib/sequel/packer.rb, line 330
def pack_models(models)
  models.map {|m| pack_model(m)}
end
precompute(&block) click to toggle source

See the definition of self.precompute. This method accepts the exact same arguments. When used within a trait block, this method is called rather than the class method.

# File lib/sequel/packer.rb, line 400
def precompute(&block)
  if !block
    raise ArgumentError, 'Sequel::Packer.precompute must be passed a block'
  end
  @instance_precomputations << block
end
run_precomputations(models) click to toggle source

Run any blocks declared using precompute on the given models, as well as any precompute blocks declared by subpackers.

# File lib/sequel/packer.rb, line 277
def run_precomputations(models)
  @instance_packers.each do |association, _|
    subpacker = @subpackers[association]
    next if !subpacker.send(:has_precomputations?)

    reflection = class_model.association_reflection(association)

    if reflection.returns_array?
      all_associated_records = models.flat_map {|m| m.send(association)}.uniq
    else
      all_associated_records = models.map {|m| m.send(association)}.compact
    end

    subpacker.send(:run_precomputations, all_associated_records)
  end

  @instance_precomputations.each do |block|
    instance_exec(models, &block)
  end
end
set_association_packer(association, subpacker, *traits) click to toggle source

See the definition of self.set_association_packer. This method accepts the exact same arguments. When used within a trait block, this method is called rather than the class method.

# File lib/sequel/packer.rb, line 380
def set_association_packer(association, subpacker, *traits)
  Validation.check_association_packer(
    class_model, association, subpacker, traits)

  @instance_packers[association] = [subpacker, traits]
end
with_context(&block) click to toggle source

See the definition of self.with_context. This method accepts the exact same arguments. When used within a trait block, this method is called rather than the class method.

# File lib/sequel/packer.rb, line 410
def with_context(&block)
  raise(
    UnnecessaryWithContextError,
    'There is no need to call with_context from within a trait block; ' +
      '@context can be accessed directly.',
  )
end