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 +42 -8
- data/lib/stepford/cli.rb +5 -1
- data/lib/stepford/common.rb +1 -1
- data/lib/stepford/factory_girl.rb +91 -36
- data/lib/stepford/version.rb +1 -1
- metadata +2 -2
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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'
|
data/lib/stepford/common.rb
CHANGED
@@ -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 #{
|
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
|
-
#
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
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.
|
37
|
-
|
38
|
-
|
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 =
|
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
|
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
|
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
|
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
|
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
|
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
|
data/lib/stepford/version.rb
CHANGED
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.
|
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-
|
12
|
+
date: 2012-11-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: thor
|