From 82746c2ae0a30774b38569e10368cdf15aef1b07 Mon Sep 17 00:00:00 2001 From: Joshua Clayton Date: Fri, 12 Aug 2011 14:38:33 -0400 Subject: [PATCH] Add attribute lists --- lib/factory_girl.rb | 1 + lib/factory_girl/attribute_group.rb | 33 +++---- lib/factory_girl/attribute_list.rb | 59 ++++++++++++ lib/factory_girl/factory.rb | 57 ++++-------- spec/factory_girl/attribute_list_spec.rb | 111 +++++++++++++++++++++++ spec/factory_girl/factory_spec.rb | 18 ---- 6 files changed, 198 insertions(+), 81 deletions(-) create mode 100644 lib/factory_girl/attribute_list.rb create mode 100644 spec/factory_girl/attribute_list_spec.rb diff --git a/lib/factory_girl.rb b/lib/factory_girl.rb index 13eeae5..489437a 100644 --- a/lib/factory_girl.rb +++ b/lib/factory_girl.rb @@ -14,6 +14,7 @@ require 'factory_girl/attribute/sequence' require 'factory_girl/attribute/implicit' require 'factory_girl/attribute/attribute_group' require 'factory_girl/sequence' +require 'factory_girl/attribute_list' require 'factory_girl/attribute_group' require 'factory_girl/aliases' require 'factory_girl/definition_proxy' diff --git a/lib/factory_girl/attribute_group.rb b/lib/factory_girl/attribute_group.rb index 3283f82..251a391 100644 --- a/lib/factory_girl/attribute_group.rb +++ b/lib/factory_girl/attribute_group.rb @@ -1,40 +1,29 @@ module FactoryGirl - class AttributeGroup attr_reader :name - attr_reader :attributes - + def initialize(name, &block) #:nodoc: @name = name - @attributes = [] + @attribute_list = AttributeList.new + proxy = FactoryGirl::DefinitionProxy.new(self) proxy.instance_eval(&block) if block_given? end def define_attribute(attribute) - name = attribute.name - if attribute_defined?(name) - raise AttributeDefinitionError, "Attribute already defined: #{name}" - end - @attributes << attribute + @attribute_list.define_attribute(attribute) end - + def add_callback(name, &block) - unless [:after_build, :after_create, :after_stub].include?(name.to_sym) - raise InvalidCallbackNameError, "#{name} is not a valid callback name. Valid callback names are :after_build, :after_create, and :after_stub" - end - @attributes << Attribute::Callback.new(name.to_sym, block) + @attribute_list.add_callback(name, &block) end - + + def attributes + @attribute_list.to_a + end + def names [@name] end - - private - - def attribute_defined? (name) - !@attributes.detect {|attr| attr.name == name && !attr.is_a?(Attribute::Callback) }.nil? - end - end end diff --git a/lib/factory_girl/attribute_list.rb b/lib/factory_girl/attribute_list.rb new file mode 100644 index 0000000..590f7a8 --- /dev/null +++ b/lib/factory_girl/attribute_list.rb @@ -0,0 +1,59 @@ +module FactoryGirl + class AttributeList + include Enumerable + + def initialize + @attributes = [] + end + + def define_attribute(attribute) + if attribute_defined?(attribute.name) + raise AttributeDefinitionError, "Attribute already defined: #{attribute.name}" + end + + @attributes << attribute + end + + def add_callback(name, &block) + unless valid_callback_names.include?(name.to_sym) + raise InvalidCallbackNameError, "#{name} is not a valid callback name. Valid callback names are #{valid_callback_names.inspect}" + end + + @attributes << Attribute::Callback.new(name.to_sym, block) + end + + def each(&block) + @attributes.each(&block) + end + + def attribute_defined?(attribute_name) + !@attributes.detect do |attribute| + attribute.name == attribute_name && + !attribute.is_a?(FactoryGirl::Attribute::Callback) + end.nil? + end + + def apply_attributes(attributes_to_apply) + new_attributes = [] + + attributes_to_apply.each do |attribute| + if attribute_defined?(attribute.name) + @attributes.delete_if do |attrib| + new_attributes << attrib.clone if attrib.name == attribute.name + end + else + new_attributes << attribute.clone + end + end + + @attributes.unshift *new_attributes + @attributes = @attributes.partition {|attr| attr.priority.zero? }.flatten + end + + private + + def valid_callback_names + [:after_build, :after_create, :after_stub] + end + end +end diff --git a/lib/factory_girl/factory.rb b/lib/factory_girl/factory.rb index a9864bd..3b3fb1d 100644 --- a/lib/factory_girl/factory.rb +++ b/lib/factory_girl/factory.rb @@ -13,7 +13,6 @@ module FactoryGirl class Factory attr_reader :name #:nodoc: - attr_reader :attributes #:nodoc: attr_reader :attribute_groups #:nodoc: def factory_name @@ -38,7 +37,7 @@ module FactoryGirl @name = factory_name_for(name) @parent = options[:parent] @options = options - @attributes = [] + @attribute_list = AttributeList.new @attribute_groups = [] end @@ -47,26 +46,24 @@ module FactoryGirl @options[:default_strategy] ||= parent.default_strategy apply_attributes(parent.attributes) - sort_attributes! end - def apply_attribute_groups(groups) + def apply_attribute_groups(groups) #:nodoc: groups.reverse.map { |name| attribute_group_by_name(name) }.each do |group| apply_attributes(group.attributes) end - sort_attributes! + end + + def apply_attributes(attributes_to_apply) + @attribute_list.apply_attributes(attributes_to_apply) end def define_attribute(attribute) - name = attribute.name - # TODO: move these checks into Attribute - if attribute_defined?(name) - raise AttributeDefinitionError, "Attribute already defined: #{name}" - end if attribute.respond_to?(:factory) && attribute.factory == self.name - raise AssociationDefinitionError, "Self-referencing association '#{name}' in factory '#{self.name}'" + raise AssociationDefinitionError, "Self-referencing association '#{attribute.name}' in factory '#{self.name}'" end - @attributes << attribute + + @attribute_list.define_attribute(attribute) end def define_attribute_group(group) @@ -74,16 +71,18 @@ module FactoryGirl end def add_callback(name, &block) - unless [:after_build, :after_create, :after_stub].include?(name.to_sym) - raise InvalidCallbackNameError, "#{name} is not a valid callback name. Valid callback names are :after_build, :after_create, and :after_stub" - end - @attributes << Attribute::Callback.new(name.to_sym, block) + @attribute_list.add_callback(name, &block) + end + + + def attributes + @attribute_list.to_a end def run(proxy_class, overrides) #:nodoc: proxy = proxy_class.new(build_class) overrides = symbolize_keys(overrides) - @attributes.each do |attribute| + @attribute_list.each do |attribute| factory_overrides = overrides.select { |attr, val| attribute.aliases_for?(attr) } if factory_overrides.empty? attribute.add_to(proxy) @@ -167,10 +166,6 @@ module FactoryGirl end end - def attribute_defined? (name) - !@attributes.detect {|attr| attr.name == name && !attr.is_a?(Attribute::Callback) }.nil? - end - def assert_valid_options(options) invalid_keys = options.keys - [:class, :parent, :default_strategy, :aliases, :attribute_groups] unless invalid_keys == [] @@ -213,26 +208,6 @@ module FactoryGirl end end - def apply_attributes(attributes_to_apply) - new_attributes=[] - - attributes_to_apply.each do |attribute| - if attribute_defined?(attribute.name) - @attributes.delete_if do |attrib| - new_attributes << attrib.clone if attrib.name == attribute.name - end - else - new_attributes << attribute.clone - end - end - - @attributes.unshift *new_attributes - end - - def sort_attributes! - @attributes = @attributes.partition {|attr| attr.priority.zero? }.flatten - end - def attribute_group_for(name) attribute_groups.detect {|attribute_group| attribute_group.name == name } end diff --git a/spec/factory_girl/attribute_list_spec.rb b/spec/factory_girl/attribute_list_spec.rb new file mode 100644 index 0000000..7913d77 --- /dev/null +++ b/spec/factory_girl/attribute_list_spec.rb @@ -0,0 +1,111 @@ +require "spec_helper" + +describe FactoryGirl::AttributeList, "#define_attribute" do + let(:static_attribute) { FactoryGirl::Attribute::Static.new(:full_name, "value") } + let(:dynamic_attribute) { FactoryGirl::Attribute::Dynamic.new(:email, lambda {|u| "#{u.full_name}@example.com" }) } + + it "maintains a list of attributes" do + subject.define_attribute(static_attribute) + subject.to_a.should == [static_attribute] + + subject.define_attribute(dynamic_attribute) + subject.to_a.should == [static_attribute, dynamic_attribute] + end + + it "raises if an attribute has already been defined" do + expect { + 2.times { subject.define_attribute(static_attribute) } + }.to raise_error(FactoryGirl::AttributeDefinitionError, "Attribute already defined: full_name") + end +end + +describe FactoryGirl::AttributeList, "#attribute_defined?" do + let(:static_attribute) { FactoryGirl::Attribute::Static.new(:full_name, "value") } + let(:callback_attribute) { FactoryGirl::Attribute::Callback.new(:after_create, lambda { }) } + let(:static_attribute_named_after_create) { FactoryGirl::Attribute::Static.new(:after_create, "funky!") } + + it "knows if an attribute has been defined" do + subject.attribute_defined?(static_attribute.name).should == false + + subject.define_attribute(static_attribute) + + subject.attribute_defined?(static_attribute.name).should == true + subject.attribute_defined?(:undefined_attribute).should == false + end + + it "doesn't reference callbacks" do + subject.define_attribute(callback_attribute) + + subject.attribute_defined?(:after_create).should == false + + subject.define_attribute(static_attribute_named_after_create) + subject.attribute_defined?(:after_create).should == true + end +end + +describe FactoryGirl::AttributeList, "#add_callback" do + let(:proxy_class) { mock("klass") } + let(:proxy) { FactoryGirl::Proxy.new(proxy_class) } + let(:valid_callback_names) { [:after_create, :after_build, :after_stub] } + let(:invalid_callback_names) { [:before_create, :before_build, :bogus] } + + + it "allows for defining adding a callback" do + subject.add_callback(:after_create) { "Called after_create" } + + subject.first.name.should == :after_create + + subject.first.add_to(proxy) + proxy.callbacks[:after_create].first.call.should == "Called after_create" + end + + it "allows valid callback names to be assigned" do + valid_callback_names.each do |callback_name| + expect do + subject.add_callback(callback_name) { "great name!" } + end.to_not raise_error(FactoryGirl::InvalidCallbackNameError) + end + end + + it "raises if an invalid callback name is assigned" do + invalid_callback_names.each do |callback_name| + expect do + subject.add_callback(callback_name) { "great name!" } + end.to raise_error(FactoryGirl::InvalidCallbackNameError, "#{callback_name} is not a valid callback name. Valid callback names are [:after_build, :after_create, :after_stub]") + end + end +end + +describe FactoryGirl::AttributeList, "#apply_attributes" do + let(:full_name_attribute) { FactoryGirl::Attribute::Static.new(:full_name, "John Adams") } + let(:city_attribute) { FactoryGirl::Attribute::Static.new(:city, "Boston") } + let(:email_attribute) { FactoryGirl::Attribute::Dynamic.new(:email, lambda {|model| "#{model.full_name}@example.com" }) } + let(:login_attribute) { FactoryGirl::Attribute::Dynamic.new(:login, lambda {|model| "username-#{model.full_name}" }) } + + it "prepends applied attributes" do + subject.define_attribute(full_name_attribute) + subject.apply_attributes([city_attribute]) + subject.to_a.should == [city_attribute, full_name_attribute] + end + + it "moves non-static attributes to the end of the list" do + subject.define_attribute(full_name_attribute) + subject.apply_attributes([city_attribute, email_attribute]) + subject.to_a.should == [city_attribute, full_name_attribute, email_attribute] + end + + it "maintains order of non-static attributes" do + subject.define_attribute(full_name_attribute) + subject.define_attribute(login_attribute) + subject.apply_attributes([city_attribute, email_attribute]) + subject.to_a.should == [city_attribute, full_name_attribute, email_attribute, login_attribute] + end + + it "overwrites attributes that are already defined" do + subject.define_attribute(full_name_attribute) + attribute_with_same_name = FactoryGirl::Attribute::Static.new(:full_name, "Benjamin Franklin") + + subject.apply_attributes([attribute_with_same_name]) + subject.to_a.should == [attribute_with_same_name] + end +end diff --git a/spec/factory_girl/factory_spec.rb b/spec/factory_girl/factory_spec.rb index 07df127..34dd07f 100644 --- a/spec/factory_girl/factory_spec.rb +++ b/spec/factory_girl/factory_spec.rb @@ -25,24 +25,6 @@ describe FactoryGirl::Factory do @factory.default_strategy.should == :create end - it "should not allow the same attribute to be added twice" do - lambda { - 2.times { @factory.define_attribute FactoryGirl::Attribute::Static.new(:name, 'value') } - }.should raise_error(FactoryGirl::AttributeDefinitionError) - end - - it "should add a callback attribute when defining a callback" do - mock(FactoryGirl::Attribute::Callback).new(:after_create, is_a(Proc)) { 'after_create callback' } - @factory.add_callback(:after_create) {} - @factory.attributes.should include('after_create callback') - end - - it "should raise an InvalidCallbackNameError when defining a callback with an invalid name" do - lambda{ - @factory.add_callback(:invalid_callback_name) {} - }.should raise_error(FactoryGirl::InvalidCallbackNameError) - end - describe "after adding an attribute" do before do @attribute = "attribute"