standard-procedure-plumbing 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,63 @@
1
+ require_relative "actor/kernel"
2
+ require_relative "actor/inline"
3
+
4
+ module Plumbing
5
+ module Actor
6
+ def self.included base
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ # Create a new actor instance and build a proxy for it using the current mode
12
+ # @return [Object] the proxy for the actor instance
13
+ def start(*, **, &)
14
+ build_proxy_for(new(*, **, &))
15
+ end
16
+
17
+ # Define the async messages that this actor can respond to
18
+ # @param names [Array<Symbol>] the names of the async messages
19
+ def async(*names) = async_messages.concat(names.map(&:to_sym))
20
+
21
+ # List the async messages that this actor can respond to
22
+ def async_messages = @async_messages ||= []
23
+
24
+ def inherited subclass
25
+ subclass.async_messages.concat async_messages
26
+ end
27
+
28
+ private
29
+
30
+ def build_proxy_for(target)
31
+ proxy_class_for(target.class).new(target)
32
+ end
33
+
34
+ def proxy_class_for target_class
35
+ Plumbing.config.actor_proxy_class_for(target_class) || register_actor_proxy_class_for(target_class)
36
+ end
37
+
38
+ def proxy_base_class = const_get PROXY_BASE_CLASSES[Plumbing.config.mode]
39
+
40
+ PROXY_BASE_CLASSES = {
41
+ inline: "Plumbing::Actor::Inline",
42
+ async: "Plumbing::Actor::Async",
43
+ threaded: "Plumbing::Actor::Threaded",
44
+ rails: "Plumbing::Actor::Rails"
45
+ }.freeze
46
+ private_constant :PROXY_BASE_CLASSES
47
+
48
+ def register_actor_proxy_class_for target_class
49
+ Plumbing.config.register_actor_proxy_class_for(target_class, build_proxy_class)
50
+ end
51
+
52
+ def build_proxy_class
53
+ Class.new(proxy_base_class).tap do |proxy_class|
54
+ async_messages.each do |message|
55
+ proxy_class.define_method message do |*args, &block|
56
+ send_message(message, *args, &block)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -1,12 +1,12 @@
1
- # Pipes, pipelines, valves and rubber ducks
1
+ # Pipes, pipelines, actors and rubber ducks
2
2
  module Plumbing
3
- Config = Data.define :mode, :valve_proxy_classes, :timeout do
4
- def valve_proxy_class_for target_class
5
- valve_proxy_classes[target_class]
3
+ Config = Data.define :mode, :actor_proxy_classes, :timeout do
4
+ def actor_proxy_class_for target_class
5
+ actor_proxy_classes[target_class]
6
6
  end
7
7
 
8
- def register_valve_proxy_class_for target_class, proxy_class
9
- valve_proxy_classes[target_class] = proxy_class
8
+ def register_actor_proxy_class_for target_class, proxy_class
9
+ actor_proxy_classes[target_class] = proxy_class
10
10
  end
11
11
  end
12
12
  private_constant :Config
@@ -23,7 +23,7 @@ module Plumbing
23
23
  # @option timeout [Integer] the timeout (in seconds) to use (30s is the default)
24
24
  # @yield optional block - after the block has completed its execution, the configuration is restored to its previous state (useful for test suites)
25
25
  def self.configure(**params, &block)
26
- new_config = Config.new(**config.to_h.merge(params).merge(valve_proxy_classes: {}))
26
+ new_config = Config.new(**config.to_h.merge(params).merge(actor_proxy_classes: {}))
27
27
  if block.nil?
28
28
  set_configuration_to new_config
29
29
  else
@@ -45,7 +45,7 @@ module Plumbing
45
45
  private_class_method :set_configuration_and_yield
46
46
 
47
47
  def self.configs
48
- @configs ||= [Config.new(mode: :inline, timeout: 30, valve_proxy_classes: {})]
48
+ @configs ||= [Config.new(mode: :inline, timeout: 30, actor_proxy_classes: {})]
49
49
  end
50
50
  private_class_method :configs
51
51
  end
