grpc_service_mesh 0.16.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
+ SHA256:
3
+ metadata.gz: 241b9590dcd6bc041d6e575e74ece01d4fa165d4380c16a275b9823a05cadb94
4
+ data.tar.gz: a5d716495824f89fc9f3bf9eaf6ed1b163459fee48d5f8270d4a8905538ac5c5
5
+ SHA512:
6
+ metadata.gz: 821daff5b253055c2baa16b9ebc41e38f624752ac0b21d4d01d510680327daaf7a040c8c740aa03211258d2d60a443464a0620411e146abb765f21cb1f7bffb1
7
+ data.tar.gz: 01417c334a7980dc1da34d37266b3caeba22d01ec95d0db793abab0f025ee548c919e4754ccddfc7597a809571e144d7ca86a0d28ef13457964cabdbe4520bec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Paymentbox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # grpc-service-mesh-ruby
2
+
3
+ `grpc_service_mesh` is the Ruby implementation of the
4
+ [gRPC Service Mesh API](https://github.com/Paymentbox-com/grpc-service-mesh-api).
5
+ It carries protobuf messages over any transport that implements the
6
+ [Service Mesh API Specification](https://github.com/Paymentbox-com/service-mesh-api)
7
+ through the Ruby contract in the `service_mesh` gem, from
8
+ [service-mesh-ruby](https://github.com/Paymentbox-com/service-mesh-ruby).
9
+
10
+ The `GrpcServiceMesh` module holds the specification's non-generated types:
11
+ * `TransportRouter`
12
+ * `Registry`
13
+ * `RPCRuntime`
14
+ * `MeshError`
15
+
16
+ It also holds the two base classes that generated code builds on:
17
+ * `RPCService`
18
+ * `RPCClient`
19
+
20
+ Generated code comes from `grpc-service-mesh-gen` in the specification
21
+ repository and lives in a definitions project.
22
+
23
+ ## Install
24
+
25
+ The gem and its `service_mesh` dependency are installed from their
26
+ repositories at a tag, so the Gemfile names both git sources.
27
+
28
+ ```ruby
29
+ # Gemfile
30
+ gem "grpc_service_mesh", git: "https://github.com/Paymentbox-com/grpc-service-mesh-ruby", tag: "v0.15.0"
31
+ gem "service_mesh", git: "https://github.com/Paymentbox-com/service-mesh-ruby", tag: "v0.4.2"
32
+ ```
33
+
34
+ Requires Ruby 3.3 or newer. The gem depends on `service_mesh`,
35
+ `google-protobuf`, and `googleapis-common-protos-types`. The last of these
36
+ provides `Google::Rpc::Status`, `Google::Rpc::Code`, and the detail types in
37
+ `google/rpc/error_details.proto`.
38
+
39
+ No transport is a dependency. The application adds the transport gem it uses.
40
+
41
+ ## Usage
42
+
43
+ An application uses this library through the generated code that depends on it. The
44
+ [gRPC Service Mesh API](https://github.com/Paymentbox-com/grpc-service-mesh-api) describes how Ruby code is
45
+ generated from `.proto` files.
46
+
47
+ ### Reference Examples
48
+
49
+ The examples in these docs use the reference definitions in `spec/support/definitions/`. They declare
50
+ `shop.OrderService`, with a `ROUTE` method `Place` and a `TOPIC` method `Placed`, in deployment group `shop` over the
51
+ transport named `mem`, the in-memory transport the specs use. The generated module is `Shop`, and the generated
52
+ per-transport maps are in `ServiceMaps`. The generated code is in `spec/support/testproto/`, written by
53
+ `grpc-service-mesh-gen` as a consumer would generate it, and described under [Generated Code](docs/generated-code.md).
54
+
55
+ ## Documentation
56
+
57
+ - [Setup](docs/setup.md): configuring the `TransportRouter`, registering services, and running an `RPCRuntime`
58
+ - [Handlers](docs/handlers.md): implementing an `RPCService`, endpoint and subscriber handlers, returning errors, reply metadata, and message metadata
59
+ - [Calling](docs/calling.md): calling generated clients, metadata and transport options, reply metadata, and the wire format
60
+ - [MeshError](docs/mesherror.md): constructing and reading `MeshError`, and the errors the library raises
61
+ - [Generated Code](docs/generated-code.md): the generated reference output for Ruby, and what generated code relies on in this library
62
+ - [Public API](docs/public-api.md): every public constant and method in `GrpcServiceMesh`
63
+ - [Development](docs/development.md): the specification protos, the recipes, and the tests
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrpcServiceMesh
4
+ # Base of the errors this library raises for misuse of its own contract.
5
+ # MeshError is an application error and stands apart from this tree.
6
+ class Error < StandardError; end
7
+
8
+ # A Target's transport metadata names a transport the router does not hold.
9
+ class UnknownTransport < Error
10
+ def initialize(name) = super("unknown transport #{name.inspect}")
11
+ end
12
+
13
+ # TransportRouter#close could not close one or more clients. +failures+ maps
14
+ # each transport name to the exception its client's close raised.
15
+ class CloseFailed < Error
16
+ attr_reader :failures
17
+
18
+ def initialize(failures)
19
+ @failures = failures
20
+ super(failures.map { |name, e| "#{name}: #{e.message}" }.join("; "))
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "google/protobuf"
4
+ require "google/protobuf/well_known_types"
5
+ require "google/rpc/code_pb"
6
+ require "google/rpc/status_pb"
7
+ require "google/rpc/error_details_pb"
8
+
9
+ module GrpcServiceMesh
10
+ # The application error. A ROUTE handler raises it; the caller rescues it.
11
+ # It wraps a google.rpc.Status and carries the message metadata of the
12
+ # reply that reports it.
13
+ class MeshError < StandardError
14
+ include Metadata
15
+
16
+ attr_reader :proto
17
+
18
+ # MeshError.new(code, message, *details, mesh_metadata: {}). Returns the
19
+ # subclass for the code when one exists; a subclass called directly takes
20
+ # (message, *details, mesh_metadata: {}) and builds itself.
21
+ def self.new(*args, mesh_metadata: {})
22
+ if equal?(MeshError)
23
+ unless args.first.is_a?(Symbol) || args.first.is_a?(Integer)
24
+ raise ArgumentError, "MeshError.new takes a Google::Rpc::Code first, such as MeshError.new(:NOT_FOUND, \"no such order\"); " \
25
+ "UnknownError.new(message) raises an UNKNOWN error"
26
+ end
27
+ klass = BY_CODE[code_number(args.first)]
28
+ return klass.new(*args.drop(1), mesh_metadata: mesh_metadata) if klass
29
+ end
30
+ super
31
+ end
32
+
33
+ # What +raise Klass, "msg"+ calls; builds the error as new does.
34
+ def self.exception(*args, **kwargs)
35
+ new(*args, **kwargs)
36
+ end
37
+
38
+ # Wraps an existing Google::Rpc::Status.
39
+ def self.from_proto(status)
40
+ new(status.code, status.message, *status.details)
41
+ end
42
+
43
+ # +code+ is a Google::Rpc::Code name (:NOT_FOUND) or number (5).
44
+ # +details+ are protobuf messages, packed into Google::Protobuf::Any,
45
+ # or Any values already packed. +mesh_metadata+ is the metadata the
46
+ # error reply carries.
47
+ def initialize(code, message, *details, mesh_metadata: {})
48
+ text = message.to_s
49
+ @proto = Google::Rpc::Status.new(
50
+ code: self.class.code_number(code),
51
+ message: text,
52
+ details: details.map { |d| pack(d) }
53
+ )
54
+ self.mesh_metadata = mesh_metadata
55
+ super(text)
56
+ end
57
+
58
+ # The Google::Rpc::Code name, or the number when it has no name.
59
+ def code
60
+ Google::Rpc::Code.lookup(@proto.code) || @proto.code
61
+ end
62
+
63
+ # The wrapped Status's details, as Google::Protobuf::Any values.
64
+ def details
65
+ @proto.details.to_a
66
+ end
67
+
68
+ private def pack(detail)
69
+ return detail if detail.is_a?(Google::Protobuf::Any)
70
+ unless detail.class.respond_to?(:descriptor)
71
+ raise TypeError, "a detail must be a protobuf message, got #{detail.class}"
72
+ end
73
+
74
+ Google::Protobuf::Any.pack(detail)
75
+ end
76
+
77
+ def self.code_number(code)
78
+ case code
79
+ when Integer then code
80
+ when Symbol
81
+ Google::Rpc::Code.resolve(code) or raise ArgumentError, "unknown Google::Rpc::Code #{code.inspect}"
82
+ else
83
+ raise ArgumentError, "code must be a Google::Rpc::Code name or number, got #{code.inspect}"
84
+ end
85
+ end
86
+ end
87
+
88
+ # One subclass per Google::Rpc::Code other than OK, each fixing its code.
89
+ # MeshError.new and from_proto return the subclass for the code.
90
+ {
91
+ CancelledError: :CANCELLED,
92
+ UnknownError: :UNKNOWN,
93
+ InvalidArgumentError: :INVALID_ARGUMENT,
94
+ DeadlineExceededError: :DEADLINE_EXCEEDED,
95
+ NotFoundError: :NOT_FOUND,
96
+ AlreadyExistsError: :ALREADY_EXISTS,
97
+ PermissionDeniedError: :PERMISSION_DENIED,
98
+ UnauthenticatedError: :UNAUTHENTICATED,
99
+ ResourceExhaustedError: :RESOURCE_EXHAUSTED,
100
+ FailedPreconditionError: :FAILED_PRECONDITION,
101
+ AbortedError: :ABORTED,
102
+ OutOfRangeError: :OUT_OF_RANGE,
103
+ UnimplementedError: :UNIMPLEMENTED,
104
+ InternalError: :INTERNAL,
105
+ UnavailableError: :UNAVAILABLE,
106
+ DataLossError: :DATA_LOSS
107
+ }.each do |name, code|
108
+ klass = Class.new(MeshError) do
109
+ const_set(:CODE, code)
110
+
111
+ def self.new(message, *details, mesh_metadata: {})
112
+ super(self::CODE, message, *details, mesh_metadata: mesh_metadata)
113
+ end
114
+ end
115
+ const_set(name, klass)
116
+ end
117
+
118
+ MeshError::BY_CODE = MeshError.subclasses.to_h { |k| [MeshError.code_number(k::CODE), k] }.freeze
119
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrpcServiceMesh
4
+ # Message metadata carried on a message object. Generated code includes it
5
+ # into every message class an rpc takes or returns; MeshError includes it
6
+ # too. The library sets it on the objects it builds and reads it from the
7
+ # objects the application gives it, and never writes to an object the
8
+ # application built.
9
+ module Metadata
10
+ EMPTY = {}.freeze
11
+ private_constant :EMPTY
12
+
13
+ # The metadata Hash of this object. On first read it is a new empty Hash
14
+ # stored on the object, so keys can be written into it directly. A frozen
15
+ # object with none set has a frozen empty Hash.
16
+ def mesh_metadata
17
+ return @mesh_metadata if instance_variable_defined?(:@mesh_metadata) && @mesh_metadata
18
+ return EMPTY if frozen?
19
+
20
+ @mesh_metadata = {}
21
+ end
22
+
23
+ # Sets the metadata. Takes a Hash, or nil for none.
24
+ def mesh_metadata=(metadata)
25
+ @mesh_metadata = Hash(metadata)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrpcServiceMesh
4
+ # Collects every Endpoint and Subscriber the process serves. The process
5
+ # registry is GrpcServiceMesh.registry.
6
+ class Registry
7
+ def initialize
8
+ @endpoints = []
9
+ @subscribers = []
10
+ end
11
+
12
+ # Adds the bindings of +service+, an RPCService instance. Every binding
13
+ # is kept, so two registrations of one Target hand the transport two
14
+ # bindings for it.
15
+ def register(service)
16
+ @endpoints.concat(service.endpoints)
17
+ @subscribers.concat(service.subscribers)
18
+ nil
19
+ end
20
+
21
+ # Endpoints whose Target carries +deployment_group+.
22
+ def endpoints(deployment_group)
23
+ @endpoints.select { |e| in_group?(e, deployment_group) }
24
+ end
25
+
26
+ # Subscribers whose Target carries +deployment_group+.
27
+ def subscribers(deployment_group)
28
+ @subscribers.select { |s| in_group?(s, deployment_group) }
29
+ end
30
+
31
+ private
32
+
33
+ def in_group?(binding, deployment_group)
34
+ binding.target.metadata[ServiceMesh::DEPLOYMENT_GROUP_KEY] == deployment_group
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrpcServiceMesh
4
+ # One rpc method as the generated code declares it. +owner+ is the class
5
+ # the declaration appeared in.
6
+ Rpc = Data.define(:name, :target, :input, :output, :kind, :owner) do
7
+ def initialize(name:, target:, input:, kind:, owner:, output: nil)
8
+ raise ServiceMesh::KindMismatch, "rpc #{name}: declared #{kind}, target is #{target.kind}" unless kind == target.kind
9
+
10
+ super(name: name, target: target, input: input, output: output, kind: kind, owner: owner)
11
+ end
12
+
13
+ def route? = kind == :route
14
+ end
15
+
16
+ # Class-level declaration of rpcs, extended by RPCService and RPCClient.
17
+ module RpcDSL
18
+ # Declares one rpc. +name+ is the Ruby method name, +target+ the
19
+ # ServiceMesh::Target, +input+ and +output+ the message classes, +kind+
20
+ # :route or :topic. +output+ is not used for a topic.
21
+ def rpc(name, target:, input:, kind:, output: nil)
22
+ rpc = Rpc.new(name: name, target: target, input: input, output: output, kind: kind, owner: self)
23
+ own_rpcs[rpc.name] = rpc
24
+ define_rpc(rpc)
25
+ rpc
26
+ end
27
+
28
+ # Every declared rpc by name, including those of superclasses.
29
+ def rpcs
30
+ inherited = superclass.respond_to?(:rpcs) ? superclass.rpcs : {}
31
+ inherited.merge(own_rpcs)
32
+ end
33
+
34
+ private
35
+
36
+ def own_rpcs
37
+ @own_rpcs ||= {}
38
+ end
39
+
40
+ def define_rpc(rpc)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrpcServiceMesh
4
+ # Base of every generated client class. Each declared rpc becomes a class
5
+ # method, +name(request)+, that resolves the transport's Client through the
6
+ # process router on every call. The request's mesh_metadata is read and left
7
+ # unchanged: its keys that start with OPTION_PREFIX are the transport
8
+ # options, with the prefix removed, and the other keys are the message
9
+ # metadata.
10
+ class RPCClient
11
+ extend RpcDSL
12
+
13
+ class << self
14
+ private
15
+
16
+ def define_rpc(rpc)
17
+ if rpc.route?
18
+ define_singleton_method(rpc.name) do |request|
19
+ RPCClient.request(rpc, request)
20
+ end
21
+ else
22
+ define_singleton_method(rpc.name) do |request|
23
+ RPCClient.publish(rpc, request)
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ # Sends +request+ to a route and returns the decoded response, whose
30
+ # mesh_metadata is the reply's metadata without keys that start with
31
+ # OPTION_PREFIX. Raises MeshError for a reply carrying Grpc-Status, or
32
+ # INTERNAL when either payload does not decode; either error's
33
+ # mesh_metadata is the same reply metadata.
34
+ def self.request(rpc, request)
35
+ message, options = outbound(rpc, request)
36
+ reply = client_for(rpc.target).request(message, options)
37
+ reply_metadata = Wire.without_options(reply.metadata)
38
+ begin
39
+ if reply_metadata.key?(Wire::GRPC_STATUS_KEY)
40
+ raise MeshError.from_proto(decode(Google::Rpc::Status, reply.payload))
41
+ end
42
+
43
+ response = decode(rpc.output, reply.payload)
44
+ rescue MeshError => e
45
+ e.mesh_metadata = reply_metadata
46
+ raise
47
+ end
48
+ response.mesh_metadata = reply_metadata
49
+ response
50
+ end
51
+
52
+ # Publishes +request+ to a topic. Returns nil.
53
+ def self.publish(rpc, request)
54
+ message, options = outbound(rpc, request)
55
+ client_for(rpc.target).publish(message, options)
56
+ nil
57
+ end
58
+
59
+ def self.client_for(target)
60
+ GrpcServiceMesh.transport_router.client(target.metadata["transport"])
61
+ end
62
+
63
+ # The message for +request+ and the transport options, split from the
64
+ # request's mesh_metadata.
65
+ def self.outbound(rpc, request)
66
+ raise TypeError, "#{rpc.name} takes a #{rpc.input}, got #{request.class}" unless request.is_a?(rpc.input)
67
+
68
+ metadata, options = Wire.split_options(request.mesh_metadata)
69
+ [ServiceMesh::Message.new(target: rpc.target, metadata: Wire.metadata(metadata), payload: request.to_proto), options]
70
+ end
71
+
72
+ def self.decode(klass, payload)
73
+ klass.decode(payload)
74
+ rescue Google::Protobuf::ParseError => e
75
+ raise MeshError.new(:INTERNAL, "reply does not decode as #{klass.descriptor.name}: #{e.message}")
76
+ end
77
+
78
+ private_class_method :client_for, :outbound, :decode
79
+ end
80
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrpcServiceMesh
4
+ # The service process for one transport and one deployment group. new
5
+ # takes the transport's client from the process router and the matching
6
+ # bindings from the process registry, and builds the transport's Runtime
7
+ # from them with the +runtime+ constructor.
8
+ class RPCRuntime
9
+ attr_reader :transport, :deployment_group, :t_runtime
10
+
11
+ # +runtime+ is ->(client, config, endpoints:, subscribers:) returning the
12
+ # transport's Runtime. It receives +config+ with deployment_group set.
13
+ # +endpoints+ and +subscribers+, when given, replace the registry's list
14
+ # of that kind, and [] serves none. Raises ArgumentError for a given
15
+ # binding whose Target's deployment_group or transport is not this
16
+ # runtime's, UnknownTransport, and whatever +runtime+ raises.
17
+ def initialize(transport:, deployment_group:, runtime:, config: {}, endpoints: nil, subscribers: nil)
18
+ raise ArgumentError, "runtime: must be a callable that builds the transport's Runtime, got #{runtime.inspect}" unless runtime.respond_to?(:call)
19
+
20
+ registry = GrpcServiceMesh.registry
21
+ client = GrpcServiceMesh.transport_router.client(transport)
22
+ @transport = transport
23
+ @deployment_group = deployment_group
24
+ endpoints&.each { |binding| check_target("endpoint", binding.target) }
25
+ subscribers&.each { |binding| check_target("subscriber", binding.target) }
26
+ @t_runtime = runtime.call(
27
+ client,
28
+ config.to_h.merge(ServiceMesh::DEPLOYMENT_GROUP_KEY => deployment_group),
29
+ endpoints: endpoints || registry.endpoints(deployment_group),
30
+ subscribers: subscribers || registry.subscribers(deployment_group)
31
+ )
32
+ end
33
+
34
+ def start = @t_runtime.start
35
+
36
+ def stop(drain) = @t_runtime.stop(drain)
37
+
38
+ def running? = @t_runtime.running?
39
+
40
+ def client = @t_runtime.client
41
+
42
+ private
43
+
44
+ def check_target(kind, target)
45
+ {ServiceMesh::DEPLOYMENT_GROUP_KEY => @deployment_group, "transport" => @transport}.each do |key, want|
46
+ got = target.metadata[key]
47
+ next if got == want
48
+
49
+ raise ArgumentError, "#{kind} #{target.segments.inspect} has #{key} #{got.inspect}, want #{want.inspect}"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrpcServiceMesh
4
+ # Base of every generated service class. The generated subclass declares its
5
+ # rpcs; the application subclasses that and defines a method per rpc it
6
+ # serves, taking the decoded request, whose mesh_metadata is the inbound
7
+ # message metadata. A route method returns the response; the response's
8
+ # mesh_metadata, without keys that start with OPTION_PREFIX, is the reply's
9
+ # metadata. The same holds for a raised MeshError.
10
+ class RPCService
11
+ extend RpcDSL
12
+
13
+ # A ServiceMesh::Endpoint for each route rpc this instance implements.
14
+ def endpoints
15
+ implemented.select(&:route?).map { |rpc| ServiceMesh::Endpoint.new(target: rpc.target, handler: endpoint_handler(rpc)) }
16
+ end
17
+
18
+ # A ServiceMesh::Subscriber for each topic rpc this instance implements.
19
+ def subscribers
20
+ implemented.reject(&:route?).map { |rpc| ServiceMesh::Subscriber.new(target: rpc.target, handler: subscriber_handler(rpc)) }
21
+ end
22
+
23
+ private
24
+
25
+ # Rpcs whose method is defined in the declaring class or below it.
26
+ def implemented
27
+ self.class.rpcs.values.select do |rpc|
28
+ self.class.method_defined?(rpc.name) && self.class.instance_method(rpc.name).owner <= rpc.owner
29
+ end
30
+ end
31
+
32
+ def endpoint_handler(rpc)
33
+ lambda do |message|
34
+ request = begin
35
+ decode_inbound(rpc, message)
36
+ rescue Google::Protobuf::ParseError => e
37
+ next status_reply(message, MeshError.new(:INTERNAL, e.message))
38
+ end
39
+
40
+ response = public_send(rpc.name, request)
41
+ unless response.is_a?(rpc.output)
42
+ raise TypeError, "#{rpc.name} returned #{response.class}, expected #{rpc.output}"
43
+ end
44
+
45
+ metadata = Wire.metadata(Wire.without_options(response.mesh_metadata).except(Wire::GRPC_STATUS_KEY))
46
+ ServiceMesh::Message.new(target: message.target, metadata: metadata, payload: response.to_proto)
47
+ rescue MeshError => e
48
+ status_reply(message, e)
49
+ rescue => e
50
+ status_reply(message, MeshError.new(:UNKNOWN, e.message))
51
+ end
52
+ end
53
+
54
+ def subscriber_handler(rpc)
55
+ lambda do |message|
56
+ public_send(rpc.name, decode_inbound(rpc, message))
57
+ nil
58
+ end
59
+ end
60
+
61
+ # The decoded payload carrying the message's metadata. A payload that
62
+ # does not decode raises Google::Protobuf::ParseError naming the input
63
+ # type.
64
+ def decode_inbound(rpc, message)
65
+ request = begin
66
+ rpc.input.decode(message.payload)
67
+ rescue Google::Protobuf::ParseError => e
68
+ raise Google::Protobuf::ParseError, "request does not decode as #{rpc.input.descriptor.name}: #{e.message}"
69
+ end
70
+ request.mesh_metadata = message.metadata
71
+ request
72
+ end
73
+
74
+ def status_reply(message, error)
75
+ ServiceMesh::Message.new(target: message.target, metadata: Wire.status_metadata(error, Wire.without_options(error.mesh_metadata)), payload: error.proto.to_proto)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrpcServiceMesh
4
+ # Holds one Client per transport name: the transport-specific Client the
5
+ # application built and owns. The process router is
6
+ # GrpcServiceMesh.transport_router. Clients are added at boot; client is a
7
+ # hash read.
8
+ class TransportRouter
9
+ def initialize
10
+ @clients = {}
11
+ end
12
+
13
+ # Adds a client. Adding under a name already present replaces the client.
14
+ def add(name, client)
15
+ @clients[name] = client
16
+ nil
17
+ end
18
+
19
+ def names
20
+ @clients.keys
21
+ end
22
+
23
+ # The Client for +name+. Raises UnknownTransport.
24
+ def client(name)
25
+ @clients.fetch(name) { raise UnknownTransport.new(name) }
26
+ end
27
+
28
+ # Closes every client, even when some fail to close. Returns nil, or
29
+ # raises CloseFailed carrying each client's exception by transport name.
30
+ def close
31
+ failures = {}
32
+ @clients.each do |name, client|
33
+ client.close
34
+ rescue => e
35
+ failures[name] = e
36
+ end
37
+ raise CloseFailed.new(failures) unless failures.empty?
38
+
39
+ nil
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrpcServiceMesh
4
+ VERSION = "0.16.0"
5
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrpcServiceMesh
4
+ # The start of an outgoing metadata key that is a transport option. A client
5
+ # method passes each such key, with the prefix removed, to the transport's
6
+ # request or publish options, and sends the other keys as message metadata.
7
+ # Reply metadata never carries such keys. The match is exact and
8
+ # case-sensitive.
9
+ OPTION_PREFIX = "Mesh-Option-"
10
+
11
+ # Metadata keys and values the specification fixes on every message.
12
+ module Wire
13
+ CONTENT_TYPE_KEY = "Content-Type"
14
+ CONTENT_TYPE = "application/x-protobuf"
15
+ GRPC_STATUS_KEY = "Grpc-Status"
16
+
17
+ # Metadata for a message carrying a payload of the method's type.
18
+ def self.metadata(extra = {})
19
+ extra.to_h.merge(CONTENT_TYPE_KEY => CONTENT_TYPE)
20
+ end
21
+
22
+ # Splits +metadata+ into the message metadata and the transport options:
23
+ # the keys that start with OPTION_PREFIX become options with the prefix
24
+ # removed. Returns [metadata, options], both new Hashes.
25
+ def self.split_options(metadata)
26
+ options = {}
27
+ rest = {}
28
+ metadata.to_h.each do |key, value|
29
+ if key.to_s.start_with?(OPTION_PREFIX)
30
+ options[key.to_s.delete_prefix(OPTION_PREFIX)] = value
31
+ else
32
+ rest[key] = value
33
+ end
34
+ end
35
+ [rest, options]
36
+ end
37
+
38
+ # A copy of +metadata+ without the keys that start with OPTION_PREFIX.
39
+ def self.without_options(metadata)
40
+ split_options(metadata).first
41
+ end
42
+
43
+ # Metadata for a reply whose payload is an encoded google.rpc.Status.
44
+ def self.status_metadata(error, extra = {})
45
+ extra.to_h.merge(CONTENT_TYPE_KEY => CONTENT_TYPE, GRPC_STATUS_KEY => error.proto.code.to_s)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "service_mesh"
4
+
5
+ require_relative "grpc_service_mesh/version"
6
+ require_relative "grpc_service_mesh/errors"
7
+ require_relative "grpc_service_mesh/wire"
8
+ require_relative "grpc_service_mesh/metadata"
9
+ require_relative "grpc_service_mesh/mesh_error"
10
+ require_relative "grpc_service_mesh/transport_router"
11
+ require_relative "grpc_service_mesh/registry"
12
+ require_relative "grpc_service_mesh/rpc"
13
+ require_relative "grpc_service_mesh/rpc_service"
14
+ require_relative "grpc_service_mesh/rpc_client"
15
+ require_relative "grpc_service_mesh/rpc_runtime"
16
+
17
+ # Ruby library for the gRPC Service Mesh API. Generated code declares rpcs on
18
+ # RPCService and RPCClient subclasses; the application configures the process
19
+ # TransportRouter, registers its services, and starts an RPCRuntime.
20
+ module GrpcServiceMesh
21
+ class << self
22
+ # The process TransportRouter.
23
+ def transport_router
24
+ @transport_router ||= TransportRouter.new
25
+ end
26
+
27
+ # The process Registry.
28
+ def registry
29
+ @registry ||= Registry.new
30
+ end
31
+
32
+ # Shortcut for transport_router.add.
33
+ def add_transport(name, client)
34
+ transport_router.add(name, client)
35
+ end
36
+
37
+ # Shortcut for registry.register.
38
+ def register(service)
39
+ registry.register(service)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: mesh/options.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+ require 'google/protobuf/descriptor_pb'
8
+
9
+
10
+ descriptor_data = "\n\x12mesh/options.proto\x12\x04mesh\x1a google/protobuf/descriptor.proto*\x1c\n\x04Kind\x12\t\n\x05ROUTE\x10\x00\x12\t\n\x05TOPIC\x10\x01::\n\x04kind\x12\x1e.google.protobuf.MethodOptions\x18\xd1\x86\x03 \x01(\x0e\x32\n.mesh.Kind:8\n\x0e\x63onsumer_group\x12\x1e.google.protobuf.MethodOptions\x18\xd2\x86\x03 \x01(\t:8\n\x10\x64\x65ployment_group\x12\x1c.google.protobuf.FileOptions\x18\xd3\x86\x03 \x01(\t:1\n\ttransport\x12\x1c.google.protobuf.FileOptions\x18\xd4\x86\x03 \x01(\t:3\n\x0broot_prefix\x12\x1c.google.protobuf.FileOptions\x18\xd5\x86\x03 \x01(\tB<Z:github.com/Paymentbox-com/grpc-service-mesh-go/meshoptionsb\x06proto3"
11
+
12
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
13
+ pool.add_serialized_file(descriptor_data)
14
+
15
+ module Mesh
16
+ Kind = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("mesh.Kind").enummodule
17
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: grpc_service_mesh
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.16.0
5
+ platform: ruby
6
+ authors:
7
+ - Bryant Morrill
8
+ - Paymentbox
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2026-09-29 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: google-protobuf
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '4.26'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '4.26'
28
+ - !ruby/object:Gem::Dependency
29
+ name: googleapis-common-protos-types
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '1.15'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '1.15'
42
+ - !ruby/object:Gem::Dependency
43
+ name: service_mesh
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '0.4'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '0.4'
56
+ description:
57
+ email:
58
+ - bryantreadmorrill@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - LICENSE
64
+ - README.md
65
+ - lib/grpc_service_mesh.rb
66
+ - lib/grpc_service_mesh/errors.rb
67
+ - lib/grpc_service_mesh/mesh_error.rb
68
+ - lib/grpc_service_mesh/metadata.rb
69
+ - lib/grpc_service_mesh/registry.rb
70
+ - lib/grpc_service_mesh/rpc.rb
71
+ - lib/grpc_service_mesh/rpc_client.rb
72
+ - lib/grpc_service_mesh/rpc_runtime.rb
73
+ - lib/grpc_service_mesh/rpc_service.rb
74
+ - lib/grpc_service_mesh/transport_router.rb
75
+ - lib/grpc_service_mesh/version.rb
76
+ - lib/grpc_service_mesh/wire.rb
77
+ - lib/mesh/options_pb.rb
78
+ homepage: https://github.com/Paymentbox-com/grpc-service-mesh-ruby
79
+ licenses:
80
+ - MIT
81
+ metadata:
82
+ homepage_uri: https://github.com/Paymentbox-com/grpc-service-mesh-ruby
83
+ source_code_uri: https://github.com/Paymentbox-com/grpc-service-mesh-ruby
84
+ rubygems_mfa_required: 'true'
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '3.3'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubygems_version: 3.5.22
101
+ signing_key:
102
+ specification_version: 4
103
+ summary: Ruby library for the gRPC Service Mesh API
104
+ test_files: []