Class: Sequel::Packer

Inherits:
Object
  • Object
show all
Defined in:
lib/sequel/packer.rb,
lib/sequel/packer/version.rb,
lib/sequel/packer/eager_hash.rb,
lib/sequel/packer/validation.rb,
lib/sequel/packer/eager_loading.rb

Defined Under Namespace

Modules: EagerHash, EagerLoading, Validation Classes: AssociationDoesNotExistError, FieldArgumentError, InvalidAssociationPackerError, ModelNotYetDeclaredError, NoAssociationSubpackerDefinedError, UnknownTraitError, UnnecessaryWithContextError

Constant Summary collapse

METHOD_FIELD =

field(:foo)

:method_field
BLOCK_FIELD =

field(:foo, &block)

:block_field
ASSOCIATION_FIELD =

field(:association, subpacker)

:association_field
ARBITRARY_MODIFICATION_FIELD =

field(&block)

:arbitrary_modification_field
VERSION =
"1.0.2"

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*traits, **context) ⇒ Packer

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



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# 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

Class Method Details

.eager(*associations) ⇒ Object

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.



145
146
147
148
149
150
# 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) ⇒ Object

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.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# 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) ⇒ Object

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.



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 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) ⇒ Object

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



39
40
41
42
43
44
45
46
47
48
49
50
# 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

.pack(data, *traits, **context) ⇒ Object

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.



186
187
188
189
# 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) ⇒ Object

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.



158
159
160
161
162
163
# 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) ⇒ Object

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



118
119
120
121
122
# 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) ⇒ Object

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.



127
128
129
130
131
132
133
134
135
# 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) ⇒ Object

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.



169
170
171
172
173
174
# 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

Instance Method Details

#pack(data) ⇒ Object

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.



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# 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