hooked 0.1.2 → 0.2.0

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/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile CHANGED
@@ -3,3 +3,4 @@ source "http://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  gem "awesome_print"
6
+ gem "rake"
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010 Lars Gierth
1
+ Copyright (c) 2010-2011 Lars Gierth
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,170 +1,85 @@
1
- Ruby Library For Aspect Oriented Programming
2
- ============================================
1
+ Aspect Orientation Made Simple
2
+ ==============================
3
3
 
4
- Hooked makes AOP a breeze. It lets you define and invoke code that gems or other
5
- parts of your application can hook onto.
4
+ Hooked lets you transparently aspectify your methods and blocks.
6
5
 
7
6
  Getting Started
8
7
  ---------------
9
8
 
9
+ By including `Hooked` into a class you basically say "everything can attach code
10
+ to instance methods of this class".
11
+
10
12
  require "hooked"
11
13
 
12
- class User
13
- include Hooked::Container
14
-
15
- def save
16
- hookable(:save_user, self)
17
- end
14
+ class Foo
15
+ include Hooked
18
16
 
19
- hookable :save_user do |ctx|
20
- ctx.return(Database.save(ctx.args))
17
+ def breakfast
18
+ puts "No milk?!"
21
19
  end
22
20
  end
23
21
 
24
- class PermissionCheck
25
- include Hooked::Container
26
-
27
- def has_permissions?
28
- false
22
+ class BetterFoo
23
+ def shower
24
+ puts "Oooh..."
29
25
  end
30
26
 
31
- before :save_user, :check_permissions do |ctx|
32
- ctx.break unless has_permissions?
27
+ def fuuu(inner)
28
+ puts "FUUU!"
29
+ inner.call
30
+ puts "FUUU!"
33
31
  end
34
32
  end
35
33
 
36
- user = User.new
37
- hooking = Hooked::Controller.new(user, PermissionCheck.new)
38
- user.save # => false
39
-
40
- Defining Hookable Code
41
- ----------------------
42
-
43
- To be able to define hookable code (as well as hooks) you need to mixin the
44
- Hooked::Container module. Afterwards you can use the `::hookable` method which
45
- takes a symbol as the hookable's name as well as a block.
46
-
47
- class Something
48
- include Hooked::Container
49
-
50
- hookable :breakfast do |ctx|
51
- puts "I'm having breakfast"
52
- end
53
- end
54
-
55
- The hookable will always be executed in the context of its container's object.
56
-
57
- Defining Hooks
58
- --------------
59
-
60
- For defining hooks you can use the methods `::before`, `::after` and
61
- `::around`. They are shortcuts to `::hook` and take the arguments
62
- `hookable_name` (name of the hookable you want to hook onto), `hook_name` and
63
- an optional hash of before/after options.
64
-
65
- before :breakfast, :get_up do |ctx|
66
- puts "I'm getting out of the bed"
67
- end
34
+ foo, better_foo = Foo.new, BetterFoo.new
35
+ foo.before :breakfast, better_foo.method(:shower)
36
+ foo.after :breakfast, proc { puts "Mmmh..." }
37
+ foo.around :breakfast, better_foo.method(:fuuu)
68
38
 
69
- before :breakfast, :make_coffee, :after => :get_up do |ctx|
70
- puts "I'm making coffee"
71
- end
72
-
73
- `:before` and `:after` can be a single hook name, an array of hook names or
74
- `:all`. More documentation on before/after relations and how they are being
75
- resolved can be found in the
76
- [depression gem](http://rubygems.org/gems/depression).
77
-
78
- Hooks will always be executed in the context of their container's object (just
79
- as hookables).
80
-
81
- **Note:** Hooked will at no point guarantee that hooks will be executed in the
82
- order of definition. If you rely on execution order, you should use the
83
- `:before` and `:after` options.
84
-
85
- Hooking Controller
86
- ------------------
87
-
88
- The controller is responsible for bringing hooks into the desired order and
89
- executing them correctly.
39
+ foo.breakfast
40
+
41
+ This will output:
90
42
 
91
- hooking = Hooked::Controller.new(container1, container2)
92
- result = hooking.invoke(:my_hookable, arguments)
43
+ FUUU!
44
+ Oooh...
45
+ No milk?!
46
+ Mmmh...
47
+ FUUU!
93
48
 
94
- Arguments And Return Values
95
- ---------------------------
49
+ Execution Model
50
+ ---------------
96
51
 
97
- All argument and return value stuff (and control flow) is managed through a
98
- context object that is being passed between the hooks as well as the hookable.
99
- You can pass in and out whatever you want.
52
+ TOOD
100
53
 
