lib/gorillib/collection/model_collection.rb in gorillib-0.4.1pre vs lib/gorillib/collection/model_collection.rb in gorillib-0.4.2pre

- old
+ new

@@ -1,63 +1,156 @@ module Gorillib - class ModelCollection < Gorillib::Collection - # [String, Symbol] Method invoked on a new item to generate its collection key; :to_key by default - attr_accessor :key_method - # The default `key_method` invoked on a new item to generate its collection key - DEFAULT_KEY_METHOD = :to_key + # + # A collection of Models + # + # ### Item Type + # + # `item_type` is a class attribute -- you can make a "collection of Foo's" by + # subclassing ModelCollection and set the item item_type at the class level: + # + # class ClusterCollection < ModelCollection + # self.item_type = Cluster + # end + # + # + # + # A model collection serializes as an array does, but indexes labelled objects + # as a hash does. + # + # + class ModelCollection < Gorillib::Collection # [Class, #receive] Factory for generating a new collection item. - class_attribute :factory, :instance_writer => false - singleton_class.class_eval{ protected :factory= } + class_attribute :item_type, :instance_writer => false + singleton_class.send(:protected, :item_type=) - def initialize(key_meth=nil, obj_factory=nil) - @factory = Gorillib::Factory(obj_factory) if obj_factory - @clxn = Hash.new - @key_method = key_meth || DEFAULT_KEY_METHOD + def initialize(options={}) + @item_type = Gorillib::Factory(options[:item_type]) if options[:item_type] + super end - # Adds an item in-place - # @return [Gorillib::Collection] the collection - def <<(val) - receive! [val] - self + def receive_item(label, *args, &block) + item = item_type.receive(*args, &block) + super(label, item) + rescue StandardError => err ; err.polish("#{item_type} #{label} as #{args.inspect} to #{self}") rescue nil ; raise end - def create(*args, &block) - item = factory.receive(*args) - self << item - item - end - - def update_or_create(key, *args, &block) - if include?(key) - obj = fetch(key) - obj.receive!(*args, &block) - obj + def update_or_add(label, attrs, &block) + if label && include?(label) + item = fetch(label) + item.receive!(attrs, &block) + item else - attrs = args.extract_options!.merge(key_method => key) - create(*args, attrs, &block) + attrs = attrs.merge(key_method => label) if key_method && label + receive_item(label, attrs, &block) end + rescue StandardError => err ; err.polish("#{item_type} #{label} as #{attrs} to #{self}") rescue nil ; raise end - protected + # @return [Array] serializable array representation of the collection + def to_wire(options={}) + to_a.map{|el| el.respond_to?(:to_wire) ? el.to_wire(options) : el } + end + # same as #to_wire + def as_json(*args) to_wire(*args) ; end + # @return [String] JSON serialization of the collection's array representation + def to_json(*args) to_wire(*args).to_json(*args) ; end + end - def convert_value(val) - return val unless factory - return nil if val.nil? - factory.receive(val) + class Collection + + # + # + # class ClusterCollection < ModelCollection + # self.item_type = Cluster + # end + # class Organization + # field :clusters, ClusterCollection, default: ->{ ClusterCollection.new(common_attrs: { organization: self }) } + # end + # + module CommonAttrs + extend Gorillib::Concern + + included do + # [Class, #receive] Attributes to mix in to each added item + class_attribute :common_attrs, :instance_writer => false + singleton_class.send(:protected, :common_attrs=) + self.common_attrs = Hash.new + end + + def initialize(options={}) + super + @common_attrs = self.common_attrs.merge(options[:common_attrs]) if options.include?(:common_attrs) + end + + # + # * a factory-native object: item is updated with common_attrs, then added + # * raw materials for the object: item is constructed (from the merged attrs and common_attrs), then added + # + def receive_item(label, *args, &block) + attrs = args.extract_options!.merge(common_attrs) + super(label, *args, attrs, &block) + end + + def update_or_add(label, *args, &block) + attrs = args.extract_options!.merge(common_attrs) + super(label, *args, attrs, &block) + end + end - # - if the given collection responds_to `to_hash`, it is received into the internal collection; each hash key *must* match the id of its value or results are undefined. - # - otherwise, it receives a hash generates from the id/value pairs of each object in the given collection. - def convert_collection(cc) - return cc.to_hash if cc.respond_to?(:to_hash) - cc.inject({}) do |acc, val| - val = convert_value(val) - key = val.public_send(key_method) - acc[key] = val - acc + # + # @example + # class Smurf + # include Gorillib::Model + # end + # + # # Sets the 'village' attribute on each item it receives to the object + # # this collection belongs to. + # class SmurfCollection < ModelCollection + # include Gorillib::Collection::ItemsBelongTo + # self.item_type = Smurf + # self.parentage_method = :village + # end + # + # # SmurfVillage makes sure its SmurfCollection knows that it `belongs_to` the village + # class SmurfVillage + # include Gorillib::Model + # field :name, Symbol + # field :smurfs, SmurfCollection, default: ->{ SmurfCollection.new(belongs_to: self) } + # end + # + # # all the normal stuff works as you'd expect + # smurf_town = SmurfVillage.new('smurf_town') # #<SmurfVillage name=smurf_town> + # smurf_town.smurfs # c{ } + # smurf_town.smurfs.belongs_to # #<SmurfVillage name=smurf_town> + # + # # when a new smurf moves to town, it knows what village it belongs_to + # smurf_town.smurfs.receive_item(:novel_smurf, smurfiness: 10) + # # => #<Smurf name=:novel_smurf smurfiness=10 village=#<SmurfVillage name=smurf_town>> + # + module ItemsBelongTo + extend Gorillib::Concern + include Gorillib::Collection::CommonAttrs + + included do + # [Class, #receive] Name of the attribute to set on + class_attribute :parentage_method, :instance_writer => false + singleton_class.send(:protected, :common_attrs=) end + + # add this collection's belongs_to to the common attrs, so that a + # newly-created object knows its parentage from birth. + def initialize(*args) + super + @common_attrs = self.common_attrs.merge(parentage_method => self.belongs_to) + end + + def add(item, *args) + item.send("#{parentage_method}=", belongs_to) + super + end + end end end