Module: StandardLedger::Projector

Extended by:
ActiveSupport::Concern
Defined in:
lib/standard_ledger/projector.rb

Overview

Adds the ‘projects_onto` DSL to an Entry class. Each `projects_onto` declaration registers a single (target_association, mode, projector) tuple; multi-target fan-out is two declarations.

Examples:

block form

class VoucherRecord < ApplicationRecord
  include StandardLedger::Entry
  include StandardLedger::Projector

  ledger_entry kind: :action, idempotency_key: :serial_no, scope: :organisation_id

  projects_onto :voucher_scheme, mode: :inline do
    on(:grant)    { |scheme, _| scheme.increment(:granted_vouchers_count) }
    on(:redeem)   { |scheme, _| scheme.increment(:redeemed_vouchers_count) }
    on(:consume)  { |scheme, _| scheme.increment(:consumed_vouchers_count) }
    on(:clawback) { |scheme, _| scheme.increment(:clawed_back_vouchers_count) }
  end
end

class form

projects_onto :order, mode: :async, via: Orders::FulfillableProjector

Defined Under Namespace

Classes: Definition, HandlerDsl, SqlDsl, TriggerDsl

Instance Method Summary collapse

Instance Method Details

#apply_projection!(definition) ⇒ Boolean

Apply a single projection definition to this entry. Resolves the target association, evaluates the optional ‘if:` guard, looks up the per-kind handler (or falls back to the projector class), and invokes it.

‘lock:` is interpreted by the mode strategies (`Modes::Inline`, …) — not here. The inline strategy needs the lock to span both the handler invocation and the coalesced `target.save!`, so it wraps an entire per-target group rather than a single `apply_projection!` call. See `Modes::Inline#call` for the lock-spans-save guarantee.

The mode strategies call this method; hosts typically do not call it directly.

Parameters:

  • definition (Definition)

    one of the entry class’s registered projections.

Returns:

  • (Boolean)

    ‘true` when the handler (or projector class) ran; `false` when the projection short-circuited (guard returned false, target was nil, or permissive mode found no matching handler).

Raises:



519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
# File 'lib/standard_ledger/projector.rb', line 519

def apply_projection!(definition)
  if definition.mode == :sql
    raise Error,
          "apply_projection! is not supported for mode: :sql; " \
          "the recompute SQL runs through `Modes::Sql#call` directly with no per-kind dispatch"
  end

  if definition.mode == :trigger
    raise Error,
          "apply_projection! is not supported for mode: :trigger; " \
          "the database trigger fires from the DB on INSERT, not from Ruby"
  end

  return false if definition.guard && !instance_exec(&definition.guard)

  target = public_send(definition.target_association)
  return false if target.nil?

  if definition.projector_class
    definition.projector_class.new.apply(target, self)
    return true
  end

  kind = resolve_kind!
  handler = definition.handlers[kind.to_sym]

  if handler.nil?
    if definition.permissive
      handler = definition.handlers[:_]
      return false if handler.nil?
    else
      raise UnhandledKind,
            "#{self.class.name} has no handler for kind=#{kind.inspect} on projection :#{definition.target_association}"
    end
  end

  handler.call(target, self)
  true
end