lspace 0.1.pre.1 → 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,129 @@
1
+ class LSpace
2
+
3
+ # Create a new clean LSpace.
4
+ #
5
+ # This LSpace does not inherit any LSpace variables in the currently active LSpace.
6
+ #
7
+ # @example
8
+ # RSpec.configure do |c|
9
+ # c.around(:each) do |example|
10
+ # LSpace.clean do
11
+ # example.run
12
+ # end
13
+ # end
14
+ # end
15
+ #
16
+ # @param [Proc] block The logical block that will be run in the clean LSpace
17
+ # @see LSpace.enter
18
+ def self.clean(&block)
19
+ enter new({}, nil), &block
20
+ end
21
+
22
+ # Create a new LSpace with the given keys set to the given values.
23
+ #
24
+ # The LSpace will inherit any unspecified keys from the currently active LSpace.
25
+ #
26
+ # @example
27
+ # LSpace.update :user_id => 6 do
28
+ # LSpace.update :job_id => 7 do
29
+ # LSpace[:user_id] == 6
30
+ # LSpace[:job_id] == 7
31
+ # end
32
+ # end
33
+ #
34
+ # @param [Hash] hash The keys to update
35
+ # @param [Proc] block The logical block to run with the updated LSpace
36
+ # @see LSpace.enter
37
+ def self.update(hash={}, &block)
38
+ enter new(hash, current), &block
39
+ end
40
+
41
+ # Enter an LSpace for the logical duration of the block.
42
+ #
43
+ # The LSpace will be active at least for the duration of the block's callstack,
44
+ # but if the block creates any closures (using LSpace.preserve directly, or in
45
+ # library form) then the logical duration will also encompass code run in those
46
+ # closures.
47
+ #
48
+ # Entering an LSpace will also cause any around_filters defined on it and its parents to
49
+ # be run.
50
+ #
51
+ # @example
52
+ # class Job
53
+ # def initialize
54
+ # @lspace = LSpace.new(:job_id => self.id)
55
+ # LSpace.enter(@lspace){ setup_lspace }
56
+ # end
57
+ #
58
+ # def run!
59
+ # LSpace.enter(@lspace) { run }
60
+ # end
61
+ # end
62
+ #
63
+ # @param [LSpace] lspace The LSpace to enter
64
+ # @param [Proc] block The logical block to run with the given LSpace
65
+ def self.enter(lspace, &block)
66
+ previous = current
67
+ self.current = lspace
68
+
69
+ filters = lspace.hierarchy.take_while{ |lspace| lspace != previous }.map(&:around_filters).flatten
70
+
71
+ filters.inject(block) do |blk, filter|
72
+ lambda{ filter.call(&blk) }
73
+ end.call
74
+ ensure
75
+ self.current = previous
76
+ end
77
+
78
+ # Create a closure that will re-enter the current LSpace when the block is called.
79
+ #
80
+ # @example
81
+ # class TaskQueue
82
+ # def queue(&block)
83
+ # @queue << LSpace.preserve(&block)
84
+ # end
85
+ # end
86
+ #
87
+ # @see Proc#in_lspace
88
+ # @param [Proc] block The logical block to wrap
89
+ def self.preserve(&block)
90
+ current.wrap(&block)
91
+ end
92
+
93
+ # Get the value for the key in the current LSpace or its parents
94
+ #
95
+ # @see LSpace#[]
96
+ def self.[](key)
97
+ current[key]
98
+ end
99
+
100
+ # Set the value for the key in the current LSpace
101
+ #
102
+ # @see LSpace#[]=
103
+ def self.[]=(key, value)
104
+ current[key] = value
105
+ end
106
+
107
+ # Add an around filter to the current LSpace
108
+ #
109
+ # @see LSpace#around_filter
110
+ def self.around_filter(&filter)
111
+ current.around_filter(&filter)
112
+ end
113
+
114
+ # Get the current LSpace
115
+ #
116
+ # @return [LSpace]
117
+ def self.current
118
+ Thread.current[:lspace] ||= LSpace.new({}, nil)
119
+ end
120
+
121
+ private
122
+
123
+ # Update the current LSpace
124
+ #
125
+ # @see LSpace.enter
126
+ def self.current=(lspace)
127
+ Thread.current[:lspace] = lspace
128
+ end
129
+ end
@@ -1,4 +1,48 @@
1
1
  class Module
