stepford 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -11,9 +11,13 @@ Stepford is a CLI to create starter [Factory Girl][factory_girl] factories for a
11
11
  author
12
12
  association :edited_by, factory: :user
13
13
  FactoryGirl.create_list :comments, 2
14
+ trait :with_notes do; FactoryGirl.create_list :note, 2; end
15
+ trait :complete do; complete true; end
16
+ trait :not_complete do; complete false; end
14
17
  created_at { 2.weeks.ago }
15
18
  name 'Test Name'
16
19
  price 1.23
20
+ trait :with_summary do; template 'Test Summary'; end
17
21
  updated_at { 2.weeks.ago }
18
22
  end
19
23
 
@@ -37,6 +41,16 @@ Then run:
37
41
 
38
42
  #### Factory Girl
39
43
 
44
+ ##### How NOT NULL, and Other Database Constraints and Active Record Validations Are Handled
45
+
46
+ If the ActiveRecord column `null` property for the attribute is true for the attribute or foreign key for the association, or if there is a presence validator for an attribute or foreign key for the association, then that attribute or association will be defined by the default factory.
47
+
48
+ Currently uniqueness constraints are ignored and must be handled by FactoryGirl sequence or similar if not automatically populated by your model or the database, e.g. in your factory, if username uniqueness is enforced by a unique constraint on the database-side, you'll need to do something like this manually in the factory:
49
+
50
+ sequence(:username) {|n| "user#{n}" }
51
+
52
+ ##### Creating Factories
53
+
40
54
  The default will assume a `test/factories` directory exists. In that directory, it will create a factory file for each model containing example values for all attributes except primary keys, foreign keys, created_at, and updated_at:
41
55
 
42
56
  bundle exec stepford factories
@@ -49,29 +63,47 @@ It will figure out that you want a single file, if the path ends in `.rb`:
49
63
 
50
64
  bundle exec stepford factories --path spec/support/factories.rb
51
65
 
52
- ### Associations
66
+ ##### Traits
67
+
68
+ To generate traits for each attribute that would be included with `--attributes`, but isn't because `--attributes` is not specified:
69
+
70
+ bundle exec stepford factories --attribute-traits
53
71
 
54
- To include associations:
72
+ To generate traits for each association that would be included with `--associations`, but isn't because `--associations` is not specified:
73
+
74
+ bundle exec stepford factories --association-traits
75
+
76
+ ##### Associations
77
+
78
+ To include all associations even if they aren't deemed to be required by not null ActiveRecord constraints defined in the model:
55
79
 
56
80
  bundle exec stepford factories --associations
57
81
 
58
- ### Stepford Checks Model Associations
82
+ ##### Stepford Checks Model Associations
59
83
 
60
84
  If `--associations` or `--validate-associations` is specified, Stepford first loads Rails and attempts to check your models for broken associations.
61
85
 
62
86
  If associations are deemed broken, it will output proposed changes.
63
87
 
64
- ### No IDs
88
+ ##### No IDs
65
89
 
66
90
  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.
67
91
 
68
- ### Specifying Models
92
+ ##### Singleton Values
93
+
94
+ Use `--cache-associations` to store and use factories to avoid 'stack level too deep' errors.
95
+
96
+ ##### Specifying Models
69
97
 
70
98
  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:
71
99
 
72
100
  bundle exec stepford factories --path spec/support/put_into_factories.rb --models foo,bar,foo_bar
73
101
 
74
- ### Troubleshooting
102
+ ##### Testing Factories
103
+
104
+ See [Testing all Factories (with RSpec)][test_factories] in the FG wiki.
105
+
106
+ ##### Troubleshooting
75
107
 
76
108
  If you have duplicate factory definitions during Rails load, it may complain. Just move, rename, or remove the offending files and factories and retry.
77
109
 
@@ -93,12 +125,12 @@ or maybe:
93
125
 
94
126
  you might either need to modify those factories to set associations that are required or specify `--associations` in Stepford to attempt generate them.