101
- after(:lowercase, :confuse_programmer) do |ctx|
102
- ctx.result = ctx.args.map {|s| s.upper }
103
- end
54
+ Dependencies / Execution Order of Aspects
55
+ -----------------------------------------
104
56
 
105
- res = hooking.invoke(:lowercase, ["asdf", :foo, 123]) do |ctx|
106
- ctx.result = ctx.args.map {|s| s.to_s.lower }
107
- end
108
-
109
- p res # => ["ASDF", "FOO", "123"]
57
+ TODO
110
58
 
111
- Control Flow
112
- ------------
59
+ Possible Use Cases
60
+ ------------------
113
61
 
114
- If you want to cancel the execution of a hook and immediately start with the
115
- next one, use `Context#next`. You would probably want to set a return value
116
- first with `Context#result=`. `Context#return` basically does the same thing,
117
- but additionally it always sets the return value to whatever you pass as an
118
- argument.
119
-
120
- # this
121
- ctx.return "asd"
122
- # is equal to
123
- ctx.result = "asd"
124
- ctx.next
125
-
126
- # and this will result in ctx.result being nil after returning
127
- ctx.result = 123
128
- ctx.return
129
-
130
- If you want to cancel the whole invocation you can use `Context#break`. This
131
- will immediately return to where `Controller#invoke` was called. So if you call
132
- it before the hookable was executed, it won't be executed at all.
133
-
134
- hookable :foo do |ctx|
135
- puts "You won't see me :/"
136
- end
137
-
138
- before :foo, :break_the_chain do |ctx|
139
- ctx.break
140
- end
62
+ You can use the `Hook` class to programmatically build graphs of aspects.
141
63
 
142
- You can pass a Fixnum to `Context#next` and `Context#break` to specify the
143
- stack depth you want to jump. If you jump to high (e.g. you're hooking two
144
- invocations deep but do `ctx.break 3`) a `NameError: uncaught throw 'hooked'`
145
- will be raised.
64
+ # code
146
65
 
147
- **NOTE** You should definitely _not_ use Ruby's control flow constructs, they
148
- may cause serious trouble that can be hard to debug. This may change in future
149
- versions of Hooked. (I appreciate Pull Requests! :>)
66
+ TODO
150
67
 
151
68
  Dependencies
152
69
  ------------
153
70
 