2
+ # Create getter methods for LSpace
3
+ #
4
+ # Assumes that your LSpace keys are Symbols.
5
+ #
6
+ # @example
7
+ # class Job
8
+ # lspace_reader :user_id
9
+ #
10
+ # def user
11
+ # User.find(user_id)
12
+ # end
13
+ # end
14
+ #
15
+ # LSpace[:user_id] = 6
16
+ # Job.new.user == #<User:6>
17
+ #
18
+ # @param [Symbol] *attrs The accessors to create
19
+ def lspace_reader(*attrs)
20
+ attrs.each do |attr|
21
+ define_method(attr) do
22
+ LSpace[attr]
23
+ end
24
+ end
25
+ end
26
+
27
+ # Preserve lspace of all blocks passed to this function.
28
+ #
29
+ # This wraps both the &block parameter, and also any Procs
30
+ # that are passed into the function directly.
31
+ #
32
+ # If you need more complicated logic (i.e. wrapping Procs
33
+ # that are passed to a function in a dictionary) you're
34
+ # on your own.
35
+ #
36
+ # @example
37
+ # class << Thread
38
+ # in_lspace :new, :start, :fork
39
+ # end
40
+ #
41
+ # LSpace.new :user_id => 2 do
42
+ # Thread.new{ LSpace[:user_id] == 2 }
43
+ # end
44
+ #
45
+ # @param [Symbol] *methods The methods to wrap
2
46
  def in_lspace(*methods)
3
47
  methods.each do |method|
4
48
  method_without_lspace = "#{method}_without_lspace"
@@ -16,21 +60,18 @@ class Module
16
60
  protected method if protected_method_defined?(method_without_lspace)
17
61
  end
18
62
  end
19
-
20
- def attr_lspace(*attrs)
21
- attrs.each do |attr|
22
- define_method(attr) do
23
- LSpace[attr]
24
- end
25
-
26
- define_method("#{attr}=") do |value|
27
- LSpace[attr] = value
28
- end
29
- end
30
- end
31
63
  end
32
64
 
33
65
  class Proc
66
+ # Preserve LSpace when this Proc is run.
67
+ #
68
+ # @example
69
+ # todo = LSpace.new :user_id => 2 do
70
+ # proc{ LSpace[:user_id] }.in_lspace
71
+ # end
72
+ # todo.call == 2
73
+ # @see LSpace.preserve
74
+ # @return [Proc] A version of self that re-enters LSpace before running
34
75
  def in_lspace
35
76
  LSpace.preserve(&self)
36
77
  end
@@ -1,39 +1,79 @@
1
1
  require 'eventmachine'
2
+ require 'lspace'
2
3
  module EventMachine
4
+
5
+ # Most of the low-level EventMachine stuff goes through singleton methods on the
6
+ # EventMachine class.
7
+ #
8
+ # We use in_lspace to preserve the LSpace of any blocks passed to these methods.
9
+ class << self
10
+ in_lspace :add_timer, :next_tick, :error_handler, :defer, :run, :run_block, :schedule, :fork_reactor
11
+ in_lspace :add_shutdown_hook if method_defined?(:add_shutdown_hook) # only some versions of EM
12
+ end
13
+
14
+ # Many EM APIs (e.g. em-http-request) are based on deferrables. Preserving lspace for
15
+ # both callbacks and errbacks makes these libraries "just work".
3
16
  module Deferrable
4
17
  in_lspace :callback, :errback
5
18
  end
6
19
 
7
- class << Connection
8
- alias_method :allocate_without_lspace, :allocate
20
+ class Connection
21
+
22
+ class << self
23
+ # As EM uses a custom implementation of {new}, the only sane way to
24
+ # set up the LSpace in advance is to override allocate.
25
+ alias_method :allocate_without_lspace, :allocate
26
+ end
9
27
 
10
- def allocate
28
+ # Overridden allocate which sets up a new LSpace.
29
+ #
30
+ # Each connection object is run in its own LSpace, which can be
31
+ # configured by implementing the {setup_lspace} method.
32
+ def self.allocate
11
33
  allocate_without_lspace.instance_eval do
12
34
  extend EventMachine::LSpacePreserver
