Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Refactored hooks code. Hooks are now inheritable, and can be defined …

…by supplying a block or a method name, or by overriding the hook instance method. Hook chains can now be broken by returning false.
  • Loading branch information...
commit 0b9b536cab6ef8da434f09c4d0132db61a0c59ce 1 parent 6b25eab
Sharon Rosner authored January 01, 2008
4  model/CHANGELOG
... ...
@@ -1,3 +1,7 @@
  1
+=== SVN
  2
+
  3
+* Refactored hooks code. Hooks are now inheritable, and can be defined by supplying a block or a method name, or by overriding the hook instance method. Hook chains can now be broken by returning false.
  4
+
1 5
 === 0.1 (2007-12-30)
2 6
 
3 7
 * Moved model code from sequel into separate model sub-project.
6  model/lib/sequel_model.rb
@@ -287,11 +287,7 @@ def self.delete_all
287 287
 
288 288
     # Like delete_all, but invokes before_destroy and after_destroy hooks if used.
289 289
     def self.destroy_all
290  
-      if has_hooks?(:before_destroy) || has_hooks?(:after_destroy)
291  
-        dataset.destroy
292  
-      else
293  
-        dataset.delete
294  
-      end
  290
+      dataset.destroy
295 291
     end
296 292
         
297 293
     def self.is_dataset_magic_method?(m)
161  model/lib/sequel_model/hooks.rb
... ...
@@ -1,122 +1,55 @@
1 1
 module Sequel
2 2
   class Model
3  
-    # This Hash translates verbs to methodnames used in chain manipulation
4  
-    # methods.
5  
-    VERB_TO_METHOD = {:prepend => :unshift, :append => :push}
6  
-
7  
-    # Returns @hooks which is an instance of Hash with its hook identifier
8  
-    # (Symbol) as key and the chain of hooks (Array) as value.
9  
-    #
10  
-    # If it is not already set it'll be with an empty set of hooks.
11  
-    # This behaviour will change in the future to allow inheritance.
12  
-    #
13  
-    # For the time being, you should be able to do:
14  
-    #
15  
-    #   class A < Sequel::Model(:a)
16  
-    #     before_save { 'Do something...' }
17  
-    #   end
18  
-    #
19  
-    #   class B < A
20  
-    #     @hooks = superclass.hooks.clone
21  
-    #     before_save # => [#<Proc:0x0000c6e8@(example.rb):123>]
22  
-    #   end
23  
-    #
24  
-    # In this case you should remember that the clone doesn't create any new
25  
-    # instances of your chains, so if you change the chain here it changes in
26  
-    # its superclass, too.
27  
-    def self.hooks
28  
-      @hooks ||= Hash.new { |h, k| h[k] = [] }
29  
-    end
30  
-
31  
-    # Adds block to chain of Hooks for <tt>:before_save</tt>.
32  
-    # It can either be prepended (default) or appended.
33  
-    #
34  
-    # Returns the chain itself.
35  
-    #
36  
-    # Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
37  
-    def self.before_save(verb = :prepend, &block)
38  
-      hooks[:before_save].send VERB_TO_METHOD.fetch(verb), block if block
39  
-      hooks[:before_save]
40  
-    end
41  
-    # Adds block to chain of Hooks for <tt>:before_create</tt>.
42  
-    # It can either be prepended (default) or appended.
43  
-    #
44  
-    # Returns the chain itself.
45  
-    #
46  
-    # Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
47  
-    def self.before_create(verb = :prepend, &block)
48  
-      hooks[:before_create].send VERB_TO_METHOD.fetch(verb), block if block
49  
-      hooks[:before_create]
50  
-    end
51  
-    # Adds block to chain of Hooks for <tt>:before_update</tt>.
52  
-    # It can either be prepended (default) or appended.
53  
-    #
54  
-    # Returns the chain itself.
55  
-    #
56  
-    # Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
57  
-    def self.before_update(verb = :prepend, &block)
58  
-      hooks[:before_update].send VERB_TO_METHOD.fetch(verb), block if block
59  
-      hooks[:before_update]
60  
-    end
61  
-    # Adds block to chain of Hooks for <tt>:before_destroy</tt>.
62  
-    # It can either be prepended (default) or appended.
63  
-    #
64  
-    # Returns the chain itself.
65  
-    #
66  
-    # Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
67  
-    def self.before_destroy(verb = :prepend, &block)
68  
-      hooks[:before_destroy].send VERB_TO_METHOD.fetch(verb), block if block
69  
-      hooks[:before_destroy]
70  
-    end
71  
-
72  
-    # Adds block to chain of Hooks for <tt>:after_save</tt>.
73  
-    # It can either be prepended or appended (default).
74  
-    #
75  
-    # Returns the chain itself.
76  
-    #
77  
-    # Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
78  
-    def self.after_save(verb = :append, &block)
79  
-      hooks[:after_save].send VERB_TO_METHOD.fetch(verb), block if block
80  
-      hooks[:after_save]
81  
-    end
82  
-    # Adds block to chain of Hooks for <tt>:after_create</tt>.
83  
-    # It can either be prepended or appended (default).
84  
-    #
85  
-    # Returns the chain itself.
86  
-    #
87  
-    # Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
88  
-    def self.after_create(verb = :append, &block)
89  
-      hooks[:after_create].send VERB_TO_METHOD.fetch(verb), block if block
90  
-      hooks[:after_create]
91  
-    end
92  
-    # Adds block to chain of Hooks for <tt>:after_update</tt>.
93  
-    # It can either be prepended or appended (default).
94  
-    #
95  
-    # Returns the chain itself.
96  
-    #
97  
-    # Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
98  
-    def self.after_update(verb = :append, &block)
99  
-      hooks[:after_update].send VERB_TO_METHOD.fetch(verb), block if block
100  
-      hooks[:after_update]
101  
-    end
102  
-    # Adds block to chain of Hooks for <tt>:after_destroy</tt>.
103  
-    # It can either be prepended or appended (default).
104  
-    #
105  
-    # Returns the chain itself.
106  
-    #
107  
-    # Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
108  
-    def self.after_destroy(verb = :append, &block)
109  
-      hooks[:after_destroy].send VERB_TO_METHOD.fetch(verb), block if block
110  
-      hooks[:after_destroy]
  3