data/lib/plumbing/pipe.rb CHANGED
@@ -1,10 +1,9 @@
1
1
  module Plumbing
2
2
  # A basic pipe
3
3
  class Pipe
4
- include Plumbing::Valve
4
+ include Plumbing::Actor
5
5
 
6
- command :notify, :<<, :remove_observer, :shutdown
7
- query :add_observer, :is_observer?
6
+ async :notify, :<<, :remove_observer, :add_observer, :is_observer?, :shutdown
8
7
 
9
8
  # Push an event into the pipe
10
9
  # @param event [Plumbing::Event] the event to push into the pipe
@@ -0,0 +1,13 @@
1
+ module Plumbing
2
+ class RubberDuck
3
+ ::Module.class_eval do
4
+ def rubber_duck
5
+ @rubber_duck ||= Plumbing::RubberDuck.define(*instance_methods)
6
+ end
7
+
8
+ def proxy_for object
9
+ rubber_duck.proxy_for object
10
+ end
11
+ end
12
+ end
13
+ end
@@ -2,9 +2,10 @@ module Plumbing
2
2
  class RubberDuck
3
3
  ::Object.class_eval do
4
4
  # Cast the object to a duck-type
5
+ # @param type [Plumbing::RubberDuck, Module]
5
6
  # @return [Plumbing::RubberDuck::Proxy] the duck-type proxy
6
- def as duck_type
7
- duck_type.proxy_for self
7
+ def as type
8
+ Plumbing::RubberDuck.cast self, type: type
8
9
  end
9
10
  end
10
11
  end
@@ -1,6 +1,7 @@
1
1
  module Plumbing
2
2
  # A type-checker for duck-types
3
3
  class RubberDuck
4
+ require_relative "rubber_duck/module"
4
5
  require_relative "rubber_duck/object"
5
6
  require_relative "rubber_duck/proxy"
6
7
 
@@ -26,10 +27,19 @@ module Plumbing
26
27
  is_a_proxy?(object) || build_proxy_for(object)
27
28
  end
28
29
 
29
- # Define a new rubber duck type
30
- # @param *methods [Array<Symbol>] the methods that the duck-type should respond to
31
- def self.define *methods
32
- new(*methods)
30
+ class << self
31
+ # Define a new rubber duck type
32
+ # @param *methods [Array<Symbol>] the methods that the duck-type should respond to
33
+ def define *methods
34
+ new(*methods)
35
+ end
36
+
37
+ # Cast the object to the given type
38
+ # @param object [Object] to be csat
39
+ # @param to [Module, Plumbing::RubberDuck] the type to cast into
40
+ def cast object, type:
41
+ type.proxy_for object
42
+ end
33
43
  end
34
44
 
35
45
  private
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumbing
4
- VERSION = "0.3.2"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/plumbing.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Plumbing
4
4
  require_relative "plumbing/config"
5
- require_relative "plumbing/valve"
5
+ require_relative "plumbing/actor"
6
6
  require_relative "plumbing/rubber_duck"
7
7
  require_relative "plumbing/types"
8
8
  require_relative "plumbing/error"
