Skip to content
Browse files

version 0.5.0. using after(:create) per Keenan Brock to solve create_…

…list issue. adding --cache-associations to avoid circular references
  • Loading branch information...
1 parent 17f175d commit d36cb8d967c68cc0f11aec9955e9eaba1fb1f76d @garysweaver committed Nov 5, 2012
Showing with 83 additions and 36 deletions.
  1. +5 −3 README.md
  2. +2 −1 lib/stepford/cli.rb
  3. +75 −31 lib/stepford/factory_girl.rb
  4. +1 −1 lib/stepford/version.rb
View
8 README.md
@@ -89,6 +89,10 @@ If associations are deemed broken, it will output proposed changes.
If working with a legacy schema, you may have models with foreign_key columns that you don't have associations defined for in the model. If that is the case, we don't want to assign arbitrary integers to them and try to create a record. If that is the case, try `--exclude-all-ids`, which will exclude those ids as attributes defined in the factories and you can add associations as needed to get things working.
+##### Singleton Values
+
+Use `--cache-associations` to store and use factories to avoid 'stack level too deep' errors.
+
##### Specifying Models
Specify `--models` and a comma-delimited list of models to only output the models you specify. If you don't want to overwrite existing factory files, you should direct the output to another file and manually copy each in:
@@ -121,13 +125,11 @@ or maybe:
you might either need to modify those factories to set associations that are required or specify `--associations` in Stepford to attempt generate them.
-If you specify `--associations`, you might get circular associations and could easily end up with:
+Without `--cache-associations`, you might get circular associations and could easily end up with:
SystemStackError:
stack level too deep
-One way to do it would be to [return singleton instances from factory create][singletons], which would require some changes to the factories.
-
ThoughtBot's Josh Clayton also provided some suggestions for this, including using methods to generate more complex object structures:
def post_containing_comment_by_author
View
3 lib/stepford/cli.rb
@@ -11,7 +11,8 @@ class CLI < Thor
method_option :models, :desc => "A comma delimited list of only the models you want to include"
method_option :attributes, :desc => "Include all attributes except foreign keys and primary keys, not just those that are required due to ActiveRecord presence validation or column not null restriction", :type => :boolean
method_option :attribute_traits, :desc => "Include traits for attributes that would be output with --attributes that wouldn't be otherwise when --attributes is not specified", :type => :boolean
- method_option :association_traits, :desc => "Include traits for attributes that would be output with --associations that wouldn't be otherwise when --associations is not specified", :type => :boolean
+ method_option :association_traits, :desc => "Include traits for associations that would be output with --associations that wouldn't be otherwise when --associations is not specified", :type => :boolean
+ method_option :cache_associations, :desc => "Use singleton values to avoid 'stack level too deep' circular reference(s)", :type => :boolean
def factories()
# load Rails environment
require './config/environment'
View
106 lib/stepford/factory_girl.rb
@@ -2,8 +2,26 @@
module Stepford
class FactoryGirl
+ CACHE_VALUES_FILENAME = 'fg_cache.rb'
+
def self.generate_factories(options={})
# guard against circular references
+ if options[:cache_associations]
+ File.open(File.join(File.dirname(get_factories_rb_pathname(options)), CACHE_VALUES_FILENAME), "w") do |f|
+ #TODO: just copy this file from the gem to project vs. writing it like this
+ f.puts '# originally created by Stepford: https://github.com/garysweaver/stepford'
+ f.puts '# idea somewhat based on d2vid and snowangel\'s answer in http://stackoverflow.com/questions/2015473/using-factory-girl-in-rails-with-associations-that-have-unique-constraints-gett/3569062#3569062'
+ f.puts 'fg_cachehash = {}'
+ f.puts 'def fg_cache(class_sym, assc_sym = nil, number = nil)'
+ # if missing 3rd arg, assume 2nd arg is 3rd arg or use default
+ # if missing 2nd and 3rd arg, assume 2nd arg is 1st arg
+ f.puts ' number ||= assc_sym'
+ f.puts ' assc_sym ||= class_sym'
+ f.puts ' fg_cachehash[factory_sym, assc_sym, number] ||= (number ? FactoryGirl.create_list(class_sym, number) : FactoryGirl.create(class_sym))'
+ f.puts 'end'
+ end
+ end
+
factories = {}
expected = {}
included_models = options[:models] ? options[:models].split(',').collect{|s|s.strip}.compact : nil
@@ -21,15 +39,24 @@ def self.generate_factories(options={})
assc_sym = reflection.name.to_sym
clas_sym = reflection.class_name.underscore.to_sym
# if has a foreign key, then if NOT NULL or is a presence validate, the association is required and should be output. unfortunately this could mean a circular reference that will have to be manually fixed
- required = reflection.foreign_key ? (model_class.validators_on(assc_sym).collect{|v|v.class}.include?(ActiveModel::Validations::PresenceValidator) || model_class.columns.any?{|c| !c.null && c.name.to_sym == reflection.foreign_key.to_sym}) : false
+ has_presence_validator = model_class.validators_on(assc_sym).collect{|v|v.class}.include?(ActiveModel::Validations::PresenceValidator)
+ required = reflection.foreign_key ? (has_presence_validator || model_class.columns.any?{|c| !c.null && c.name.to_sym == reflection.foreign_key.to_sym}) : false
should_be_trait = !(options[:associations] || required) && options[:association_traits]
if options[:associations] || required || should_be_trait
- if reflection.macro == :has_many
- "#{should_be_trait ? "trait #{"with_#{assc_sym}".to_sym.inspect} do; " : ''}#{should_be_trait ? '' : '#'}FactoryGirl.create_list #{clas_sym.inspect}, 2#{should_be_trait ? '; end' : ''}#{should_be_trait ? '' : ' # commented to avoid circular reference'}"
- elsif assc_sym != clas_sym
- "#{should_be_trait ? "trait #{"with_#{assc_sym}".to_sym.inspect} do; " : ''}#{(should_be_trait || reflection.macro == :belongs_to) ? '' : '#'}association #{assc_sym.inspect}#{assc_sym != clas_sym ? ", factory: #{clas_sym.inspect}" : ''}#{should_be_trait ? '; end' : ''}#{should_be_trait || reflection.macro == :belongs_to ? '' : ' # commented to avoid circular reference'}"
+ if options[:cache_associations]
+ if reflection.macro == :has_many
+ "#{should_be_trait ? "trait #{"with_#{assc_sym}".to_sym.inspect} do; " : ''}after(:create) do |user, evaluator|; #{is_reserved?(assc_sym) ? 'self.' : ''}#{assc_sym} = fg_cache(#{clas_sym.inspect}#{clas_sym == assc_sym ? '' : ", #{assc_sym.inspect}"}, 2); end"
+ else
+ "#{should_be_trait ? "trait #{"with_#{assc_sym}".to_sym.inspect} do; " : ''}after(:create) do |user, evaluator|; #{is_reserved?(assc_sym) ? 'self.' : ''}#{assc_sym} = fg_cache(#{clas_sym.inspect}#{clas_sym == assc_sym ? '' : ", #{assc_sym.inspect}"}); end"
+ end
else
- "#{should_be_trait ? "trait #{"with_#{assc_sym}".to_sym.inspect} do; " : ''}#{(should_be_trait || reflection.macro == :belongs_to) ? '' : '#'}#{assc_sym}#{should_be_trait ? '; end' : ''}#{should_be_trait || reflection.macro == :belongs_to ? '' : ' # commented to avoid circular reference'}"
+ if reflection.macro == :has_many
+ "#{should_be_trait ? "trait #{"with_#{assc_sym}".to_sym.inspect} do; " : ''}#{should_be_trait || has_presence_validator ? '' : '#'}after(:create) do |user, evaluator|; FactoryGirl.create_list #{clas_sym.inspect}, 2; end#{should_be_trait ? '; end' : ''}#{should_be_trait ? '' : ' # commented to avoid circular reference'}"
+ elsif assc_sym != clas_sym
+ "#{should_be_trait ? "trait #{"with_#{assc_sym}".to_sym.inspect} do; " : ''}#{should_be_trait || reflection.macro == :belongs_to || has_presence_validator ? '' : '#'}association #{assc_sym.inspect}#{assc_sym != clas_sym ? ", factory: #{clas_sym.inspect}" : ''}#{should_be_trait ? '; end' : ''}#{should_be_trait || reflection.macro == :belongs_to ? '' : ' # commented to avoid circular reference'}"
+ else
+ "#{should_be_trait ? "trait #{"with_#{assc_sym}".to_sym.inspect} do; " : ''}#{should_be_trait || reflection.macro == :belongs_to || has_presence_validator ? '' : '#'}#{is_reserved?(assc_sym) ? 'self.' : ''}#{assc_sym}#{should_be_trait ? '; end' : ''}#{should_be_trait || reflection.macro == :belongs_to ? '' : ' # commented to avoid circular reference'}"
+ end
end
else
nil
@@ -71,18 +98,7 @@ def self.generate_factories(options={})
return false if failed
end
- path = File.join('test','factories.rb')
- if options[:path]
- if options[:path].end_with?('.rb')
- path = options[:path]
- else
- if options[:single]
- path = File.join(options[:path],'factories.rb')
- else
- path = options[:path]
- end
- end
- end
+ path = get_factories_rb_pathname(options)
if path.end_with?('.rb')
dirpath = File.dirname(path)
@@ -92,16 +108,12 @@ def self.generate_factories(options={})
end
File.open(path, "w") do |f|
- f.puts 'require \'factory_girl_rails\''
- f.puts ''
- f.puts 'FactoryGirl.define do'
- f.puts ' '
+ write_header(f, options)
factories.keys.sort.each do |factory_name|
factory = factories[factory_name]
write_factory(factory_name, factory, f)
- f.puts ' '
end
- f.puts "end"
+ write_footer(f)
end
else
unless File.directory?(path)
@@ -112,13 +124,9 @@ def self.generate_factories(options={})
factories.keys.sort.each do |factory_name|
factory = factories[factory_name]
File.open(File.join(path,"#{factory_name}.rb"), "w") do |f|
- f.puts 'require \'factory_girl_rails\''
- f.puts ''
- f.puts 'FactoryGirl.define do'
- f.puts ' '
+ write_header(f, options)
write_factory(factory_name, factory, f)
- f.puts ' '
- f.puts "end"
+ write_footer(f)
end
end
end
@@ -127,13 +135,49 @@ def self.generate_factories(options={})
end
private
+
+ def self.is_reserved?(s)
+ # from http://stackoverflow.com/questions/6461303/built-in-way-to-determine-whether-a-string-is-a-ruby-reserved-word/6461673#6461673
+ %w{__FILE__ __LINE__ alias and begin BEGIN break case class def defined? do else elsif end END ensure false for if in module next nil not or redo rescue retry return self super then true undef unless until when while yield}.include? s.to_s
+ end
+
+ def self.get_factories_rb_pathname(options)
+ path = File.join('test','factories.rb')
+ if options[:path]
+ if options[:path].end_with?('.rb')
+ path = options[:path]
+ else
+ if options[:single]
+ path = File.join(options[:path],'factories.rb')
+ else
+ path = options[:path]
+ end
+ end
+ end
+ path
+ end
+
+ def self.write_header(f, options)
+ f.puts 'require \'factory_girl_rails\''
+ f.puts "require_relative \'#{CACHE_VALUES_FILENAME.chomp('.rb')}\'" if options[:cache_associations]
+ f.puts ''
+ f.puts '# originally created by Stepford: https://github.com/garysweaver/stepford'
+ f.puts ''
+ f.puts 'FactoryGirl.define do'
+ f.puts ' '
+ end
def self.write_factory(factory_name, factory, f)
f.puts " factory #{factory_name.inspect} do"
factory.each do |line|
f.puts " #{line}"
end
- f.puts " end"
+ f.puts ' end'
+ f.puts ' '
+ end
+
+ def self.write_footer(f)
+ f.puts 'end'
end
end
end
View
2 lib/stepford/version.rb
@@ -1,3 +1,3 @@
module Stepford
- VERSION = '0.4.0'
+ VERSION = '0.5.0'
end

0 comments on commit d36cb8d

Please sign in to comment.
Something went wrong with that request. Please try again.