interop 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|