loams 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,320 @@
1
+ # The descriptor set, and every binding derived from it.
2
+ #
3
+ # Design §44 §7.3 has `protoc-gen-loams-facade` render one facade
4
+ # per language from the `loams.options.v1.module` and
5
+ # `loams.options.v1.facade` annotations, so thirteen SDKs cannot
6
+ # drift. This SDK has no generated code: it reads the **committed**
7
+ # `FileDescriptorSet` in `gen/` and derives every binding, module
8
+ # name and message type from it, which is the same property by a
9
+ # different route (D652) — a proto that adds an annotation changes
10
+ # the surface in the next build with nobody editing this file.
11
+ #
12
+ # ## Where the annotations come from
13
+ #
14
+ # `buf build` writes the options as **extension fields** on the
15
+ # `ServiceOptions` and `MethodOptions` messages, and the descriptor
16
+ # set carries them as unknown fields. `Google::Protobuf` parses the
17
+ # options into messages that keep those bytes, so `#to_proto` on an
18
+ # options message returns them, and `Loams::Wire` reads field 50001
19
+ # (the `module` annotation) and 50002 (the `facade` annotation) out
20
+ # of the raw bytes. The annotations are read from the same bytes
21
+ # protoc wrote, through the same descriptors, with no generated code
22
+ # and no registry to keep in step.
23
+ #
24
+ # ## One load, read once
25
+ #
26
+ # The set is parsed once and the resulting descriptors are immutable,
27
+ # so the tables are constants rather than something rebuilt per call.
28
+ require "google/protobuf"
29
+ require "google/protobuf/descriptor_pb"
30
+ require "loams/wire"
31
+
32
+ module Loams
33
+ module Descriptors
34
+ # The resource the descriptor set is read from, copied into the
35
+ # gem by the build.
36
+ DESCRIPTOR_SET = File.expand_path("../../gen/loams-descriptor.binpb", __dir__)
37
+
38
+ # The proto revision this SDK was generated from (design §44 §10.3).
39
+ PROTO_REV = "v1"
40
+
41
+ # The field number the `module` extension occupies on `ServiceOptions`.
42
+ MODULE_EXTENSION = 50001
43
+
44
+ # The field number the `facade` extension occupies on `MethodOptions`.
45
+ FACADE_EXTENSION = 50002
46
+
47
+ # The parsed set, as a `Google::Protobuf::FileDescriptorSet`.
48
+ SET = Google::Protobuf::FileDescriptorSet.decode(File.read(DESCRIPTOR_SET))
49
+
50
+ # The pool every message is looked up in. Built in dependency
51
+ # order: `add_serialized_file` links a file against the files
52
+ # already in the pool, so a file whose imports are not yet added
53
+ # is retried on a later pass, and a pass that makes no progress
54
+ # is a missing import rather than an ordering accident.
55
+ POOL = begin
56
+ pool = Google::Protobuf::DescriptorPool.new
57
+ pending = SET.file.to_a
58
+ until pending.empty?
59
+ progressed = false
60
+ pending.reject! do |file|
61
+ ready = file.dependency.all? { |dep| pool.lookup(dep) rescue false }
62
+ if ready
63
+ pool.add_serialized_file(file.to_proto)
64
+ progressed = true
65
+ true
66
+ else
67
+ false
68
+ end
69
+ end
70
+ raise "the descriptor set has files whose imports are absent: #{pending.map(&:name).join(", ")}" unless progressed
71
+ end
72
+ pool
73
+ end
74
+
75
+ # Recursively collect every message in a file into MESSAGES,
76
+ # keyed by fully qualified name.
77
+ def self.collect_messages(messages, package, prefix)
78
+ messages.each do |msg|
79
+ full = prefix.empty? ? "#{package}.#{msg.name}" : "#{prefix}.#{msg.name}"
80
+ MESSAGES[full] = POOL.lookup(full)
81
+ collect_messages(msg.nested_type.to_a, package, full)
82
+ end
83
+ end
84
+
85
+ # Every message in the set, keyed by its fully qualified proto name.
86
+ MESSAGES = {}
87
+ SET.file.each do |file|
88
+ collect_messages(file.message_type, file.package, "")
89
+ end
90
+
91
+ # Every service in the set, in the order the files declare them.
92
+ SERVICES = SET.file.flat_map do |file|
93
+ file.service.to_a.map do |raw_svc|
94
+ fqdn = "#{file.package}.#{raw_svc.name}"
95
+ POOL.lookup(fqdn)
96
+ end
97
+ end
98
+
99
+ # Every service in the set, keyed by its fully qualified name.
100
+ SERVICES_BY_NAME = SERVICES.to_h do |svc|
101
+ [svc.name, svc]
102
+ end
103
+
104
+ # One RPC, as the runtime dispatches it: everything a module
105
+ # method hands the invoker and nothing the invoker has to guess.
106
+ #
107
+ # Every field is derived from the descriptor set.
108
+ CallBinding = Struct.new(
109
+ :rpc, :service, :method, :package_name, :streaming, :idempotency,
110
+ :retry, :request, :response, :module, :facade_name, :summary,
111
+ :unstable, :pagination,
112
+ keyword_init: true,
113
+ ) do
114
+ # Whether the call answers with one message or with a stream.
115
+ def server_streaming?
116
+ streaming == :server
117
+ end
118
+
119
+ # Whether the request's **schema** declares a string
120
+ # `idempotency_key`, which is what makes a mutation keyed (D610).
121
+ def takes_idempotency_key?
122
+ field = request.lookup("idempotency_key")
123
+ !field.nil? && field.type == :string
124
+ end
125
+
126
+ # The two fields a paged call pages on, as
127
+ # `FacadeOptions.pagination` names them (`"<items>:<next_page_token>"`),
128
+ # or nil.
129
+ def pagination_fields
130
+ spec = pagination
131
+ return nil if spec.nil? || spec.empty?
132
+ parts = spec.split(":")
133
+ return nil unless parts.size == 2
134
+ items = response.lookup(parts[0]) || response.find { |f| f.json_name == parts[0] }
135
+ token = response.lookup(parts[1]) || response.find { |f| f.json_name == parts[1] }
136
+ return nil if items.nil? || token.nil?
137
+ [items, token]
138
+ end
139
+ end
140
+
141
+ # One module and the calls it exposes, both derived from the
142
+ # descriptor set.
143
+ ModuleBinding = Struct.new(
144
+ :name, :summary, :package_name, :service, :unstable, :calls,
145
+ keyword_init: true,
146
+ )
147
+
148
+ # `camelCase` or `PascalCase` to `snake_case`, which is what the
149
+ # facade's method names are.
150
+ def self.snake(name)
151
+ out = +""
152
+ name.each_char.with_index do |c, i|
153
+ if c.match?(/[A-Z]/)
154
+ out << "_" if i.positive? && (out[-1] != "_")
155
+ out << c.downcase
156
+ else
157
+ out << c
158
+ end
159
+ end
160
+ out
161
+ end
162
+
163
+ # The `loams.options.v1.ModuleOptions` on a service, read out of
164
+ # the raw `ServiceOptions` bytes: `{name, summary, unstable}`.
165
+ def self.parse_module_options(raw)
166
+ payload = Wire.find_bytes(raw, MODULE_EXTENSION)
167
+ return {} if payload.nil?
168
+ {
169
+ name: Wire.find_bytes(payload, 1),
170
+ summary: Wire.find_bytes(payload, 2),
171
+ unstable: !Wire.find_varint(payload, 3).nil? && Wire.find_varint(payload, 3) != 0,
172
+ }
173
+ end
174
+
175
+ # The `loams.options.v1.FacadeOptions` on a method, read out of
176
+ # the raw `MethodOptions` bytes: `{name, module, retry_safe, pagination}`.
177
+ def self.parse_facade_options(raw)
178
+ payload = Wire.find_bytes(raw, FACADE_EXTENSION)
179
+ return {} if payload.nil?
180
+ {
181
+ module: Wire.find_bytes(payload, 1),
182
+ name: Wire.find_bytes(payload, 2),
183
+ retry_safe: !Wire.find_varint(payload, 3).nil? && Wire.find_varint(payload, 3) != 0,
184
+ pagination: Wire.find_bytes(payload, 4),
185
+ }
186
+ end
187
+
188
+ # Builds one binding from a service and a method, deciding every
189
+ # field from the descriptor rather than from a table.
190
+ def self.binding_for(svc, method)
191
+ fd = svc.file_descriptor
192
+ package = fd.to_proto.package
193
+ service_full = svc.name
194
+
195
+ svc_opts = svc.to_proto.options
196
+ module_opts = svc_opts ? parse_module_options(svc_opts.to_proto) : {}
197
+
198
+ method_opts = method.to_proto.options
199
+ facade_opts = method_opts ? parse_facade_options(method_opts.to_proto) : {}
200
+
201
+ idempotency = if method_opts
202
+ case method_opts.idempotency_level
203
+ when :IDEMPOTENT then :idempotent
204
+ when :NO_SIDE_EFFECTS then :no_side_effects
205
+ else :none
206
+ end
207
+ else
208
+ :none
209
+ end
210
+
211
+ retry_class = if facade_opts[:retry_safe]
212
+ :safe
213
+ elsif idempotency == :none
214
+ :manual
215
+ else
216
+ :safe
217
+ end
218
+
219
+ mod = facade_opts[:module]
220
+ mod = mod.nil? || mod.empty? ? module_opts[:name] : mod
221
+ facade_name = facade_opts[:name]
222
+ facade_name = facade_name.nil? || facade_name.empty? ? snake(method.name) : facade_name
223
+
224
+ request = method.input_type
225
+ response = method.output_type
226
+
227
+ CallBinding.new(
228
+ rpc: "#{service_full}/#{method.name}",
229
+ service: service_full,
230
+ method: method.name,
231
+ package_name: package,
232
+ streaming: method.server_streaming ? :server : :unary,
233
+ idempotency: idempotency,
234
+ retry: retry_class,
235
+ request: request,
236
+ response: response,
237
+ module: mod,
238
+ facade_name: facade_name,
239
+ summary: module_opts[:summary],
240
+ unstable: module_opts[:unstable],
241
+ pagination: facade_opts[:pagination],
242
+ )
243
+ end
244
+
245
+ # Every RPC in the set, keyed by `<package>.Service/Method`.
246
+ BINDINGS_BY_RPC = begin
247
+ bindings = {}
248
+ SERVICES.each do |svc|
249
+ full = svc.name
250
+ svc.each do |m|
251
+ bindings["#{full}/#{m.name}"] = binding_for(svc, m)
252
+ end
253
+ end
254
+ bindings
255
+ end
256
+
257
+ # The binding an RPC path names, or nil when no service in the set
258
+ # declares it.
259
+ def self.binding_for_rpc(rpc)
260
+ BINDINGS_BY_RPC[rpc]
261
+ end
262
+
263
+ # The message a fully qualified proto name names, or nil.
264
+ def self.message(full_name)
265
+ MESSAGES[full_name]
266
+ end
267
+
268
+ # The service a fully qualified name names, or nil.
269
+ def self.service(full_name)
270
+ SERVICES_BY_NAME[full_name]
271
+ end
272
+
273
+ # The binding whose request or response type is +full_name+, or nil.
274
+ def self.binding_for_message(full_name)
275
+ BINDINGS_BY_RPC.values.find do |b|
276
+ b.request.name == full_name || b.response.name == full_name
277
+ end
278
+ end
279
+
280
+ # Every proto package in the set, as `GetInstance` names them.
281
+ def self.proto_packages
282
+ SET.file.map(&:package).select { |p| p.start_with?("loams.") }.uniq.sort
283
+ end
284
+
285
+ # The module catalogue, derived from the descriptor set and nothing else.
286
+ def self.modules
287
+ by_name = {}
288
+ BINDINGS_BY_RPC.values.each do |binding|
289
+ mod = binding.module
290
+ next if mod.nil? || mod.empty?
291
+ existing = by_name[mod]
292
+ if existing.nil?
293
+ by_name[mod] = ModuleBinding.new(
294
+ name: mod,
295
+ summary: binding.summary || "",
296
+ package_name: binding.package_name,
297
+ service: binding.service,
298
+ unstable: binding.unstable,
299
+ calls: [binding],
300
+ )
301
+ else
302
+ existing.calls << binding
303
+ end
304
+ end
305
+ by_name.values.map { |m| m.calls.sort_by!(&:facade_name); m }.sort_by(&:name)
306
+ end
307
+
308
+ # The module a name identifies, or nil.
309
+ def self.find_module(name)
310
+ modules.find { |m| m.name == name }
311
+ end
312
+
313
+ # The binding a module and a call name identify, or nil.
314
+ def self.find_binding(module_name, call_name)
315
+ mod = find_module(module_name)
316
+ return nil if mod.nil?
317
+ mod.calls.find { |c| c.facade_name == call_name }
318
+ end
319
+ end
320
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Loams
4
+ module Envelopes
5
+ FLAG_DATA = 0x00
6
+ FLAG_END_STREAM = 0x02
7
+ FLAG_GRPC_TRAILERS = 0x80
8
+
9
+ Frame = Struct.new(:flags, :payload) do
10
+ def data?
11
+ flags == FLAG_DATA
12
+ end
13
+
14
+ def end_stream?
15
+ (flags & FLAG_END_STREAM) != 0
16
+ end
17
+
18
+ def trailers?
19
+ (flags & FLAG_GRPC_TRAILERS) != 0
20
+ end
21
+ end
22
+
23
+ # Wraps payload into 5-byte envelope frame (1 byte flags, 4 bytes big-endian length)
24
+ def self.wrap(payload, flags = FLAG_DATA)
25
+ payload = payload.b
26
+ [flags, payload.bytesize].pack("CN") + payload
27
+ end
28
+
29
+ # Splits a byte buffer containing one or more frames
30
+ def self.split(bytes)
31
+ return [] if bytes.nil? || bytes.empty?
32
+
33
+ bytes = bytes.b
34
+ frames = []
35
+ pos = 0
36
+ len = bytes.bytesize
37
+
38
+ while pos + 5 <= len
39
+ flags, payload_len = bytes.byteslice(pos, 5).unpack("CN")
40
+ pos += 5
41
+ if pos + payload_len > len
42
+ raise ArgumentError, "Frame length #{payload_len} exceeds remaining bytes #{len - pos}"
43
+ end
44
+
45
+ payload = bytes.byteslice(pos, payload_len)
46
+ pos += payload_len
47
+ frames << Frame.new(flags, payload)
48
+ end
49
+
50
+ frames
51
+ end
52
+ end
53
+ end