13
- @lspace_context = LSpace.new
14
- lspace_setup
35
+ LSpace.update do
36
+ setup_lspace
37
+ @lspace = LSpace.current
38
+ end
15
39
  self
16
40
  end
17
41
  end
18
42
 
43
+ # Override this method to setup the LSpace in a manner which you require.
44
+ #
45
+ # This method is called before initialize() and before post_init().
46
+ #
47
+ # @example
48
+ # module EchoServer
49
+ # def setup_lspace
50
+ # LSpace.around_filter do |&block|
51
+ # begin
52
+ # block.call
53
+ # rescue => e
54
+ # self.rescue(e)
55
+ # end
56
+ # end
57
+ # end
58
+ #
59
+ # def rescue(e)
60
+ # puts "An exception occurred!: #{e}"
61
+ # end
62
+ # end
63
+ def setup_lspace; end
19
64
  end
20
65
 
21
- class Connection
22
- def lspace_setup; end
23
- end
24
-
66
+ # A module that's included at the beginning of the method-resolution chain of
67
+ # connections which restores LSpace whenever a callback is fired by the eventloop.
25
68
  module LSpacePreserver
26
69
  [:initialize, :post_init, :connection_completed, :receive_data, :ssl_verify_peer, :ssl_handshake_completed].each do |method|
27
- define_method(method) { |*a, &b| LSpace.enter(@lspace_context) { super(*a, &b) } }
70
+ define_method(method) { |*a, &b| LSpace.enter(@lspace) { super(*a, &b) } }
28
71
  end
29
72
 
73
+ # EM uses the arity of unbind to decide which arguments to pass it.
74
+ # AFAIK the no-argument version is considerably more popular, so we use that here.
30
75
  [:unbind].each do |method|
31
- define_method(method) { LSpace.enter(@lspace_context) { super() } }
76
+ define_method(method) { LSpace.enter(@lspace) { super() } }
32
77
  end
33
78
  end
34
79
  end
35
-
36
- class << EventMachine
37
- in_lspace :add_timer, :next_tick, :error_handler, :defer, :run, :run_block, :schedule, :fork_reactor
38
- in_lspace :add_shutdown_hook if method_defined?(:add_shutdown_hook)
39
- end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "lspace"
3
- s.version = "0.1.pre.1"
3
+ s.version = "0.1"
4
4
  s.platform = Gem::Platform::RUBY
5
5
  s.author = "Conrad Irwin"
6
6
  s.email = "conrad.irwin@gmail.com"
@@ -9,4 +9,8 @@ Gem::Specification.new do |s|
9
9
  s.description = "Provides the convenience of global variables, without the safety concerns."
10
10
  s.files = `git ls-files`.split("\n")
11
11
  s.require_path = "lib"
12
+
13
+ s.add_development_dependency 'rspec'
14
+ s.add_development_dependency 'pry-rescue'
15
+ s.add_development_dependency 'eventmachine'
12
16
  end
