protein 0.1.0

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