Skip to content

Commit

Permalink
Implements adapters for AR and DM as inner constants so that there ca…
Browse files Browse the repository at this point in the history
…n be pickles for everyone
  • Loading branch information
Daniel Neighman authored and ianwhite committed Jun 23, 2010
1 parent 381a1ad commit 36aaa1c
Show file tree
Hide file tree
Showing 11 changed files with 332 additions and 260 deletions.
65 changes: 40 additions & 25 deletions README.rdoc
Expand Up @@ -35,7 +35,7 @@ Install pickle either as a rails plugin, or a gem

# plugin
script/plugin install git://github.com/ianwhite/pickle.git

# or, plugin as submodule
git submodule add git://github.com/ianwhite/pickle.git vendor/plugins/pickle

Expand All @@ -48,7 +48,7 @@ It's tested against all stable branches of 2.x rails, and edge, with the latest
To run the specs do:

rake spec

To run the features (rails 2.3 only ATM):

rake cucumber
Expand Down Expand Up @@ -77,9 +77,11 @@ If you want path steps and email steps then just add 'paths' and/or 'email'. Th
written to <tt>features/env/paths.rb</tt> and
<tt>features/step_definitions/email_steps.rb</tt> respectively.

=== Using with plain ole Active Record
=== Using with plain ole Active Record or DataMapper

If you have an AR called 'Post', with required fields 'title', and 'body', then you can now write
Pickle comes with adapters for Active Record and DataMapper.

If you have an AR/DM called 'Post', with required fields 'title', and 'body', then you can now write
steps like this

Given a post exists with title: "My Post", body: "My body"
Expand All @@ -98,7 +100,7 @@ you've written, you can just do stuff like

==== Machinst: require your blueprints and reset Shams

