stepford 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -72,7 +72,7 @@ Put this in your `test/spec_helper.rb`, `spec/spec_helper.rb`, or some other fil
72
72
 
73
73
  #### Stepford::FactoryGirl
74
74
 
75
- Stepford::FactoryGirl acts just like FactoryGirl, but it goes through all the null=false associations in the factory and/or its presence validated associations and attempts to create/build/build_stub depending on what you called originally, but also lets you pass in an `:with_factory_options` that can contain a hash of factory name symbols to the arguments and block you'd pass to it. You specify the block using a `:blk` option with a proc/lambda (probably a lambda) to use in that method.
75
+ Stepford::FactoryGirl acts just like FactoryGirl, but it goes through all the null=false associations for foreign keys that aren't primary keys in the factory and/or its presence validated associations and attempts to create/build/build_stub depending on what you called originally, but also lets you pass in an `:with_factory_options` that can contain a hash of factory name symbols to the arguments and block you'd pass to it. You specify the block using a `:blk` option with a proc/lambda (probably a lambda) to use in that method.
76
76
 
77
77
  If you don't specify options, it's easy (note: it is even easier with the rspec helper- see below). If Foo requires Bar and Bar requires a list of Foobars and a Barfoo, and you have factories for each of those, you'd only have to do:
78
78
 
