Skip to content

Commit

Permalink
adds Pickle::Jar that has responsibility for storing and retrieving m…
Browse files Browse the repository at this point in the history
…odels using PickleRefs
  • Loading branch information
ianwhite committed Aug 23, 2010
1 parent 9620257 commit 316d763
Show file tree
Hide file tree
Showing 2 changed files with 250 additions and 0 deletions.
123 changes: 123 additions & 0 deletions lib/pickle/jar.rb
@@ -0,0 +1,123 @@
module Pickle
# handles storing and retrieveing models using pickle_refs to do so. This is not the smae as creating/finding models
#
# For cucumber use, it's better to use the methods with the same names found in in Pickle::Scenario
#
# Examples: (FYI - the pickle.create_and_store methods do all of this automatically)
#
# # store a user
# jar.store User.make
#
# # retrieve with:
# jar.retrieve Pickle::Ref.new('the user')
# jar.retrieve Pickle::Ref.new('user')
# jar.retrieve Pickle::Ref.new('the first user')
#
# # store another user, this time labelled
# jar.store User.make, :label => "fred"
#
# # retrieve with:
# jar.retrieve Pickle::Ref.new('the user "fred"')
# jar.retrieve Pickle::Ref.new('"fred"')
#
# # store a user made with the admin blueprint
# jar.store User.make(:admin), :factory => "admin_user"
#
# # retrieve with:
# jar.retrieve Pickle::Ref.new('the admin_user')
# jar.retrieve Pickle::Ref.new('the user')
#
# # retrieve all 1st, 2nd and 3rd - in different ways
# jar.retrieve Pickle::Ref.new('the first user')
# jar.retrieve Pickle::Ref.new('fred')
# jar.retrieve Pickle::Ref.new('admin_user')
#
# # what if it's not in the jar?
# jar.retrieve Pickle::Ref.new('the 4th user')
# # => Pickle::Jar::UnknownModelError
#
# jar.include? Pickle::Ref.new('4th user')
# # => false
#
# jar.include? Pickle::Ref.new('user')
# # => true
class Jar
include Parser::Canonical

# store the given model in the pickle jar.
#
# By default it will be stored under its class name (canononicalized), can also be given a label
# If given a factory, it will also be stored under that name (canononicalized)
#
# @examples
# store object
# store object, :label => "fred"
# store object, :factory => "admin_user"
# store object, :label => "fred", :label => "admin_user"
#
# @return Object the stored object
def store(model, opts = {})
store_by_factory_and_label(model, model.class.name, opts[:label])
unless canonical(opts[:factory]) == canonical(model.class.name)
store_by_factory_and_label(model, opts[:factory], opts[:label])
end
model
end

# retreive the model matching the pickle_ref
# @raise Pickle::Jar::UnknownModelError raised if the pickle_ref does not refer to a model in this jar
# @raise Pickle::Jar::AmbiguousLabel raised if a label by its own refers to more than one model
def retrieve(ref)
begin
if ref.factory && ref.label
labeled_models(ref.label)[ref.factory]
elsif ref.label
retrieve_by_label ref.label
elsif ref.index
factory_models(ref.factory)[ref.index]
else
factory_models(ref.factory).last
end
end or raise UnknownModelError, '#{ref} is not known'
end

# Does the given pickle_ref refer to a model in this jar?
# @return boolean
def include?(ref)
retrieve(ref)
rescue UnknownModelError
false
end

private
def store_by_factory_and_label(model, factory, label)
factory = canonical(factory)
factory_models(factory) << model
labeled_models(label)[factory] = model if label.present?
end

def retrieve_by_label(label)
models = labeled_models(label).values.uniq
raise AmbiguiousLabelError, "#{label.inspect} refers to #{models.length} models. Specify factory to narrow it down." if models.length > 1
models[0]
end

# Hash of arrays, each key is a factory name, each value is an ordered array of models
def factory_models(factory)
@factory_models ||= {}
@factory_models[factory] ||= []
end

# Hash of hashes, each key is a label, each value is a hash of factory name => model
def labeled_models(label)
@labeled_models ||= {}
@labeled_models[label] ||= {}
end

class AmbiguiousLabelError < RuntimeError
end

class UnknownModelError < RuntimeError
end
end
end
127 changes: 127 additions & 0 deletions spec/pickle/jar_spec.rb
@@ -0,0 +1,127 @@
require 'spec_helper'

