standard-procedure-plumbing 0.3.2 → 0.4.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.
@@ -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