decko-commons/decko

View on GitHub
card/lib/card/director.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
90%
class Card
  # Directs the symphony of a card {Card::Act **act**}.
  #
  # Each act is divided into {Card::Action **actions**}: one action for each card.
  # There are three action types: _create_, _update_, and _delete_.
  #
  # Each action is divided into three **phases**: _validation_, _storage_, and
  # _integration_.
  #
  # Each phase is divided into three **stages**, as follows:
  #
  # #### Validation Stages
  #
  # * __VI__: initialize
  # * __VP__: prepare_to_validate
  # * __VV__: validate
  #
  # #### Storage Stages
  #
  # * __SP__: prepare_to_store
  # * __SS__: store
  # * __SF__: finalize
  #
  # #### Integration Stages
  #
  # * __II__: integrate
  # * __IA__: after_integrate
  # * __ID__: integrate_with_delay
  #
  # And each stage can have many {Card::Set::Event::Api **events**}, each of which is
  # defined using the {Card::Set::Event::Api Event API}.
  #
  # The table below gives you an overview events can/should do in each stage:
  #
  # |    Phase:                 |    validation    |     storage     |  integration
  # |    Stage:                 |   VI - VP - VV   |   SP - SS - SF  |  II - IA  - ID
  # |---------------------------| :---:            | :---:           |:---:
  # | **tasks**
  # |  attach subcard           | yes!  yes!  yes  | yes   yes   yes | yes   yes   no
  # |  detach subcard           | yes!  yes!  yes  | yes   no    no! |       no!
  # |  validate                 | yes   yes   yes! |       no        |       no
  # |  insecure change [^1]     | yes   yes!  no   |       no!       |       no!
  # |  secure change [^2]       |       yes        | yes!  no!   no! |       no!
  # |  abort                    |       yes!       |       yes       |       yes
  # |  add errors               |       yes!       |       no!       |       no!
  # |  subsave                  |       yes        |       yes       |       yes!
  # |  has id (new card)        |       no         | no    no?   yes |       yes
  # |  within web request       |       yes        |       yes       | yes   yes   no
  # |  within transaction [^3]  |       yes        |       yes       |       no
  # | **values**
  # |    dirty attributes       |       yes        |       yes       |       yes
  # |    params                 |       yes        |       yes       |       yes
  # |    success                |       yes        |       yes       |       yes
  # |    session                |       yes        |       yes       | yes   yes   no
  #
  # #### Understanding the Table
  #
  #  - **yes!**  the recommended stage to do that
  #  - **yes**   ok to do it here
  #  - **no**    not recommended; risky but not guaranteed to fail
  #  - **no!**   never do it here. it won't work or will break things
  #
  # If there is only a single entry in a phase column it counts for all stages
  # of that phase
  #
  # [^1]: 'insecure' means a change that might make the card invalid to save
  # [^2]: 'secure' means you're sure that the change won't invalidate the card
  # [^3]: If an exception is raised in the validation or storage phase
  #       everything will rollback. If an integration event fails, db changes
  #       of the other two phases will remain persistent, and other integration
  #       events will continue to run.
  #
  # ## Director, Directors, and Subdirectors
  #
  # Only one act can be performed at a time in any given Card process. Information about
  # that act is managed by _Director class methods_. Every act is associated with a
  # single "main" card.
  #
  # The act, however, may involve many cards/actions. Each action has its own _Director
  # instance_ that leads the card through all its stages. When a card action (A1)
  # initiates a new action on a different card (A2), a new Director object is initialized.
  # The new A2 subdirector's @parent is the director of the A1 card. Conversely, the
  # A1 card stores a SubdirectorArray in @subdirectors to give it access to A2's
  # Director and any little Director babies to which it gave birth.
  #
  # Subdirectors follow one of two distinct patterns:
  #
  # 1. {Card::Subcards **Subcards**}. When a card is altered using the subcards API, the
  #    director follows a "breadth-first" pattern. For each stage a card runs its
  #    stage events and then triggers its subcards to run that stage before proceeding
  #    to the next stage. If a subcard is added in a stage then by the end of that
  #    stage the director will catch it up to the current stage.
  # 2. **Subsaves**. When a card is altered by a direct save (`Card.create(!)`,
  #    `card.update(!)`, `card.delete(!)`, `card.save(!)`...), then the validation and
  #    storage phases are executed immediately (depth-first), returning the saved card.
  #    The integration phase, however, is executed following the same pattern as with
  #    subcards.
  #
  # Let's consider a subcard example. Suppose you define the following event on
  # self/bar.rb
  #
  #     event :met_a_foo_at_the_bar, :prepare_to_store, on: :update do
  #       subcard "foo"
  #     end
  #
  # And then you run `Card[:bar].update!({})`.
  #
  # When bar reaches the event in its `prepare_to_store` stage, the "foo" subcard will be
  # added. After that stage ends, the stages `initialize`, `prepare_to_validate`,
  # `validate`, and `prepare_to_store` are executed for foo so that it is now caught
  # up with Bar at the `prepare_to_store` stage.
  #
  # If you have subcards within subcards, stages are executed preorder depth-first.
  #
  # Eg, assuming:
  #
  # - A has subcards AA and AB
  # - AA has subcard AAA
  # - AB has subcard ABA
  #
  # ...then the order of execution is:
  #
  # 1. A
  # 2. AA
  # 3. AAA
  # 4. AB
  # 5. ABA
  #
  # A special case can happen in the store stage when a supercard needs a subcard's id
  # (for example as left_id or as type_id) and the subcard doesn't have an id yet
  # (because it gets created in the same act). In this case the subcard's store stage
  # is executed BEFORE the supercard's store stage.
  #
  # ---
  class Director
    extend ClassMethods

    include Stages
    include Phases
    include Run
    include Store

    attr_accessor :act, :card, :current_stage_index, :parent, :subdirectors, :head
    attr_reader :running
    alias_method :running?, :running

    def initialize card, parent
      @card = card
      @card.director = self
      # for read actions there is no validation phase
      # so we have to set the action here
      @current_stage_index = nil
      @running = false
      @prepared = false
      @parent = parent
      @subdirectors = SubdirectorArray.initialize_with_subcards(self)
      register
    end

    def main?
      parent.nil?
    end

    def head?
      @head || main?
    end

    def register
      Director.add self
    end

    def unregister
      Director.delete self
    end

    def delete
      @parent&.subdirectors&.delete self
      @card.director = nil
      @subdirectors.clear
      @current_stage_index = nil
      @action = nil
      @running = false
    end

    def appoint card
      reset_stage
      update_card card
      @head = true
    end

    def abort
      @abort = true
    end

    def need_act
      act_director = main_director
      raise Card::Error, "act requested without a main director" unless act_director

      @act = act_director.act ||= Director.need_act
    end

    def main_director
      return self if main?

      Director.act_director || @parent&.main_director
    end

    def to_s level=1
      str = @card.name.to_s.clone
      if @subdirectors.present?
        subs = subdirectors.map { |d| "  " * level + d.to_s(level + 1) }.join "\n"
        str << "\n#{subs}"
      end
      str
    end

    def replace_card card
      card.action = @card.action
      card.director = self
      @card = card
      reset_stage
      catch_up_to_stage @current_stage_index if @current_stage_index
    end

    def update_card card
      old_card = @card
      @card = card
      Director.card_changed old_card
    end
  end
end