@@ -0,0 +1,81 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.shared_examples "an example actor" do |runs_in_background|
4
+ it "queries an object" do
5
+ @person = Employee.start "Alice"
6
+
7
+ expect(await { @person.name }).to eq "Alice"
8
+ expect(await { @person.job_title }).to eq "Sales assistant"
9
+
10
+ @time = Time.now
11
+ # `greet_slowly` is a query so will block until a response is received
12
+ expect(await { @person.greet_slowly }).to eq "H E L L O"
13
+ expect(Time.now - @time).to be > 0.1
14
+
15
+ @time = Time.now
16
+ # we're not awaiting the result, so this should run in the background (unless we're using inline mode)
17
+ @person.greet_slowly
18
+
19
+ expect(Time.now - @time).to be < 0.1 if runs_in_background
20
+ expect(Time.now - @time).to be > 0.1 if !runs_in_background
21
+ end
22
+
23
+ it "commands an object" do
24
+ @person = Employee.start "Alice"
25
+ @person.promote
26
+ @job_title = await { @person.job_title }
27
+ expect(@job_title).to eq "Sales manager"
28
+ end
29
+ end
30
+
31
+ RSpec.describe "Actor example: " do
32
+ # standard:disable Lint/ConstantDefinitionInBlock
33
+ class Employee
34
+ include Plumbing::Actor
35
+ async :name, :job_title, :greet_slowly, :promote
36
+
37
+ def initialize(name)
38
+ @name = name
39
+ @job_title = "Sales assistant"
40
+ end
41
+
42
+ attr_reader :name, :job_title
43
+
44
+ def promote
45
+ sleep 0.5
46
+ @job_title = "Sales manager"
47
+ end
48
+
49
+ def greet_slowly
50
+ sleep 0.2
51
+ "H E L L O"
52
+ end
53
+ end
54
+ # standard:enable Lint/ConstantDefinitionInBlock
55
+
56
+ context "inline mode" do
57
+ around :example do |example|
58
+ Plumbing.configure mode: :inline, &example
59
+ end
60
+
61
+ it_behaves_like "an example actor", false
62
+ end
63
+
64
+ context "async mode" do
65
+ around :example do |example|
66
+ Plumbing.configure mode: :async do
67
+ Kernel::Async(&example)
68
+ end
69
+ end
70
+
71
+ it_behaves_like "an example actor", true
72
+ end
73
+
74
+ context "threaded mode" do
75
+ around :example do |example|
76
+ Plumbing.configure mode: :threaded, &example
77
+ end
78
+
79
+ it_behaves_like "an example actor", true
80
+ end
81
+ end
@@ -6,7 +6,7 @@ RSpec.describe "Pipe examples" do
6
6
  @source = Plumbing::Pipe.start
7
7
 
8
8
  @result = []
9
- @observer = @source.add_observer do |event|
9
+ @source.add_observer do |event|
10
10
  @result << event.type
11
11
  end
12
12
 
@@ -22,7 +22,7 @@ RSpec.describe "Pipe examples" do
22
22
  end
23
23
 
24
24
  @result = []
25
- @observer = @filter.add_observer do |event|
25
+ @filter.add_observer do |event|
26
26
  @result << event.type
27
27
  end
28
28
 
@@ -55,7 +55,7 @@ RSpec.describe "Pipe examples" do
55
55
  @filter = EveryThirdEvent.new(source: @source)
56
56
 
57
57
  @result = []
58
- @observer = @filter.add_observer do |event|
58
+ @filter.add_observer do |event|
59
59
  @result << event.type
60
60
  end
61
61
 
@@ -73,7 +73,7 @@ RSpec.describe "Pipe examples" do
73
73
  @junction = Plumbing::Junction.start @first_source, @second_source
74
74
 
75
75
  @result = []
76
- @observer = @junction.add_observer do |event|
76
+ @junction.add_observer do |event|
77
77
  @result << event.type
78
78
  end
79
79
 
@@ -1,26 +1,109 @@
1
1
  require "spec_helper"
2
2
 
3
3
  RSpec.describe "Rubber Duck examples" do
4
- it "casts objects as duck types" do
4
+ it "casts objects into duck types" do
5
5
  # standard:disable Lint/ConstantDefinitionInBlock
6
- Person = Plumbing::RubberDuck.define :first_name, :last_name, :email
7
- LikesFood = Plumbing::RubberDuck.define :favourite_food
6
+ module DuckExample
7
+ Person = Plumbing::RubberDuck.define :first_name, :last_name, :email
8
+ LikesFood = Plumbing::RubberDuck.define :favourite_food
9
+
10
+ PersonData = Struct.new(:first_name, :last_name, :email, :favourite_food)
11
+ CarData = Struct.new(:make, :model, :colour)
12
+ end
8
13
 
9
- PersonData = Struct.new(:first_name, :last_name, :email, :favourite_food)
10
- CarData = Struct.new(:make, :model, :colour)
11
14
  # standard:enable Lint/ConstantDefinitionInBlock
