Skip to content

Commit 078994a

Browse files
authored
improve resolution for deeply nested messages (#83)
* improve resolution for deeply nested messages * linting
1 parent edef380 commit 078994a

2 files changed

Lines changed: 56 additions & 30 deletions

File tree

lib/grpc_reflection/service/builder.ex

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ defmodule GrpcReflection.Service.Builder do
1414
new_state = process_service(service)
1515
State.merge(state, new_state)
1616
end)
17+
# shrink_cycles must run first so the symbol table reflects final merged filenames
18+
# before resolve_dependencies rewrites dep strings through it
1719
|> State.shrink_cycles()
20+
|> resolve_dependencies()
1821

1922
{:ok, tree}
2023
end
@@ -76,40 +79,48 @@ defmodule GrpcReflection.Service.Builder do
7679
end
7780

7881
defp trace_message_fields(state, parent_symbol, module, fields) do
79-
# nested types arent a "separate file", they return their parents' response
8082
nested_types = Util.get_nested_types(parent_symbol, module.descriptor())
8183

8284
module.__message_props__().field_props
8385
|> Map.values()
8486
|> Enum.map(fn %{name: name, type: type} ->
85-
%{
86-
mod:
87-
case type do
88-
{_, mod} -> mod
89-
mod -> mod
90-
end,
91-
symbol: Enum.find(fields, fn f -> f.name == name end).type_name
92-
}
93-
end)
94-
|> Enum.reject(fn %{symbol: s} -> is_nil(s) or State.has_symbol?(state, s) end)
95-
|> Enum.reduce(state, fn %{mod: mod, symbol: symbol}, state ->
96-
symbol = Util.trim_symbol(symbol)
97-
98-
response =
99-
if symbol in nested_types do
100-
build_response(parent_symbol, module)
101-
else
102-
build_response(symbol, mod)
87+
mod =
88+
case type do
89+
{_, mod} -> mod
90+
mod -> mod
10391
end
10492

105-
state
106-
|> Extensions.add_extensions(symbol, mod)
107-
|> State.add_file(response)
108-
|> State.add_symbol(symbol, response.name)
109-
|> trace_message_refs(symbol, mod)
93+
%{mod: mod, symbol: Enum.find(fields, fn f -> f.name == name end).type_name}
94+
end)
95+
|> Enum.reject(fn %{symbol: s} -> is_nil(s) end)
96+
|> Enum.reduce(state, fn %{mod: mod, symbol: symbol}, state ->
97+
trace_field_ref(state, parent_symbol, Util.trim_symbol(symbol), mod, nested_types)
11098
end)
11199
end
112100

101+
defp trace_field_ref(state, _parent_symbol, symbol, _mod, _nested_types)
102+
when is_map_key(state.symbols, symbol),
103+
do: state
104+
105+
defp trace_field_ref(state, parent_symbol, symbol, mod, nested_types) do
106+
{file, filename} =
107+
if symbol in nested_types do
108+
# nested types belong in the same file as their ancestor — look it up rather
109+
# than building a new synthetic file (which would have wrong FQDNs)
110+
parent_filename = state.symbols[parent_symbol]
111+
{state.files[parent_filename], parent_filename}
112+
else
113+
response = build_response(symbol, mod)
114+
{response, response.name}
115+
end
116+
117+
state
118+
|> Extensions.add_extensions(symbol, mod)
119+
|> State.add_file(file)
120+
|> State.add_symbol(symbol, filename)
121+
|> trace_message_refs(symbol, mod)
122+
end
123+
113124
defp build_response(symbol, module) do
114125
# we build our own file responses, so unwrap any present
115126
descriptor = get_descriptor(module)
@@ -140,6 +151,27 @@ defmodule GrpcReflection.Service.Builder do
140151
end
141152
end
142153

154+
# Rewrite each file's dependency list to use actual filenames from the symbol table.
155+
# build_response names deps as `type_symbol <> ".proto"`, but nested types share a file
156+
# with their ancestor, so that filename may not exist. Must run after shrink_cycles so
157+
# merged cycle filenames are already reflected in the symbol table.
158+
defp resolve_dependencies(%State{files: files, symbols: symbols} = state) do
159+
resolved_files =
160+
Map.new(files, fn {filename, descriptor} ->
161+
resolved_deps =
162+
descriptor.dependency
163+
|> Enum.map(fn dep ->
164+
type_symbol = String.trim_trailing(dep, ".proto")
165+
symbols[type_symbol] || dep
166+
end)
167+
|> Enum.uniq()
168+
169+
{filename, %{descriptor | dependency: resolved_deps}}
170+
end)
171+
172+
%{state | files: resolved_files}
173+
end
174+
143175
# protoc with the elixir generator and protobuf.generate slightly differ for how they
144176
# generate descriptors. Use this to potentially unwrap the service proto when dealing
145177
# with descriptors that could come from a service module.

test/case/nested_messages_test.exs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,6 @@ defmodule GrpcReflection.Case.NestedMessagesTest do
33

44
use GrpcCase, service: Nested.NestedService.Service
55

6-
# nested_messages.proto defines two services sharing the same deeply nested types.
7-
# The builder raises a symbol conflict when processing the second traversal of
8-
# OuterMessage.MiddleMessage.InnerMessage via AnotherNestedService.
9-
# Tagged skip until the library supports multi-service files with shared nested types.
10-
@moduletag :skip
11-
126
versions = ["v1", "v1alpha"]
137

148
for version <- versions do

0 commit comments

Comments
 (0)