95
127
 
96
- If you specify `--associations`, you might get circular associations and could easily end up with:
128
+ Without `--cache-associations`, you might get circular associations and could easily end up with:
97
129
 
98
130
  SystemStackError:
99
131
  stack level too deep
100
132
 
101
- ThoughtBot's Josh Clayton provided some suggestions for this, including using methods to generate more complex object structures:
133
+ ThoughtBot's Josh Clayton also provided some suggestions for this, including using methods to generate more complex object structures:
102
134
 
103
135
  def post_containing_comment_by_author
104
136
  author = FactoryGirl.create(:user)
@@ -132,5 +164,7 @@ or referring to created objects through associations, though he said multiple ne
132
164
 
133
165
  Copyright (c) 2012 Gary S. Weaver, released under the [MIT license][lic].
134
166
 
167
+ [singletons]: http://stackoverflow.com/questions/2015473/using-factory-girl-in-rails-with-associations-that-have-unique-constraints-gett/3569062#3569062
168
+ [test_factories]: https://github.com/thoughtbot/factory_girl/wiki/Testing-all-Factories-%28with-RSpec%29
135
169
  [factory_girl]: https://github.com/thoughtbot/factory_girl/
136
170
  [lic]: http://github.com/garysweaver/stepford/blob/master/LICENSE
data/lib/stepford/cli.rb CHANGED
@@ -5,10 +5,14 @@ module Stepford
5
5
  desc "factories", "create FactoryGirl factories"
6
6
  method_option :single, :desc => "Put all factories into a single file", :type => :boolean
7
7
  method_option :path, :desc => "Pathname of file to contain factories or path of directory to contain factory/factories"
8
- method_option :associations, :desc => "Include associations in factories", :type => :boolean
8
+ method_option :associations, :desc => "Include all associations in factories, not just those that are required due to ActiveRecord presence validation or column not null restriction", :type => :boolean
9
9
  method_option :validate_associations, :desc => "Validate associations in factories even if not including associations", :type => :boolean
10
10
  method_option :exclude_all_ids, :desc => "Exclude attributes with names ending in _id even if they aren't foreign or primary keys", :type => :boolean
11
11
  method_option :models, :desc => "A comma delimited list of only the models you want to include"
12
+ 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
13
+ 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
14
+ 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
15
+ method_option :cache_associations, :desc => "Use singleton values to avoid 'stack level too deep' circular reference(s)", :type => :boolean
12
16
  def factories()
13
17
  # load Rails environment
14
18
  require './config/environment'
@@ -20,7 +20,7 @@ module Stepford
20
20
  when :boolean
21
21
  column.default.nil? ? 'true' : column.default.to_s
22
22
  when :xml
23
- column.default.nil? ? '<test>Test #{column_name.titleize}</test>' : column.default.to_s
23
+ column.default.nil? ? '<test>Test #{column.name.titleize}</test>' : column.default.to_s
24
24
  when :ts_vector
25
25
  column.default.nil? ? 'nil' : column.default.to_s
26
26
  else
@@ -2,8 +2,26 @@ require 'stepford/common'
2
2
 
3
3
  module Stepford
4
4
  class FactoryGirl
5
+ CACHE_VALUES_FILENAME = 'fg_cache.rb'
6
+
5
7
  def self.generate_factories(options={})
6
8
  # guard against circular references
9
+ if options[:cache_associations]
10
+ File.open(File.join(File.dirname(get_factories_rb_pathname(options)), CACHE_VALUES_FILENAME), "w") do |f|
11
+ #TODO: just copy this file from the gem to project vs. writing it like this
12
+ f.puts '# originally created by Stepford: https://github.com/garysweaver/stepford'
13
+ 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'
14
+ f.puts 'fg_cachehash = {}'
15
+ f.puts 'def fg_cache(class_sym, assc_sym = nil, number = nil)'
16
+ # if missing 3rd arg, assume 2nd arg is 3rd arg or use default
17
+ # if missing 2nd and 3rd arg, assume 2nd arg is 1st arg
18
+ f.puts ' number ||= assc_sym'
19
+ f.puts ' assc_sym ||= class_sym'
20
+ f.puts ' fg_cachehash[factory_sym, assc_sym, number] ||= (number ? FactoryGirl.create_list(class_sym, number) : FactoryGirl.create(class_sym))'
21
+ f.puts 'end'
22
+ end
23
+ end
24
+
7
25
  factories = {}
