interop 0.1.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.
- checksums.yaml +7 -0
- data/lib/interop.rb +16 -0
- data/lib/interop/channel.rb +118 -0
- data/lib/interop/connection.rb +43 -0
- data/lib/interop/error.rb +14 -0
- data/lib/interop/headers.rb +90 -0
- data/lib/interop/interceptor.rb +60 -0
- data/lib/interop/interceptor/base.rb +15 -0
- data/lib/interop/interceptor/read.rb +30 -0
- data/lib/interop/interceptor/write.rb +19 -0
- data/lib/interop/message.rb +83 -0
- data/lib/interop/middleware.rb +62 -0
- data/lib/interop/pipe.rb +30 -0
- data/lib/interop/reader.rb +36 -0
- data/lib/interop/reader_writer.rb +10 -0
- data/lib/interop/rpc/base.rb +60 -0
- data/lib/interop/rpc/client.rb +63 -0
- data/lib/interop/rpc/controller.rb +37 -0
- data/lib/interop/rpc/dispatcher.rb +70 -0
- data/lib/interop/rpc/magic.rb +25 -0
- data/lib/interop/rpc/server.rb +42 -0
- data/lib/interop/stream_reader.rb +58 -0
- data/lib/interop/stream_writer.rb +31 -0
- data/lib/interop/version.rb +5 -0
- data/lib/interop/writer.rb +28 -0
- metadata +69 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6369ad96be9d4e4a7946a23c56d4a06b96dc23553b2bb602e6708e34154561c7
|
4
|
+
data.tar.gz: fdf11d65dec06324fc6207c18318b17130a51ddaf580b5069f48710ee12605ed
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 06c118089ef25d6c1f122bacdefd11e7ecdd923fb82ccc3ad853e9782d768e374c43b6a55cd96f4bb9d11f582262facc50f9f2e4c0edf0f601e403b68e278369
|
7
|
+
data.tar.gz: 70b6a14d430ffb08785a731aec8a0cea4a782faae584b9b9758af5306ba5d4483fee103d3e5ed5c371501dbed5fc0eecfadec8541deef222248f3af7dd22a2f9
|
data/lib/interop.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
require 'interop/error'
|
4
|
+
require 'interop/version'
|
5
|
+
require 'interop/connection'
|
6
|
+
require 'interop/pipe'
|
7
|
+
require 'interop/interceptor'
|
8
|
+
require 'interop/middleware'
|
9
|
+
require 'interop/rpc/client'
|
10
|
+
require 'interop/rpc/server'
|
11
|
+
|
12
|
+
module Hx
|
13
|
+
module Interop
|
14
|
+
ROOT = Pathname(__dir__).parent
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'interop/error'
|
2
|
+
|
3
|
+
# TODO: the only reason this class exists is because SizedQueue doesn't allow a zero max.
|
4
|
+
# Figure out a way to block on the first push to a SizedQueue, and this messy situation
|
5
|
+
# can go away for good. In the meantime, a buffered version of this class is like a
|
6
|
+
# SizedQueue already, so that functionality can be removed.
|
7
|
+
|
8
|
+
module Hx
|
9
|
+
module Interop
|
10
|
+
# :nodoc:
|
11
|
+
class Channel
|
12
|
+
# :nodoc:
|
13
|
+
class Blocker
|
14
|
+
def initialize
|
15
|
+
@queue = Queue.new
|
16
|
+
@mutex = Mutex.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def resolve
|
20
|
+
@queue << nil
|
21
|
+
@queue.close
|
22
|
+
@value
|
23
|
+
end
|
24
|
+
|
25
|
+
def wait
|
26
|
+
@mutex.synchronize do
|
27
|
+
next unless @queue
|
28
|
+
|
29
|
+
@queue.shift
|
30
|
+
@queue = nil
|
31
|
+
end
|
32
|
+
@value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# :nodoc:
|
37
|
+
class Put < Blocker
|
38
|
+
def initialize(value)
|
39
|
+
@value = value
|
40
|
+
super()
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# :nodoc:
|
45
|
+
class Get < Blocker
|
46
|
+
def resolve(value)
|
47
|
+
@value = value
|
48
|
+
super()
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
Deadlock = Class.new(Error)
|
53
|
+
Closed = Class.new(Error)
|
54
|
+
|
55
|
+
def initialize(butter_limit = 0)
|
56
|
+
@buffer_limit = butter_limit
|
57
|
+
@buffer = []
|
58
|
+
@gets = []
|
59
|
+
@puts = []
|
60
|
+
@mutex = Mutex.new
|
61
|
+
end
|
62
|
+
|
63
|
+
def put(*objects)
|
64
|
+
objects.each &method(:<<)
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
def <<(obj)
|
69
|
+
put = nil
|
70
|
+
@mutex.synchronize do
|
71
|
+
raise Closed if @closed
|
72
|
+
|
73
|
+
if (get = @gets.shift)
|
74
|
+
get.resolve obj
|
75
|
+
elsif @buffer.length < @buffer_limit
|
76
|
+
@buffer << obj
|
77
|
+
else
|
78
|
+
raise Deadlock if Thread.list.one?
|
79
|
+
|
80
|
+
put = Put.new(obj)
|
81
|
+
@puts << put
|
82
|
+
end
|
83
|
+
end
|
84
|
+
put&.wait
|
85
|
+
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
def get
|
90
|
+
get = nil
|
91
|
+
@mutex.synchronize do
|
92
|
+
return if @closed
|
93
|
+
|
94
|
+
put = @puts.shift
|
95
|
+
return put.resolve if put
|
96
|
+
return @buffer.shift if @buffer.any?
|
97
|
+
|
98
|
+
raise Deadlock if Thread.list.one?
|
99
|
+
|
100
|
+
get = Get.new
|
101
|
+
@gets << get
|
102
|
+
end
|
103
|
+
get.wait
|
104
|
+
end
|
105
|
+
|
106
|
+
def close
|
107
|
+
raise Closed if @closed
|
108
|
+
|
109
|
+
@mutex.synchronize do
|
110
|
+
@closed = true
|
111
|
+
@puts.each &:resolve
|
112
|
+
@gets.each { |g| g.resolve nil }
|
113
|
+
end
|
114
|
+
@buffer
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'interop/stream_reader'
|
2
|
+
require 'interop/stream_writer'
|
3
|
+
require 'interop/reader_writer'
|
4
|
+
|
5
|
+
module Hx
|
6
|
+
module Interop
|
7
|
+
# Combines a Reader and a Writer
|
8
|
+
class Connection
|
9
|
+
include ReaderWriter
|
10
|
+
|
11
|
+
# @param [Reader, IO, StringIO] reader
|
12
|
+
# @param [Writer, IO, StringIO] writer
|
13
|
+
def self.build(reader, writer = reader)
|
14
|
+
return reader if reader.is_a?(ReaderWriter) && writer.equal?(reader)
|
15
|
+
|
16
|
+
reader = StreamReader.new(reader) if reader.respond_to? :readline
|
17
|
+
writer = StreamWriter.new(writer) if writer.respond_to? :puts
|
18
|
+
|
19
|
+
new reader, writer
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param [Reader] reader
|
23
|
+
# @param [Writer] writer
|
24
|
+
def initialize(reader, writer)
|
25
|
+
raise ArgumentError, "Expected a #{Reader}" unless reader.is_a? Reader
|
26
|
+
raise ArgumentError, "Expected a #{Writer}" unless writer.is_a? Writer
|
27
|
+
|
28
|
+
@reader = reader
|
29
|
+
@writer = writer
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def _read
|
35
|
+
@reader.read
|
36
|
+
end
|
37
|
+
|
38
|
+
def _write(message)
|
39
|
+
@writer.write message
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Hx
|
2
|
+
module Interop
|
3
|
+
# Represents MIME headers for a message.
|
4
|
+
class Headers
|
5
|
+
ID = 'Interop-Rpc-Id'.freeze
|
6
|
+
CLASS = 'Interop-Rpc-Class'.freeze
|
7
|
+
ERROR = 'Interop-Error'.freeze
|
8
|
+
CONTENT_TYPE = 'Content-Type'.freeze
|
9
|
+
CONTENT_LENGTH = 'Content-Length'.freeze
|
10
|
+
|
11
|
+
def initialize(headers = nil)
|
12
|
+
@headers = {}
|
13
|
+
merge! headers unless headers.nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
def [](key)
|
17
|
+
@headers[canonical_key key]
|
18
|
+
end
|
19
|
+
|
20
|
+
def []=(key, value)
|
21
|
+
key = canonical_key(key)
|
22
|
+
if value.nil?
|
23
|
+
@headers.delete key
|
24
|
+
else
|
25
|
+
@headers[key] = value.to_s
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def merge!(*others)
|
30
|
+
others.each do |other|
|
31
|
+
other.each do |key, value|
|
32
|
+
self[key] = value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
def merge(*others)
|
39
|
+
dup.merge! others
|
40
|
+
end
|
41
|
+
|
42
|
+
def fetch(key, *args, &block)
|
43
|
+
@headers.fetch canonical_key(key, *args, &block)
|
44
|
+
end
|
45
|
+
|
46
|
+
def dup
|
47
|
+
headers = @headers.dup
|
48
|
+
self.class.new.instance_exec do
|
49
|
+
@headers = headers
|
50
|
+
self
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def freeze
|
55
|
+
@headers.freeze
|
56
|
+
super
|
57
|
+
end
|
58
|
+
|
59
|
+
def inspect
|
60
|
+
"<##{self.class.name} #{@headers.inspect[1..-2]}>"
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_s
|
64
|
+
map { |k, v| "#{k}: #{v}" }.join "\n"
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def method_missing(symbol, *args, &block)
|
70
|
+
if @headers.respond_to? symbol
|
71
|
+
@headers.__send__ symbol, *args, &block
|
72
|
+
else
|
73
|
+
super
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def respond_to_missing?(symbol, *)
|
78
|
+
@headers.respond_to?(symbol) or super
|
79
|
+
end
|
80
|
+
|
81
|
+
def canonical_key(key)
|
82
|
+
key
|
83
|
+
.to_s
|
84
|
+
.split(/[-_\s]+/)
|
85
|
+
.map { |str| str[0].upcase + str[1..].downcase }
|
86
|
+
.join('-')
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'interop/connection'
|
2
|
+
require 'interop/interceptor/read'
|
3
|
+
require 'interop/interceptor/write'
|
4
|
+
|
5
|
+
module Hx
|
6
|
+
module Interop
|
7
|
+
# Module for building simple duplex interceptors
|
8
|
+
module Interceptor
|
9
|
+
# :nodoc:
|
10
|
+
class Builder
|
11
|
+
# @param [ReaderWriter, nil] connection
|
12
|
+
def initialize(connection)
|
13
|
+
@conn = connection
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param [Reader] reader
|
17
|
+
def read(reader = @conn, &block)
|
18
|
+
raise 'Reader already declared' if @reader
|
19
|
+
raise TypeError, 'Expected a Reader' unless reader.is_a? Reader
|
20
|
+
|
21
|
+
@reader = Read.new(reader, &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param [Writer] writer
|
25
|
+
def write(writer = @conn, &block)
|
26
|
+
raise 'Reader already declared' if @writer
|
27
|
+
raise TypeError, 'Expected a Writer' unless writer.is_a? Writer
|
28
|
+
|
29
|
+
@writer = Write.new(writer, &block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def build(&block)
|
33
|
+
if block.arity.zero?
|
34
|
+
instance_exec &block
|
35
|
+
else
|
36
|
+
block.call self
|
37
|
+
end
|
38
|
+
|
39
|
+
consolidate!
|
40
|
+
|
41
|
+
return Connection.new @reader, @writer if @reader && @writer
|
42
|
+
|
43
|
+
@reader || @writer || @conn or raise 'Nothing to build'
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def consolidate!
|
49
|
+
@reader ||= @writer && @conn if @conn.is_a?(Reader)
|
50
|
+
@writer ||= @reader && @conn if @conn.is_a?(Writer)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# @param [ReaderWriter, nil] connection
|
55
|
+
def self.build(connection = nil, &block)
|
56
|
+
Builder.new(connection).build(&block)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Hx
|
2
|
+
module Interop
|
3
|
+
module Interceptor
|
4
|
+
# Base class for read and write interceptors
|
5
|
+
class Base
|
6
|
+
def initialize(stream, &block)
|
7
|
+
raise ArgumentError, 'Expected a block that takes exactly 2 arguments' unless [2, -1].include? block&.arity
|
8
|
+
|
9
|
+
@stream = stream
|
10
|
+
@handler = block
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'interop/interceptor/base'
|
2
|
+
require 'interop/reader'
|
3
|
+
|
4
|
+
module Hx
|
5
|
+
module Interop
|
6
|
+
module Interceptor
|
7
|
+
# Intercept reads
|
8
|
+
# TODO: consider a background-processed version (like the Go version)
|
9
|
+
class Read < Base
|
10
|
+
include Reader
|
11
|
+
|
12
|
+
# @param [Reader] reader
|
13
|
+
def initialize(reader, &block)
|
14
|
+
super
|
15
|
+
@queue = []
|
16
|
+
@mutex = Mutex.new
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def _read
|
22
|
+
@mutex.synchronize do
|
23
|
+
@handler[@stream.read, @queue] while @queue.empty?
|
24
|
+
@queue.shift
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'interop/interceptor/base'
|
2
|
+
require 'interop/writer'
|
3
|
+
|
4
|
+
module Hx
|
5
|
+
module Interop
|
6
|
+
module Interceptor
|
7
|
+
# Intercept writes
|
8
|
+
class Write < Base
|
9
|
+
include Writer
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def _write(message)
|
14
|
+
@handler[message, @stream]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
require 'interop/headers'
|
4
|
+
|
5
|
+
module Hx
|
6
|
+
module Interop
|
7
|
+
# Represents a single interop message, with headers and a body.
|
8
|
+
class Message
|
9
|
+
module Types
|
10
|
+
JSON = 'application/json'.freeze
|
11
|
+
BINARY = 'application/octet-stream'.freeze
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def json_parse_options
|
16
|
+
@json_parse_options ||= {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def build(*args)
|
20
|
+
return args.first if args.one? && args.first.is_a?(Message)
|
21
|
+
|
22
|
+
new.tap do |message|
|
23
|
+
args.each do |arg|
|
24
|
+
assign_build_arg message, arg
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def json(object, *args)
|
30
|
+
build JSON.generate(object) << "\n", {Headers::CONTENT_TYPE => Types::JSON}, *args
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def assign_build_arg(message, arg)
|
36
|
+
case arg
|
37
|
+
when nil
|
38
|
+
# Ignore nils
|
39
|
+
when Hash
|
40
|
+
message.headers.merge! arg
|
41
|
+
when Message
|
42
|
+
message.headers.merge! arg.headers
|
43
|
+
message.body = arg.body
|
44
|
+
else
|
45
|
+
message.body = arg.to_s
|
46
|
+
message[Headers::CONTENT_LENGTH] = message.body.bytesize
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :headers, :body
|
52
|
+
|
53
|
+
def initialize(headers = nil, body = nil)
|
54
|
+
@headers = Headers.new(headers)
|
55
|
+
self.body = body unless body.nil?
|
56
|
+
end
|
57
|
+
|
58
|
+
def [](key)
|
59
|
+
@headers[key]
|
60
|
+
end
|
61
|
+
|
62
|
+
def []=(key, value)
|
63
|
+
@headers[key] = value
|
64
|
+
end
|
65
|
+
|
66
|
+
def body=(value)
|
67
|
+
@body = value.to_s
|
68
|
+
end
|
69
|
+
|
70
|
+
def decode
|
71
|
+
case @headers[Headers::CONTENT_TYPE]
|
72
|
+
when Types::JSON
|
73
|
+
return JSON.parse @body, self.class.json_parse_options
|
74
|
+
end
|
75
|
+
raise Error::NotDecodable, 'Message is not in a decodable format'
|
76
|
+
end
|
77
|
+
|
78
|
+
def dup
|
79
|
+
Message.new headers.to_h, body.dup
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'interop/reader_writer'
|
2
|
+
|
3
|
+
module Hx
|
4
|
+
module Interop
|
5
|
+
# Base class for interop middleware.
|
6
|
+
class Middleware
|
7
|
+
include ReaderWriter
|
8
|
+
|
9
|
+
# :nodoc:
|
10
|
+
module BuildMessageOnWrite
|
11
|
+
def write(*args)
|
12
|
+
super Message.build(*args)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.inherited(subclass)
|
17
|
+
subclass.prepend BuildMessageOnWrite
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param [Array<Class>] classes
|
22
|
+
# @param [ReaderWriter] connection
|
23
|
+
def self.stack(*classes, connection)
|
24
|
+
raise ArgumentError, "Expected an instance of #{ReaderWriter}" unless connection.is_a? ReaderWriter
|
25
|
+
|
26
|
+
classes.reverse.reduce connection do |wrapped, klass|
|
27
|
+
raise ArgumentError, "Expected subclasses of #{self}" unless klass.is_a?(Class) && klass < self
|
28
|
+
|
29
|
+
klass.new(wrapped)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :connection
|
34
|
+
|
35
|
+
def initialize(connection)
|
36
|
+
@connection = connection
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return the complete Middleware stack as an array, with this instance as the first item, and
|
40
|
+
# the core (or shallowest non-Middleware layer) as the last.
|
41
|
+
def stack
|
42
|
+
result = [self]
|
43
|
+
if @connection.is_a? Middleware
|
44
|
+
result.concat @connection.stack
|
45
|
+
else
|
46
|
+
result << @connection
|
47
|
+
end
|
48
|
+
result
|
49
|
+
end
|
50
|
+
|
51
|
+
protected
|
52
|
+
|
53
|
+
def _read
|
54
|
+
@connection.read
|
55
|
+
end
|
56
|
+
|
57
|
+
def _write(message)
|
58
|
+
@connection.write message
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/interop/pipe.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'interop/channel'
|
2
|
+
require 'interop/message'
|
3
|
+
require 'interop/reader_writer'
|
4
|
+
|
5
|
+
module Hx
|
6
|
+
module Interop
|
7
|
+
# A message pipe. You can read exactly what is written to it.
|
8
|
+
class Pipe
|
9
|
+
include ReaderWriter
|
10
|
+
|
11
|
+
def initialize(buffer_size = 0)
|
12
|
+
@channel = Channel.new(buffer_size)
|
13
|
+
end
|
14
|
+
|
15
|
+
def close
|
16
|
+
@channel.close
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def _read
|
22
|
+
@channel.get or raise EOFError
|
23
|
+
end
|
24
|
+
|
25
|
+
def _write(message)
|
26
|
+
@channel.put message
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Hx
|
2
|
+
module Interop
|
3
|
+
# Anything from which you can read a message.
|
4
|
+
module Reader
|
5
|
+
def self.new(*args, &block)
|
6
|
+
StreamReader.new *args, &block
|
7
|
+
end
|
8
|
+
|
9
|
+
# Read a message from the stream, blocking until a completed message is read.
|
10
|
+
# @return [Message]
|
11
|
+
def read
|
12
|
+
_read
|
13
|
+
end
|
14
|
+
|
15
|
+
# Yields messages as they are read. Returns self when EOF is reached.
|
16
|
+
# @return [Enumerable, Reader]
|
17
|
+
# @yieldparam [Message]
|
18
|
+
def read_all
|
19
|
+
return enum_for :read_all unless block_given?
|
20
|
+
|
21
|
+
loop do
|
22
|
+
yield read
|
23
|
+
rescue EOFError
|
24
|
+
break
|
25
|
+
end
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def _read
|
32
|
+
raise NotImplementedError
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'interop/connection'
|
2
|
+
require 'interop/rpc/dispatcher'
|
3
|
+
require 'interop/rpc/controller'
|
4
|
+
|
5
|
+
module Hx
|
6
|
+
module Interop
|
7
|
+
module RPC
|
8
|
+
# Base class for RPC Client and Server
|
9
|
+
class Base
|
10
|
+
def initialize(reader, writer = reader)
|
11
|
+
@connection = Connection.build(reader, writer)
|
12
|
+
@dispatcher = Dispatcher.new
|
13
|
+
@io_thread = Thread.new do
|
14
|
+
run
|
15
|
+
rescue StandardError => e
|
16
|
+
@error = e
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# TODO: custom exception handler?
|
21
|
+
|
22
|
+
# Wait for the process to finish (i.e. for the connection to close).
|
23
|
+
def wait
|
24
|
+
@io_thread.join
|
25
|
+
raise @error if @error # TODO: wrap in something specific, to preserve backtrace
|
26
|
+
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def on(criteria, *handler, &block)
|
31
|
+
@dispatcher.on criteria, *handler, &block
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
attr_reader :dispatcher
|
38
|
+
|
39
|
+
def build_message(first, *rest)
|
40
|
+
first = {Headers::CLASS => first} if first.is_a?(String) || first.is_a?(Symbol)
|
41
|
+
Message.build first, *rest
|
42
|
+
end
|
43
|
+
|
44
|
+
def write(*args)
|
45
|
+
@connection.write *args
|
46
|
+
end
|
47
|
+
|
48
|
+
def run
|
49
|
+
@connection.read_all do |request|
|
50
|
+
Thread.new do
|
51
|
+
yield request
|
52
|
+
rescue StandardError => e
|
53
|
+
@io_thread.raise e
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'interop/rpc/base'
|
2
|
+
require 'interop/rpc/magic'
|
3
|
+
|
4
|
+
module Hx
|
5
|
+
module Interop
|
6
|
+
# Contains RPC-specific classes.
|
7
|
+
module RPC
|
8
|
+
# An RPC client.
|
9
|
+
class Client < Base
|
10
|
+
attr_accessor :id_prefix
|
11
|
+
|
12
|
+
def initialize(*)
|
13
|
+
@next_id = 1
|
14
|
+
@queues = {}
|
15
|
+
@mutex = Mutex.new
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param [Message] request
|
20
|
+
# @return [Message]
|
21
|
+
def call(request, *args)
|
22
|
+
request = build_message(request, *args)
|
23
|
+
|
24
|
+
queue = Queue.new
|
25
|
+
|
26
|
+
@mutex.synchronize do
|
27
|
+
id = "#{id_prefix}#{@next_id}"
|
28
|
+
@next_id += 1
|
29
|
+
request[Headers::ID] = id
|
30
|
+
@queues[id] = queue
|
31
|
+
end
|
32
|
+
|
33
|
+
write request
|
34
|
+
|
35
|
+
queue.pop
|
36
|
+
end
|
37
|
+
|
38
|
+
alias [] call
|
39
|
+
|
40
|
+
def call_json(message_class, unencoded_body, *args)
|
41
|
+
call message_class, Message.json(unencoded_body, *args)
|
42
|
+
end
|
43
|
+
|
44
|
+
def magic(&block)
|
45
|
+
Magic.new self, :call, &block
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def run
|
51
|
+
super do |message|
|
52
|
+
if (id = message[Headers::ID]) && (queue = @mutex.synchronize { @queues.delete id })
|
53
|
+
queue << message
|
54
|
+
queue.close
|
55
|
+
else
|
56
|
+
dispatcher << message
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'interop/message'
|
2
|
+
|
3
|
+
module Hx
|
4
|
+
module Interop
|
5
|
+
module RPC
|
6
|
+
# Abstract class for RPC server controllers.
|
7
|
+
class Controller
|
8
|
+
# TODO: hooks, error handlers
|
9
|
+
|
10
|
+
def self.make_handler(action)
|
11
|
+
action = action.to_sym
|
12
|
+
|
13
|
+
unless public_instance_methods(false).include? action
|
14
|
+
raise ArgumentError, "Invalid action '#{action}' on #{self}"
|
15
|
+
end
|
16
|
+
|
17
|
+
klass = self
|
18
|
+
lambda do |message|
|
19
|
+
controller = klass.new(message)
|
20
|
+
controller.__send__ action
|
21
|
+
controller.response
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :request
|
26
|
+
|
27
|
+
def initialize(request)
|
28
|
+
@request = request
|
29
|
+
end
|
30
|
+
|
31
|
+
def response
|
32
|
+
@response ||= Message.new
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'interop/rpc/controller'
|
2
|
+
|
3
|
+
module Hx
|
4
|
+
module Interop
|
5
|
+
module RPC
|
6
|
+
# Message dispatcher used by Client and Server.
|
7
|
+
class Dispatcher
|
8
|
+
Route = Struct.new :matcher, :handler do
|
9
|
+
def call(message)
|
10
|
+
handler[message]
|
11
|
+
end
|
12
|
+
|
13
|
+
def match?(message)
|
14
|
+
matcher[message]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@routes = []
|
20
|
+
end
|
21
|
+
|
22
|
+
def on(criteria, *handler, &block)
|
23
|
+
@routes << Route.new(
|
24
|
+
make_matcher(criteria),
|
25
|
+
make_handler(*handler, &block)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def dispatch(message)
|
30
|
+
@routes.each do |route|
|
31
|
+
route.call message if route.match? message
|
32
|
+
end
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def match(message)
|
37
|
+
@routes.find { |r| r.match? message }
|
38
|
+
end
|
39
|
+
|
40
|
+
alias << dispatch
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def make_matcher(criteria)
|
45
|
+
case criteria
|
46
|
+
when Proc
|
47
|
+
criteria
|
48
|
+
when String, Regexp
|
49
|
+
-> message { criteria === message.headers[Headers::CLASS] }
|
50
|
+
when Symbol
|
51
|
+
make_matcher criteria.to_s
|
52
|
+
else
|
53
|
+
raise ArgumentError, 'Invalid message match criteria'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def make_handler(*args, &block)
|
58
|
+
return block if block && args.empty?
|
59
|
+
|
60
|
+
if args.length == 2
|
61
|
+
controller, action = args
|
62
|
+
return controller.make_handler(action) if controller.is_a?(Class) && controller < Controller
|
63
|
+
end
|
64
|
+
|
65
|
+
raise ArgumentError, 'Invalid message handler'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Hx
|
2
|
+
module Interop
|
3
|
+
module RPC
|
4
|
+
# :nodoc:
|
5
|
+
class Magic < BasicObject
|
6
|
+
def initialize(receiver, symbol, &transformer)
|
7
|
+
@receiver = receiver
|
8
|
+
@symbol = symbol
|
9
|
+
@transformer = transformer
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def method_missing(symbol, *args)
|
15
|
+
args = [@transformer.call(*args)] if @transformer
|
16
|
+
@receiver.__send__ @symbol, symbol, *args
|
17
|
+
end
|
18
|
+
|
19
|
+
def respond_to_missing?(*)
|
20
|
+
true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'interop/rpc/base'
|
2
|
+
|
3
|
+
module Hx
|
4
|
+
module Interop
|
5
|
+
# Contains RPC-specific classes.
|
6
|
+
module RPC
|
7
|
+
# An RPC server.
|
8
|
+
class Server < Base
|
9
|
+
# @param [Message] event
|
10
|
+
def send(event, *args)
|
11
|
+
event = build_message(event, *args)
|
12
|
+
|
13
|
+
raise ArgumentError, 'Cannot send an event with an ID' if event.headers.key? Headers::ID
|
14
|
+
|
15
|
+
@connection.write event
|
16
|
+
end
|
17
|
+
|
18
|
+
alias << send
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def run
|
23
|
+
super do |request|
|
24
|
+
response = make_response(dispatcher.match(request)&.call request)
|
25
|
+
response[Headers::ID] = request[Headers::ID]
|
26
|
+
write response
|
27
|
+
rescue StandardError => e
|
28
|
+
write Headers::ERROR => "Unhandled exception: #{e}"
|
29
|
+
raise
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def make_response(result)
|
34
|
+
return result if result.is_a? Message
|
35
|
+
|
36
|
+
result = [result] unless result.is_a? Array
|
37
|
+
Message.build *result
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'interop/message'
|
2
|
+
require 'interop/reader'
|
3
|
+
|
4
|
+
module Hx
|
5
|
+
module Interop
|
6
|
+
# Reads messages from a stream (e.g. STDIN)
|
7
|
+
class StreamReader
|
8
|
+
include Reader
|
9
|
+
|
10
|
+
# Acceptable line terminators
|
11
|
+
NEWLINES = Set.new(%W[\n \r\n]).freeze
|
12
|
+
|
13
|
+
# @param [IO, StringIO] stream
|
14
|
+
def initialize(stream)
|
15
|
+
@stream = stream
|
16
|
+
@mutex = Mutex.new
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def _read
|
22
|
+
@mutex.synchronize do
|
23
|
+
message = Message.new(read_headers)
|
24
|
+
length = message.headers[Headers::CONTENT_LENGTH]&.to_i
|
25
|
+
message.body =
|
26
|
+
if length&.positive?
|
27
|
+
@stream.read length
|
28
|
+
elsif length.nil?
|
29
|
+
read_paragraph.join
|
30
|
+
else
|
31
|
+
''
|
32
|
+
end
|
33
|
+
message
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# @return [Hash]
|
40
|
+
def read_headers
|
41
|
+
read_paragraph.to_h do |line|
|
42
|
+
line.strip.split(/:\s*/).tap do |pair|
|
43
|
+
raise Error::InvalidHeader unless pair.length == 2
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def read_paragraph
|
49
|
+
lines = []
|
50
|
+
while (line = @stream.readline("\n"))
|
51
|
+
return lines if NEWLINES.include? line
|
52
|
+
|
53
|
+
lines << line
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'interop/message'
|
2
|
+
require 'interop/writer'
|
3
|
+
|
4
|
+
module Hx
|
5
|
+
module Interop
|
6
|
+
# Writes messages to a stream (e.g. STDOUT)
|
7
|
+
class StreamWriter
|
8
|
+
include Writer
|
9
|
+
|
10
|
+
# @param [IO, StringIO] stream
|
11
|
+
def initialize(stream)
|
12
|
+
@stream = stream
|
13
|
+
@mutex = Mutex.new
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
# @param [Message] message
|
19
|
+
def _write(message)
|
20
|
+
@mutex.synchronize do
|
21
|
+
message.headers.each do |k, v|
|
22
|
+
@stream.puts "#{k}: #{v}"
|
23
|
+
end
|
24
|
+
@stream.puts
|
25
|
+
@stream.write message.body
|
26
|
+
@stream.puts
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'interop/message'
|
2
|
+
|
3
|
+
module Hx
|
4
|
+
module Interop
|
5
|
+
# Anything to which you can write a message
|
6
|
+
module Writer
|
7
|
+
def self.new(*args, &block)
|
8
|
+
StreamWriter.new(*args, &block)
|
9
|
+
end
|
10
|
+
|
11
|
+
# @param [Message] message
|
12
|
+
def write(message, *args)
|
13
|
+
_write Message.build(message, *args)
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def <<(*args)
|
18
|
+
write *args
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def _write(*)
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: interop
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Neil E. Pearson
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-12-03 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Ruby implementation of hx/interop cross-language interop abstraction
|
14
|
+
email:
|
15
|
+
- neil@pearson.sydney
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/interop.rb
|
21
|
+
- lib/interop/channel.rb
|
22
|
+
- lib/interop/connection.rb
|
23
|
+
- lib/interop/error.rb
|
24
|
+
- lib/interop/headers.rb
|
25
|
+
- lib/interop/interceptor.rb
|
26
|
+
- lib/interop/interceptor/base.rb
|
27
|
+
- lib/interop/interceptor/read.rb
|
28
|
+
- lib/interop/interceptor/write.rb
|
29
|
+
- lib/interop/message.rb
|
30
|
+
- lib/interop/middleware.rb
|
31
|
+
- lib/interop/pipe.rb
|
32
|
+
- lib/interop/reader.rb
|
33
|
+
- lib/interop/reader_writer.rb
|
34
|
+
- lib/interop/rpc/base.rb
|
35
|
+
- lib/interop/rpc/client.rb
|
36
|
+
- lib/interop/rpc/controller.rb
|
37
|
+
- lib/interop/rpc/dispatcher.rb
|
38
|
+
- lib/interop/rpc/magic.rb
|
39
|
+
- lib/interop/rpc/server.rb
|
40
|
+
- lib/interop/stream_reader.rb
|
41
|
+
- lib/interop/stream_writer.rb
|
42
|
+
- lib/interop/version.rb
|
43
|
+
- lib/interop/writer.rb
|
44
|
+
homepage: https://github.com/hx/interop
|
45
|
+
licenses:
|
46
|
+
- Apache-2.0
|
47
|
+
metadata:
|
48
|
+
homepage_uri: https://github.com/hx/interop
|
49
|
+
source_code_uri: https://github.com/hx/interop
|
50
|
+
post_install_message:
|
51
|
+
rdoc_options: []
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: 2.6.0
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
requirements: []
|
65
|
+
rubygems_version: 3.0.8
|
66
|
+
signing_key:
|
67
|
+
specification_version: 4
|
68
|
+
summary: Cross-language interop abstraction
|
69
|
+
test_files: []
|