+    HOOKS = [
  4
+      :after_initialize,
  5
+      :before_create,
  6
+      :after_create,
  7
+      :before_update,
  8
+      :after_update,
  9
+      :before_save,
  10
+      :after_save,
  11
+      :before_destroy,
  12
+      :after_destroy
  13
+    ]
  14
+    
  15
+    def self.def_hook_method(m) #:nodoc:
  16
+      # write hook def
  17
+      hook_def = "
  18
+        def self.#{m}(method = nil, &block)
  19
+          unless block
  20
+            (raise SequelError, 'No hook method specified') unless method
  21
+            block = proc {send method}
  22
+          end
  23
+          add_hook(#{m.inspect}, &block)
  24
+        end
  25
+      "
  26
+      
  27
+      instance_eval(hook_def)
111 28
     end
112  
-
113  
-    # Evaluates specified chain of Hooks through <tt>instance_eval</tt>.
114  
-    def run_hooks(key)
115  
-      model.hooks[key].each {|h| instance_eval(&h)}
  29
+    
  30
+    HOOKS.each {|h| define_method(h) {}}
  31
+    HOOKS.each {|h| def_hook_method(h)}
  32
+    
  33
+    # Returns the hooks hash for the model class.
  34
+    def self.hooks
  35
+      @hooks ||= Hash.new {|h, k| h[k] = []}
116 36
     end
117 37
     
  38
+    # Returns true if the model class or any of its ancestors have defined
  39
+    # hooks for the given hook key. Notice that this method cannot detect 
  40
+    # hooks defined using overridden methods.
118 41
     def self.has_hooks?(key)
119  
-      hooks[key] && !hooks[key].empty?
  42
+      has = hooks[key] && !hooks[key].empty?
  43
+      has || ((self != Model) && superclass.has_hooks?(key))
  44
+    end
  45
+    
  46
+    def self.add_hook(hook, &block) #:nodoc:
  47
+      chain = hooks[hook]
  48
+      chain << block
  49
+      define_method(hook) do 
  50
+        return false if super == false
  51
+        chain.each {|h| break false if instance_eval(&h) == false}
  52
+      end
120 53
     end
121 54
   end
122  
-end
  55
+end
17  model/lib/sequel_model/record.rb
@@ -192,6 +192,7 @@ def initialize(values = {}, new_record = false, &block)
192 192
       end
193 193
       
194 194
       block[self] if block
  195
+      after_initialize
195 196
     end
196 197
     
197 198
     # Returns true if the current instance represents a new record.
@@ -208,9 +209,9 @@ def exists?
208 209
     # Creates or updates the associated record. This method can also
209 210
     # accept a list of specific columns to update.
210 211
     def save(*columns)
211  
-      run_hooks(:before_save)
  212
+      before_save
212 213
       if @new
213  
-        run_hooks(:before_create)
  214
+        before_create
214 215
         iid = model.dataset.insert(@values)
215 216
         # if we have a regular primary key and it's not set in @values,
216 217
         # we assume it's the last inserted id
@@ -222,9 +223,9 @@ def save(*columns)
222 223
           refresh
223 224
         end
224 225
         @new = false
225  
-        run_hooks(:after_create)
  226
+        after_create
226 227
       else
227  
-        run_hooks(:before_update)
  228
+        before_update
228 229
         if columns.empty?
229 230
           this.update(@values)
230 231
           @changed_columns = []
@@ -232,9 +233,9 @@ def save(*columns)
232 233
           this.update(@values.reject {|k, v| !columns.include?(k)})
233 234
           @changed_columns.reject! {|c| columns.include?(c)}
234 235
         end
235  
-        run_hooks(:after_update)
  236
+        after_update
236 237
       end
237  
-      run_hooks(:after_save)
  238
+      after_save
238 239
       self
239 240
     end
240 241
     
@@ -260,9 +261,9 @@ def refresh
260 261
     # Like delete but runs hooks before and after delete.
261 262
     def destroy
262 263
       db.transaction do
263  
-        run_hooks(:before_destroy)
  264
+        before_destroy
264 265
         delete
265  
-        run_hooks(:after_destroy)
  266
+        after_destroy
266 267
       end
267 268
     end
268 269
     
287  model/spec/hooks_spec.rb
... ...
@@ -1,107 +1,246 @@
1 1
 require File.join(File.dirname(__FILE__), "spec_helper")
2 2
 
3  
-describe Sequel::Model, "hooks" do
4  
-
  3
+describe "Model hooks" do
5 4
   before do
6 5
     MODEL_DB.reset
7  
-    Sequel::Model.hooks.clear
8 6
 
9  
-    @hooks = %w[
10  
-      before_save before_create before_update before_destroy
11  
-      after_save after_create after_update after_destroy
12  
-    ].select { |hook| !hook.empty? }
  7
+    @hooks = [
  8
+      :after_initialize,
  9
+      :before_create,
  10
+      :after_create,
  11
+      :before_update,
  12
+      :after_update,
  13
+      :before_save,
  14
+      :after_save,
  15
+      :before_destroy,
  16
+      :after_destroy
  17
+    ]
  18
+    
  19
+    # @hooks.each {|h| Sequel::Model.class_def(h) {}}
13 20
   end
14  
-
15  
-  it "should have hooks for everything" do
16  
-    Sequel::Model.methods.should include('hooks')
17  
-    Sequel::Model.methods.should include(*@hooks)
18  
-    @hooks.each do |hook|
19  
-      Sequel::Model.hooks[hook.to_sym].should be_an_instance_of(Array)
  21
+  
  22
+  specify "should be definable using def <hook name>" do
  23
+    c = Class.new(Sequel::Model) do
  24
+      def before_save
  25
+        "hi there"
  26
+      end
20 27
     end
  28
+    
  29
+    c.new.before_save.should == 'hi there'
21 30
   end
22  
-
23  
-  it "should be inherited" do
24  
-    pending 'soon'
25  
-
26  
-    @hooks.each do |hook|
27  
-      Sequel::Model.send(hook.to_sym) { nil }
  31
+  
  32
+  specify "should be definable using a block" do
  33
+    $adds = []
  34
+    c = Class.new(Sequel::Model) do
  35
+      before_save {$adds << 'hi'}
28 36
     end
29  
-
30  
-    model = Class.new Sequel::Model(:models)
31  
-    model.hooks.should == Sequel::Model.hooks
  37
+    
  38
+    c.new.before_save
  39
+    $adds.should == ['hi']
32 40
   end
33  
-
34  
-  it "should run hooks" do
35  
-    pending 'soon'
36  
-
37  
-    test = mock 'Test'
38  
-    test.should_receive(:run).exactly(@hooks.length)
39  
-
40  
-    @hooks.each do |hook|
41  
-      Sequel::Model.send(hook.to_sym) { test.run }
  41
+  
  42
+  specify "should be definable using a method name" do
  43
+    $adds = []
  44
+    c = Class.new(Sequel::Model) do
  45
+      def bye; $adds << 'bye'; end
  46
+      before_save :bye
42 47
     end
43  
-
44  
-    model = Class.new Sequel::Model(:models)
45  
-    model.hooks.should == Sequel::Model.hooks
46  
-
47  
-    model_instance = model.new
48  
-    @hooks.each { |hook| model_instance.run_hooks(hook) }
  48
+    
  49
+    c.new.before_save
  50
+    $adds.should == ['bye']
49 51
   end
50  
-
51  
-  it "should run hooks around save and create" do
52  
-    pending 'test execution'
  52
+  
  53
+  specify "should be additive" do
  54
+    $adds = []
  55
+    c = Class.new(Sequel::Model) do
  56
+      before_save {$adds << 'hyiyie'}
  57
+      before_save {$adds << 'byiyie'}
  58
+    end
  59
+    
  60
+    c.new.before_save
  61
+    $adds.should == ['hyiyie', 'byiyie']
53 62
   end
54  
-
55  
-  it "should run hooks around save and update" do
56  
-    pending 'test execution'
  63
+  
  64
+  specify "should be inheritable" do
  65
+    # pending
  66
+    
  67
+    $adds = []
  68
+    a = Class.new(Sequel::Model) do
  69
+      before_save {$adds << '123'}
  70
+    end
  71
+    
  72
+    b = Class.new(a) do
  73
+      before_save {$adds << '456'}
  74
+      before_save {$adds << '789'}
  75
+    end
  76
+    
  77
+    b.new.before_save
  78
+    $adds.should == ['123', '456', '789']
57 79
   end
58  
-
59  
-  it "should run hooks around delete" do
60  
-    pending 'test execution'
  80
+  
  81
+  specify "should be overridable in descendant classes" do
  82
+    $adds = []
  83
+    a = Class.new(Sequel::Model) do
  84
+      before_save {$adds << '123'}
  85
+    end
  86
+    
  87
+    b = Class.new(a) do
  88
+      def before_save; $adds << '456'; end
  89
+    end
  90
+    
  91
+    a.new.before_save
  92
+    $adds.should == ['123']
  93
+    $adds = []
  94
+    b.new.before_save
  95
+    $adds.should == ['456']
  96
+  end
  97
+  
  98
+  specify "should stop processing if a hook returns false" do
  99
+    $flag = true
  100
+    $adds = []
  101
+    
  102
+    a = Class.new(Sequel::Model) do
  103
+      before_save {$adds << 'blah'; $flag}
  104
+      before_save {$adds << 'cruel'}
  105
+    end
  106
+    
  107
+    a.new.before_save
  108
+    $adds.should == ['blah', 'cruel']
  109
+
  110
+    # chain should not break on nil
  111
+    $adds = []
  112
+    $flag = nil
  113
+    a.new.before_save
  114
+    $adds.should == ['blah', 'cruel']
  115
+    
  116
+    $adds = []
  117
+    $flag = false
  118
+    a.new.before_save
  119
+    $adds.should == ['blah']
  120
+    
  121
+    b = Class.new(a) do
  122
+      before_save {$adds << 'mau'}
  123
+    end
  124
+    
  125
+    $adds = []
  126
+    b.new.before_save
  127
+    $adds.should == ['blah']
61 128
   end
62  
-
63 129
 end
64 130
 
65  
-describe "Model.after_create" do
  131
+describe "Model#after_initialize" do
  132
+  specify "should be called after initialization" do
  133
+    $values1 = nil
  134
+    
  135
+    a = Class.new(Sequel::Model) do
  136
+      after_initialize do
  137
+        $values1 = @values.clone
  138
+        raise Sequel::Error if @values[:blow]
  139
+      end
  140
+    end
  141
+    
  142
+    a.new(:x => 1, :y => 2)
  143
+    $values1.should == {:x => 1, :y => 2}
  144
+    
  145
+    proc {a.new(:blow => true)}.should raise_error(Sequel::Error)
  146
+  end
  147
+end
66 148
 
67  
-  before(:each) do
  149
+describe "Model#before_create && Model#after_create" do
  150
+  setup do
68 151
     MODEL_DB.reset
69 152
 
70 153
     @c = Class.new(Sequel::Model(:items)) do
71  
-      def columns
72  
-        [:id, :x, :y]
73  
-      end
  154
+      no_primary_key
  155
+      
  156
+      before_create {MODEL_DB << "BLAH before"}
  157
+      after_create {MODEL_DB << "BLAH after"}
74 158
     end
  159
+  end
  160
+  
  161
+  specify "should be called around new record creation" do
  162
+    @c.create(:x => 2)
  163
+    MODEL_DB.sqls.should == [
  164
+      'BLAH before',
  165
+      'INSERT INTO items (x) VALUES (2)',
  166
+      'BLAH after'
  167
+    ]
  168
+  end
  169
+end
75 170
 
76  
-    ds = @c.dataset
77  
-    def ds.insert(*args)
78  
-      super(*args)
79  
-      1
  171
+describe "Model#before_update && Model#after_update" do
  172
+  setup do
  173
+    MODEL_DB.reset
  174
+
  175
+    @c = Class.new(Sequel::Model(:items)) do
  176
+      before_update {MODEL_DB << "BLAH before"}
  177
+      after_update {MODEL_DB << "BLAH after"}
80 178
     end
81 179
   end
  180
+  
  181
+  specify "should be called around record update" do
  182
+    m = @c.new(:id => 2233)
  183
+    m.save
  184
+    MODEL_DB.sqls.should == [
  185
+      'BLAH before',
  186
+      'UPDATE items SET id = 2233 WHERE (id = 2233)',
  187
+      'BLAH after'
  188
+    ]
  189
+  end
  190
+end
82 191
 
83  
-  it "should be called after creation" do
84  
-    s = []
  192
+describe "Model#before_save && Model#after_save" do
  193
+  setup do
  194
+    MODEL_DB.reset
85 195
 
86  
-    @c.after_create do
87  
-      s = MODEL_DB.sqls.dup
  196
+    @c = Class.new(Sequel::Model(:items)) do
  197
+      before_save {MODEL_DB << "BLAH before"}
  198
+      after_save {MODEL_DB << "BLAH after"}
88 199
     end
89  
-
90  
-    n = @c.create(:x => 1)
91  
-    MODEL_DB.sqls.should == ["INSERT INTO items (x) VALUES (1)", "SELECT * FROM items WHERE (id = 1) LIMIT 1"]
92  
-    s.should == ["INSERT INTO items (x) VALUES (1)", "SELECT * FROM items WHERE (id = 1) LIMIT 1"]
93 200
   end
  201
+  
  202
+  specify "should be called around record update" do
  203
+    m = @c.new(:id => 2233)
  204
+    m.save
  205
+    MODEL_DB.sqls.should == [
  206
+      'BLAH before',
  207
+      'UPDATE items SET id = 2233 WHERE (id = 2233)',
  208
+      'BLAH after'
  209
+    ]
  210
+  end
  211
+  
  212
+  specify "should be called around record creation" do
  213
+    @c.no_primary_key
  214
+    @c.create(:x => 2)
  215
+    MODEL_DB.sqls.should == [
  216
+      'BLAH before',
  217
+      'INSERT INTO items (x) VALUES (2)',
  218
+      'BLAH after'
  219
+    ]
  220
+  end
  221
+end
94 222
 
95  
-  it "should allow calling save in the hook" do
96  
-    @c.after_create do
97  
-      values.delete(:x)
98  
-      self.id = 2
99  
-      save
100  
-    end
  223
+describe "Model#before_destroy && Model#after_destroy" do
  224
+  setup do
  225
+    MODEL_DB.reset
101 226
 
102  
-    n = @c.create(:id => 1)
103  
-    MODEL_DB.sqls.should == ["INSERT INTO items (id) VALUES (1)", "SELECT * FROM items WHERE (id = 1) LIMIT 1", "UPDATE items SET id = 2 WHERE (id = 1)"]
  227
+    @c = Class.new(Sequel::Model(:items)) do
  228
+      before_destroy {MODEL_DB << "BLAH before"}
  229
+      after_destroy {MODEL_DB << "BLAH after"}
  230
+      
  231
+      def delete
  232
+        MODEL_DB << "DELETE BLAH"
  233
+      end
  234
+    end
  235
+  end
  236
+  
  237
+  specify "should be called around record update" do
  238
+    m = @c.new(:id => 2233)
  239
+    m.destroy
  240
+    MODEL_DB.sqls.should == [
  241
+      'BLAH before',
  242
+      'DELETE BLAH',
  243
+      'BLAH after'
  244
+    ]
104 245
   end
105  
-
106 246
 end
107  
-
16  model/spec/model_spec.rb
@@ -344,23 +344,15 @@ def fetch_rows(sql)
344 344
   end
345 345
 
346 346
   it "should delete all records in the dataset" do
  347
+    @c.dataset.meta_def(:destroy) {MODEL_DB << "DESTROY this stuff"}
347 348
     @c.destroy_all
348  
-    MODEL_DB.sqls.should == ["DELETE FROM items"]
  349
+    MODEL_DB.sqls.should == ["DESTROY this stuff"]
349 350
   end
350 351
   
351  
-  it "should call dataset destroy method if *_destroy hooks exist" do
352  
-    @c.dataset.stub!(:destroy).and_return(true)
353  
-    @c.should_receive(:has_hooks?).with(:before_destroy).and_return(true)
  352
+  it "should call dataset.destroy" do
  353
+    @c.dataset.should_receive(:destroy).and_return(true)
354 354
     @c.destroy_all
355 355
   end
356  
-  
357  
-  it "should call dataset delete method if no hooks are present" do
358  
-    @c.dataset.stub!(:delete).and_return(true)
359  
-    @c.should_receive(:has_hooks?).with(:before_destroy).and_return(false)
360  
-    @c.should_receive(:has_hooks?).with(:after_destroy).and_return(false)
361  
-    @c.destroy_all
362  
-  end
363  
-
364 356
 end
365 357
 
366 358
 describe Sequel::Model, ".join" do

0 notes on commit 0b9b536

Please sign in to comment.
Something went wrong with that request. Please try again.