module Sequel module Plugins # The composition plugin allows you to easily define getter and # setter instance methods for a class where the backing data # is composed of other getters and decomposed to other setters. # # A simple example of this is when you have a database table with # separate columns for year, month, and day, but where you want # to deal with Date objects in your ruby code. This can be handled # with: # # Album.plugin :composition # Album.composition :date, :mapping=>[:year, :month, :day] # # The :mapping option is optional, but you can define custom # composition and decomposition procs via the :composer and # :decomposer options. # # Note that when using the composition object, you should not # modify the underlying columns if you are also instantiating # the composition, as otherwise the composition object values # will override any underlying columns when the object is saved. module Composition # Define the necessary class instance variables. def self.apply(model) model.instance_eval{@compositions = {}} end module ClassMethods # A hash with composition name keys and composition reflection # hash values. attr_reader :compositions # A module included in the class holding the composition # getter and setter methods. attr_reader :composition_module # Define a composition for this model, with name being the name of the composition. # You must provide either a :mapping option or both the :composer and :decomposer options. # # Options: # * :class - if using the :mapping option, the class to use, as a Class, String or Symbol. # * :composer - A proc that is instance evaled when the composition getter method is called # to create the composition. # * :decomposer - A proc that is instance evaled before saving the model object, # if the composition object exists, which sets the columns in the model object # based on the value of the composition object. # * :mapping - An array where each element is either a symbol or an array of two symbols. # A symbol is treated like an array of two symbols where both symbols are the same. # The first symbol represents the getter method in the model, and the second symbol # represents the getter method in the composition object. Example: # # Uses columns year, month, and day in the current model # # Uses year, month, and day methods in the composition object # :mapping=>[:year, :month, :day] # # Uses columns year, month, and day in the current model # # Uses y, m, and d methods in the composition object where # # for example y in the composition object represents year # # in the model object. # :mapping=>[[:year, :y], [:month, :m], [:day, :d]] def composition(name, opts={}) opts = opts.dup compositions[name] = opts if mapping = opts[:mapping] keys = mapping.map{|k| k.is_a?(Array) ? k.first : k} if !opts[:composer] late_binding_class_option(opts, name) klass = opts[:class] class_proc = proc{klass || constantize(opts[:class_name])} opts[:composer] = proc do if values = keys.map{|k| send(k)} and values.any?{|v| !v.nil?} class_proc.call.new(*values) else nil end end end if !opts[:decomposer] setter_meths = keys.map{|k| :"#{k}="} cov_methods = mapping.map{|k| k.is_a?(Array) ? k.last : k} setters = setter_meths.zip(cov_methods) opts[:decomposer] = proc do if (o = compositions[name]).nil? setter_meths.each{|sm| send(sm, nil)} else setters.each{|sm, cm| send(sm, o.send(cm))} end end end end raise(Error, "Must provide :composer and :decomposer options, or :mapping option") unless opts[:composer] && opts[:decomposer] define_composition_accessor(name, opts) end # Copy the necessary class instance variables to the subclass. def inherited(subclass) super c = compositions.dup subclass.instance_eval{@compositions = c} end # Define getter and setter methods for the composition object. def define_composition_accessor(name, opts={}) include(@composition_module ||= Module.new) unless composition_module composer = opts[:composer] composition_module.class_eval do define_method(name) do compositions.include?(name) ? compositions[name] : (compositions[name] = instance_eval(&composer)) end define_method("#{name}=") do |v| modified! compositions[name] = v end end end end module InstanceMethods # Clear the cached compositions when refreshing. def _refresh(ds) v = super compositions.clear v end # For each composition, set the columns in the model class based # on the composition object. def before_save @compositions.keys.each{|n| instance_eval(&model.compositions[n][:decomposer])} if @compositions super end # Cache of composition objects for this class. def compositions @compositions ||= {} end end end end end