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.
@@ -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: []