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 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
@@ -0,0 +1,10 @@
1
+ require 'rake'
2
+ require 'rake/gempackagetask'
3
+
4
+ gem_spec = eval(File.read('object_daddy.gemspec'))
5
+
6
+ Rake::GemPackageTask.new(gem_spec) do |p|
7
+ p.need_tar = false
8
+ p.need_zip = false
9
+ end
10
+
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
@@ -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