interop 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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,14 @@
1
+ module Hx
2
+ module Interop
3
+ class Error < StandardError
4
+ class Fatal < self
5
+ end
6
+
7
+ class InvalidHeader < Fatal
8
+ end
9
+
10
+ class NotDecodable < self
11
+ end
12
+ end
13
+ end
14
+ 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
@@ -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,10 @@
1
+ require 'interop/reader'
2
+ require 'interop/writer'
3
+
4
+ module Hx
5
+ module Interop
6
+ module ReaderWriter
7
+ include Reader, Writer
8
+ end
9
+ end
10
+ 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,5 @@
1
+ module Hx
2
+ module Interop
3
+ VERSION = '0.1.0'.freeze
4
+ end
5
+ 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: []