hooked 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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