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 +7 -0
- data/LICENSE +21 -0
- data/README.md +59 -0
- data/lib/mesh/options_pb.rb +17 -0
- data/lib/proto_service_mesh/errors.rb +23 -0
- data/lib/proto_service_mesh/mesh_error.rb +119 -0
- data/lib/proto_service_mesh/metadata.rb +28 -0
- data/lib/proto_service_mesh/registry.rb +54 -0
- data/lib/proto_service_mesh/rpc.rb +45 -0
- data/lib/proto_service_mesh/rpc_client.rb +80 -0
- data/lib/proto_service_mesh/rpc_runtime.rb +41 -0
- data/lib/proto_service_mesh/rpc_service.rb +88 -0
- data/lib/proto_service_mesh/transport_router.rb +42 -0
- data/lib/proto_service_mesh/version.rb +5 -0
- data/lib/proto_service_mesh/wire.rb +48 -0
- data/lib/proto_service_mesh.rb +42 -0
- metadata +110 -0
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,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: []
|