mconnell-object_daddy 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.markdown +314 -0
- data/Rakefile +10 -0
- data/init.rb +12 -0
- data/install.rb +28 -0
- data/lib/object_daddy.rb +227 -0
- data/rails/init.rb +1 -0
- data/spec/install_spec.rb +123 -0
- data/spec/object_daddy_spec.rb +848 -0
- data/spec/resources/config/database.yml +3 -0
- data/spec/resources/schema +30 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +20 -0
- metadata +68 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2007 Flawed Logic, OG Consulting, Rick Bradley, Yossef Mendelssohn
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,314 @@
|
|
1
|
+
Object Daddy
|
2
|
+
============
|
3
|
+
_Version 0.4.1 (April 28, 2009)_
|
4
|
+
|
5
|
+
__Authors:__ [Rick Bradley](mailto:blogicx@rickbradley.com), [Yossef Mendelssohn](mailto:ymendel@pobox.com)
|
6
|
+
|
7
|
+
__Copyright:__ Copyright (c) 2007, Flawed Logic, OG Consulting, Rick Bradley, Yossef Mendelssohn
|
8
|
+
|
9
|
+
__License:__ MIT License. See MIT-LICENSE file for more details.
|
10
|
+
|
11
|
+
Object Daddy is a library (as well as a Ruby on Rails plugin) designed to
|
12
|
+
assist in automating testing of large collections of objects, especially webs
|
13
|
+
of ActiveRecord models. It is a descendent of the "Object Mother" pattern for
|
14
|
+
creating objects for testing, and is related to the concept of an "object
|
15
|
+
exemplar" or _stereotype_.
|
16
|
+
|
17
|
+
**WARNING** This code is very much at an _alpha_ development stage. Usage, APIs,
|
18
|
+
etc., are all subject to change.
|
19
|
+
|
20
|
+
See [http://b.logi.cx/2007/11/26/object-daddy](http://b.logi.cx/2007/11/26/object-daddy) for inspiration, historical drama, and too much reading.
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Presuming your version of Rails has git plugin installation support:
|
25
|
+
|
26
|
+
script/plugin install git://github.com/flogic/object_daddy.git
|
27
|
+
|
28
|
+
Otherwise, you can install object_daddy by hand:
|
29
|
+
|
30
|
+
1. Unpack the object_daddy directory into vendor/plugins/ in your rails project.
|
31
|
+
2. Run the object_daddy/install.rb Ruby script.
|
32
|
+
|
33
|
+
|
34
|
+
## Testing
|
35
|
+
|
36
|
+
Install the rspec gem and cd into the object_daddy directory. Type `spec
|
37
|
+
spec/` and you should see all specs run successfully. If you have autotest
|
38
|
+
from the ZenTest gem installed you can run autotest in that directory.
|
39
|
+
|
40
|
+
## Using Object Daddy
|
41
|
+
|
42
|
+
|
43
|
+
Object Daddy adds a `.generate` method to every ActiveRecord model which can be
|
44
|
+
called to generate a valid instance object of that model class, for use in
|
45
|
+
testing:
|
46
|
+
|
47
|
+
it "should have a comment for every forum the user posts to" do
|
48
|
+
@user = User.generate
|
49
|
+
@post = Post.generate
|
50
|
+
@post.comments << Comment.generate
|
51
|
+
@user.should have(1).comments
|
52
|
+
end
|
53
|
+
|
54
|
+
This allows us to generate custom model objects without relying on fixtures,
|
55
|
+
and without knowing, in our various widespread tests and specs, the details of
|
56
|
+
creating a User, Post, Comment, etc. Not having to know this information means
|
57
|
+
the information isn't coded into dozens (or hundreds) of tests, and won't need
|
58
|
+
to be changed when the User (Post, Comment, ...) model is refactored later.
|
59
|
+
|
60
|
+
Object Daddy will identify associated classes that need to be instantiated to
|
61
|
+
make the main model valid. E.g., given the following models:
|
62
|
+
|
63
|
+
class User < ActiveRecord::Base
|
64
|
+
belongs_to :login
|
65
|
+
validates_presence_of :login
|
66
|
+
end
|
67
|
+
|
68
|
+
class Login < ActiveRecord::Base
|
69
|
+
has_one :user
|
70
|
+
end
|
71
|
+
|
72
|
+
A call to `User.generate` will also make a call to `Login.generate` so that
|
73
|
+
`User#login` is present, and therefore valid.
|
74
|
+
|
75
|
+
If all models were able to be created in a valid form by the default Model.new
|
76
|
+
call with no knowledge of the model itself, there'd be no need for Object
|
77
|
+
Daddy. So, when we deal with models which have validity requirements,
|
78
|
+
requiring fields which have format constraints, we need a means of expressing
|
79
|
+
how to create those models -- how to satisfy those validity constraints.
|
80
|
+
|
81
|
+
Object Daddy provides a `generator_for` method which allows the developer to
|
82
|
+
specify, for a specific model attribute, how to make a valid value. Note that
|
83
|
+
`validates_uniqueness_of` can require that, even if we make 100,000 instances
|
84
|
+
of a model that unique attributes cannot have the same values.
|
85
|
+
|
86
|
+
Object Daddy's `generator_for` method can take three main forms corresponding to
|
87
|
+
the means of finding a value for the associated attribute: a block, a method
|
88
|
+
call, or using a generator class.
|
89
|
+
|
90
|
+
class User < ActiveRecord::Base
|
91
|
+
validates_presence_of :email
|
92
|
+
validates_uniqueness_of :email
|
93
|
+
validates_format_of :email,
|
94
|
+
:with => /^[-a-z_+0-9.]+@(?:[-a-z_+0-9.]\.)+[a-z]+$/i
|
95
|
+
validates_presence_of :username
|
96
|
+
validates_format_of :username, :with => /^[a-z0-9_]{4,12}$/i
|
97
|
+
|
98
|
+
generator_for :email, :start => 'test@domain.com' do |prev|
|
99
|
+
user, domain = prev.split('@')
|
100
|
+
user.succ + '@' + domain
|
101
|
+
end
|
102
|
+
|
103
|
+
generator_for :username, :method => :next_user
|
104
|
+
|
105
|
+
generator_for :ssn, :class => SSNGenerator
|
106
|
+
|
107
|
+
def self.next_user
|
108
|
+
@last_username ||= 'testuser'
|
109
|
+
@last_username.succ
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class SSNGenerator
|
114
|
+
def self.next
|
115
|
+
@last ||= '000-00-0000'
|
116
|
+
@last = ("%09d" % (@last.gsub('-', '').to_i + 1)).sub(/^(\d{3})(\d{2})(\d{4})$/, '\1-\2-\3')
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
Note that the block method of invocation (as used with _:email_ above) takes an
|
121
|
+
optional _:start_ argument, to specify the value of that attribute on the first
|
122
|
+
run. The block will be called thereafter with the previous value of the
|
123
|
+
attribute and will generate the next attribute value to be used.
|
124
|
+
|
125
|
+
A simple default block is provided for any generator with a :start value.
|
126
|
+
|
127
|
+
class User < ActiveRecord::Base
|
128
|
+
generator_for :name, :start => 'Joe' do |prev|
|
129
|
+
prev.succ
|
130
|
+
end
|
131
|
+
|
132
|
+
generator_for :name, :start => 'Joe' # equivalent to the above
|
133
|
+
end
|
134
|
+
|
135
|
+
The _:method_ form takes a symbol naming a class method in the model class to be
|
136
|
+
called to generate a new value for the attribute in question. If the method
|
137
|
+
takes a single argument, it will act much like the block method of invocation,
|
138
|
+
being called with the previous value and generating the next.
|
139
|
+
|
140
|
+
The _:class_ form calls the .next class method on the named class to generate a
|
141
|
+
new value for the attribute in question.
|
142
|
+
|
143
|
+
The argument (previous value) to the block invocation form can be omitted if
|
144
|
+
it's going to be ignored, and simple invocation forms are provided for literal
|
145
|
+
values.
|
146
|
+
|
147
|
+
class User < ActiveRecord::Base
|
148
|
+
generator_for(:start_time) { Time.now }
|
149
|
+
generator_for :name, 'Joe'
|
150
|
+
generator_for :age => 25
|
151
|
+
end
|
152
|
+
|
153
|
+
The developer would then simply call `User.generate` when testing.
|
154
|
+
|
155
|
+
If some attribute values are known (or are being controlled during testing)
|
156
|
+
then these can simply be passed in to `.generate`:
|
157
|
+
|
158
|
+
@bad_login = Login.generate(:expiry => 1.week.ago)
|
159
|
+
@expired_user = User.generate(:login => @bad_login)
|
160
|
+
|
161
|
+
A `.generate!` method is also provided. The _generate/generate!_ pair of methods
|
162
|
+
can be thought of as analogs to create/create!, one merely providing an instance
|
163
|
+
that may or may not be valid and the other raising an exception if any
|
164
|
+
problem comes up.
|
165
|
+
|
166
|
+
Finally, a `.spawn` method is provided that only gives a new, unsaved object. Note
|
167
|
+
that this is the only method of the three that is available if you happen to be
|
168
|
+
using Object Daddy outside of Rails.
|
169
|
+
|
170
|
+
## Exemplars
|
171
|
+
|
172
|
+
In the examples given above we are using `generator_for` in the bodies of the
|
173
|
+
models themselves. Given that Object Daddy is primarily geared towards
|
174
|
+
annotating models with information useful for testing, we anticipate that
|
175
|
+
`generator_for` should not normally be included inline in models. Rather, we
|
176
|
+
will provide a place where model classes can be re-opened and `generator_for`
|
177
|
+
calls (and support methods) can be written without polluting the model files
|
178
|
+
with Object Daddy information.
|
179
|
+
|
180
|
+
Object Daddy, when installed as a Rails plugin, will create
|
181
|
+
*RAILS_ROOT/spec/exemplars/* as a place to hold __exemplar__ files for Rails model
|
182
|
+
classes. (We are seeking perhaps some better terminology)
|
183
|
+
|
184
|
+
An __exemplar__ for the User model would then be found in
|
185
|
+
*RAILS_ROOT/spec/exemplars/user_exemplar.rb* (when you are using a testing tool
|
186
|
+
which works from *RAILS_ROOT/test*, Object Daddy will create
|
187
|
+
*RAILS_ROOT/test/exemplars* and look for your exemplars in that directory
|
188
|
+
instead). Exemplar files are completely optional, and no model need have
|
189
|
+
exemplar files. The `.generate` method will still exist and be callable, and
|
190
|
+
`generator_for` can be declared in the model files themselves. If an exemplar
|
191
|
+
file is available when `.generate` is called on a model, the exemplar file will
|
192
|
+
be loaded and used. An example *user_exemplar.rb* appears below:
|
193
|
+
|
194
|
+
require 'ssn_generator'
|
195
|
+
|
196
|
+
class User < ActiveRecord::Base
|
197
|
+
generator_for :email, :start => 'test@domain.com' do |prev|
|
198
|
+
user, domain = prev.split('@')
|
199
|
+
user.succ + '@' + domain
|
200
|
+
end
|
201
|
+
|
202
|
+
generator_for :username, :method => :next_user
|
203
|
+
|
204
|
+
generator_for :ssn, :class => SSNGenerator
|
205
|
+
|
206
|
+
def self.next_user
|
207
|
+
@last_username ||= 'testuser'
|
208
|
+
@last_username.succ
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
## Blocks
|
213
|
+
|
214
|
+
The `spawn`, `generate` and `generate!` methods can all accept a block, to which
|
215
|
+
they'll yield the generated object. This provides a nice scoping mechanism in
|
216
|
+
your code examples. Consider:
|
217
|
+
|
218
|
+
describe "admin user" do
|
219
|
+
it "should be authorized to create company profiles"
|
220
|
+
admin_user = User.generate!
|
221
|
+
admin_user.activate!
|
222
|
+
admin_user.add_role("admin")
|
223
|
+
|
224
|
+
admin_user.should be_authorized(:create, Company)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
This could be refactored to:
|
229
|
+
|
230
|
+
describe "admin user" do
|
231
|
+
it "should be authorized to create company profiles" do
|
232
|
+
admin_user = User.generate! do |user|
|
233
|
+
user.activate!
|
234
|
+
user.add_role("admin")
|
235
|
+
end
|
236
|
+
|
237
|
+
admin_user.should be_authorized(:create, Company)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
Or:
|
242
|
+
|
243
|
+
describe "admin user" do
|
244
|
+
it "should be authorized to create company profiles"
|
245
|
+
User.generate! do |user|
|
246
|
+
user.activate!
|
247
|
+
user.add_role("admin")
|
248
|
+
end.should be_authorized(:create, Company)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
Or even:
|
253
|
+
|
254
|
+
describe "admin user" do
|
255
|
+
def admin_user
|
256
|
+
@admin_user ||= User.generate! do |user|
|
257
|
+
user.activate!
|
258
|
+
user.add_role("admin")
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
it "should be authorized to create company profiles"
|
263
|
+
admin_user.should be_authorized(:create, Company)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
This last refactoring allows you to reuse the admin_user method across
|
268
|
+
multiple code examples, balancing DRY with local data.
|
269
|
+
|
270
|
+
## Object Daddy and Fixtures
|
271
|
+
|
272
|
+
While Object Daddy is meant to obviate the hellish devilspawn that are test
|
273
|
+
fixtures, Object Daddy should work alongside fixtures just fine. To each his
|
274
|
+
own, I suppose.
|
275
|
+
|
276
|
+
## Known Issues
|
277
|
+
|
278
|
+
The simple invocation forms for `generator_for` when using literal values do not
|
279
|
+
work if the literal value is a Hash. Don't do that.
|
280
|
+
|
281
|
+
class User < ActiveRecord::Base
|
282
|
+
generator_for :thing_hash, { 'some key' => 'some value' }
|
283
|
+
generator_for :other_hash => { 'other key' => 'other value' }
|
284
|
+
end
|
285
|
+
|
286
|
+
I'm not sure why this would even ever come up, but seriously, don't.
|
287
|
+
|
288
|
+
Required `belongs_to` associations are automatically generated when generating an instance,
|
289
|
+
but only if necessary.
|
290
|
+
|
291
|
+
class Category < ActiveRecord::Base
|
292
|
+
has_many :items
|
293
|
+
end
|
294
|
+
|
295
|
+
class Item < ActiveRecord::Base
|
296
|
+
belongs_to :category
|
297
|
+
validates_presence_of :category
|
298
|
+
end
|
299
|
+
|
300
|
+
`Item.generate` will generate a new category, but `some_category.items.generate` will not.
|
301
|
+
Unless, of course, you are foolish enough to define a generator in the exemplar.
|
302
|
+
|
303
|
+
class Item
|
304
|
+
generator_for(:category) { Category.generate }
|
305
|
+
end
|
306
|
+
|
307
|
+
Once again, don't do that.
|
308
|
+
|
309
|
+
## Rails _surprises_
|
310
|
+
|
311
|
+
Due to the way Rails handles associations, cascading generations (as a result of
|
312
|
+
required associations) are always generated-and-saved, even if the original generation
|
313
|
+
call was a mere `spawn` (`new`). This may come as a surprise, but it would probably be more
|
314
|
+
of a surprise if `User.spawn.save` and `User.generate` weren't comparable.
|
data/Rakefile
ADDED
data/init.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
unless ActiveRecord::Base.respond_to? :inherited_with_object_daddy
|
2
|
+
class ActiveRecord::Base
|
3
|
+
def self.inherited_with_object_daddy(subclass)
|
4
|
+
self.inherited_without_object_daddy(subclass)
|
5
|
+
subclass.send(:include, ObjectDaddy) unless subclass < ObjectDaddy
|
6
|
+
end
|
7
|
+
|
8
|
+
class << self
|
9
|
+
alias_method_chain :inherited, :object_daddy
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/install.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
def readme_contents
|
4
|
+
IO.read(File.join(File.dirname(__FILE__), 'README.markdown'))
|
5
|
+
end
|
6
|
+
|
7
|
+
rails_root = File.dirname(__FILE__) + '/../../../'
|
8
|
+
|
9
|
+
if File.directory?(rails_root + 'spec')
|
10
|
+
unless File.directory?(rails_root + 'spec/exemplars')
|
11
|
+
puts "Creating directory [#{rails_root + 'spec/exemplars'}]"
|
12
|
+
FileUtils.mkdir(rails_root + 'spec/exemplars')
|
13
|
+
end
|
14
|
+
else
|
15
|
+
if File.directory?(rails_root + 'test')
|
16
|
+
unless File.directory?(rails_root + 'test/exemplars')
|
17
|
+
puts "Creating directory [#{rails_root + 'test/exemplars'}]"
|
18
|
+
FileUtils.mkdir(rails_root + 'test/exemplars')
|
19
|
+
end
|
20
|
+
else
|
21
|
+
puts "Creating directory [#{rails_root + 'spec'}]"
|
22
|
+
FileUtils.mkdir(rails_root + 'spec')
|
23
|
+
puts "Creating directory [#{rails_root + 'spec/exemplars'}]"
|
24
|
+
FileUtils.mkdir(rails_root + 'spec/exemplars')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
puts readme_contents
|
data/lib/object_daddy.rb
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
module ObjectDaddy
|
2
|
+
|
3
|
+
def self.included(klass)
|
4
|
+
klass.extend ClassMethods
|
5
|
+
if defined? ActiveRecord and klass < ActiveRecord::Base
|
6
|
+
klass.extend RailsClassMethods
|
7
|
+
|
8
|
+
class << klass
|
9
|
+
alias_method :validates_presence_of_without_object_daddy, :validates_presence_of
|
10
|
+
alias_method :validates_presence_of, :validates_presence_of_with_object_daddy
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
attr_accessor :exemplars_generated, :exemplar_path, :generators
|
17
|
+
attr_reader :presence_validated_attributes
|
18
|
+
protected :exemplars_generated=
|
19
|
+
|
20
|
+
# :call-seq:
|
21
|
+
# spawn()
|
22
|
+
# spawn() do |obj| ... end
|
23
|
+
# spawn(args)
|
24
|
+
# spawn(args) do |obj| ... end
|
25
|
+
#
|
26
|
+
# Creates a valid instance of this class, using any known generators. The
|
27
|
+
# generated instance is yielded to a block if provided.
|
28
|
+
def spawn(args = {})
|
29
|
+
gather_exemplars
|
30
|
+
if @concrete_subclass_name
|
31
|
+
return block_given? \
|
32
|
+
? const_get(@concrete_subclass_name).spawn(args) {|instance| yield instance} \
|
33
|
+
: const_get(@concrete_subclass_name).spawn(args)
|
34
|
+
end
|
35
|
+
generate_values(args)
|
36
|
+
instance = new(args)
|
37
|
+
yield instance if block_given?
|
38
|
+
instance
|
39
|
+
end
|
40
|
+
|
41
|
+
# register a generator for an attribute of this class
|
42
|
+
# generator_for :foo do |prev| ... end
|
43
|
+
# generator_for :foo do ... end
|
44
|
+
# generator_for :foo, value
|
45
|
+
# generator_for :foo => value
|
46
|
+
# generator_for :foo, :class => GeneratorClass
|
47
|
+
# generator_for :foo, :method => :method_name
|
48
|
+
def generator_for(handle, args = {}, &block)
|
49
|
+
if handle.is_a?(Hash)
|
50
|
+
raise ArgumentError, "only specify one attr => value pair at a time" unless handle.keys.length == 1
|
51
|
+
gen_data = handle
|
52
|
+
handle = gen_data.keys.first
|
53
|
+
args = gen_data[handle]
|
54
|
+
end
|
55
|
+
|
56
|
+
raise ArgumentError, "an attribute name must be specified" unless handle = handle.to_sym
|
57
|
+
|
58
|
+
unless args.is_a?(Hash)
|
59
|
+
unless block
|
60
|
+
retval = args
|
61
|
+
block = lambda { retval } # lambda { args } results in returning the empty hash that args gets changed to
|
62
|
+
end
|
63
|
+
args = {} # args is assumed to be a hash for the rest of the method
|
64
|
+
end
|
65
|
+
|
66
|
+
if args[:start]
|
67
|
+
block ||= lambda { |prev| prev.succ }
|
68
|
+
end
|
69
|
+
|
70
|
+
if args[:method]
|
71
|
+
h = { :method => args[:method].to_sym }
|
72
|
+
h[:start] = args[:start] if args[:start]
|
73
|
+
record_generator_for(handle, h)
|
74
|
+
elsif args[:class]
|
75
|
+
raise ArgumentError, "generator class [#{args[:class].name}] does not have a :next method" unless args[:class].respond_to?(:next)
|
76
|
+
record_generator_for(handle, :class => args[:class])
|
77
|
+
elsif block
|
78
|
+
raise ArgumentError, "generator block must take an optional single argument" unless (-1..1).include?(block.arity) # NOTE: lambda {} has an arity of -1, while lambda {||} has an arity of 0
|
79
|
+
h = { :block => block }
|
80
|
+
h[:start] = args[:start] if args[:start]
|
81
|
+
record_generator_for(handle, h)
|
82
|
+
else
|
83
|
+
raise ArgumentError, "a block, :class generator, :method generator, or value must be specified to generator_for"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def generates_subclass(subclass_name)
|
88
|
+
@concrete_subclass_name = subclass_name.to_s
|
89
|
+
end
|
90
|
+
|
91
|
+
def gather_exemplars
|
92
|
+
return if exemplars_generated
|
93
|
+
if superclass.respond_to?(:gather_exemplars)
|
94
|
+
superclass.gather_exemplars
|
95
|
+
self.generators = (superclass.generators || {}).dup
|
96
|
+
end
|
97
|
+
|
98
|
+
exemplar_path.each do |raw_path|
|
99
|
+
path = File.join(raw_path, "#{underscore(name)}_exemplar.rb")
|
100
|
+
load(path) if File.exists?(path)
|
101
|
+
end
|
102
|
+
self.exemplars_generated = true
|
103
|
+
end
|
104
|
+
|
105
|
+
def presence_validated_attributes
|
106
|
+
@presence_validated_attributes ||= {}
|
107
|
+
attrs = @presence_validated_attributes
|
108
|
+
if superclass.respond_to?(:presence_validated_attributes)
|
109
|
+
attrs = superclass.presence_validated_attributes.merge(attrs)
|
110
|
+
end
|
111
|
+
attrs
|
112
|
+
end
|
113
|
+
|
114
|
+
protected
|
115
|
+
|
116
|
+
# we define an underscore helper ourselves since the ActiveSupport isn't available if we're not using Rails
|
117
|
+
def underscore(string)
|
118
|
+
string.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
|
119
|
+
end
|
120
|
+
|
121
|
+
def record_generator_for(handle, generator)
|
122
|
+
self.generators ||= {}
|
123
|
+
raise ArgumentError, "a generator for attribute [:#{handle}] has already been specified" if (generators[handle] || {})[:source] == self
|
124
|
+
generators[handle] = { :generator => generator, :source => self }
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def generate_values(args)
|
130
|
+
(generators || {}).each_pair do |handle, gen_data|
|
131
|
+
next if args.include?(handle) or args.include?(handle.to_s)
|
132
|
+
|
133
|
+
generator = gen_data[:generator]
|
134
|
+
if generator[:block]
|
135
|
+
process_generated_value(args, handle, generator, generator[:block])
|
136
|
+
elsif generator[:method]
|
137
|
+
method = method(generator[:method])
|
138
|
+
if method.arity == 1
|
139
|
+
process_generated_value(args, handle, generator, method)
|
140
|
+
else
|
141
|
+
args[handle] = method.call
|
142
|
+
end
|
143
|
+
elsif generator[:class]
|
144
|
+
args[handle] = generator[:class].next
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
generate_missing(args)
|
149
|
+
end
|
150
|
+
|
151
|
+
def process_generated_value(args, handle, generator, block)
|
152
|
+
if generator[:start]
|
153
|
+
value = generator[:start]
|
154
|
+
generator.delete(:start)
|
155
|
+
else
|
156
|
+
value = block.call(generator[:prev])
|
157
|
+
end
|
158
|
+
generator[:prev] = args[handle] = value
|
159
|
+
end
|
160
|
+
|
161
|
+
def generate_missing(args)
|
162
|
+
if presence_validated_attributes and !presence_validated_attributes.empty?
|
163
|
+
req = {}
|
164
|
+
(presence_validated_attributes.keys - args.keys).each {|a| req[a.to_s] = true } # find attributes required by validates_presence_of not already set
|
165
|
+
|
166
|
+
belongs_to_associations = reflect_on_all_associations(:belongs_to).to_a
|
167
|
+
missing = belongs_to_associations.select { |a| req[a.name.to_s] or req[a.primary_key_name.to_s] }
|
168
|
+
if create_scope = scope(:create)
|
169
|
+
missing.reject! { |a| create_scope.include?(a.primary_key_name) }
|
170
|
+
end
|
171
|
+
missing.reject! { |a| [a.name, a.primary_key_name].any? { |n| args.stringify_keys.include?(n.to_s) } }
|
172
|
+
missing.each {|a| args[a.name] = a.class_name.constantize.generate }
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
module RailsClassMethods
|
178
|
+
def exemplar_path
|
179
|
+
dir = File.directory?(File.join(RAILS_ROOT, 'spec')) ? 'spec' : 'test'
|
180
|
+
File.join(RAILS_ROOT, dir, 'exemplars')
|
181
|
+
end
|
182
|
+
|
183
|
+
def validates_presence_of_with_object_daddy(*attr_names)
|
184
|
+
@presence_validated_attributes ||= {}
|
185
|
+
new_attr = attr_names.dup
|
186
|
+
new_attr.pop if new_attr.last.is_a?(Hash)
|
187
|
+
new_attr.each {|a| @presence_validated_attributes[a] = true }
|
188
|
+
validates_presence_of_without_object_daddy(*attr_names)
|
189
|
+
end
|
190
|
+
|
191
|
+
# :call-seq:
|
192
|
+
# generate()
|
193
|
+
# generate() do |obj| ... end
|
194
|
+
# generate(args)
|
195
|
+
# generate(args) do |obj| ... end
|
196
|
+
#
|
197
|
+
# Creates and tries to save an instance of this class, using any known
|
198
|
+
# generators. The generated instance is yielded to a block if provided.
|
199
|
+
#
|
200
|
+
# This will not raise errors on a failed save. Use generate! if you
|
201
|
+
# want errors raised.
|
202
|
+
def generate(args = {})
|
203
|
+
spawn(args) do |instance|
|
204
|
+
instance.save
|
205
|
+
yield instance if block_given?
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# :call-seq:
|
210
|
+
# generate()
|
211
|
+
# generate() do |obj| ... end
|
212
|
+
# generate(args)
|
213
|
+
# generate(args) do |obj| ... end
|
214
|
+
#
|
215
|
+
# Creates and tries to save! an instance of this class, using any known
|
216
|
+
# generators. The generated instance is yielded to a block if provided.
|
217
|
+
#
|
218
|
+
# This will raise errors on a failed save. Use generate if you do not want
|
219
|
+
# errors raised.
|
220
|
+
def generate!(args = {})
|
221
|
+
spawn(args) do |instance|
|
222
|
+
instance.save!
|
223
|
+
yield instance if block_given?
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|