card/lib/card/director.rb
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