8
26
  expected = {}
9
27
  included_models = options[:models] ? options[:models].split(',').collect{|s|s.strip}.compact : nil
@@ -17,25 +35,45 @@ module Stepford
17
35
  excluded_attributes = Array.wrap(model_class.primary_key).collect{|pk|pk.to_sym} + [:updated_at, :created_at]
18
36
  association_lines = model_class.reflections.collect {|association_name, reflection|
19
37
  (expected[reflection.class_name.underscore.to_sym] ||= []) << model_name
20
- excluded_attributes << reflection.foreign_key.to_sym
38
+ excluded_attributes << reflection.foreign_key.to_sym if reflection.foreign_key
21
39
  assc_sym = reflection.name.to_sym
22
40
  clas_sym = reflection.class_name.underscore.to_sym
23
- # we have to do the part above to not set arbitrary values in foreign key attributes
24
- if options[:associations]
25
- if reflection.macro == :has_many
26
- "FactoryGirl.create_list #{assc_sym.inspect}, 2"
27
- elsif assc_sym != clas_sym
28
- "association #{assc_sym.inspect}#{assc_sym != clas_sym ? ", factory: #{clas_sym.inspect}" : ''}"
41
+ # 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
42
+ has_presence_validator = model_class.validators_on(assc_sym).collect{|v|v.class}.include?(ActiveModel::Validations::PresenceValidator)
43
+ required = reflection.foreign_key ? (has_presence_validator || model_class.columns.any?{|c| !c.null && c.name.to_sym == reflection.foreign_key.to_sym}) : false
44
+ should_be_trait = !(options[:associations] || required) && options[:association_traits]
45
+ if options[:associations] || required || should_be_trait
46
+ if options[:cache_associations]
47
+ if reflection.macro == :has_many
48
+ "#{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"
49
+ else
50
+ "#{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"
51
+ end
29
52
  else
30
- "#{assc_sym}"
53
+ if reflection.macro == :has_many
54
+ "#{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'}"
55
+ elsif assc_sym != clas_sym
56
+ "#{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'}"
57
+ else
58
+ "#{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'}"
59
+ end
31
60
  end
32
61
  else
33
62
  nil
34
63
  end
35
64
  }.compact.sort.each {|l|factory << l}
36
- model_class.columns.collect {|c|
37
- "#{c.name.to_sym} #{Stepford::Common.value_for(c)}" unless (excluded_attributes.include?(c.name.to_sym) || ((c.name.downcase.end_with?('_id') && options[:exclude_all_ids])))
38
- }.compact.sort.each {|l|factory << l}
65
+ model_class.columns.sort_by {|c|[c.name]}.each {|c|
66
+ if !excluded_attributes.include?(c.name.to_sym) && !(c.name.downcase.end_with?('_id') && options[:exclude_all_ids]) && (options[:attributes] || !c.null)
67
+ factory << "#{c.name} #{Stepford::Common.value_for(c)}"
68
+ elsif options[:attribute_traits]
69
+ if c.type == :boolean
70
+ factory << "trait #{c.name.underscore.to_sym.inspect} do; #{c.name} true; end"
71
+ factory << "trait #{"not_#{c.name.underscore}".to_sym.inspect} do; #{c.name} false; end"
72
+ else
73
+ factory << "trait #{"with_#{c.name.underscore}".to_sym.inspect} do; #{c.name} #{Stepford::Common.value_for(c)}; end"
74
+ end
75
+ end
76
+ }
39
77
  end
40
78
 