154
- * [depression](https://rubygems.org/gems/depression)
155
- * [mocha](https://rubygems.org/gems/mocha) and
156
- [test-unit](https://rubygems.org/gems/test-unit) for development
71
+ * stdlib's [TSort](http://rubydoc.info/stdlib/tsort/1.9.2/frames)
72
+ * [RSpec](http://relishapp.com/rspec) for development
157
73
 
158
- Runs fine on Ruby 1.9.2 and JRuby 1.5.6.
74
+ Specs pass on MRI 1.9.2, others have not been tested yet.
159
75
 
160
76
  To do & Ideas
161
77
  -------------
162
78
 
163
- * Skipping hooks
79
+ * Let before, after, around and Hook.new also take a block
80
+ * Use insertion order as execution order if no aspect has dependencies
164
81
  * Visualization of hook flow
165
82
  * Make backtraces more readable
166
- * Make Ruby's control flow constructs work for hooks
167
- * Allow control flow jumps with hookable names
168
83
 
169
84
  Contributing
170
85
  ------------
@@ -179,4 +94,4 @@ You can also open an issue for discussion first, if you like.
179
94
  License
180
95
  -------
181
96
 
182
- Hooked is subject to an MIT-style license that can be found in the LICENSE file.
97
+ Hooked is subject to an MIT-style license (see LICENSE).
data/Rakefile CHANGED
@@ -1,10 +1,9 @@
1
1
  require "bundler"
2
- Bundler::GemHelper.install_tasks
2
+ Bundler.setup :development
3
+
4
+ require "rspec/core/rake_task"
3
5
 
4
- task :default => :test
6
+ task :default => :spec
7
+ RSpec::Core::RakeTask.new :spec
5
8
 
6
- require "rake/testtask"
7
- Rake::TestTask.new do |t|
8
- t.libs = ["lib"]
9
- t.test_files = FileList["test/*_test.rb"]
10
- end
9
+ Bundler::GemHelper.install_tasks
data/hooked.gemspec CHANGED
@@ -10,15 +10,10 @@ Gem::Specification.new do |s|
10
10
  s.authors = ["Lars Gierth"]
11
11
  s.email = ["lars.gierth@gmail.com"]
12
12
  s.homepage = "http://rubygems.org/gems/hooked"
13
- s.summary = %q{Ruby Library For Aspect Oriented Programming}
14
- s.description = %q{Hooked makes AOP a breeze. It lets you define and invoke
15
- code that gems or other parts of your application can hook
16
- onto.}
13
+ s.summary = %q{Aspect Orientation Made Simple}
14
+ s.description = %q{Hooked lets you transparently aspectify your methods and blocks.}
17
15
 
18
- s.add_dependency "depression"
19
-
20
- s.add_development_dependency "test-unit"
21
- s.add_development_dependency "mocha"
16
+ s.add_development_dependency "rspec"
22
17
 
23
18
  s.files = `git ls-files`.split("\n") - [".gitignore", ".rvmrc"]
24
19
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
@@ -0,0 +1,35 @@
1
+ module Hooked
2
+ class Aspect
3
+ attr_reader :type, :advice, :dependencies
4
+ attr_accessor :pointcut
5
+
6
+ def initialize(type, advice, dependencies = {})
7
+ [:before, :after].each {|d| dependencies[d] ||= [] }
8
+ @type, @advice, @dependencies = type, advice, dependencies
9
+ end
10
+
11
+ def call(*args, &block)
12
+ args = advice.call(*args, &block) if before?
13
+
14
+ result = if around?
15
+ advice.call pointcut, *args, &block
16
+ else
17
+ pointcut.call *args, &block
18
+ end
19
+
20
+ advice.call result if after?
21
+ end
22
+
23
+ def before?
24
+ type == :before
25
+ end
26
+
27
+ def after?
28
+ type == :after
29
+ end
30
+
31
+ def around?
32
+ type == :around
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,57 @@
1
+ require "tsort"
2
+
3
+ module Hooked
4
+ class Graph
5
+ class Node < Struct.new(:children, :aspect); end
6
+
7
+ class CircularDependencyError < TSort::Cyclic; end
8
+
9
+ include TSort
10
+
11
+ attr_reader :input, :output
12
+
13
+ def initialize
14
+ @input = []
15
+ end
16
+
17
+ def <<(node)
18
+ @changed = true
19
+ @input << node
20
+ end
21
+
22
+ def changed?
23
+ !!@changed
24
+ end
25
+
26
+ def sort
27
+ @nodes = input.inject({}) do |nodes, node|
28
+ children = node.dependencies[:after].map {|d| d.object_id }
29
+ nodes[node.advice.object_id] = Node.new(children, node); nodes
30
+ end
31
+ @nodes.each do |name, node|
32
+ node.aspect.dependencies[:before].each do |dep|
33
+ dep_name = dep.object_id
34
+ next unless @nodes[dep_name]
35
+ @nodes[dep_name].children << name
36
+ end
37
+ end
38
+
39
+ strongly_connected_components.each do |name|
40
+ next unless Array === name && name.length > 1
41
+ raise CircularDependencyError,
42
+ "Sorting failed: #{name.map {|n| @nodes[n].aspect.advice.inspect }.join ', '}"
43
+ end
44
+
45
+ @output = tsort.map {|name| @nodes[name].aspect }
46
+ @changed = false
47
+ end
48
+
49
+ def tsort_each_node(&block)
50
+ @nodes.each_key &block
51
+ end
52
+
53
+ def tsort_each_child(name, &block)
54
+ @nodes[name].children.each &block if @nodes[name]
55
+ end
56
+ end
57
+ end
data/lib/hooked/hook.rb CHANGED
@@ -1,37 +1,25 @@
1
1
  module Hooked
2
2
  class Hook
3
- attr_reader :type, :name, :relations
4
- attr_accessor :container, :weight
3
+ attr_reader :pointcut, :graph
5
4
 
6
- def initialize(type, name, relations = {}, container = nil, &block)
7
- raise ArgumentError, "Invalid hook type `#{type}'" unless [
8
- :before, :after, :around
9
- ].include?(type.to_sym)
10
- @type, @name, @relations = type.to_sym, name.to_sym, relations
11
-
12
- raise ArgumentError, "Hooked::Hookable.new expects a block." unless block
13
- @container, @block = container, block
5
+ def initialize(pointcut)
6
+ @pointcut, @graph = pointcut, Graph.new
14
7
  end
15
8
 
16
- def call(context, hookable = nil)
17
- raise RuntimeError, "No container set for hook `#{name}'" unless container
18
- if hookable
19
- container.instance_exec(context, hookable, &@block)
20
- else
21
- container.instance_exec(context, &@block)
22
- end
23
- end
24
-
25
- def before?
26
- type == :before
9
+ def add_aspect(*args)
10
+ graph << Aspect.new(*args)
27
11
  end
28
12
 
29
- def after?
30
- type == :after
13
+ def call(*args, &block)
14
+ @chain = build_chain if graph.changed? || !@chain
15
+ @chain.call *args, &block
31
16
  end
32
17
 
33
- def around?
34
- type == :around
18
+ def build_chain
19
+ graph.sort
20
+ graph.output.reverse.inject(pointcut) do |pointcut, aspect|
21
+ aspect.pointcut = pointcut; aspect
22
+ end
35
23
  end
36
24
  end
37
25
  end
@@ -1,3 +1,3 @@
1
1
  module Hooked
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/hooked.rb CHANGED
@@ -1,7 +1,38 @@
1
- require "depression"
2
-
3
- require "hooked/container"
4
- require "hooked/context"
5
- require "hooked/controller"
1
+ require "hooked/aspect"
2
+ require "hooked/graph"
6
3
  require "hooked/hook"
7
- require "hooked/hookable"
4
+
5
+ module Hooked
6
+ attr_reader :hooked
7
+
8
+ [:before, :after, :around].each do |type|
9
+ define_method type do |pointcut, *args|
10
+ hook! pointcut
11
+ hooked[pointcut].add_aspect type, *args
12
+ end
13
+ end
14
+
15
+ def hook!(pointcut)
16
+ @hooked ||= {}
17
+ return if hooked[pointcut]
18
+
19
+ hooked[pointcut] = Hooked::Hook.new method(pointcut)
20
+ singleton_class.class_eval do
21
+ undef_method pointcut
22
+ define_method pointcut do |*args, &block|
23
+ hooked[pointcut].call *args, &block
24
+ end
25
+ end
26
+ end
27
+
28
+ def unhook!(pointcut)
29
+ return unless hooked && hooked[pointcut]
30
+
31
+ p = hooked[pointcut].pointcut
32
+ singleton_class.class_eval do
33
+ remove_method pointcut
34
+ define_method pointcut, &p
35
+ end
36
+ hooked.delete pointcut
37
+ end
38
+ end
@@ -0,0 +1,119 @@
1
+ require "spec_helper"
2
+
3
+ describe Hooked::Aspect do
4
+ describe "#initialize" do
5
+ it "sets type, advice and depencency list" do
6
+ type, advice = stub("type"), stub("advice")
7
+ deps = {:before => [:foo], :after => [:bar]}
8
+ obj = Hooked::Aspect.new type, advice, deps
9
+
10
+ obj.type.should == type
11
+ obj.advice.should == advice
12
+ obj.dependencies.should == deps
13
+ end
14
+
15
+ it "sets default dependency lists" do
16
+ obj = Hooked::Aspect.new nil, nil
17
+ obj.dependencies[:before].should == []
18
+ obj.dependencies[:after].should == []
19
+ end
20
+ end
21
+
22
+ describe "#call" do
23
+ order = []
24
+ advice = proc { order << :advice }
25
+ pointcut = proc { order << :pointcut }
26
+
27
+ before :each do
28
+ order.clear
29
+ end
30
+
31
+ describe "with type == :before" do
32
+ it "calls the advice before the pointcut" do
33
+ obj = Hooked::Aspect.new :before, advice
34
+ obj.pointcut = pointcut
35
+ obj.call
36
+
37
+ order.should == [:advice, :pointcut]
38
+ end
39
+
40
+ it "manipulates the arguments" do
41
+ args_before, args_after = [:foo, 123], [456, :bar]
42
+ obj = Hooked::Aspect.new :before, mock("advice")
43
+ obj.pointcut = mock "pointcut"
44
+
45
+ obj.advice.should_receive(:call).with(*args_before).and_return args_after
46
+ obj.pointcut.should_receive(:call).with *args_after
47
+
48
+ obj.call *args_before
49
+ end
50
+ end
51
+
52
+ describe "with type == :after" do
53
+ it "calls the advice after the pointcut" do
54
+ obj = Hooked::Aspect.new :after, advice
55
+ obj.pointcut = pointcut
56
+ obj.call
57
+
58
+ order.should == [:pointcut, :advice]
59
+ end
60
+
61
+ it "manipulates the return value" do
62
+ return_before, return_after = :foo, :bar
63
+ obj = Hooked::Aspect.new :after, mock("advice")
64
+ obj.pointcut = mock "pointcut"
65
+
66
+ obj.pointcut.stub(:call).and_return return_before
67
+ obj.advice.should_receive(:call).with(return_before).and_return return_after
68
+
69
+ obj.call.should == return_after
70
+ end
71
+ end
72
+
73
+ describe "with type == :around" do
74
+ it "doesn't call the pointcut but passes it to the advice" do
75
+ advice, pointcut = mock("advice"), mock("pointcut")
76
+ obj = Hooked::Aspect.new :around, advice
77
+ obj.pointcut = pointcut
78
+
79
+ advice.should_receive(:call).with pointcut
80
+ pointcut.should_not_receive :call
81
+
82
+ obj.call
83
+ end
84
+ end
85
+
86
+ describe "with unknown type" do
87
+ it "doesn't call the advice" do
88
+ obj = Hooked::Aspect.new :foo, stub("advice")
89
+ obj.pointcut = stub("pointcut")
90
+
91
+ obj.advice.should_not_receive :call
92
+ obj.pointcut.should_receive :call
93
+
94
+ obj.call
95
+ end
96
+ end
97
+ end
98
+
99
+ describe "#before?" do
100
+ it "returns true if #type == :before" do
101
+ Hooked::Aspect.new(:before, nil).before?.should be_true
102
+ Hooked::Aspect.new(:foo, nil).before?.should_not be_true
103
+ end
104
+ end
105
+
106
+ describe "#after?" do
107
+ it "returns true if #type == :after" do
108
+ Hooked::Aspect.new(:after, nil).after?.should be_true
109
+ Hooked::Aspect.new(:foo, nil).after?.should_not be_true
110
+ end
111
+ end
112
+
113
+ describe "#around?" do
114
+ it "returns true if #type == :around" do
115
+ Hooked::Aspect.new(:around, nil).around?.should be_true
116
+ Hooked::Aspect.new(:foo, nil).around?.should_not be_true
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,66 @@
1
+ require "spec_helper"
2
+
3
+ describe Hooked::Graph do
4
+ describe "#initialize" do
5
+ it "starts with empty input" do
6
+ Hooked::Graph.new.input.should be_empty
7
+ end
8
+ end
9
+
10
+ describe "#<<" do
11
+ before :each do
12
+ @aspect, @graph = Hooked::Aspect.new(nil, nil), Hooked::Graph.new
13
+ @graph << @aspect
14
+ end
15
+
16
+ it "sets the changed flag" do
17
+ @graph.changed?.should be_true
18
+ end
19
+
20
+ it "adds the node to the input" do
21
+ @graph.input.last.should == @aspect
22
+ end
23
+ end
24
+
25
+ describe "#sort" do
26
+ it "removes the changed flag" do
27
+ graph = Hooked::Graph.new
28
+ graph << Hooked::Aspect.new(nil, mock("advice"))
29
+ graph.sort
30
+ graph.changed?.should == false
31
+ end
32
+
33
+ it "performs a topological sort" do
34
+ advices = (0..3).map {|i| mock "advice##{i}", :inspect => "advice##{i}" }
35
+ aspects = [
36
+ Hooked::Aspect.new(nil, advices[0], :after => [advices[2]]),
37
+ Hooked::Aspect.new(nil, advices[1], :after => [advices[0]]),
38
+ Hooked::Aspect.new(nil, advices[2], :before => [advices[3]]),
39
+ Hooked::Aspect.new(nil, advices[3], :before => [advices[1]])
40
+ ]
41
+
42
+ graph = Hooked::Graph.new
43
+ aspects.each {|a| graph << a }
44
+ graph.sort
45
+
46
+ graph.output.should == [aspects[2], aspects[0], aspects[3], aspects[1]]
47
+ end
48
+
49
+ it "detects circular dependencies" do
50
+ advices = (0..3).map {|i| mock "advice##{i}", :inspect => "advice##{i}" }
51
+ aspects = [
52
+ Hooked::Aspect.new(nil, advices[0], :before => [advices[1]]),
53
+ Hooked::Aspect.new(nil, advices[1], :before => [advices[2]]),
54
+ Hooked::Aspect.new(nil, advices[2], :before => [advices[0]])
55
+ ]
56
+
57
+ graph = Hooked::Graph.new
58
+ aspects.each {|a| graph << a }
59
+ proc do
60
+ graph.sort
61
+ end.should raise_error(Hooked::Graph::CircularDependencyError) {|error|
62
+ error.message =~ /failed: advice#0, advice#2, advice#1/
63
+ }
64
+ end
65
+ end
66
+ end