Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
grammar for boolean attributes
  • Loading branch information
triskweline committed Sep 4, 2010
1 parent e091e39 commit 9bfb905
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 52 deletions.
7 changes: 6 additions & 1 deletion README.rdoc
Expand Up @@ -2,7 +2,7 @@

Cucumber Factory allows you to create ActiveRecord objects directly from your {Cucumber}[http://cukes.info/] features. No step definitions required.

== Example
== Examples

The following will call {Movie.make}[http://github.com/notahat/machinist], {Factory.create(:movie)}[http://github.com/thoughtbot/factory_girl], Movie.create! or Movie.new, depending on what's available:
Given there is a movie
Expand All @@ -21,6 +21,11 @@ You can also refer to the last created object of a kind by saying "above":
Given there is a movie with the title "Before Sunrise"
And "Before Sunset" is a movie with the prequel above

Boolean attributes can be set like this
Given there is a movie that is awesome
And there is a movie with the name "Sunshine" that is not a comedy
And there is a director who is popular

== Factory support

{Machinist blueprints}[http://github.com/notahat/machinist] and {factory_girl factories}[http://github.com/thoughtbot/factory_girl] will be used when available.
Expand Down
88 changes: 46 additions & 42 deletions lib/cucumber/factory.rb
Expand Up @@ -2,6 +2,8 @@ module Cucumber
module Factory
class << self

ATTRIBUTES_PATTERN = '( with the .+?)?( (?:which|who|that) is .+?)?'

# List of Cucumber step definitions created by #add_steps
attr_reader :step_definitions

Expand All @@ -13,60 +15,64 @@ def add_steps(main)
end)
end
end

def steps
[ { :pattern => /^"([^\"]*)" is an? (.+?)( \(.+?\))?( with the .+?)?$/,
:action => lambda { |name, raw_model, raw_variant, raw_attributes| Cucumber::Factory.parse_named_creation(self, name, raw_model, raw_variant, raw_attributes) } },
{ :pattern => /^there is an? (.+?)( \(.+?\))?( with the .+?)?$/,
:action => lambda { |raw_model, raw_variant, raw_attributes| Cucumber::Factory.parse_creation(self, raw_model, raw_variant, raw_attributes) } } ]
# we cannot use vararg blocks here in Ruby 1.8, as explained by Aslak: http://www.ruby-forum.com/topic/182927
[ { :pattern => /^"([^\"]*)" is an? (.+?)( \(.+?\))?#{ATTRIBUTES_PATTERN}?$/,
:action => lambda { |a1, a2, a3, a4, a5| Cucumber::Factory.parse_named_creation(self, a1, a2, a3, a4, a5) } },
{ :pattern => /^there is an? (.+?)( \(.+?\))?#{ATTRIBUTES_PATTERN}$/,
:action => lambda { |a1, a2, a3, a4| Cucumber::Factory.parse_creation(self, a1, a2, a3, a4) } } ]
end

# # Emulate Cucumber step matching for specs
# def parse(world, command)
# command = command.sub(/^When |Given |Then /, "")
# steps.each do |step|
# match = step[:pattern].match(command)
# if match
# step[:action].bind(world).call(*match.captures)
# return
# end
# end
# raise "No step definition for: #{command}"
# end

def parse_named_creation(world, name, raw_model, raw_variant, raw_attributes)
record = parse_creation(world, raw_model, raw_variant, raw_attributes)
def parse_named_creation(world, name, raw_model, raw_variant, raw_attributes, raw_boolean_attributes)
record = parse_creation(world, raw_model, raw_variant, raw_attributes, raw_boolean_attributes)
variable = variable_name_from_prose(name)
world.instance_variable_set variable, record
end

def parse_creation(world, raw_model, raw_variant, raw_attributes)
def parse_creation(world, raw_model, raw_variant, raw_attributes, raw_boolean_attributes)
model_class = model_class_from_prose(raw_model)
attributes = {}
if raw_attributes.present? && raw_attributes.strip.present?
raw_attributes.scan(/(the|and|with| )+(.*?) ("([^\"]*)"|above)/).each do |fragment|
value = nil
attribute = fragment[1].downcase.gsub(" ", "_").to_sym
value_type = fragment[2] # 'above' or a quoted string
value = fragment[3]
association = model_class.reflect_on_association(attribute) if model_class.respond_to?(:reflect_on_association)
if association.present?
if value_type == "above"
# Don't use class.last, in sqlite that is not always the last inserted element
value = association.klass.find(:last, :order => "id") or raise "There is no last #{attribute}"
else
value = world.instance_variable_get(variable_name_from_prose(value))
end
else
value = world.Transform(value)
end
attributes[attribute] = value
if raw_attributes.try(:strip).present?
raw_attributes.scan(/(?:the|and|with| )+(.*?) ("([^\"]*)"|above)/).each do |fragment|
attribute = attribute_name_from_prose(fragment[0])
value_type = fragment[1] # 'above' or a quoted string
value = fragment[2] # the value string without quotes
attributes[attribute] = attribute_value(world, model_class, attribute, value_type, value)
end
end
if raw_boolean_attributes.try(:strip).present?
raw_boolean_attributes.scan(/(?:which|who|that|is| )*(not )?(.+?)(?: and |$)/).each do |fragment|
flag = !fragment[0] # if not ain't there, this is true
attribute = attribute_name_from_prose(fragment[1])
attributes[attribute] = flag
end
end
variant = raw_variant.present? && /\((.*?)\)/.match(raw_variant)[1].downcase.gsub(" ", "_")
create_record(model_class, variant, attributes)
end


private

def attribute_value(world, model_class, attribute, value_type, value)
association = model_class.respond_to?(:reflect_on_association) ? model_class.reflect_on_association(attribute) : nil
if association.present?
if value_type == "above"
# Don't use class.last, in sqlite that is not always the last inserted element
value = association.klass.find(:last, :order => "id") or raise "There is no last #{attribute}"
else
value = world.instance_variable_get(variable_name_from_prose(value))
end
else
value = world.Transform(value)
end
value
end

def attribute_name_from_prose(prose)
prose.downcase.gsub(" ", "_").to_sym
end

def model_class_from_prose(prose)
# don't use \w which depends on the system locale
prose.gsub(/[^A-Za-z0-9_]+/, "_").camelize.constantize
Expand All @@ -80,8 +86,6 @@ def variable_name_from_prose(prose)
:"@#{name}"
end

private

def factory_girl_factory_name(model_class)
model_class.to_s.underscore.to_sym
end
Expand Down
5 changes: 5 additions & 0 deletions spec/app_root/db/migrate/002_create_users.rb
Expand Up @@ -4,6 +4,11 @@ def self.up
create_table :users do |t|
t.string :email
t.string :name
t.boolean :deleted
t.boolean :locked
t.boolean :subscribed
t.boolean :scared
t.boolean :scared_by_spiders
end
end

Expand Down
12 changes: 6 additions & 6 deletions spec/factory_spec.rb
Expand Up @@ -15,22 +15,22 @@
describe 'model_class_from_prose' do

it "should return the class matching a natural language expression" do
Cucumber::Factory.model_class_from_prose("movie").should == Movie
Cucumber::Factory.model_class_from_prose("job offer").should == JobOffer
Cucumber::Factory.send(:model_class_from_prose, "movie").should == Movie
Cucumber::Factory.send(:model_class_from_prose, "job offer").should == JobOffer
end

end

describe 'variable_name_from_prose' do

it "should translate natural language to instance variable names" do
Cucumber::Factory.variable_name_from_prose("movie").should == :'@movie'
Cucumber::Factory.variable_name_from_prose("Some Movie").should == :'@some_movie'
Cucumber::Factory.send(:variable_name_from_prose, "movie").should == :'@movie'
Cucumber::Factory.send(:variable_name_from_prose, "Some Movie").should == :'@some_movie'
end

it "should make sure the generated instance variable names are legal" do
Cucumber::Factory.variable_name_from_prose("1973").should == :'@_1973'
Cucumber::Factory.variable_name_from_prose("%$§").should == :'@_'
Cucumber::Factory.send(:variable_name_from_prose, "1973").should == :'@_1973'
Cucumber::Factory.send(:variable_name_from_prose, "%$§").should == :'@_'
end

end
Expand Down
46 changes: 43 additions & 3 deletions spec/steps_spec.rb
Expand Up @@ -115,9 +115,49 @@ def self.factories
@step_mother.invoke('there is a user with the name "Jane"')
@step_mother.invoke('there is a movie with the title "Before Sunrise"')
@step_mother.invoke('there is a movie with the title "Before Sunset" and the reviewer above and the prequel above')
@before_sunset = Movie.find_by_title!("Before Sunset")
@before_sunset.prequel.title.should == "Before Sunrise"
@before_sunset.reviewer.name.should == "Jane"
before_sunset = Movie.find_by_title!("Before Sunset")
before_sunset.prequel.title.should == "Before Sunrise"
before_sunset.reviewer.name.should == "Jane"
end

it "should allow to set positive boolean attributes with which/who after the attribute list" do
@step_mother.invoke('there is a user with the name "Jane" who is deleted')
@step_mother.invoke('there is a user with the name "Jack" which is deleted')
jane = User.find_by_name!('Jane')
jane.name.should == 'Jane'
jane.deleted.should equal(true)
jack = User.find_by_name!('Jack')
jack.name.should == 'Jack'
jack.deleted.should equal(true)
end

it "should allow to set boolean attributes without regular attributes preceding them" do
@step_mother.invoke('there is a user who is deleted')
jane = User.find(:last, :order => 'id')
jane.deleted.should equal(true)
end

it "should allow to set negative boolean attribute" do
@step_mother.invoke('there is a user who is not deleted')
jane = User.find(:last, :order => 'id')
jane.deleted.should equal(false)
end

it "should allow to set multiple boolean attributes" do
@step_mother.invoke('there is a user who is locked and not deleted and subscribed')
jane = User.find(:last, :order => 'id')
jane.locked.should equal(true)
jane.deleted.should equal(false)
jane.subscribed.should equal(true)
end

it "should allow to set boolean attributes that are named from multiple words" do
@step_mother.invoke('there is a user who is locked and not scared and scared by spiders and deleted')
jane = User.find(:last, :order => 'id')
jane.locked.should equal(true)
jane.scared.should equal(false)
jane.scared_by_spiders.should equal(true)
jane.deleted.should equal(true)
end

end

0 comments on commit 9bfb905

Please sign in to comment.