Model Views and Events

We can think of all of these like rules in code.  Back to our example set, we have this file: mod/05_standard/set/type/set.rb  Which has all of the code customizations related to this set (Set+*type), and you can see in the path as 'type/set.rb'.  The key difference is we look up a data rule when we want to use it, but we load all the code rules with the application and expect it to be there to run when called.

Easy enough to load up all the code, but how does each card instance get all the rules it needs?  A typical way to do this with active record support is with subclasses and single table inheritance.  We started that way with cards, and found that it made initializing cards a pain because you had to have the type very early on in the object initialization.  And it doesn't support other kinds of sets redily either.

Instead, we load up the singleton class.  In ruby and similar object oriented languages, an object has two classes.  The one we normally think of with all the common methods, and one that has methods that only apply to this instance.  The '*all' set (path .../set/all/name.rb) modules are just included on the regular (shared) class one time when they are loaded, but the rest are handled per-instance.  When loaded, our example module will have defined a module Card::Set::Type::Set, and when we are initializing a card there is a step that can be optionally skipped to load the set modules.  We skip it if we only want to see if a card exists or similar, and you lose the modules on cards retrieved from the cache.  The card remembers whether this step was done, and it will check/load as needed by the code context.

To load the modules, it goes through the sets from least specific to most and include any modules it finds (remember *all is already loaded, so we don't do those).  Our example 'Set+*type' has the type Set, so it is in it's own type.  When we load 'Set+*type' (or other Set cards like '*rstar' or 'tag+*right') it will include Card::Set::Type::Set on that instance.  It would also load Card::Set::Rstar because *type is a star card and is on the right.  It loads the more specific (the Set::Type here) last so that if the same methods are defined, the most specific version will win.

Events build on active record callbacks.  That system is designed for all objects of a class having the same events triggered.  If I want some events that are specific to updating *account+*right, it will still want to run that event on all cards, but only cards in the set will have the final callback method defined.  That just means we have to test whether the card has included the module that defined the event before triggering.

  define_callbacks :approve, :store, :extend

  before_validation :approve
  around_save :store
  after_save :extend

Thats how our events relate to active record events.  All other events are defined relative to (:before, :after or :around) these three: :approve, :store and :extend.  If you need more, you could add them in a Card::Set::All module.  You can even reference the active record events directly, but that will make your module depend on active record.  If it depends on it for other reasons already, then you don't give up much, but if you can avoid it, the module will be more broadly applicable in the world of cards.

Views are similarly defined in sets, but they will also be inside a format block.  When defined, they are defined into a formatter class or module, so views on *all sets are defined straight into the class (Card::TextFormat < Card::Format as an example), or into nested modules for sets: Card::TextFormat::Type::Set for views on Set+*type.  Then when the formatter is initialized, always with a card to format, it will load the formatter modules for the card's sets onto the sigleton (instance) class.  Because recursive inclusion will instantiate a new formatter instance for each included card, the set based views will get loaded on each sub-formatter in turn.