41
79
  if options[:associations] || options[:validate_associations]
@@ -60,18 +98,7 @@ module Stepford
60
98
  return false if failed
61
99
  end
62
100
 
63
- path = File.join('test','factories.rb')
64
- if options[:path]
65
- if options[:path].end_with?('.rb')
66
- path = options[:path]
67
- else
68
- if options[:single]
69
- path = File.join(options[:path],'factories.rb')
70
- else
71
- path = options[:path]
72
- end
73
- end
74
- end
101
+ path = get_factories_rb_pathname(options)
75
102
 
76
103
  if path.end_with?('.rb')
77
104
  dirpath = File.dirname(path)
@@ -81,16 +108,12 @@ module Stepford
81
108
  end
82
109
 
83
110
  File.open(path, "w") do |f|
84
- f.puts 'require \'factory_girl_rails\''
85
- f.puts ''
86
- f.puts 'FactoryGirl.define do'
87
- f.puts ' '
111
+ write_header(f, options)
88
112
  factories.keys.sort.each do |factory_name|
89
113
  factory = factories[factory_name]
90
114
  write_factory(factory_name, factory, f)
91
- f.puts ' '
92
115
  end
93
- f.puts "end"
116
+ write_footer(f)
94
117
  end
95
118
  else
96
119
  unless File.directory?(path)
@@ -101,13 +124,9 @@ module Stepford
101
124
  factories.keys.sort.each do |factory_name|
102
125
  factory = factories[factory_name]
103
126
  File.open(File.join(path,"#{factory_name}.rb"), "w") do |f|
104
- f.puts 'require \'factory_girl_rails\''
105
- f.puts ''
106
- f.puts 'FactoryGirl.define do'
107
- f.puts ' '
127
+ write_header(f, options)
108
128
  write_factory(factory_name, factory, f)
109
- f.puts ' '
110
- f.puts "end"
129
+ write_footer(f)
111
130
  end
112
131
  end
113
132
  end
@@ -116,13 +135,49 @@ module Stepford
116
135
  end
117
136
 
118
137
  private
138
+
139
+ def self.is_reserved?(s)
140
+ # from http://stackoverflow.com/questions/6461303/built-in-way-to-determine-whether-a-string-is-a-ruby-reserved-word/6461673#6461673
141
+ %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
142
+ end
143
+
144
+ def self.get_factories_rb_pathname(options)
145
+ path = File.join('test','factories.rb')
146
+ if options[:path]
147
+ if options[:path].end_with?('.rb')
148
+ path = options[:path]
149
+ else
150
+ if options[:single]
151
+ path = File.join(options[:path],'factories.rb')
152
+ else
153
+ path = options[:path]
154
+ end
155
+ end
156
+ end
157
+ path
158
+ end
159
+
160
+ def self.write_header(f, options)
161
+ f.puts 'require \'factory_girl_rails\''
162
+ f.puts "require_relative \'#{CACHE_VALUES_FILENAME.chomp('.rb')}\'" if options[:cache_associations]
163
+ f.puts ''
164
+ f.puts '# originally created by Stepford: https://github.com/garysweaver/stepford'
165
+ f.puts ''
166
+ f.puts 'FactoryGirl.define do'
167
+ f.puts ' '
168
+ end
119
169
 
120
170
  def self.write_factory(factory_name, factory, f)
121
171
  f.puts " factory #{factory_name.inspect} do"
122
172
  factory.each do |line|
123
173
  f.puts " #{line}"
124
174
  end
125
- f.puts " end"
175
+ f.puts ' end'
176
+ f.puts ' '
177
+ end
178
+
179
+ def self.write_footer(f)
180
+ f.puts 'end'
126
181
  end
127
182
  end
128
183
  end
@@ -1,3 +1,3 @@
1
1
  module Stepford
2
- VERSION = '0.3.0'
2
+ VERSION = '0.5.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stepford
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-02 00:00:00.000000000 Z
12
+ date: 2012-11-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: thor