Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/grpc_reflection/service/builder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ defmodule GrpcReflection.Service.Builder do
new_state = process_service(service)
State.merge(state, new_state)
end)
|> State.group_symbols_by_namespace()
|> State.shrink_cycles()

{:ok, tree}
end
Expand Down
3 changes: 3 additions & 0 deletions lib/grpc_reflection/service/builder/util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ defmodule GrpcReflection.Service.Builder.Util do
[]
end

# in gRPC, the leading "." signifies it is a FQDN
# we trim it and assume everything is a FQDN
# it works so far, but there may be corner cases
def trim_symbol("." <> symbol), do: symbol
def trim_symbol(symbol), do: symbol
end
65 changes: 65 additions & 0 deletions lib/grpc_reflection/service/cycle.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
defmodule GrpcReflection.Service.Cycle do
@moduledoc """
Find and identify cycles in a state graph
"""

defmodule DFS do
@moduledoc false
defstruct visited: [], path: [], cycles: []
end

def get_cycles(%GrpcReflection.Service.State{files: files}) do
files
|> Map.values()
|> Enum.reject(fn file ->
String.ends_with?(file.name, "Extension.proto")
end)
|> Map.new(fn file -> {file.name, file.dependency} end)
|> find_cycles()
end

def find_cycles(graph) do
graph
|> Map.keys()
|> Enum.reduce(%DFS{}, fn node, acc ->
%{
dfs(node, graph, acc)
| path: acc.path
}
end)
|> Map.fetch!(:cycles)
|> Enum.map(&Enum.sort/1)
|> Enum.sort()
|> Enum.uniq()
end

defp dfs(node, graph, state) do
cond do
Enum.member?(state.path, node) ->
# Node is in current path → cycle found
cycle = [node | Enum.take_while(state.path, &(&1 != node))]
%{state | cycles: [cycle | state.cycles]}

Enum.member?(state.visited, node) ->
state

true ->
# Mark as visited and extend path
state = %{
state
| visited: [node | state.visited],
path: [node | state.path]
}

# Visit neighbors
graph
|> Map.get(node, [])
|> Enum.reduce(state, fn neighbor, acc ->
%{
dfs(neighbor, graph, acc)
| path: acc.path
}
end)
end
end
end
63 changes: 60 additions & 3 deletions lib/grpc_reflection/service/state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,66 @@ defmodule GrpcReflection.Service.State do
end
end

def group_symbols_by_namespace(%__MODULE__{} = state) do
# group symbols by namespace and combine
# IO.inspect(state)
# reduce state size and complexity by combining files into fewer, larger responses
def shrink_cycles(%__MODULE__{} = state) do
state
|> GrpcReflection.Service.Cycle.get_cycles()
|> Enum.reduce(state, fn
filenames, state_acc ->
files = Enum.map(filenames, &state.files[&1])
combined_file = combine_file_descriptors(files)
update_state_with_combined_file(state_acc, combined_file, filenames)
end)
end

defp update_state_with_combined_file(state, combined_file, combined_filenames) do
# Update files: remove old entries, add new one with updated dependencies
new_files =
state.files
|> Map.drop(combined_filenames)
|> Map.new(fn {filename, descriptor} ->
if Enum.any?(descriptor.dependency, &Enum.member?(combined_filenames, &1)) do
{
filename,
%{
descriptor
| dependency: (descriptor.dependency -- combined_filenames) ++ [combined_file.name]
}
}
else
{filename, descriptor}
end
end)
|> Map.put(combined_file.name, combined_file)

# Update symbols: map old symbols to point to the new combined file
new_symbols =
Map.new(state.symbols, fn {symbol, filename} ->
if filename in combined_filenames do
{symbol, combined_file.name}
else
{symbol, filename}
end
end)

%{state | files: new_files, symbols: new_symbols}
end

defp combine_file_descriptors(file_descriptors) do
combined_names = Enum.map(file_descriptors, & &1.name)

Enum.reduce(file_descriptors, %Google.Protobuf.FileDescriptorProto{}, fn descriptor, acc ->
%{
acc
| syntax: acc.syntax || descriptor.syntax,
package: acc.package || descriptor.package,
name: acc.name || descriptor.name,
message_type: Enum.uniq(acc.message_type ++ descriptor.message_type),
service: Enum.uniq(acc.service ++ descriptor.service),
enum_type: Enum.uniq(acc.enum_type ++ descriptor.enum_type),
dependency: Enum.uniq(acc.dependency ++ (descriptor.dependency -- combined_names)),
extension: Enum.uniq(acc.extension ++ descriptor.extension)
}
end)
end
end
90 changes: 11 additions & 79 deletions test/integration/v1_reflection_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -112,26 +112,7 @@ defmodule GrpcReflection.V1ReflectionTest do
assert response.dependency == ["google.protobuf.Timestamp.proto"]

