grpc-server-reflection 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,153 @@
1
+ module GrpcServerReflection
2
+ class DescriptorRegistry
3
+ class ObjectSpaceIndexer
4
+ include TypeMapping
5
+
6
+ def initialize(allowed_services:)
7
+ @allowed_services = allowed_services
8
+ @proto_builder = ProtoBuilder.new
9
+ end
10
+
11
+ def build_index(files_by_symbol:, serialized_files:, dependencies:, service_names:)
12
+ file_messages = {} # filename => { file_descriptor:, descriptors: [] }
13
+ file_enums = {} # filename => [Google::Protobuf::EnumDescriptor, ...]
14
+ file_services = {} # filename => [{ service_name:, klass: }, ...]
15
+
16
+ scan_object_space(file_messages, file_enums, file_services, service_names)
17
+
18
+ symbol_to_filename = build_symbol_index(file_messages, file_enums, file_services)
19
+
20
+ build_serialized_files(
21
+ file_messages, file_enums, file_services, symbol_to_filename,
22
+ files_by_symbol: files_by_symbol, serialized_files: serialized_files, dependencies: dependencies
23
+ )
24
+ end
25
+
26
+ private
27
+
28
+ def scan_object_space(file_messages, file_enums, file_services, service_names)
29
+ ObjectSpace.each_object(Class).each do |klass|
30
+ scan_message_class(klass, file_messages)
31
+ scan_service_class(klass, file_services, service_names)
32
+ end
33
+
34
+ ObjectSpace.each_object(Google::Protobuf::EnumDescriptor).each do |enum_desc|
35
+ fd = enum_desc.file_descriptor
36
+ next unless fd
37
+
38
+ filename = fd.name
39
+ file_enums[filename] ||= []
40
+ file_enums[filename] << enum_desc
41
+
42
+ file_messages[filename] ||= { file_descriptor: fd, descriptors: [] }
43
+ end
44
+ end
45
+
46
+ def scan_message_class(klass, file_messages)
47
+ return unless safe_respond_to?(klass, :descriptor)
48
+
49
+ desc = safe_call(klass, :descriptor)
50
+ return unless desc.is_a?(Google::Protobuf::Descriptor)
51
+
52
+ fd = desc.file_descriptor
53
+ return unless fd
54
+
55
+ filename = fd.name
56
+ file_messages[filename] ||= { file_descriptor: fd, descriptors: [] }
57
+ file_messages[filename][:descriptors] << desc
58
+ end
59
+
60
+ def scan_service_class(klass, file_services, service_names)
61
+ return unless safe_respond_to?(klass, :service_name)
62
+
63
+ begin
64
+ return unless klass.included_modules.include?(GRPC::GenericService)
65
+ rescue StandardError
66
+ return
67
+ end
68
+
69
+ service_name = klass.service_name
70
+ return if service_name.nil? || service_name.empty?
71
+ return if service_names.include?(service_name)
72
+ return if @allowed_services && !@allowed_services.include?(service_name)
73
+
74
+ service_names << service_name
75
+
76
+ filename = find_service_filename(klass)
77
+ if filename
78
+ file_services[filename] ||= []
79
+ file_services[filename] << { service_name: service_name, klass: klass }
80
+ end
81
+ end
82
+
83
+ def find_service_filename(klass)
84
+ return nil unless klass.respond_to?(:rpc_descs)
85
+
86
+ klass.rpc_descs.each_value do |rpc_desc|
87
+ input_type = rpc_desc.input
88
+ input_type = input_type.type if input_type.is_a?(GRPC::RpcDesc::Stream)
89
+ if safe_respond_to?(input_type, :descriptor)
90
+ input_desc = safe_call(input_type, :descriptor)
91
+ if input_desc && input_desc.respond_to?(:file_descriptor) && input_desc.file_descriptor
92
+ return input_desc.file_descriptor.name
93
+ end
94
+ end
95
+ end
96
+ nil
97
+ end
98
+
99
+ def build_symbol_index(file_messages, file_enums, file_services)
100
+ symbol_to_filename = {}
101
+
102
+ file_messages.each do |filename, data|
103
+ data[:descriptors].each { |desc| symbol_to_filename[desc.name] = filename }
104
+ end
105
+ file_enums.each do |filename, enums|
106
+ enums.each { |desc| symbol_to_filename[desc.name] = filename }
107
+ end
108
+ file_services.each do |filename, entries|
109
+ entries.each do |entry|
110
+ symbol_to_filename[entry[:service_name]] = filename
111
+ if entry[:klass].respond_to?(:rpc_descs)
112
+ entry[:klass].rpc_descs.each_key do |method_name|
113
+ symbol_to_filename["#{entry[:service_name]}.#{method_name}"] = filename
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ symbol_to_filename
120
+ end
121
+
122
+ def build_serialized_files(file_messages, file_enums, file_services, symbol_to_filename, files_by_symbol:, serialized_files:, dependencies:)
123
+ all_filenames = (file_messages.keys + file_services.keys + file_enums.keys).uniq
124
+
125
+ all_filenames.each do |filename|
126
+ msgs = file_messages[filename]
127
+ fd_obj = msgs ? msgs[:file_descriptor] : nil
128
+ msg_descriptors = msgs ? msgs[:descriptors] : []
129
+ enum_descriptors = file_enums[filename] || []
130
+ svc_entries = file_services[filename] || []
131
+
132
+ file_deps = []
133
+ serialized = @proto_builder.build_file_descriptor_proto(
134
+ filename, fd_obj, msg_descriptors, enum_descriptors, svc_entries, symbol_to_filename, file_deps
135
+ )
136
+ serialized_files[filename] = serialized
137
+ dependencies[filename] = file_deps
138
+
139
+ msg_descriptors.each { |desc| files_by_symbol[desc.name] = filename }
140
+ enum_descriptors.each { |desc| files_by_symbol[desc.name] = filename }
141
+ svc_entries.each do |entry|
142
+ files_by_symbol[entry[:service_name]] = filename
143
+ if entry[:klass].respond_to?(:rpc_descs)
144
+ entry[:klass].rpc_descs.each_key do |method_name|
145
+ files_by_symbol["#{entry[:service_name]}.#{method_name}"] = filename
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,259 @@
1
+ module GrpcServerReflection
2
+ class DescriptorRegistry
3
+ class ProtoBuilder
4
+ include TypeMapping
5
+
6
+ def initialize
7
+ @dependency_resolver = DependencyResolver.new
8
+ end
9
+
10
+ def build_file_descriptor_proto(filename, fd_obj, msg_descriptors, enum_descriptors, svc_entries, symbol_to_filename, out_deps)
11
+ package = extract_package(msg_descriptors, enum_descriptors, svc_entries)
12
+ dependencies = []
13
+
14
+ local_symbols = {}
15
+ msg_descriptors.each { |desc| local_symbols[desc.name] = true }
16
+ enum_descriptors.each { |desc| local_symbols[desc.name] = true }
17
+
18
+ file_proto = Google::Protobuf::FileDescriptorProto.new(
19
+ name: filename,
20
+ package: package,
21
+ syntax: fd_obj ? fd_obj.syntax.to_s : 'proto3'
22
+ )
23
+
24
+ # Deduplicate and separate top-level from nested
25
+ seen_names = {}
26
+ top_level = []
27
+ nested = []
28
+
29
+ msg_descriptors.each do |desc|
30
+ short_name = remove_package(desc.name, package)
31
+ next if seen_names[short_name]
32
+ seen_names[short_name] = true
33
+
34
+ if short_name.include?('.')
35
+ nested << desc
36
+ else
37
+ top_level << desc
38
+ end
39
+ end
40
+
41
+ # Build top-level messages
42
+ top_level_protos = {}
43
+ top_level.each do |desc|
44
+ short_name = remove_package(desc.name, package)
45
+ msg_proto = build_message_descriptor_proto(desc, package)
46
+ top_level_protos[short_name] = msg_proto
47
+ file_proto.message_type << msg_proto
48
+ end
49
+
50
+ # Nest child messages inside their parents
51
+ nested.sort_by { |desc| remove_package(desc.name, package).count('.') }.each do |desc|
52
+ short_name = remove_package(desc.name, package)
53
+ parts = short_name.split('.')
54
+ child_name = parts.last
55
+ parent_name = parts[0..-2].join('.')
56
+
57
+ child_proto = build_message_descriptor_proto(desc, package)
58
+ child_proto.name = child_name
59
+
60
+ parent = top_level_protos[parent_name]
61
+ if parent
62
+ parent.nested_type << child_proto
63
+ top_level_protos[short_name] = child_proto
64
+ else
65
+ file_proto.message_type << child_proto
66
+ end
67
+ end
68
+
69
+ # Add enums (top-level and nested)
70
+ seen_enums = {}
71
+ enum_descriptors.each do |enum_desc|
72
+ short_name = remove_package(enum_desc.name, package)
73
+ next if seen_enums[short_name]
74
+ seen_enums[short_name] = true
75
+
76
+ enum_proto = build_enum_descriptor_proto(enum_desc, package)
77
+
78
+ if short_name.include?('.')
79
+ parts = short_name.split('.')
80
+ parent_name = parts[0..-2].join('.')
81
+ enum_proto.name = parts.last
82
+ if top_level_protos[parent_name]
83
+ top_level_protos[parent_name].enum_type << enum_proto
84
+ else
85
+ file_proto.enum_type << enum_proto
86
+ end
87
+ else
88
+ file_proto.enum_type << enum_proto
89
+ end
90
+ end
91
+
92
+ # Add services
93
+ svc_entries.each do |entry|
94
+ file_proto.service << build_service_descriptor_proto(entry, package)
95
+ end
96
+
97
+ # Collect cross-file dependencies
98
+ @dependency_resolver.collect_file_dependencies(file_proto, local_symbols, dependencies, symbol_to_filename)
99
+ dependencies.uniq!
100
+ dependencies.each { |dep| file_proto.dependency << dep }
101
+ out_deps.concat(dependencies)
102
+
103
+ Google::Protobuf::FileDescriptorProto.encode(file_proto)
104
+ end
105
+
106
+ private
107
+
108
+ def build_message_descriptor_proto(desc, package)
109
+ short_name = remove_package(desc.name, package)
110
+
111
+ msg_proto = Google::Protobuf::DescriptorProto.new(name: short_name)
112
+ pool = Google::Protobuf::DescriptorPool.generated_pool
113
+
114
+ desc.each do |field|
115
+ field_proto = Google::Protobuf::FieldDescriptorProto.new(
116
+ name: field.name,
117
+ number: field.number,
118
+ type: proto_field_type(field.type),
119
+ label: proto_field_label(field.label),
120
+ json_name: field.json_name
121
+ )
122
+
123
+ if field.type == :message || field.type == :enum
124
+ if field.submsg_name && field.submsg_name.include?('_MapEntry_')
125
+ map_entry = build_map_entry(field, pool)
126
+ if map_entry
127
+ msg_proto.nested_type << map_entry
128
+ field_proto.type_name = ".#{desc.name}.#{map_entry.name}"
129
+ else
130
+ field_proto.type_name = ".#{field.submsg_name}"
131
+ end
132
+ elsif field.submsg_name
133
+ field_proto.type_name = ".#{field.submsg_name}"
134
+ end
135
+ end
136
+
137
+ msg_proto.field << field_proto
138
+ end
139
+
140
+ # Add oneofs
141
+ oneof_index = 0
142
+ desc.each_oneof do |oneof|
143
+ msg_proto.oneof_decl << Google::Protobuf::OneofDescriptorProto.new(name: oneof.name)
144
+
145
+ desc.each do |field|
146
+ if belongs_to_oneof?(field, oneof)
147
+ msg_proto.field.each do |fp|
148
+ if fp.name == field.name
149
+ fp.oneof_index = oneof_index
150
+ break
151
+ end
152
+ end
153
+ end
154
+ end
155
+ oneof_index += 1
156
+ end
157
+
158
+ msg_proto
159
+ end
160
+
161
+ def build_enum_descriptor_proto(enum_desc, package)
162
+ short_name = remove_package(enum_desc.name, package)
163
+
164
+ enum_proto = Google::Protobuf::EnumDescriptorProto.new(name: short_name)
165
+
166
+ enum_desc.each do |name, number|
167
+ enum_proto.value << Google::Protobuf::EnumValueDescriptorProto.new(
168
+ name: name.to_s,
169
+ number: number
170
+ )
171
+ end
172
+
173
+ enum_proto
174
+ end
175
+
176
+ def build_service_descriptor_proto(entry, package)
177
+ short_name = remove_package(entry[:service_name], package)
178
+ klass = entry[:klass]
179
+
180
+ svc_proto = Google::Protobuf::ServiceDescriptorProto.new(name: short_name)
181
+
182
+ if klass.respond_to?(:rpc_descs)
183
+ klass.rpc_descs.each do |method_name, rpc_desc|
184
+ input_type = rpc_desc.input
185
+ output_type = rpc_desc.output
186
+ client_streaming = input_type.is_a?(GRPC::RpcDesc::Stream)
187
+ server_streaming = output_type.is_a?(GRPC::RpcDesc::Stream)
188
+ input_type = input_type.type if client_streaming
189
+ output_type = output_type.type if server_streaming
190
+
191
+ input_name = descriptor_full_name(input_type)
192
+ output_name = descriptor_full_name(output_type)
193
+
194
+ method_proto = Google::Protobuf::MethodDescriptorProto.new(
195
+ name: method_name.to_s,
196
+ input_type: ".#{input_name}",
197
+ output_type: ".#{output_name}",
198
+ client_streaming: client_streaming,
199
+ server_streaming: server_streaming
200
+ )
201
+ svc_proto['method'] << method_proto
202
+ end
203
+ end
204
+
205
+ svc_proto
206
+ end
207
+
208
+ def build_map_entry(field, pool)
209
+ entry_desc = pool.lookup(field.submsg_name)
210
+ return nil unless entry_desc
211
+
212
+ raw_suffix = field.submsg_name.split('_MapEntry_').last
213
+ entry_name = raw_suffix.split('_').map(&:capitalize).join + 'Entry'
214
+
215
+ entry_proto = Google::Protobuf::DescriptorProto.new(
216
+ name: entry_name,
217
+ options: Google::Protobuf::MessageOptions.new(map_entry: true)
218
+ )
219
+
220
+ entry_desc.each do |entry_field|
221
+ fp = Google::Protobuf::FieldDescriptorProto.new(
222
+ name: entry_field.name,
223
+ number: entry_field.number,
224
+ type: proto_field_type(entry_field.type),
225
+ label: proto_field_label(entry_field.label)
226
+ )
227
+ if (entry_field.type == :message || entry_field.type == :enum) && entry_field.submsg_name
228
+ fp.type_name = ".#{entry_field.submsg_name}"
229
+ end
230
+ entry_proto.field << fp
231
+ end
232
+
233
+ entry_proto
234
+ end
235
+
236
+ def extract_package(msg_descriptors, enum_descriptors, svc_entries)
237
+ all_names = []
238
+ svc_entries.each { |e| all_names << e[:service_name] }
239
+ msg_descriptors.each { |d| all_names << d.name }
240
+ enum_descriptors.each { |d| all_names << d.name }
241
+
242
+ return '' if all_names.empty?
243
+
244
+ shortest = all_names.min_by { |n| n.split('.').length }
245
+ parts = shortest.split('.')
246
+ parts.length > 1 ? parts[0..-2].join('.') : ''
247
+ end
248
+
249
+ def belongs_to_oneof?(field, oneof)
250
+ oneof.each do |oneof_field|
251
+ return true if oneof_field.name == field.name
252
+ end
253
+ false
254
+ rescue
255
+ false
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,63 @@
1
+ module GrpcServerReflection
2
+ class DescriptorRegistry
3
+ module TypeMapping
4
+ FIELD_TYPE_MAP = {
5
+ double: :TYPE_DOUBLE, float: :TYPE_FLOAT,
6
+ int64: :TYPE_INT64, uint64: :TYPE_UINT64, int32: :TYPE_INT32,
7
+ fixed64: :TYPE_FIXED64, fixed32: :TYPE_FIXED32,
8
+ bool: :TYPE_BOOL, string: :TYPE_STRING, bytes: :TYPE_BYTES,
9
+ uint32: :TYPE_UINT32, enum: :TYPE_ENUM,
10
+ sfixed32: :TYPE_SFIXED32, sfixed64: :TYPE_SFIXED64,
11
+ sint32: :TYPE_SINT32, sint64: :TYPE_SINT64,
12
+ message: :TYPE_MESSAGE,
13
+ }.freeze
14
+
15
+ LABEL_MAP = {
16
+ optional: :LABEL_OPTIONAL,
17
+ required: :LABEL_REQUIRED,
18
+ repeated: :LABEL_REPEATED,
19
+ }.freeze
20
+
21
+ def proto_field_type(type)
22
+ mapped = FIELD_TYPE_MAP[type]
23
+ unless mapped
24
+ warn "[grpc-server-reflection] Unknown protobuf field type: #{type.inspect}, defaulting to TYPE_STRING"
25
+ return :TYPE_STRING
26
+ end
27
+ mapped
28
+ end
29
+
30
+ def proto_field_label(label)
31
+ LABEL_MAP[label] || :LABEL_OPTIONAL
32
+ end
33
+
34
+ def descriptor_full_name(type)
35
+ if safe_respond_to?(type, :descriptor) && (desc = safe_call(type, :descriptor))
36
+ desc.name
37
+ else
38
+ type.name.gsub('::', '.')
39
+ end
40
+ end
41
+
42
+ def safe_respond_to?(obj, method)
43
+ obj.respond_to?(method)
44
+ rescue StandardError, NotImplementedError
45
+ false
46
+ end
47
+
48
+ def safe_call(obj, method)
49
+ obj.send(method)
50
+ rescue StandardError, NotImplementedError
51
+ nil
52
+ end
53
+
54
+ def remove_package(full_name, package)
55
+ if package && !package.empty? && full_name.start_with?("#{package}.")
56
+ full_name.sub("#{package}.", '')
57
+ else
58
+ full_name
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,92 @@
1
+ require 'set'
2
+ require_relative 'descriptor_registry/type_mapping'
3
+ require_relative 'descriptor_registry/dependency_resolver'
4
+ require_relative 'descriptor_registry/proto_builder'
5
+ require_relative 'descriptor_registry/file_descriptor_indexer'
6
+ require_relative 'descriptor_registry/object_space_indexer'
7
+
8
+ module GrpcServerReflection
9
+ class DescriptorRegistry
10
+ attr_reader :service_names
11
+
12
+ def initialize(services: nil, allowed_service_names: nil)
13
+ @files_by_symbol = {} # symbol => filename
14
+ @serialized_files = {} # filename => serialized bytes
15
+ @dependencies = {} # filename => [dep_filename, ...]
16
+ @service_names = Set.new
17
+ @extensions_by_type = {}
18
+ @allowed_services = if allowed_service_names
19
+ allowed_service_names
20
+ elsif services
21
+ services.map { |s| s.service_name }.compact
22
+ end
23
+
24
+ build_index
25
+ end
26
+
27
+ def list_services
28
+ @service_names.to_a
29
+ end
30
+
31
+ def find_file_by_name(filename)
32
+ @serialized_files[filename]
33
+ end
34
+
35
+ def find_filename_by_symbol(symbol)
36
+ @files_by_symbol[symbol]
37
+ end
38
+
39
+ def find_file_by_symbol(symbol)
40
+ filename = @files_by_symbol[symbol]
41
+ return nil unless filename
42
+ @serialized_files[filename]
43
+ end
44
+
45
+ def file_descriptors_with_dependencies(filename)
46
+ return [] unless @serialized_files.key?(filename)
47
+
48
+ visited = Set.new
49
+ result = []
50
+ collect_dependencies(filename, visited, result)
51
+ result
52
+ end
53
+
54
+ def find_extension_numbers(type)
55
+ @extensions_by_type.fetch(type, [])
56
+ end
57
+
58
+ private
59
+
60
+ def build_index
61
+ pool = Google::Protobuf::DescriptorPool.generated_pool
62
+
63
+ index_data = {
64
+ files_by_symbol: @files_by_symbol,
65
+ serialized_files: @serialized_files,
66
+ dependencies: @dependencies,
67
+ service_names: @service_names,
68
+ }
69
+
70
+ if pool.respond_to?(:each_file_descriptor)
71
+ FileDescriptorIndexer.new(allowed_services: @allowed_services).build_index(pool, **index_data)
72
+ else
73
+ ObjectSpaceIndexer.new(allowed_services: @allowed_services).build_index(**index_data)
74
+ end
75
+ end
76
+
77
+ def collect_dependencies(filename, visited, result)
78
+ return if visited.include?(filename)
79
+ visited << filename
80
+
81
+ serialized = @serialized_files[filename]
82
+ return unless serialized
83
+
84
+ result << serialized
85
+
86
+ deps = @dependencies[filename]
87
+ if deps
88
+ deps.each { |dep| collect_dependencies(dep, visited, result) }
89
+ end
90
+ end
91
+ end
92
+ end