lspace 0.1.pre.1 → 0.1

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.
@@ -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