mconnell-object_daddy 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|