12
15
 
13
- @porsche_911 = CarData.new "Porsche", "911", "black"
14
- expect { @porsche_911.as Person }.to raise_error(TypeError)
16
+ @porsche_911 = DuckExample::CarData.new "Porsche", "911", "black"
17
+ expect { @porsche_911.as DuckExample::Person }.to raise_error(TypeError)
18
+
19
+ @alice = DuckExample::PersonData.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
20
+
21
+ @person = @alice.as DuckExample::Person
22
+ expect(@person.first_name).to eq "Alice"
23
+ expect(@person.email).to eq "alice@example.com"
24
+ expect { @person.favourite_food }.to raise_error(NoMethodError)
25
+
26
+ @hungry = @person.as DuckExample::LikesFood
27
+ expect(@hungry.favourite_food).to eq "Ice cream"
28
+ end
29
+
30
+ it "casts objects into modules" do
31
+ # standard:disable Lint/ConstantDefinitionInBlock
32
+ module ModuleExample
33
+ module Person
34
+ def first_name = @first_name
35
+
36
+ def last_name = @last_name
37
+
38
+ def email = @email
39
+ end
40
+
41
+ module LikesFood
42
+ def favourite_food = @favourite_food
43
+ end
44
+
45
+ PersonData = Struct.new(:first_name, :last_name, :email, :favourite_food)
46
+ CarData = Struct.new(:make, :model, :colour)
47
+ end
48
+ # standard:enable Lint/ConstantDefinitionInBlock
49
+ @porsche_911 = ModuleExample::CarData.new "Porsche", "911", "black"
50
+ expect { @porsche_911.as ModuleExample::Person }.to raise_error(TypeError)
51
+
52
+ @alice = ModuleExample::PersonData.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
53
+
54
+ @person = @alice.as ModuleExample::Person
55
+ expect(@person.first_name).to eq "Alice"
56
+ expect(@person.email).to eq "alice@example.com"
57
+ expect { @person.favourite_food }.to raise_error(NoMethodError)
58
+
59
+ @hungry = @person.as ModuleExample::LikesFood
60
+ expect(@hungry.favourite_food).to eq "Ice cream"
61
+ end
62
+
63
+ it "casts objects into clases" do
64
+ # standard:disable Lint/ConstantDefinitionInBlock
65
+ module ClassExample
66
+ class Person
67
+ def initialize first_name, last_name, email
68
+ @first_name = first_name
69
+ @last_name = last_name
70
+ @email = email
71
+ end
72
+
73
+ attr_reader :first_name
74
+ attr_reader :last_name
75
+ attr_reader :email
76
+ end
77
+
78
+ class PersonWhoLikesFood < Person
79
+ def initialize first_name, last_name, email, favourite_food
80
+ super(first_name, last_name, email)
81
+ @favourite_food = favourite_food
82
+ end
83
+
84
+ attr_reader :favourite_food
85
+ end
86
+
87
+ class CarData
88
+ def initialize make, model, colour
89
+ @make = make
90
+ @model = model
91
+ @colour = colour
92
+ end
93
+ end
94
+ end
95
+ # standard:enable Lint/ConstantDefinitionInBlock
96
+ @porsche_911 = ClassExample::CarData.new "Porsche", "911", "black"
97
+ expect { @porsche_911.as ClassExample::Person }.to raise_error(TypeError)
15
98
 
16
- @alice = PersonData.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
99
+ @alice = ClassExample::PersonWhoLikesFood.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
17
100
 
18
- @person = @alice.as Person
101
+ @person = @alice.as ClassExample::Person
19
102
  expect(@person.first_name).to eq "Alice"
20
103
  expect(@person.email).to eq "alice@example.com"
21
104
  expect { @person.favourite_food }.to raise_error(NoMethodError)
22
105
 
23
- @hungry = @person.as LikesFood
106
+ @hungry = @person.as ClassExample::PersonWhoLikesFood
24
107
  expect(@hungry.favourite_food).to eq "Ice cream"
25
108
  end
26
109
  end