@@ -0,0 +1,116 @@
1
+ require 'spec_helper'
2
+ describe LSpace do
3
+ before do
4
+ @lspace = LSpace.new({}, nil)
5
+ end
6
+
7
+ describe ".current" do
8
+ it "should return the most recently entered LSpace" do
9
+ LSpace.enter @lspace do
10
+ LSpace.current.should == @lspace
11
+ end
12
+ end
13
+
14
+ it "should ignore changes on other threads" do
15
+ LSpace.enter @lspace do
16
+ Thread.new{ LSpace.current.should_not == @lspace }.join
17
+ end
18
+ end
19
+ end
20
+
21
+ describe ".update" do
22
+ it "should enter a new LSpace with the new variables set" do
23
+ LSpace.update(:foo => 5) do
24
+ LSpace[:foo].should == 5
25
+ end
26
+ end
27
+
28
+ it "should enter a new LSpace which delegates to the current parent" do
29
+ LSpace.update(:foo => 5) do
30
+ LSpace.update(:bar => 4) do
31
+ LSpace[:foo].should == 5
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ describe ".clean" do
38
+ it "should enter a new LSpace with no parent" do
39
+ LSpace.update(:foo => 5) do
40
+ LSpace.clean do
41
+ LSpace[:foo].should == nil
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ describe ".enter" do
48
+ it "should update LSpace.current" do
49
+ LSpace.enter(@lspace) do
50
+ LSpace.current.should == @lspace
51
+ end
52
+ end
53
+
54
+ it "should revert that change at the end of the block" do
55
+ lspace = LSpace.current
56
+ LSpace.enter(@lspace) do
57
+ # yada yada
58
+ end
59
+ LSpace.current.should == lspace
60
+ end
61
+
62
+ it "should revert that change even if the block raises an exception" do
63
+ lspace = LSpace.current
64
+ lambda do
65
+ LSpace.enter(@lspace) do
66
+ raise "OOPS"
67
+ end
68
+ end.should raise_error /OOPS/
69
+ LSpace.current.should == lspace
70
+ end
71
+
72
+ it "should not effect other threads" do
73
+ LSpace.enter(@lspace) do
74
+ Thread.new{ LSpace.current.should_not == @lspace }.join
75
+ end
76
+ end
77
+
78
+ it "should not effect other fibers" do
79
+ LSpace.enter(@lspace) do
80
+ Fiber.new{ LSpace.current.should_not == @lspace }.resume
81
+ end
82
+ end
83
+ end
84
+
85
+ describe ".preserve" do
86
+ it "should delegate to LSpace.current" do
87
+ LSpace.current.should_receive(:wrap).once
88
+ LSpace.preserve{ 5 }
89
+ end
90
+ end
91
+
92
+ describe ".[]" do
93
+ it "should delegate to LSpace.current" do
94
+ LSpace.current.should_receive(:[]).once.with(:foo).and_return(:bar)
95
+ LSpace[:foo].should == :bar
96
+ end
97
+ end
98
+
99
+ describe ".[]=" do
100
+ it "should delegate to LSpace.current" do
101
+ LSpace.current.should_receive(:[]=).once.with(:foo, :bar)
102
+ LSpace[:foo] = :bar
103
+ end
104
+ end
105
+
106
+ describe ".around_filter" do
107
+ it "should delegate to LSpace.current" do
108
+ @lspace.should_receive(:around_filter).once
109
+ LSpace.enter @lspace do
110
+ LSpace.around_filter do |&block|
111
+ block.call
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ describe Module do
4
+ describe "#lspace_reader" do
5
+ it "should define a reader for LSpace" do
6
+ klass = Class.new{ lspace_reader :user_id }
7
+
8
+ LSpace.update(:user_id => 8) do
9
+ klass.new.user_id.should == 8
10
+ end
11
+ end
12
+ end
13
+
14
+ describe "#in_lspace" do
15
+ before do
16
+ @klass = Class.new do
17
+ def initialize(task=nil, &block)
18
+ @block = block || task
19
+ end
20
+ in_lspace :initialize
21
+
22
+ def call
23
+ @block.call
24
+ end
25
+
26
+ private
27
+
28
+ def private_test(&block)
29
+ block
30
+ end
31
+
32
+ protected
33
+
34
+ def protected_test(&block)
35
+ block
36
+ end
37
+
38
+ in_lspace :private_test, :protected_test
39
+ end
40
+ end
41
+
42
+ it "should automatically preserve LSpace for blocks that are passed in" do
43
+ @task = LSpace.update :user_id => 6 do
44
+ @klass.new{ LSpace[:user_id] }
45
+ end
46
+
47
+ @task.call.should == 6
48
+ end
49
+
50
+ it "should automatically preserve LSpace for procs that are passed in" do
51
+ @task = LSpace.update :user_id => 6 do
52
+ @klass.new proc{ LSpace[:user_id] }
53
+ end
54
+
55
+ @task.call.should == 6
56
+ end
57
+
58
+ it "should preserve visibility of methods" do
59
+ @klass.private_method_defined?(:private_test).should == true
60
+ @klass.protected_method_defined?(:protected_test).should == true
61
+ end
62
+ end
63
+ end
64
+
65
+ describe Proc do
66
+ describe "#in_lspace" do
67
+ it "should create a wrapper which preserves the LSpace" do
68
+ p = LSpace.update(:job_id => 19) do
69
+ lambda{ LSpace[:job_id] }.in_lspace
70
+ end
71
+
72
+ p.call.should == 19
73
+ end
74
+ end
75
+ end