describe Pickle::Jar do
let(:model_class) { Class.new.tap {|c| c.stub!(:name).and_return('Module::ModelClass') } }
let(:model) { model_class.new }

shared_examples_for "after storing a model" do
specify "can be retrieved with Pickle::Ref.new(<model class name>)" do
subject.retrieve(Pickle::Ref.new('Module::ModelClass')).should == model
end

specify "can be retrieved with Pickle::Ref.new('underscored_class_name')" do
subject.retrieve(Pickle::Ref.new('module_model_class')).should == model
end

specify "can be retrieve with Pickle::Ref.new('last underscored_class_name')" do
subject.retrieve(Pickle::Ref.new("last module_model_class")).should == model
end
end

describe "after storing a model," do
before { subject.store(model) }

describe "the model" do
it_should_behave_like "after storing a model"
end
end

shared_examples_for "after storing a model with an optional factory" do
specify "can be retrieved with Pickle::Ref.new('<factory>')" do
subject.retrieve(Pickle::Ref.new("#{factory}")).should == model
end

specify "can be retrieve with Pickle::Ref.new('last <factory>')" do
subject.retrieve(Pickle::Ref.new("last #{factory}")).should == model
end
end

describe "after storing a model with an optional factory," do
let(:factory) { 'factory' }
before { subject.store(model, :factory => factory) }

describe "the model" do
it_should_behave_like "after storing a model"
it_should_behave_like "after storing a model with an optional factory"
end
end

shared_examples_for "after storing a model with an optional label" do
specify "can be retrieved with Pickle::Ref.new('<model class name> \"<label>\"')" do
subject.retrieve(Pickle::Ref.new("Module::ModelClass \"#{label}\"")).should == model
end

specify "can be retrieved with Pickle::Ref.new('\"<label>\"')" do
subject.retrieve(Pickle::Ref.new("\"#{label}\"")).should == model
end
end

describe "after storing a model with an optional label," do
let(:label) { 'label' }
before { subject.store(model, :label => label) }

describe "the model" do
it_should_behave_like "after storing a model"
it_should_behave_like "after storing a model with an optional label"
end
end

describe "after storing two different models under the same label" do
before do
subject.store(model, :label => "fred", :factory => "factory")
subject.store(model_class.new, :label => "fred", :factory => "factory2")
end

specify "a model can be retrieved by factory and label" do
subject.retrieve(Pickle::Ref.new('factory "fred"')).should == model
end

specify "retrieving by label alone raises Pickle::Jar::AmbiguiousLabelError" do
lambda { subject.retrieve(Pickle::Ref.new('"fred"')) }.should raise_error(Pickle::Jar::AmbiguiousLabelError)
end
end

describe "after storing 3 models," do
let(:earliest_model) { model_class.new }
let(:middle_model) { model_class.new }

before do
subject.store(earliest_model)
subject.store(middle_model)
subject.store(model)
end

describe "the latest stored model" do
it_should_behave_like "after storing a model"

specify "can be retrieved with '3rd' in the pickle ref" do
subject.retrieve(Pickle::Ref.new("2nd module_model_class")).should == middle_model
end
end

specify "the earliest stored model can be retrieved with '2nd' in the pickle ref" do
subject.retrieve(Pickle::Ref.new("2nd module_model_class")).should == middle_model
end

specify "the earliest stored model can be retrieved with '1st' in the pickle ref" do
subject.retrieve(Pickle::Ref.new("1st module_model_class")).should == earliest_model
end

specify "the earliest stored model can be retrieved with 'first' in the pickle ref" do
subject.retrieve(Pickle::Ref.new("first module_model_class")).should == earliest_model
end

specify "attempting to retrieve the 4th model raises UnknownModelError" do
lambda { subject.retrieve(Pickle::Ref.new("4th module_model_class")) }.should raise_error(Pickle::Jar::UnknownModelError)
end

specify "asking if jar #contains? the 4th model returns false" do
subject.include?(Pickle::Ref.new("4th module_model_class")).should == false
end

it { should include(Pickle::Ref.new("1st module_model_class")) }
it { should include(Pickle::Ref.new("2nd module_model_class")) }
it { should include(Pickle::Ref.new("3rd module_model_class")) }
it { should_not include(Pickle::Ref.new("25th module_model_class")) }
end
end

0 comments on commit 316d763

Please sign in to comment.