(The latest version of pickle supports {multiple blueprints}[http://github.com/notahat/machinist/commit/d6492e6927a8aa1819926e48b22377171fd20496], for
(The latest version of pickle supports {multiple blueprints}[http://github.com/notahat/machinist/commit/d6492e6927a8aa1819926e48b22377171fd20496], for
earlier versions of machinist use pickle <= 0.1.10)

In your <tt>features/support/env.rb</tt> add the following lines at the bottom
Expand All @@ -115,6 +117,19 @@ If that doesn't solve loading issues then require your factories.rb file directl
# example features/support/factory_girl.rb
require File.dirname(__FILE__) + '/../../spec/factories'

=== Using with an ORM other than ActiveRecord or DataMapper

Pickle can be used with any Modeling library provided there is an adapter written for it.

Adapters are very simple and exist a module or class with the name "PickleAdapter" available to the class. For example

User.const_get(:PickleAdapter) #=> should return a pickle adapter

The Active Record and DataMapper ones can be found at
ActiveRecord::Base::PickleAdapter and DataMapper::Resource::PickleAdapter respectively.

See how to implement one by looking at the ones provided in the pickle source in lib/pickle/adapters/*

=== Configuring Pickle

You can tell pickle to use another factory adapter (see Pickle::Adapter), or
Expand All @@ -129,7 +144,7 @@ In: <tt>features/support/pickle.rb</tt>
config.adapters = [:machinist, YourOwnAdapterClass]
config.map 'me', 'myself', 'my', 'I', :to => 'user: "me"'
end

Out of the box pickle looks for machinist, then factory-girl, then finally active-record 'factories'.
If you find that your steps aren't working with your factories, it's probably the case that your factory
setup is not being included in your cucumber environment (see comments above regarding machinist and factory-girl).
Expand All @@ -147,29 +162,29 @@ When you run <tt>script/generate pickle</tt> you get the following steps
Given a user exists
Given a user: "fred" exists
Given the user exists

"Given <b>a model</b> exists with <b>fields</b>", e.g.

Given a user exists with name: "Fred"
Given a user exists with name: "Fred", activated: false

You can refer to other models in the fields

Given a user exists
And a post exists with author: the user

Given a person: "fred" exists
And a person: "ethel" exists
And a fatherhood exists with parent: user "fred", child: user "ethel"

"Given <b>n models</b> exist", e.g.

Given 10 users exist

"Given <b>n models</b> exist with <b>fields</b>", examples:

Given 10 users exist with activated: false

"Given the following <b>models</b> exist:", examples:

Given the following users exist
Expand All @@ -188,21 +203,21 @@ You can refer to other models in the fields
"Then <b>a model</b> should exist with <b>fields</b>", e.g.

Then a user: "fred" should exist with name: "Fred" # we can label the found user for later use

You can use other models, booleans, numerics, and strings as fields

Then a person should exist with child: person "ethel"
Then a user should exist with activated: false
Then a user should exist with activated: true, email: "fred@gmail.com"

"Then <b>n models</b> should exist", e.g.

Then 10 events should exist

"Then <b>n models</b> should exist with <b>fields</b>", e.g.

Then 2 people should exist with father: person "fred"

"Then the following <b>models</b> exist". This allows the creation of multiple models
using a table syntax. Using a column with the singularized name of the model creates a referenceable model. E.g.

Expand All @@ -213,30 +228,30 @@ using a table syntax. Using a column with the singularized name of the model cre
Then the following users exist:
| user | name | activated |
| Fred | Freddy | false |

===== Asserting associations

One-to-one assocs: "Then <b>a model</b> should be <b>other model</b>'s <b>association</b>", e.g.

Then the person: "fred" should be person: "ethel"'s father

Many-to-one assocs: "Then <b>a model</b> should be [in|one of] <b>other model</b>'s <b>association</b>", e.g.

Then the person: "ethel" should be one of person: "fred"'s children
Then the comment should be in the post's comments

===== Asserting predicate methods

"Then <b>a model</b> should [be|have] [a|an] <b>predicate</b>", e.g.

Then the user should have a status # => user.status.should be_present
Then the user should have a stale password # => user.should have_stale_password
Then the car: "batmobile" should be fast # => car.should be_fast

"Then <b>a model</b> should not [be|have] [a|an] <b>predicate</b>", e.g.

Then person: "fred" should not be childless # => fred.should_not be_childless

=== Regexps for use in your own steps

By default you get some regexps available in the main namespace for use
Expand All @@ -263,7 +278,7 @@ Pickle::Parser::Matchers for the methods available)
post = model!(post)
forum = model!(forum)
forum.posts.should include(post)
end
end

*capture_fields*

Expand All @@ -276,4 +291,4 @@ can build up composite objects with ease

# example of use
Given a user exists
And a post exists with author: the user # this step will assign the above user as :author on the post
And a post exists with author: the user # this step will assign the above user as :author on the post
97 changes: 37 additions & 60 deletions lib/pickle/adapter.rb
Expand Up @@ -4,96 +4,73 @@ module Pickle
# Abstract Factory adapter class, if you have a factory type setup, you
# can easily create an adaptor to make it work with Pickle.
#
# The factory adaptor must have a #factories class method that returns
# The factory adaptor must have a #factories class method that returns
# its instances, and each instance must respond to:
#
# #name : identifies the factory by name (default is attr_reader)
# #klass : returns the associated model class for this factory (default is attr_reader)
# #create(attrs = {}) : returns a newly created object
class Adapter
attr_reader :name, :klass

def create(attrs = {})
raise NotImplementedError, "create and return an object with the given attributes"
end

if respond_to?(:class_attribute)
class_attribute :model_classes
else
cattr_writer :model_classes
end

self.model_classes = nil


# Include this module into your adapter
# this will register the adapter with pickle and it will be picked up for you
# To create an adapter you should create an inner constant "PickleAdapter"
#
# e.g. ActiveRecord::Base::PickleAdapter
#
# @see pickle/adapters/active_record
# @see pickle/adapters/datamapper
module Base
def self.included(base)
adapters << base
end

# A collection of registered adapters
def self.adapters
@@adapters ||= []
end
end

class << self
def factories
raise NotImplementedError, "return an array of factory adapter objects"
end

def model_classes
@@model_classes ||=
if defined?(::ActiveRecord::Base)
::ActiveRecord::Base.send(:subclasses).select {|klass| suitable_for_pickle?(klass)}
elsif defined?(::DataMapper::Model)
::DataMapper::Model.descendants.to_a
else
[]
end
end

# return true if a klass should be used by pickle
def suitable_for_pickle?(klass)
!klass.abstract_class? && klass.table_exists? && !framework_class?(klass)
end

# return true if the passed class is a special framework class
def framework_class?(klass)
((defined?(CGI::Session::ActiveRecordStore::Session) && klass == CGI::Session::ActiveRecordStore::Session)) ||
((defined?(::ActiveRecord::SessionStore::Session) && klass == ::ActiveRecord::SessionStore::Session))
@@model_classes ||= self::Base.adapters.map{ |a| a.model_classes }.flatten
end

# Returns the column names for the given ORM model class.
def column_names(klass)
if defined?(::ActiveRecord::Base)
klass.column_names
elsif defined?(::DataMapper::Model)
klass.properties.map(&:name)
else
[]
end
klass.const_get(:PickleAdapter).column_names(klass)
end

def get_model(klass, id)
if defined?(ActiveRecord::Base)
klass.find(id)
elsif defined?(DataMapper::Model)
klass.get(id)
else
nil
end
klass.const_get(:PickleAdapter).get_model(klass, id)
end

def find_first_model(klass, conditions)
if defined?(ActiveRecord::Base)
klass.find(:first, :conditions => conditions)
elsif defined?(DataMapper::Model)
klass.first(conditions)
else
nil
end
klass.const_get(:PickleAdapter).find_first_model(klass, conditions)
end

def find_all_models(klass, conditions)
if defined?(ActiveRecord::Base)
klass.find(:all, :conditions => conditions)
elsif defined?(DataMapper::Model)
klass.all(conditions)
else
[]
end
klass.const_get(:PickleAdapter).find_all_models(klass, conditions)
end
end

# machinist adapter
class Machinist < Adapter
def self.factories
Expand All @@ -105,37 +82,37 @@ def self.factories
end
factories
end

def initialize(klass, blueprint)
@klass, @blueprint = klass, blueprint
@name = @klass.name.underscore.gsub('/','_')
@name = "#{@blueprint}_#{@name}" unless @blueprint == :master
end

def create(attrs = {})
@klass.send(:make, @blueprint, attrs)
end
end

# factory-girl adapter
class FactoryGirl < Adapter
def self.factories
(::Factory.factories.values rescue []).map {|factory| new(factory)}
end

def initialize(factory)
@klass, @name = factory.build_class, factory.factory_name.to_s
end

def create(attrs = {})
Factory.create(@name, attrs)
end
end

# fallback active record adapter
class ActiveRecord < Adapter
def self.factories
model_classes.map {|klass| new(klass) }
::ActiveRecord::Base::PickleAdapter.model_classes.map{|k| new(k)}
end

def initialize(klass)
Expand Down

0 comments on commit 36aaa1c

Please sign in to comment.