protein 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 640ce556cbb7f7709579ad076cd242880081d3bd
4
+ data.tar.gz: 682c86726085e8ab7c794a2bea141726a718edf9
5
+ SHA512:
6
+ metadata.gz: d1015ee49c77d00f0e16d9816f2f80a05d36750844482c9fc1d0a4ef54658e534816b41c3749ee2d39f59ae6ae77fb2c3312567060c46bcb1df768541b0e2497
7
+ data.tar.gz: 41d4c03428c44c138ff8e9be1a7d2161393c0ab6cc9c610a58feefd7bcffcbd9951f166ea565a01b4dca48192167b5fb98f40d12b14d163dcbc932bddbdb390e
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # Protein for Ruby
2
+
3
+ ***Multi-platform remote procedure call (RPC) system based on Protocol Buffers***
4
+
5
+ Features:
6
+
7
+ - Implement RPC services and clients for Elixir and Ruby platforms
8
+ - Call remote services using unified, simple client API
9
+ - Call to services for an immediate response or push non-blocking requests to async services
10
+ - Define services via unified, configurable DSL
11
+ - Define service input/outputs using the widely acclaimed Google Protocol Buffers format
12
+ - Transport your calls via HTTP or AMQP transports
13
+
14
+ Packages:
15
+
16
+ - [Protein for Elixir](http://github.com/surgeventures/protein-elixir)
17
+ - [Protein for Ruby](http://github.com/surgeventures/protein-ruby)
18
+
19
+ ## Getting Started
20
+
21
+ Add `protein` as a dependency to your project in `Gemfile`:
22
+
23
+ ```elixir
24
+ gem "protein", "~> x.x.x"
25
+ ```
26
+
27
+ Then run `bundle install` to fetch it.
28
+
29
+ ## Documentation
30
+
31
+ We don't provide documentation for Ruby package at the moment. Please look for answers in the code
32
+ and in documentation for Elixir package - basic concepts and API shapes are akin.
data/lib/protein.rb ADDED
@@ -0,0 +1,17 @@
1
+ require "protein/errors"
2
+ require "protein/proto_compiler"
3
+ require "protein/get_const"
4
+
5
+ require "protein/router"
6
+ require "protein/client"
7
+ require "protein/server"
8
+ require "protein/processor"
9
+ require "protein/payload"
10
+
11
+ require "protein/pointer"
12
+ require "protein/service_error"
13
+ require "protein/service"
14
+
15
+ require "protein/transport"
16
+ require "protein/http_adapter"
17
+ require "protein/amqp_adapter"
@@ -0,0 +1,132 @@
1
+ require "bunny"
2
+ require "thread"
3
+
4
+ module Protein
5
+ class AMQPAdapter
6
+ class << self
7
+ def from_hash(hash)
8
+ if (new_url = hash[:url])
9
+ url(new_url)
10
+ end
11
+
12
+ if (new_queue = hash[:queue])
13
+ queue(new_queue)
14
+ end
15
+
16
+ if hash.has_key?(:timeout)
17
+ timeout(hash[:timeout])
18
+ end
19
+ end
20
+
21
+ def url(url = nil)
22
+ @url = url if url
23
+ @url || raise(DefinitionError, "url is not defined")
24
+ end
25
+
26
+ def queue(queue = nil)
27
+ @queue = queue if queue
28
+ @queue || raise(DefinitionError, "queue is not defined")
29
+ end
30
+
31
+ def timeout(timeout = :not_set)
32
+ @timeout = timeout if timeout != :not_set
33
+ instance_variable_defined?("@timeout") ? @timeout : 15_000
34
+ end
35
+
36
+ attr_reader :reply_queue
37
+ attr_accessor :response, :call_id
38
+ attr_reader :lock, :condition
39
+
40
+ def call(request_payload)
41
+ prepare_client
42
+
43
+ @call_id = SecureRandom.uuid
44
+
45
+ @x.publish(request_payload,
46
+ correlation_id: @call_id,
47
+ routing_key: @server_queue,
48
+ reply_to: @reply_queue.name,
49
+ expiration: timeout)
50
+
51
+ self.response = nil
52
+ lock.synchronize { condition.wait(lock, timeout && timeout * 0.001) }
53
+
54
+ if response == nil
55
+ raise(TransportError, "timeout after #{timeout}ms")
56
+ elsif response == "ESRV"
57
+ raise(TransportError, "failed to process the request")
58
+ else
59
+ response
60
+ end
61
+ end
62
+
63
+ def push(message_payload)
64
+ prepare_client
65
+
66
+ @x.publish(message_payload,
67
+ routing_key: @server_queue)
68
+ end
69
+
70
+ def serve(router)
71
+ @conn = Bunny.new(url)
72
+ @conn.start
73
+ @ch = @conn.create_channel
74
+ @q = @ch.queue(queue)
75
+ @x = @ch.default_exchange
76
+
77
+ Rails.logger.info "Connected to #{url}, serving RPC calls from #{queue}"
78
+
79
+ loop do
80
+ begin
81
+ @q.subscribe(block: true, manual_ack: true) do |delivery_info, properties, payload|
82
+ begin
83
+ @error = nil
84
+ response = Processor.call(router, payload)
85
+ rescue Exception => error
86
+ @error = error
87
+ response = "ESRV"
88
+ end
89
+
90
+ if response
91
+ @x.publish(response,
92
+ routing_key: properties.reply_to,
93
+ correlation_id: properties.correlation_id)
94
+ end
95
+
96
+ @ch.ack(delivery_info.delivery_tag)
97
+
98
+ raise(@error) if @error
99
+ end
100
+ rescue StandardError => e
101
+ Rails.logger.info "RPC server error: #{e.inspect}, restarting the server in 5s..."
102
+ sleep 5
103
+ end
104
+ end
105
+ end
106
+
107
+ private
108
+
109
+ def prepare_client
110
+ return if @conn
111
+
112
+ @conn = Bunny.new(url)
113
+ @conn.start
114
+ @ch = @conn.create_channel
115
+ @x = @ch.default_exchange
116
+ @server_queue = queue
117
+ @reply_queue = @ch.queue("", exclusive: true)
118
+ @lock = Mutex.new
119
+ @condition = ConditionVariable.new
120
+
121
+ that = self
122
+
123
+ @reply_queue.subscribe do |delivery_info, properties, payload|
124
+ if properties[:correlation_id] == that.call_id
125
+ that.response = payload
126
+ that.lock.synchronize{that.condition.signal}
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,82 @@
1
+ module Protein
2
+ class Client
3
+ class << self
4
+ def route(router)
5
+ @router = Router.define(router)
6
+ end
7
+
8
+ def service(service)
9
+ @router ||= Class.new(Router)
10
+ @router.service(service)
11
+ end
12
+
13
+ def proto(proto)
14
+ service = Class.new(Service)
15
+ service.proto(proto)
16
+
17
+ @router ||= Class.new(Router)
18
+ @router.service(service)
19
+ end
20
+
21
+ def router
22
+ GetConst.call(@router)
23
+ end
24
+
25
+ def transport(transport, opts = {})
26
+ @transport_class = Transport.define(transport, opts)
27
+ end
28
+
29
+ def transport_class
30
+ GetConst.call(@transport_class)
31
+ end
32
+
33
+ def call(request)
34
+ service_class = router.resolve_by_request(request)
35
+
36
+ raise(ArgumentError, "called to non-responding service") unless service_class.response?
37
+
38
+ service_name = service_class.service_name
39
+ request_class = service_class.request_class
40
+ request_buf = request_class.encode(request)
41
+ request_payload = Payload::Request.encode(service_name, request_buf)
42
+
43
+ response_payload = transport_class.call(request_payload)
44
+ response_buf, errors = Payload::Response.decode(response_payload)
45
+ service_instance = service_class.new(request)
46
+
47
+ if response_buf
48
+ response = service_class.response_class.decode(response_buf)
49
+ service_instance.resolve(response)
50
+ elsif errors
51
+ service_instance.reject(errors)
52
+ end
53
+
54
+ service_instance
55
+ end
56
+
57
+ def call!(request)
58
+ service_instance = call(request)
59
+ if service_instance.failure?
60
+ raise(CallError, service_instance.errors)
61
+ end
62
+
63
+ service_instance.response
64
+ end
65
+
66
+ def push(request)
67
+ service_class = router.resolve_by_request(request)
68
+
69
+ raise(ArgumentError, "pushed to responding service") if service_class.response?
70
+
71
+ service_name = service_class.service_name
72
+ request_class = service_class.request_class
73
+ request_buf = request_class.encode(request)
74
+ request_payload = Payload::Request.encode(service_name, request_buf)
75
+
76
+ transport_class.push(request_payload)
77
+
78
+ nil
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,16 @@
1
+ module Protein
2
+ class CallError < StandardError
3
+ def initialize(errors)
4
+ super(
5
+ errors.map do |error|
6
+ error.reason.inspect + (error.pointer ? " (at #{error.pointer})" : "")
7
+ end.join(", ")
8
+ )
9
+ end
10
+ end
11
+ class DefinitionError < StandardError; end
12
+ class PointerError < StandardError; end
13
+ class ProcessingError < StandardError; end
14
+ class RoutingError < StandardError; end
15
+ class TransportError < StandardError; end
16
+ end
@@ -0,0 +1,19 @@
1
+ module Protein
2
+ class GetConst
3
+ class << self
4
+ def call(input)
5
+ if input.is_a?(String)
6
+ input.constantize
7
+ elsif input != nil
8
+ input
9
+ else
10
+ raise DefinitionError, "unset required option"
11
+ end
12
+ end
13
+
14
+ def map(array)
15
+ array.map { |input| call(input) }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,94 @@
1
+ require "net/http"
2
+ require "json"
3
+
4
+ module Protein
5
+ class HTTPAdapter
6
+ HTTPS_SCHEME = "https".freeze
7
+
8
+ class Middleware
9
+ def initialize(router, secret)
10
+ @router = router
11
+ @secret = secret
12
+ end
13
+
14
+ def call(env)
15
+ check_secret(env)
16
+
17
+ request_payload = env["rack.input"].read
18
+ response_payload = Processor.call(@router, request_payload)
19
+
20
+ ["200", {}, [response_payload]]
21
+ end
22
+
23
+ private
24
+
25
+ def check_secret(env)
26
+ return if @secret == env["HTTP_X_RPC_SECRET"]
27
+
28
+ raise(TransportError, "invalid secret")
29
+ end
30
+ end
31
+
32
+ class << self
33
+ def from_hash(hash)
34
+ if (new_url = hash[:url])
35
+ url(new_url)
36
+ end
37
+
38
+ if (new_path = hash[:path])
39
+ path(new_path)
40
+ end
41
+
42
+ if (new_secret = hash[:secret])
43
+ secret(new_secret)
44
+ end
45
+ end
46
+
47
+ def url(url = nil)
48
+ if url
49
+ @url = url
50
+ @path = URI(url).path
51
+ end
52
+
53
+ @url || raise(DefinitionError, "url is not defined")
54
+ end
55
+
56
+ def path(path = nil)
57
+ @path = path if path
58
+ @path || raise(DefinitionError, "path is not defined")
59
+ end
60
+
61
+ def secret(secret = nil)
62
+ @secret = secret if secret
63
+ @secret || raise(DefinitionError, "secret is not defined")
64
+ end
65
+
66
+ def call(request_payload)
67
+ make_http_request(request_payload, build_headers())
68
+ end
69
+
70
+ private
71
+
72
+ def build_headers
73
+ { "X-RPC-Secret" => secret }
74
+ end
75
+
76
+ def make_http_request(body, headers)
77
+ uri = URI.parse(url)
78
+ http = Net::HTTP.new(uri.host, uri.port)
79
+ http.use_ssl = uri.scheme == HTTPS_SCHEME
80
+ http_request = Net::HTTP::Post.new(uri.path, headers)
81
+ http_request.body = body
82
+ http_response = http.request(http_request)
83
+
84
+ unless http_response.is_a?(Net::HTTPSuccess)
85
+ raise(TransportError, "HTTP request failed with code #{http_response.code}")
86
+ end
87
+
88
+ http_response.body
89
+ rescue SystemCallError => e
90
+ raise(TransportError, e.to_s)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,71 @@
1
+ require "net/http"
2
+ require "json"
3
+
4
+ module Protein
5
+ module Payload
6
+ class Request
7
+ class << self
8
+ def encode(service_name, request_buf)
9
+ JSON.dump({
10
+ "service_name" => service_name,
11
+ "request_buf_b64" => Base64.strict_encode64(request_buf)
12
+ })
13
+ end
14
+
15
+ def decode(payload)
16
+ hash = JSON.parse(payload)
17
+ service_name = hash["service_name"]
18
+ request_buf_b64 = hash["request_buf_b64"]
19
+ request_buf = Base64.strict_decode64(request_buf_b64)
20
+
21
+ [service_name, request_buf]
22
+ end
23
+ end
24
+ end
25
+
26
+ class Response
27
+ class << self
28
+ def encode(response_buf, errors)
29
+ JSON.dump({
30
+ "response_buf_b64" => response_buf && Base64.strict_encode64(response_buf),
31
+ "errors" => encode_errors(errors)
32
+ })
33
+ end
34
+
35
+ def decode(payload)
36
+ hash = JSON.parse(payload)
37
+ response_buf_b64 = hash["response_buf_b64"]
38
+ response_buf = response_buf_b64 && Base64.strict_decode64(response_buf_b64)
39
+ errors = hash["errors"]
40
+ errors = errors && decode_errors(errors)
41
+
42
+ [response_buf, errors]
43
+ end
44
+
45
+ private
46
+
47
+ def encode_errors(errors)
48
+ errors && errors.map do |error|
49
+ {
50
+ "reason" => error.reason.is_a?(Symbol) ? ":#{error.reason}" : error.reason,
51
+ "pointer" => error.pointer && error.pointer.dump
52
+ }
53
+ end
54
+ end
55
+
56
+ def decode_errors(errors)
57
+ errors && errors.map do |error|
58
+ reason_string = error["reason"]
59
+ reason = reason_string =~ /^\:/ ? reason_string[1..-1].to_sym : reason_string
60
+ pointer = error["pointer"]
61
+
62
+ ServiceError.new(
63
+ reason: reason,
64
+ pointer: pointer && Pointer.new(nil, "request").load(pointer)
65
+ )
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,93 @@
1
+ module Protein
2
+ class Pointer
3
+ def initialize(base, base_name = "context", input = nil)
4
+ @base = base
5
+ @base_name = base_name
6
+
7
+ if input.is_a?(String)
8
+ parse(input)
9
+ end
10
+ end
11
+
12
+ def dump
13
+ @access_path || raise(ProcessingError, "pointer can't be empty")
14
+ end
15
+
16
+ def load(input)
17
+ @access_path = input.map do |type, key|
18
+ [type.to_sym, key]
19
+ end
20
+
21
+ traverse_access_path(traverse_values: false)
22
+
23
+ self
24
+ end
25
+
26
+ def parse(input)
27
+ unless input =~ Regexp.new("^#{@base_name}")
28
+ raise PointerError, "access path should start with `#{@base_name}`"
29
+ end
30
+
31
+ access_path = input.scan(/(\.(\w+))|(\[(\d+)\])|(\[['"](\w+)['"]\])/).map do |match|
32
+ if (key = match[1])
33
+ [:struct, key]
34
+ elsif (key = match[3])
35
+ [:repeated, key.to_i]
36
+ elsif (key = match[5])
37
+ [:map, key]
38
+ end
39
+ end
40
+
41
+ if access_path.empty?
42
+ raise PointerError, "access path should not be empty"
43
+ end
44
+
45
+ @access_path = access_path
46
+ traverse_access_path
47
+ end
48
+
49
+ def to_s
50
+ @access_strings.join
51
+ end
52
+
53
+ def inspect_path
54
+ @access_values.each_with_index do |access_value, index|
55
+ if index > 0
56
+ accessor_type, accessor_key = @access_path[index - 1]
57
+ puts "Accessing #{accessor_type} with key #{accessor_key.inspect}"
58
+ end
59
+ puts "At #{access_value.inspect}"
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def traverse_access_path(traverse_values: true)
66
+ if traverse_values
67
+ access_value = @base
68
+ access_values = [@base]
69
+ end
70
+ access_strings = [@base_name]
71
+
72
+ begin
73
+ @access_path.each do |type, key|
74
+ case type
75
+ when :struct
76
+ access_strings << ".#{key}"
77
+ access_value = access_value.send(key) if traverse_values
78
+ when :repeated, :map
79
+ access_strings << "[#{key.inspect}]"
80
+ access_value = access_value[key] if traverse_values
81
+ end
82
+
83
+ access_values << access_value if traverse_values
84
+ end
85
+ rescue StandardError
86
+ raise PointerError, "unable to access #{access_strings.join}"
87
+ end
88
+
89
+ @access_values = access_values if traverse_values
90
+ @access_strings = access_strings
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,67 @@
1
+ module Protein
2
+ class Processor
3
+ class << self
4
+ def call(router, request_payload)
5
+ service_name, request_buf = Payload::Request.decode(request_payload)
6
+ service_class = router.resolve_by_name(service_name)
7
+
8
+ if service_class.response?
9
+ process_and_log_call(service_name, service_class, request_buf)
10
+ else
11
+ process_and_log_push(service_name, service_class, request_buf)
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def process_and_log_call(service_name, service_class, request_buf)
18
+ Rails.logger.info "Processing RPC call: #{service_name}"
19
+
20
+ start_time = Time.now
21
+ response_buf, errors = process_call(service_class, request_buf)
22
+ duration_ms = ((Time.now - start_time) * 1000).round
23
+
24
+ Rails.logger.info "#{response_buf ? 'Resolved' : 'Rejected'} in #{duration_ms}ms"
25
+
26
+ Payload::Response.encode(response_buf, errors) if service_class.response?
27
+ end
28
+
29
+ def process_call(service_class, request_buf)
30
+ request_class = service_class.request_class
31
+ request = request_class.decode(request_buf)
32
+ service_instance = service_class.new(request)
33
+
34
+ service_instance.process
35
+
36
+ if service_instance.success?
37
+ response_class = service_class.response_class
38
+ response_buf = response_class.encode(service_instance.response)
39
+
40
+ [response_buf, nil]
41
+ else
42
+ [nil, service_instance.errors]
43
+ end
44
+ end
45
+
46
+ def process_and_log_push(service_name, service_class, request_buf)
47
+ Rails.logger.info "Processing RPC push: #{service_name}"
48
+
49
+ start_time = Time.now
50
+ process_push(service_class, request_buf)
51
+ duration_ms = ((Time.now - start_time) * 1000).round
52
+
53
+ Rails.logger.info "Processed in #{duration_ms}ms"
54
+
55
+ nil
56
+ end
57
+
58
+ def process_push(service_class, request_buf)
59
+ request_class = service_class.request_class
60
+ request = request_class.decode(request_buf)
61
+ service_instance = service_class.new(request)
62
+
63
+ service_instance.process
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,42 @@
1
+ module Protein
2
+ class ProtoCompiler
3
+ class << self
4
+ def call(proto_directory: "./lib", namespace: nil)
5
+ proto_files = Dir.glob("#{proto_directory}/**/*.proto")
6
+
7
+ proto_files.each do |proto_file|
8
+ puts "Compiling #{proto_file}"
9
+
10
+ cmd_args = [
11
+ "protoc",
12
+ "-I", proto_directory,
13
+ "--ruby_out", proto_directory,
14
+ proto_file
15
+ ]
16
+
17
+ output = `#{cmd_args.shelljoin} 2>&1`
18
+
19
+ unless $?.success?
20
+ raise "Proto compilation failed:\n#{output}"
21
+ end
22
+ end
23
+
24
+ rewrite_namespace(proto_directory, namespace) if namespace
25
+ end
26
+
27
+ private
28
+
29
+ def rewrite_namespace(proto_directory, namespace)
30
+ proto_files = Dir.glob("#{proto_directory}/**/*_pb.rb")
31
+
32
+ proto_files.each do |proto_file|
33
+ puts "Namespacing #{proto_file} to #{namespace}"
34
+
35
+ old_content = File.read(proto_file)
36
+
37
+ File.write(proto_file, "module #{namespace}\n#{old_content}\nend\n")
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,55 @@
1
+ module Protein
2
+ class Router
3
+ class << self
4
+ def define(router)
5
+ if router.is_a?(Class) || router.is_a?(String)
6
+ router
7
+ elsif router.is_a?(Hash)
8
+ router_class = Class.new(Router)
9
+ router_class.from_hash(router)
10
+ router_class
11
+ else
12
+ raise(DefinitionError, "invalid router definition")
13
+ end
14
+ end
15
+
16
+ def from_hash(hash)
17
+ if hash[:services]
18
+ hash[:services].each do |each_service|
19
+ service(each_service)
20
+ end
21
+ end
22
+
23
+ if hash[:protos]
24
+ hash[:protos].each do |each_proto|
25
+ service_class = Class.new(Service)
26
+ service_class.proto(each_proto)
27
+
28
+ service(service_class)
29
+ end
30
+ end
31
+ end
32
+
33
+ def service(service_class)
34
+ @services ||= []
35
+ @services << service_class
36
+ end
37
+
38
+ def services
39
+ GetConst.map(@services || [])
40
+ end
41
+
42
+ def resolve_by_name(service_name)
43
+ services.find do |service|
44
+ service.service_name == service_name.to_s
45
+ end || raise(RoutingError, "service #{service_name.inspect} not found")
46
+ end
47
+
48
+ def resolve_by_request(request)
49
+ services.find do |service|
50
+ request.is_a?(service.request_class)
51
+ end || raise(RoutingError, "service for #{request.class} not found")
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,30 @@
1
+ module Protein
2
+ class Server
3
+ class << self
4
+ def route(router)
5
+ @router = Router.define(router)
6
+ end
7
+
8
+ def service(service)
9
+ @router ||= Class.new(Router)
10
+ @router.service(service)
11
+ end
12
+
13
+ def router
14
+ GetConst.call(@router)
15
+ end
16
+
17
+ def transport(transport, opts = {})
18
+ @transport_class = Transport.define(transport, opts)
19
+ end
20
+
21
+ def transport_class
22
+ GetConst.call(@transport_class)
23
+ end
24
+
25
+ def start
26
+ transport_class.serve(router)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,130 @@
1
+ module Protein
2
+ class Service
3
+ class << self
4
+ def service(service_name)
5
+ @service_name = service_name
6
+ end
7
+
8
+ def service_name
9
+ @service_name || raise(DefinitionError, "service name is not defined")
10
+ end
11
+
12
+ def proto(proto_module = nil)
13
+ @proto_module = proto_module
14
+ @service_name ||= proto_module.to_s.split("::").last.underscore
15
+ @request_class ||= "#{proto_module}::Request"
16
+ @response_class ||= "#{proto_module}::Response"
17
+ end
18
+
19
+ def proto_module
20
+ GetConst.call(@proto_module)
21
+ end
22
+
23
+ def request(request_class)
24
+ @request_class = request_class
25
+ end
26
+
27
+ def request_class
28
+ GetConst.call(@request_class)
29
+ end
30
+
31
+ def response?
32
+ response_class != false &&
33
+ (!response_class.is_a?(String) || !response_class.safe_constantize.nil?)
34
+ rescue NameError
35
+ false
36
+ end
37
+
38
+ def response(response_class)
39
+ @response_class = response_class
40
+ end
41
+
42
+ def response_class
43
+ GetConst.call(@response_class)
44
+ end
45
+ end
46
+
47
+ attr_reader :request, :response, :errors
48
+ delegate :response?, :response_class, to: :class
49
+
50
+ def initialize(request)
51
+ @request = request
52
+ end
53
+
54
+ def process
55
+ @success = nil
56
+ @response = response_class.new if response?
57
+ @errors = []
58
+
59
+ call
60
+
61
+ raise(ProcessingError, "resolve/reject must be called") if response? && @success.nil?
62
+ end
63
+
64
+ def resolve(response = nil)
65
+ raise(ProcessingError, "can't resolve non-responding service") unless response?
66
+ raise(ProcessingError, "unable to resolve with previous errors") if @errors && @errors.any?
67
+
68
+ @success = true
69
+ @response =
70
+ if !response
71
+ @response
72
+ elsif response.is_a?(response_class)
73
+ response
74
+ else
75
+ response_class.new(response)
76
+ end
77
+ end
78
+
79
+ def reject(*args)
80
+ raise(ProcessingError, "can't reject non-responding service") unless response?
81
+ if args.any? && @errors && @errors.any?
82
+ raise(ProcessingError, "unable to reject with both rejection value and previous errors")
83
+ end
84
+
85
+ @success = false
86
+ @errors =
87
+ if args.empty? && @errors && @errors.any?
88
+ @errors
89
+ elsif args.empty?
90
+ [build_error(:error)]
91
+ elsif args.length == 1 && args[0].is_a?(Array) && args[0].any?
92
+ args[0].map { |error| build_error(error) }
93
+ else
94
+ [build_error(*args)]
95
+ end
96
+ end
97
+
98
+ def success?
99
+ raise(ProcessingError, "resolve/reject must be called first") if @success.nil?
100
+
101
+ @success
102
+ end
103
+
104
+ def failure?
105
+ !success?
106
+ end
107
+
108
+ def add_error(*args)
109
+ @errors << build_error(*args)
110
+ end
111
+
112
+ private
113
+
114
+ def build_error(*args)
115
+ if args[0].is_a?(String) || args[0].is_a?(Symbol)
116
+ reason = args[0]
117
+ pointer =
118
+ if args[1].is_a?(Hash) && (at = args[1][:at])
119
+ Pointer.new(request, "request", at)
120
+ end
121
+
122
+ ServiceError.new(reason: reason, pointer: pointer)
123
+ elsif args[0].is_a?(ServiceError)
124
+ args[0]
125
+ elsif args[0].is_a?(Hash)
126
+ ServiceError.new(reason: args[0][:reason], pointer: args[0][:pointer])
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,15 @@
1
+ module Protein
2
+ class ServiceError
3
+ attr_reader :reason
4
+ attr_accessor :pointer
5
+
6
+ def initialize(reason: nil, pointer: nil)
7
+ @reason = reason if reason
8
+ @pointer = pointer if pointer
9
+
10
+ unless @reason.is_a?(String) || @reason.is_a?(Symbol)
11
+ raise(ProcessingError, "error reason must be a string or symbol")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ module Protein
2
+ class Transport
3
+ class << self
4
+ def define(transport, opts = {})
5
+ if transport.is_a?(Class) || transport.is_a?(String)
6
+ transport_class
7
+ elsif transport.is_a?(Symbol)
8
+ transport_base_class =
9
+ case transport
10
+ when :http
11
+ Protein::HTTPAdapter
12
+ when :amqp
13
+ Protein::AMQPAdapter
14
+ else
15
+ raise(DefinitionError, "invalid transport: #{transport.inspect}")
16
+ end
17
+
18
+ transport_class = Class.new(transport_base_class)
19
+ transport_class.from_hash(opts)
20
+ transport_class
21
+ else
22
+ raise(DefinitionError, "invalid transport definition")
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ module Protein
2
+ VERSION = '0.1.0'
3
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: protein
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Karol Słuszniak
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-10-12 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: karol@shedul.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files:
18
+ - README.md
19
+ files:
20
+ - README.md
21
+ - lib/protein.rb
22
+ - lib/protein/amqp_adapter.rb
23
+ - lib/protein/client.rb
24
+ - lib/protein/errors.rb
25
+ - lib/protein/get_const.rb
26
+ - lib/protein/http_adapter.rb
27
+ - lib/protein/payload.rb
28
+ - lib/protein/pointer.rb
29
+ - lib/protein/processor.rb
30
+ - lib/protein/proto_compiler.rb
31
+ - lib/protein/router.rb
32
+ - lib/protein/server.rb
33
+ - lib/protein/service.rb
34
+ - lib/protein/service_error.rb
35
+ - lib/protein/transport.rb
36
+ - lib/protein/version.rb
37
+ homepage: http://github.com/surgeventures/protein-ruby
38
+ licenses:
39
+ - MIT
40
+ metadata: {}
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubyforge_project:
57
+ rubygems_version: 2.6.8
58
+ signing_key:
59
+ specification_version: 4
60
+ summary: Multi-platform remote procedure call (RPC) system based on Protocol Buffers
61
+ test_files: []