assert [
%Google.Protobuf.DescriptorProto{
name: "HelloReply",
field: [
%Google.Protobuf.FieldDescriptorProto{
name: "message",
number: 1,
label: :LABEL_OPTIONAL,
type: :TYPE_STRING,
json_name: "message"
},
%Google.Protobuf.FieldDescriptorProto{
name: "today",
number: 2,
label: :LABEL_OPTIONAL,
type: :TYPE_MESSAGE,
type_name: ".google.protobuf.Timestamp",
json_name: "today"
}
]
}
%Google.Protobuf.DescriptorProto{name: "HelloReply"}
] = response.message_type
end

Expand All @@ -144,25 +125,7 @@ defmodule GrpcReflection.V1ReflectionTest do
assert response.dependency == []

assert [
%Google.Protobuf.DescriptorProto{
field: [
%Google.Protobuf.FieldDescriptorProto{
json_name: "seconds",
label: :LABEL_OPTIONAL,
name: "seconds",
number: 1,
type: :TYPE_INT64
},
%Google.Protobuf.FieldDescriptorProto{
json_name: "nanos",
label: :LABEL_OPTIONAL,
name: "nanos",
number: 2,
type: :TYPE_INT32
}
],
name: "Timestamp"
}
%Google.Protobuf.DescriptorProto{name: "Timestamp"}
] = response.message_type
end

Expand Down Expand Up @@ -213,49 +176,18 @@ defmodule GrpcReflection.V1ReflectionTest do
assert {:ok, response} = run_request(message, ctx)
assert response.name == extendee <> "Extension.proto"
assert response.package == "testserviceV2"
assert response.dependency == [extendee <> ".proto"]

assert response.extension == [
%Google.Protobuf.FieldDescriptorProto{
name: "data",
extendee: extendee,
number: 10,
label: :LABEL_OPTIONAL,
type: :TYPE_STRING,
type_name: nil
},
%Google.Protobuf.FieldDescriptorProto{
name: "location",
extendee: extendee,
number: 11,
label: :LABEL_OPTIONAL,
type: :TYPE_MESSAGE,
type_name: "testserviceV2.Location"
}
]
assert response.dependency == ["testserviceV2.TestRequest.proto"]

assert response.message_type == [
assert [
%Google.Protobuf.FieldDescriptorProto{name: "data"},
%Google.Protobuf.FieldDescriptorProto{name: "location"}
] = response.extension

assert [
%Google.Protobuf.DescriptorProto{
name: "Location",
field: [
%Google.Protobuf.FieldDescriptorProto{
name: "latitude",
number: 1,
label: :LABEL_OPTIONAL,
type: :TYPE_DOUBLE,
json_name: "latitude"
},
%Google.Protobuf.FieldDescriptorProto{
name: "longitude",
extendee: nil,
number: 2,
label: :LABEL_OPTIONAL,
type: :TYPE_DOUBLE,
json_name: "longitude"
}
]
name: "Location"
}
]
] = response.message_type
end
end
end
39 changes: 4 additions & 35 deletions test/integration/v1alpha_reflection_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -115,21 +115,8 @@ defmodule GrpcReflection.V1alphaReflectionTest do
%Google.Protobuf.DescriptorProto{
name: "HelloReply",
field: [
%Google.Protobuf.FieldDescriptorProto{
name: "message",
number: 1,
label: :LABEL_OPTIONAL,
type: :TYPE_STRING,
json_name: "message"
},
%Google.Protobuf.FieldDescriptorProto{
name: "today",
number: 2,
label: :LABEL_OPTIONAL,
type: :TYPE_MESSAGE,
type_name: ".google.protobuf.Timestamp",
json_name: "today"
}
%Google.Protobuf.FieldDescriptorProto{name: "message"},
%Google.Protobuf.FieldDescriptorProto{name: "today"}
]
}
] = response.message_type
Expand All @@ -144,25 +131,7 @@ defmodule GrpcReflection.V1alphaReflectionTest do
assert response.dependency == []

assert [
%Google.Protobuf.DescriptorProto{
field: [
%Google.Protobuf.FieldDescriptorProto{
json_name: "seconds",
label: :LABEL_OPTIONAL,
name: "seconds",
number: 1,
type: :TYPE_INT64
},
%Google.Protobuf.FieldDescriptorProto{
json_name: "nanos",
label: :LABEL_OPTIONAL,
name: "nanos",
number: 2,
type: :TYPE_INT32
}
],
name: "Timestamp"
}
%Google.Protobuf.DescriptorProto{name: "Timestamp"}
] = response.message_type
end

Expand Down Expand Up @@ -216,7 +185,7 @@ defmodule GrpcReflection.V1alphaReflectionTest do
assert {:ok, response} = run_request(message, ctx)
assert response.name == extendee <> "Extension.proto"
assert response.package == "testserviceV2"
assert response.dependency == [extendee <> ".proto"]
assert response.dependency == ["testserviceV2.TestRequest.proto"]

assert response.extension == [
%Google.Protobuf.FieldDescriptorProto{
Expand Down
Loading