@@ -1,10 +1,12 @@
1
1
  RSpec.shared_examples "a pipe" do
2
2
  it "adds a block observer" do
3
3
  @pipe = described_class.start
4
- @observer = @pipe.add_observer do |event|
5
- puts event.type
4
+ @observer = await do
5
+ @pipe.add_observer do |event|
6
+ puts event.type
7
+ end
6
8
  end
7
- expect(@pipe.is_observer?(@observer)).to eq true
9
+ expect(await { @pipe.is_observer?(@observer) }).to eq true
8
10
  end
9
11
 
10
12
  it "adds a callable observer" do
@@ -13,13 +15,13 @@ RSpec.shared_examples "a pipe" do
13
15
 
14
16
  @pipe.add_observer @proc
15
17
 
16
- expect(@pipe.is_observer?(@proc)).to eq true
18
+ expect(await { @pipe.is_observer?(@proc) }).to eq true
17
19
  end
18
20
 
19
21
  it "does not allow an observer without a #call method" do
20
22
  @pipe = described_class.start
21
23
 
22
- expect { @pipe.add_observer(Object.new) }.to raise_error(TypeError)
24
+ expect { await { @pipe.add_observer(Object.new) } }.to raise_error(TypeError)
23
25
  end
24
26
 
25
27
  it "removes an observer" do
@@ -28,10 +30,10 @@ RSpec.shared_examples "a pipe" do
28
30
 
29
31
  @pipe.remove_observer @proc
30
32
 
31
- expect(@pipe.is_observer?(@proc)).to eq false
33
+ expect(await { @pipe.is_observer?(@proc) }).to eq false
32
34
  end
33
35
 
34
- it "does not send notifications for objects which are not events" do
36
+ it "does not send notifications for objects which are not events" do
35
37
  @pipe = described_class.start
36
38
  @results = []
37
39
  @observer = @pipe.add_observer do |event|
@@ -40,7 +42,7 @@ RSpec.shared_examples "a pipe" do
40
42
 
41
43
  @pipe << Object.new
42
44
 
43
- sleep 0.5
45
+ sleep 0.1
44
46
  expect(@results).to eq []
45
47
  end
46
48
 
@@ -101,6 +103,6 @@ RSpec.shared_examples "a pipe" do
101
103
 
102
104
  @pipe.shutdown
103
105
 
104
- expect(@pipe.is_observer?(@observer)).to eq false
106
+ expect(await { @pipe.is_observer?(@observer) }).to eq false
105
107
  end
106
108
  end
