From 9bfb90517dba16e8adf30c7a29c47ed9ea68ab96 Mon Sep 17 00:00:00 2001 From: Henning Koch Date: Sat, 4 Sep 2010 10:35:00 +0200 Subject: [PATCH] grammar for boolean attributes --- README.rdoc | 7 +- lib/cucumber/factory.rb | 88 ++++++++++---------- spec/app_root/db/migrate/002_create_users.rb | 5 ++ spec/factory_spec.rb | 12 +-- spec/steps_spec.rb | 46 +++++++++- 5 files changed, 106 insertions(+), 52 deletions(-) diff --git a/README.rdoc b/README.rdoc index 67422a9..094384e 100755 --- a/README.rdoc +++ b/README.rdoc @@ -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 @@ -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. diff --git a/lib/cucumber/factory.rb b/lib/cucumber/factory.rb index a76f8e4..210b89a 100755 --- a/lib/cucumber/factory.rb +++ b/lib/cucumber/factory.rb @@ -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 @@ -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 @@ -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 diff --git a/spec/app_root/db/migrate/002_create_users.rb b/spec/app_root/db/migrate/002_create_users.rb index 87f2ba5..72648bc 100644 --- a/spec/app_root/db/migrate/002_create_users.rb +++ b/spec/app_root/db/migrate/002_create_users.rb @@ -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 diff --git a/spec/factory_spec.rb b/spec/factory_spec.rb index 7aa5caa..e280168 100755 --- a/spec/factory_spec.rb +++ b/spec/factory_spec.rb @@ -15,8 +15,8 @@ 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 @@ -24,13 +24,13 @@ 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 diff --git a/spec/steps_spec.rb b/spec/steps_spec.rb index 15c49fb..59409ac 100644 --- a/spec/steps_spec.rb +++ b/spec/steps_spec.rb @@ -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