proto_service_mesh 0.18.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: fb343f9b59bc8db10b06a8466222d6a808477cc0753480a83384c940a9e9696b
4
+ data.tar.gz: cf701b106b97be15a829e464010985bd58c6252ccbd45ca6acff91a0c1621593
5
+ SHA512:
6
+ metadata.gz: a3360b9dea587ed75a43062f8f5664bdb5fe7d81cbe74763c113c59f566eb85cfde7dc515b69d22be1b8f65212958010f97244ce2707ae9c7a280d64d6a3650b
7
+ data.tar.gz: 93641536186e764ac6e48bc0f0051b1b1dfc04cf4620d68a3f546289a9e2e630451bbc53a57081ccd834f4a3baf47d7243e186bc98417af3203cec6798a2a334
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,59 @@
1
+ # proto-service-mesh-ruby
2
+
3
+ proto-service-mesh-ruby is the Ruby implementation of the
4
+ [Proto Service Mesh API](https://github.com/Paymentbox-com/proto-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 `ProtoServiceMesh` 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 `proto-service-mesh-gen` in the specification
21
+ repository and lives in a definitions project.
22
+
23
+ ## Install
24
+
25
+ ```ruby
26
+ # Gemfile
27
+ gem "proto_service_mesh"
28
+ ```
29
+
30
+ Requires Ruby 3.3 or newer. The gem depends on `service_mesh`,
31
+ `google-protobuf`, and `googleapis-common-protos-types`. The last of these
32
+ provides `Google::Rpc::Status`, `Google::Rpc::Code`, and the detail types in
33
+ `google/rpc/error_details.proto`.
34
+
35
+ No transport is a dependency. The application adds the transport gem it uses.
36
+
37
+ ## Usage
38
+
39
+ An application uses this library through the generated code that depends on it. The
40
+ [Proto Service Mesh API](https://github.com/Paymentbox-com/proto-service-mesh-api) describes how Ruby code is
41
+ generated from `.proto` files.
42
+
43
+ ### Reference Examples
44
+
45
+ The examples in these docs use the reference definitions in `spec/support/definitions/`. They declare
46
+ `shop.OrderService`, with a `ROUTE` method `Place` and a `TOPIC` method `Placed`, in deployment group `shop` over the
47
+ transport named `mem`, the in-memory transport the specs use. The generated module is `Shop`, and the generated
48
+ per-transport maps are in `ServiceMaps`. The generated code is in `spec/support/testproto/`, written by
49
+ `proto-service-mesh-gen` as a consumer would generate it, and described under [Generated Code](docs/generated-code.md).
50
+
51
+ ## Documentation
52
+
53
+ - [Setup](docs/setup.md): configuring the `TransportRouter`, registering services, and running an `RPCRuntime`
54
+ - [Handlers](docs/handlers.md): implementing an `RPCService`, endpoint and subscriber handlers, returning errors, reply metadata, and message metadata
55
+ - [Calling](docs/calling.md): calling generated clients, metadata and transport options, reply metadata, and the wire format
56
+ - [MeshError](docs/mesherror.md): constructing and reading `MeshError`, and the errors the library raises
57
+ - [Generated Code](docs/generated-code.md): the generated reference output for Ruby, and what generated code relies on in this library
58
+ - [Public API](docs/public-api.md): every public constant and method in `ProtoServiceMesh`
59
+ - [Development](docs/development.md): the specification protos, the recipes, and the tests
@@ -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: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/proto-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
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoServiceMesh
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 ProtoServiceMesh
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 ProtoServiceMesh
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,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoServiceMesh
4
+ # Collects every Endpoint and Subscriber the process serves. The process
5
+ # registry is ProtoServiceMesh.registry.
6
+ class Registry
7
+ def initialize
8
+ @endpoints = []
9
+ @subscribers = []
10
+ end
11
+
12
+ # Adds the endpoints and subscribers of +service+, an RPCService
13
+ # instance. +consumer_groups+ maps a target of the service to the
14
+ # consumer group of its endpoint or subscriber, replacing the one the
15
+ # generated code carries. "" removes it, so the runtime's deployment group
16
+ # applies, and ServiceMesh::CONSUMER_GROUP_NONE means no group. Raises
17
+ # ArgumentError, and registers nothing, when a target is not one of the
18
+ # service's. Every endpoint and subscriber is kept, so two registrations of
19
+ # one target hand the transport two of them.
20
+ def register(service, consumer_groups: {})
21
+ endpoints = service.endpoints
22
+ subscribers = service.subscribers
23
+ consumer_groups.each do |target, group|
24
+ found = false
25
+ apply = lambda do |served|
26
+ next served unless served.target.same_channel?(target)
27
+
28
+ found = true
29
+ with_group(served, group)
30
+ end
31
+ endpoints = endpoints.map(&apply)
32
+ subscribers = subscribers.map(&apply)
33
+ raise ArgumentError, "consumer_groups names target #{target.segments.inspect}, which is not an rpc method of #{service.class}" unless found
34
+ end
35
+ @endpoints.concat(endpoints)
36
+ @subscribers.concat(subscribers)
37
+ nil
38
+ end
39
+
40
+ # Every registered Endpoint.
41
+ attr_reader :endpoints
42
+
43
+ # Every registered Subscriber.
44
+ attr_reader :subscribers
45
+
46
+ private
47
+
48
+ def with_group(served, group)
49
+ metadata = served.metadata.to_h.except(ServiceMesh::CONSUMER_GROUP_KEY)
50
+ metadata[ServiceMesh::CONSUMER_GROUP_KEY] = group unless group.to_s.empty?
51
+ served.with(metadata: metadata)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoServiceMesh
4
+ # One rpc method as the generated code declares it. +owner+ is the class
5
+ # the declaration appeared in. +consumer_group+ is the method's
6
+ # consumer_group option, or nil when it has none.
7
+ Rpc = Data.define(:name, :target, :input, :output, :kind, :owner, :consumer_group) do
8
+ def initialize(name:, target:, input:, kind:, owner:, output: nil, consumer_group: nil)
9
+ raise ServiceMesh::KindMismatch, "rpc #{name}: declared #{kind}, target is #{target.kind}" unless kind == target.kind
10
+
11
+ super(name: name, target: target, input: input, output: output, kind: kind, owner: owner, consumer_group: consumer_group)
12
+ end
13
+
14
+ def route? = kind == :route
15
+ end
16
+
17
+ # Class-level declaration of rpcs, extended by RPCService and RPCClient.
18
+ module RpcDSL
19
+ # Declares one rpc. +name+ is the Ruby method name, +target+ the
20
+ # ServiceMesh::Target, +input+ and +output+ the message classes, +kind+
21
+ # :route or :topic, and +consumer_group+ the method's consumer_group
22
+ # option. +output+ is not used for a topic.
23
+ def rpc(name, target:, input:, kind:, output: nil, consumer_group: nil)
24
+ rpc = Rpc.new(name: name, target: target, input: input, output: output, kind: kind, owner: self, consumer_group: consumer_group)
25
+ own_rpcs[rpc.name] = rpc
26
+ define_rpc(rpc)
27
+ rpc
28
+ end
29
+
30
+ # Every declared rpc by name, including those of superclasses.
31
+ def rpcs
32
+ inherited = superclass.respond_to?(:rpcs) ? superclass.rpcs : {}
33
+ inherited.merge(own_rpcs)
34
+ end
35
+
36
+ private
37
+
38
+ def own_rpcs
39
+ @own_rpcs ||= {}
40
+ end
41
+
42
+ def define_rpc(rpc)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoServiceMesh
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
+ ProtoServiceMesh.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,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoServiceMesh
4
+ # The service process for one transport and one deployment group. new
5
+ # takes the transport's client from the process router and every endpoint
6
+ # and subscriber in the process registry whose target is on that transport,
7
+ # and builds the transport's Runtime from them with the +runtime+
8
+ # constructor.
9
+ class RPCRuntime
10
+ attr_reader :transport, :deployment_group, :t_runtime
11
+
12
+ # +runtime+ is ->(client, config, endpoints:, subscribers:) returning the
13
+ # transport's Runtime. It receives +config+ with deployment_group set.
14
+ # Endpoints and subscribers of other transports are left for those
15
+ # transports' runtimes. Raises ArgumentError when +runtime+ is not
16
+ # callable, UnknownTransport, and whatever +runtime+ raises.
17
+ def initialize(transport:, deployment_group:, runtime:, config: {})
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 = ProtoServiceMesh.registry
21
+ client = ProtoServiceMesh.transport_router.client(transport)
22
+ @transport = transport
23
+ @deployment_group = deployment_group
24
+ on_transport = ->(served) { served.target.metadata["transport"] == transport }
25
+ @t_runtime = runtime.call(
26
+ client,
27
+ config.to_h.merge(ServiceMesh::DEPLOYMENT_GROUP_KEY => deployment_group),
28
+ endpoints: registry.endpoints.select(&on_transport),
29
+ subscribers: registry.subscribers.select(&on_transport)
30
+ )
31
+ end
32
+
33
+ def start = @t_runtime.start
34
+
35
+ def stop(drain) = @t_runtime.stop(drain)
36
+
37
+ def running? = @t_runtime.running?
38
+
39
+ def client = @t_runtime.client
40
+ end
41
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoServiceMesh
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
+ # with the rpc's consumer_group in its metadata when it has one.
15
+ def endpoints
16
+ implemented.select(&:route?).map do |rpc|
17
+ ServiceMesh::Endpoint.new(target: rpc.target, handler: endpoint_handler(rpc), metadata: consumer_group_metadata(rpc))
18
+ end
19
+ end
20
+
21
+ # A ServiceMesh::Subscriber for each topic rpc this instance implements,
22
+ # with the rpc's consumer_group in its metadata when it has one.
23
+ def subscribers
24
+ implemented.reject(&:route?).map do |rpc|
25
+ ServiceMesh::Subscriber.new(target: rpc.target, handler: subscriber_handler(rpc), metadata: consumer_group_metadata(rpc))
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def consumer_group_metadata(rpc)
32
+ rpc.consumer_group.nil? ? {} : {ServiceMesh::CONSUMER_GROUP_KEY => rpc.consumer_group}
33
+ end
34
+
35
+ # Rpcs whose method is defined in the declaring class or below it.
36
+ def implemented
37
+ self.class.rpcs.values.select do |rpc|
38
+ self.class.method_defined?(rpc.name) && self.class.instance_method(rpc.name).owner <= rpc.owner
39
+ end
40
+ end
41
+
42
+ def endpoint_handler(rpc)
43
+ lambda do |message|
44
+ request = begin
45
+ decode_inbound(rpc, message)
46
+ rescue Google::Protobuf::ParseError => e
47
+ next status_reply(message, MeshError.new(:INTERNAL, e.message))
48
+ end
49
+
50
+ response = public_send(rpc.name, request)
51
+ unless response.is_a?(rpc.output)
52
+ raise TypeError, "#{rpc.name} returned #{response.class}, expected #{rpc.output}"
53
+ end
54
+
55
+ metadata = Wire.metadata(Wire.without_options(response.mesh_metadata).except(Wire::GRPC_STATUS_KEY))
56
+ ServiceMesh::Message.new(target: message.target, metadata: metadata, payload: response.to_proto)
57
+ rescue MeshError => e
58
+ status_reply(message, e)
59
+ rescue => e
60
+ status_reply(message, MeshError.new(:UNKNOWN, e.message))
61
+ end
62
+ end
63
+
64
+ def subscriber_handler(rpc)
65
+ lambda do |message|
66
+ public_send(rpc.name, decode_inbound(rpc, message))
67
+ nil
68
+ end
69
+ end
70
+
71
+ # The decoded payload carrying the message's metadata. A payload that
72
+ # does not decode raises Google::Protobuf::ParseError naming the input
73
+ # type.
74
+ def decode_inbound(rpc, message)
75
+ request = begin
76
+ rpc.input.decode(message.payload)
77
+ rescue Google::Protobuf::ParseError => e
78
+ raise Google::Protobuf::ParseError, "request does not decode as #{rpc.input.descriptor.name}: #{e.message}"
79
+ end
80
+ request.mesh_metadata = message.metadata
81
+ request
82
+ end
83
+
84
+ def status_reply(message, error)
85
+ ServiceMesh::Message.new(target: message.target, metadata: Wire.status_metadata(error, Wire.without_options(error.mesh_metadata)), payload: error.proto.to_proto)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoServiceMesh
4
+ # Holds one Client per transport name: the transport-specific Client the
5
+ # application built and owns. The process router is
6
+ # ProtoServiceMesh.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 ProtoServiceMesh
4
+ VERSION = "0.18.0"
5
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProtoServiceMesh
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 "proto_service_mesh/version"
6
+ require_relative "proto_service_mesh/errors"
7
+ require_relative "proto_service_mesh/wire"
8
+ require_relative "proto_service_mesh/metadata"
9
+ require_relative "proto_service_mesh/mesh_error"
10
+ require_relative "proto_service_mesh/transport_router"
11
+ require_relative "proto_service_mesh/registry"
12
+ require_relative "proto_service_mesh/rpc"
13
+ require_relative "proto_service_mesh/rpc_service"
14
+ require_relative "proto_service_mesh/rpc_client"
15
+ require_relative "proto_service_mesh/rpc_runtime"
16
+
17
+ # Ruby library for the Proto 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 ProtoServiceMesh
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, consumer_groups: {})
39
+ registry.register(service, consumer_groups: consumer_groups)
40
+ end
41
+ end
42
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: proto_service_mesh
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.18.0
5
+ platform: ruby
6
+ authors:
7
+ - Bryant Morrill
8
+ - Paymentbox
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2026-10-19 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
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '0.7'
52
+ type: :runtime
53
+ prerelease: false
54
+ version_requirements: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - "~>"
57
+ - !ruby/object:Gem::Version
58
+ version: '0.4'
59
+ - - "<"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.7'
62
+ description:
63
+ email:
64
+ - bryantreadmorrill@gmail.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - LICENSE
70
+ - README.md
71
+ - lib/mesh/options_pb.rb
72
+ - lib/proto_service_mesh.rb
73
+ - lib/proto_service_mesh/errors.rb
74
+ - lib/proto_service_mesh/mesh_error.rb
75
+ - lib/proto_service_mesh/metadata.rb
76
+ - lib/proto_service_mesh/registry.rb
77
+ - lib/proto_service_mesh/rpc.rb
78
+ - lib/proto_service_mesh/rpc_client.rb
79
+ - lib/proto_service_mesh/rpc_runtime.rb
80
+ - lib/proto_service_mesh/rpc_service.rb
81
+ - lib/proto_service_mesh/transport_router.rb
82
+ - lib/proto_service_mesh/version.rb
83
+ - lib/proto_service_mesh/wire.rb
84
+ homepage: https://github.com/Paymentbox-com/proto-service-mesh-ruby
85
+ licenses:
86
+ - MIT
87
+ metadata:
88
+ homepage_uri: https://github.com/Paymentbox-com/proto-service-mesh-ruby
89
+ source_code_uri: https://github.com/Paymentbox-com/proto-service-mesh-ruby
90
+ rubygems_mfa_required: 'true'
91
+ post_install_message:
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '3.3'
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubygems_version: 3.5.22
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: Ruby library for the Proto Service Mesh API
110
+ test_files: []