@@ -0,0 +1,158 @@
1
+ require "spec_helper"
2
+ require_relative "../../../lib/plumbing/actor/transporter"
3
+
4
+ RSpec.describe Plumbing::Actor::Transporter do
5
+ # standard:disable Lint/ConstantDefinitionInBlock
6
+ class Record
7
+ include GlobalID::Identification
8
+ attr_reader :id
9
+ def initialize id
10
+ @id = id
11
+ end
12
+
13
+ def == other
14
+ other.id == @id
15
+ end
16
+ end
17
+ # standard:enable Lint/ConstantDefinitionInBlock
18
+ before do
19
+ GlobalID.app = "rspec"
20
+ GlobalID::Locator.use :rspec do |gid, options|
21
+ Record.new gid.model_id
22
+ end
23
+ end
24
+
25
+ context "marshalling" do
26
+ it "passes simple arguments" do
27
+ @transporter = described_class.new
28
+
29
+ @transport = @transporter.marshal "Hello"
30
+ expect(@transport).to eq ["Hello"]
31
+
32
+ @transport = @transporter.marshal 1, 2, 3
33
+ expect(@transport).to eq [1, 2, 3]
34
+ end
35
+
36
+ it "copies arrays" do
37
+ @transporter = described_class.new
38
+
39
+ @source = [[1, 2, 3], [:this, :that]]
40
+
41
+ @transport = @transporter.marshal(*@source)
42
+ expect(@transport).to eq @source
43
+ expect(@transport.first.object_id).to_not eq @source.first.object_id
44
+ expect(@transport.last.object_id).to_not eq @source.last.object_id
45
+ end
46
+
47
+ it "copies hashss" do
48
+ @transporter = described_class.new
49
+
50
+ @source = [{first: "1", second: 2}]
51
+
52
+ @transport = @transporter.marshal(*@source)
53
+ expect(@transport).to eq @source
54
+ expect(@transport.first.object_id).to_not eq @source.first.object_id
55
+ end
56
+
57
+ it "converts objects to Global ID strings" do
58
+ @transporter = described_class.new
59
+
60
+ @record = Record.new 123
61
+ @global_id = @record.to_global_id.to_s
62
+
63
+ @transport = @transporter.marshal @record
64
+
65
+ expect(@transport).to eq [@global_id]
66
+ end
67
+
68
+ it "converts objects within arrays to Global ID strings" do
69
+ @transporter = described_class.new
70
+
71
+ @record = Record.new 123
72
+ @global_id = @record.to_global_id.to_s
73
+
74
+ @transport = @transporter.marshal [:this, @record]
75
+
76
+ expect(@transport).to eq [[:this, @global_id]]
77
+ end
78
+
79
+ it "converts objects within hashes to Global ID strings" do
80
+ @transporter = described_class.new
81
+
82
+ @record = Record.new 123
83
+ @global_id = @record.to_global_id.to_s
84
+
85
+ @transport = @transporter.marshal this: "that", the_other: {embedded: @record}
86
+
87
+ expect(@transport).to eq [{this: "that", the_other: {embedded: @global_id}}]
88
+ end
89
+ end
90
+
91
+ context "unmarshalling" do
92
+ it "passes simple arguments" do
93
+ @transporter = described_class.new
94
+
95
+ @transport = @transporter.unmarshal "Hello"
96
+ expect(@transport).to eq ["Hello"]
97
+
98
+ @transport = @transporter.unmarshal 1, 2, 3
99
+ expect(@transport).to eq [1, 2, 3]
100
+ end
101
+
102
+ it "passes arrays" do
103
+ @transporter = described_class.new
104
+
105
+ @transport = @transporter.unmarshal [1, 2, 3], [:this, :that]
106
+
107
+ expect(@transport.first.object_id).to_not eq [1, 2, 3]
108
+ expect(@transport.last.object_id).to_not eq [:this, :that]
109
+ end
110
+
111
+ it "passes hashss and keyword arguments" do
112
+ @transporter = described_class.new
113
+
114
+ @transport = @transporter.unmarshal first: "1", second: 2
115
+ expect(@transport).to eq [{first: "1", second: 2}]
116
+ end
117
+
118
+ it "passes mixtures of arrays and hashes" do
119
+ @transporter = described_class.new
120
+
121
+ @transport = @transporter.unmarshal :this, :that, first: "1", second: 2
122
+ expect(@transport).to eq [:this, :that, {first: "1", second: 2}]
123
+ end
124
+
125
+ it "converts Global ID strings to objects" do
126
+ @transporter = described_class.new
127
+
128
+ @record = Record.new "123"
129
+ @global_id = @record.to_global_id.to_s
130
+
131
+ @transport = @transporter.unmarshal @global_id
132
+
133
+ expect(@transport).to eq [@record]
134
+ end
135
+
136
+ it "converts Global ID strings within arrays to objects" do
137
+ @transporter = described_class.new
138
+
139
+ @record = Record.new "123"
140
+ @global_id = @record.to_global_id.to_s
141
+
142
+ @transport = @transporter.unmarshal :this, @global_id
143
+
144
+ expect(@transport).to eq [:this, @record]
145
+ end
146
+
147
+ it "converts Global ID strings within hashes to objects" do
148
+ @transporter = described_class.new
149
+
150
+ @record = Record.new "123"
151
+ @global_id = @record.to_global_id.to_s
152
+
153
+ @transport = @transporter.unmarshal this: "that", the_other: {embedded: @global_id}
154
+
155
+ expect(@transport).to eq [{this: "that", the_other: {embedded: @record}}]
156
+ end
157
+ end
158
+ end