@@ -101,14 +101,7 @@ Then you can just use `create`, `create_list`, `build`, `build_list`, or `build_
101
101
 
102
102
  ##### Stopping Circular References
103
103
 
104
- If you have a circular reference (A has NOT NULL foreign key to B that has NOT NULL foreign key to C that has NOT NULL foreign key to A) in the
105
- schema, there is a workaround. First, prepopulate one of the models involved in the interdependency chain in the database as part of test setup,
106
- or if the ids are NOT NULL but are not foreign key constrained (i.e. if you can enter an invalid ID into the foreign key column, which implies possible
107
- referential integrity issues) then you may be able to set them with an invalid id. Take that foreign id and then use the following to ensure
108
- that it will set that foreign id or instance. This is done at a global level which may not work for you, but it makes it convenient to put into
109
- your spec/spec_helper.rb, etc. For example, let's say your bar has NOT NULL on bartender_id and waiter_id, and in turn bartender and waiter
110
- both have a NOT NULL bar_id, and neither enforce foreign keys. Maybe you have preloaded data for waiter somehow as the id '123', but want to set bartender to
111
- just use an invalid id '-1', and you want to do it when they are on their second loop. You could use:
104
+ If you have a circular reference (A has NOT NULL foreign key to B that has NOT NULL foreign key to C that has NOT NULL foreign key to A) either via schema where the foreign key is not also a primary key of the model with the belongs_to, or there is an ActiveRecord presence validation), there is a workaround. First, prepopulate one of the models involved in the interdependency chain in the database as part of test setup, or if the ids are NOT NULL but are not foreign key constrained (i.e. if you can enter an invalid ID into the foreign key column, which implies possible referential integrity issues) then you may be able to set them with an invalid id. Take that foreign id and then use the following to ensure that it will set that foreign id or instance. This is done at a global level which may not work for you, but it makes it convenient to put into your spec/spec_helper.rb, etc. For example, let's say your bar has NOT NULL on bartender_id and waiter_id, and in turn bartender and waiter both have a NOT NULL bar_id, and neither enforce foreign keys. Maybe you have preloaded data for waiter somehow as the id '123', but want to set bartender to just use an invalid id '-1', and you want to do it when they are on their second loop. You could use:
112
105
 
113
106
  Stepford::FactoryGirl.stop_circular_refs = {
114
107
  [:bartender, :bar] => {on_loop: 2, set_foreign_key_to: -1},
@@ -121,7 +114,7 @@ Stepford has a CLI with a circular reference checker and a generator to automati
121
114
 
122
115
  ##### Refs
123
116
 
124
- Check ActiveRecord circular dependencies:
117
+ Check ActiveRecord circular dependencies where the foreign key for a belongs_to is not also a primary key of the model, or there is an ActiveRecord presence validation keeping an association from being null:
125
118
 
126
119
  bundle exec stepford circular
127
120
 
@@ -129,31 +122,27 @@ Then it outputs the circular dependencies, e.g.:
129
122
 
130
123
  The following non-nullable foreign keys used in ActiveRecord model associations are involved in circular dependencies:
131
124
 
132
- foo.bar_id -> bar.bartender_id -> bartender.sandwich_id -> sandwich.foo_id
125
+ beers.waitress_id -> waitresses.bartender_id -> bartenders.beer_id -> beers.waitress_id
133
126
 
134
- foo.bar_id -> bar.waiter_id -> waiter.waitress_id
127
+ beers.waitress_id -> waitresses.bartender_id -> bartenders.order_id -> order.beer_id -> beers.waitress_id
135
128
 
136
- waitress.waiter_id -> bar.waiter_id -> waiter.waitress_id
137
-
138
- ...
139
129
 
140
130
  Distinct foreign keys involved in a circular dependency:
141
131
 
142
- bar.bartender_id
143
- bar.waiter_id
144
- bartender.sandwich_id
145
- foo.bar_id
146
- sandwich.foo_id
147
- waiter.waitress_id
148
- waitress.waiter_id
132
+ beers.waitress_id
133
+ order.beer_id
134
+ bartenders.beer_id
135
+ bartenders.order_id
136
+ waitresses.bartender_id
137
+
149
138
 
150
139
  Foreign keys by number of circular dependency chains involved with:
151
140
 
152
- 3 (out of 6): bar.bartender_id -> bartender
153
- 2 (out of 6): bar.waiter_id -> waiter
154
- 1 (out of 6): bartender.sandwich_id -> sandwich
155
- 1 (out of 6): foo.bar_id -> bar
156
- ...
141
+ 2 (out of 2): beers.waitress_id -> waitresses
142
+ 2 (out of 2): waitresses.bartender_id -> bartenders
143
+ 1 (out of 2): order.beer_id -> beers
144
+ 1 (out of 2): bartenders.order_id -> order
145
+ 1 (out of 2): bartenders.beer_id -> beers
157
146
 
158
147
  ##### Factories
159
148
 
@@ -1,8 +1,6 @@
1
1
  module Stepford
2
2
  class CircularRefChecker
3
3
 
4
- @@model_and_association_names = []
5
- @@level = 0
6
4
  @@offenders = []
7
5
  @@circles_sorted = []
8
6
  @@circles = []
@@ -26,6 +24,13 @@ module Stepford
26
24
  check_associations(model_class)
27
25
  end
28
26
 
27
+ if @@circles.size == 0
28
+ puts
29
+ puts "No circular dependencies."
30
+ puts
31
+ return true
32
+ end
33
+
29
34
  puts "The following non-nullable foreign keys used in ActiveRecord model associations are involved in circular dependencies:"
30
35
  @@circles.sort.each do |c|
31
36
  puts
@@ -55,43 +60,46 @@ module Stepford
55
60
  t = arr[1]
56
61
  puts "#{t} (out of #{@@circles_sorted.size}): #{c[0]}.#{c[1]} -> #{c[2]}"
57
62
  end
63
+ puts
58
64
 
59
- return (@@offenders.size == 0)
65
+ return false
60
66
  end
61
67
 
62
- def self.check_associations(model_class)
63
- @@level += 1
64
-
68
+ def self.check_associations(model_class, model_and_association_names = [])
65
69
  model_class.reflections.collect {|association_name, reflection|
66
- @@model_and_association_names = [] if @@level == 1
67
70
  next unless reflection.macro == :belongs_to
71
+ puts "warning: #{model_class}'s association #{reflection.name}'s foreign_key was nil. can't check." unless reflection.foreign_key
68
72
  assc_sym = reflection.name.to_sym
69
73
  clas_sym = reflection.class_name.underscore.to_sym
70
74
  next_class = clas_sym.to_s.camelize.constantize
71
75
 
72
76
  # 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
73
77
  has_presence_validator = model_class.validators_on(assc_sym).collect{|v|v.class}.include?(ActiveModel::Validations::PresenceValidator)
74
- required = reflection.foreign_key ? (has_presence_validator || model_class.columns.any?{|c| !c.null && c.name.to_sym == reflection.foreign_key.to_sym}) : false
75
- if required
76
- key = [model_class.table_name.to_sym, reflection.foreign_key.to_sym, next_class.table_name]
77
- if @@model_and_association_names.include?(key)
78
- @@offenders << @@model_and_association_names.last unless @@offenders.include?(@@model_and_association_names.last)
79
- short = @@model_and_association_names.dup
78
+ # note: supports composite_primary_keys gem which stores primary_key as an array
79
+ foreign_key_is_also_primary_key = Array.wrap(model_class.primary_key).collect{|pk|pk.to_sym}.include?(reflection.foreign_key.to_sym)
80
+ is_not_null_fkey_that_is_not_primary_key = model_class.columns.any?{|c| !c.null && c.name.to_sym == reflection.foreign_key.to_sym && !foreign_key_is_also_primary_key}
81
+
82
+ if is_not_null_fkey_that_is_not_primary_key || has_presence_validator
83
+ key = [model_class.table_name.to_sym, reflection.foreign_key.to_sym, next_class.table_name.to_sym]
84
+ if model_and_association_names.include?(key)
85
+ @@offenders << model_and_association_names.last unless @@offenders.include?(model_and_association_names.last)
86
+ short = model_and_association_names.dup
80
87
  # drop all preceding keys that have nothing to do with the circle
81
88
  (short.index(key)).times {short.delete_at(0)}
82
89
  sorted = short.sort
83
90
  unless @@circles_sorted.include?(sorted)
84
91
  @@circles_sorted << sorted
85
- @@circles << "#{(short << key).collect{|b|"#{b[0]}.#{b[1]}"}.join(' -> ')}".to_sym
92
+ @@circles << "#{(short + [key]).collect{|b|"#{b[0]}.#{b[1]}"}.join(' -> ')}".to_sym
86
93
  end
87
94
  else
88
- @@model_and_association_names << key
89
- check_associations(next_class)
95
+ model_and_association_names << key
96
+ check_associations(next_class, model_and_association_names)
90
97
  end
91
98
  end
92
99
  }
93
100
 
94
- @@level -= 1
101
+ model_and_association_names.pop
102
+ model_and_association_names
95
103
  end
96
104
  end
97
105
  end
@@ -66,7 +66,13 @@ module Stepford
66
66
  has_presence_validator = model_class.validators_on(assc_sym).collect{|v|v.class}.include?(::ActiveModel::Validations::PresenceValidator)
67
67
  required = reflection.foreign_key ? (has_presence_validator || model_class.columns.any?{|c| !c.null && c.name.to_sym == reflection.foreign_key.to_sym}) : false
68
68
  orig_method_args_and_options = with_factory_options ? (with_factory_options[[clas_sym, assc_sym]] || with_factory_options[clas_sym]) : nil
69
- if required || orig_method_args_and_options
69
+ # 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
70
+ has_presence_validator = model_class.validators_on(assc_sym).collect{|v|v.class}.include?(ActiveModel::Validations::PresenceValidator)
71
+ # note: supports composite_primary_keys gem which stores primary_key as an array
72
+ foreign_key_is_also_primary_key = Array.wrap(model_class.primary_key).collect{|pk|pk.to_sym}.include?(reflection.foreign_key.to_sym)
73
+ is_not_null_fkey_that_is_not_primary_key = model_class.columns.any?{|c| !c.null && c.name.to_sym == reflection.foreign_key.to_sym && !foreign_key_is_also_primary_key}
74
+
75
+ if is_not_null_fkey_that_is_not_primary_key || has_presence_validator
70
76
  circular_ref_key = [model_sym, assc_sym]
71
77
  all_opts = ::Stepford::FactoryGirl.stop_circular_refs
72
78
  if all_opts.is_a?(Hash) && all_opts.size > 0
@@ -1,3 +1,3 @@
1
1
  module Stepford
2
- VERSION = '0.9.2'
2
+ VERSION = '0.9.3'
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.9.2
4
+ version: 0.9.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors: