Skip to content
Merged
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
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Install grpcurl
run: |
GRPCURL_VERSION="1.9.3"
curl -sSL https://github.com/fullstorydev/grpcurl/releases/download/v1.9.3/grpcurl_${GRPCURL_VERSION}_linux_x86_64.tar.gz | tar -xvz
mv grpcurl /usr/local/bin/grpcurl
grpcurl --version

- name: Set up Elixir
id: beam
uses: erlef/setup-beam@v1
Expand Down Expand Up @@ -134,6 +141,13 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Install grpcurl
run: |
GRPCURL_VERSION="1.9.3"
curl -sSL https://github.com/fullstorydev/grpcurl/releases/download/v1.9.3/grpcurl_${GRPCURL_VERSION}_linux_x86_64.tar.gz | tar -xvz
mv grpcurl /usr/local/bin/grpcurl
grpcurl --version

- name: Set up Elixir
id: beam
uses: erlef/setup-beam@v1
Expand Down
44 changes: 44 additions & 0 deletions test/integration/v1_reflection_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -436,4 +436,48 @@ defmodule GrpcReflection.V1ReflectionTest do
{:list_services, ""}
]
end

test "reflection graph is traversable using grpcurl", ctx do
ops = GrpcReflection.TestClient.grpcurl_service(ctx)

assert ops == [
{:call, "grpc.reflection.v1.ServerReflection.ServerReflectionInfo"},
{:call, "grpc.reflection.v1alpha.ServerReflection.ServerReflectionInfo"},
{:call, "helloworld.Greeter.SayHello"},
{:call, "testserviceV2.TestService.CallFunction"},
{:call, "testserviceV3.TestService.CallFunction"},
{:service, "grpc.reflection.v1.ServerReflection"},
{:service, "grpc.reflection.v1alpha.ServerReflection"},
{:service, "helloworld.Greeter"},
{:service, "testserviceV2.TestService"},
{:service, "testserviceV3.TestService"},
{:type, ".google.protobuf.Any"},
{:type, ".google.protobuf.StringValue"},
{:type, ".google.protobuf.Timestamp"},
{:type, ".grpc.reflection.v1.ErrorResponse"},
{:type, ".grpc.reflection.v1.ExtensionNumberResponse"},
{:type, ".grpc.reflection.v1.ExtensionRequest"},
{:type, ".grpc.reflection.v1.FileDescriptorResponse"},
{:type, ".grpc.reflection.v1.ListServiceResponse"},
{:type, ".grpc.reflection.v1.ServerReflectionRequest"},
{:type, ".grpc.reflection.v1.ServerReflectionResponse"},
{:type, ".grpc.reflection.v1.ServiceResponse"},
{:type, ".grpc.reflection.v1alpha.ErrorResponse"},
{:type, ".grpc.reflection.v1alpha.ExtensionNumberResponse"},
{:type, ".grpc.reflection.v1alpha.ExtensionRequest"},
{:type, ".grpc.reflection.v1alpha.FileDescriptorResponse"},
{:type, ".grpc.reflection.v1alpha.ListServiceResponse"},
{:type, ".grpc.reflection.v1alpha.ServerReflectionRequest"},
{:type, ".grpc.reflection.v1alpha.ServerReflectionResponse"},
{:type, ".grpc.reflection.v1alpha.ServiceResponse"},
{:type, ".helloworld.HelloReply"},
{:type, ".helloworld.HelloRequest"},
{:type, ".testserviceV2.Enum"},
{:type, ".testserviceV2.TestReply"},
{:type, ".testserviceV2.TestRequest"},
{:type, ".testserviceV3.Enum"},
{:type, ".testserviceV3.TestReply"},
{:type, ".testserviceV3.TestRequest"}
]
end
end
44 changes: 44 additions & 0 deletions test/integration/v1alpha_reflection_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -439,4 +439,48 @@ defmodule GrpcReflection.V1alphaReflectionTest do
{:list_services, ""}
]
end

test "reflection graph is traversable using grpcurl", ctx do
ops = GrpcReflection.TestClient.grpcurl_service(ctx)

assert ops == [
{:call, "grpc.reflection.v1.ServerReflection.ServerReflectionInfo"},
{:call, "grpc.reflection.v1alpha.ServerReflection.ServerReflectionInfo"},
{:call, "helloworld.Greeter.SayHello"},
{:call, "testserviceV2.TestService.CallFunction"},
{:call, "testserviceV3.TestService.CallFunction"},
{:service, "grpc.reflection.v1.ServerReflection"},
{:service, "grpc.reflection.v1alpha.ServerReflection"},
{:service, "helloworld.Greeter"},
{:service, "testserviceV2.TestService"},
{:service, "testserviceV3.TestService"},
{:type, ".google.protobuf.Any"},
{:type, ".google.protobuf.StringValue"},
{:type, ".google.protobuf.Timestamp"},
{:type, ".grpc.reflection.v1.ErrorResponse"},
{:type, ".grpc.reflection.v1.ExtensionNumberResponse"},
{:type, ".grpc.reflection.v1.ExtensionRequest"},
{:type, ".grpc.reflection.v1.FileDescriptorResponse"},
{:type, ".grpc.reflection.v1.ListServiceResponse"},
{:type, ".grpc.reflection.v1.ServerReflectionRequest"},
{:type, ".grpc.reflection.v1.ServerReflectionResponse"},
{:type, ".grpc.reflection.v1.ServiceResponse"},
{:type, ".grpc.reflection.v1alpha.ErrorResponse"},
{:type, ".grpc.reflection.v1alpha.ExtensionNumberResponse"},
{:type, ".grpc.reflection.v1alpha.ExtensionRequest"},
{:type, ".grpc.reflection.v1alpha.FileDescriptorResponse"},
{:type, ".grpc.reflection.v1alpha.ListServiceResponse"},
{:type, ".grpc.reflection.v1alpha.ServerReflectionRequest"},
{:type, ".grpc.reflection.v1alpha.ServerReflectionResponse"},
{:type, ".grpc.reflection.v1alpha.ServiceResponse"},
{:type, ".helloworld.HelloReply"},
{:type, ".helloworld.HelloRequest"},
{:type, ".testserviceV2.Enum"},
{:type, ".testserviceV2.TestReply"},
{:type, ".testserviceV2.TestRequest"},
{:type, ".testserviceV3.Enum"},
{:type, ".testserviceV3.TestReply"},
{:type, ".testserviceV3.TestRequest"}
]
end
end
107 changes: 106 additions & 1 deletion test/support/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ defmodule GrpcReflection.TestClient do
:v1alpha -> GrpcReflection.TestEndpoint.V1AlphaServer.Stub
end

%{channel: channel, req: req, stub: stub, version: unquote(version)}
%{channel: channel, req: req, stub: stub, version: unquote(version), host: host}
end
end
end
Expand Down Expand Up @@ -148,4 +148,109 @@ defmodule GrpcReflection.TestClient do
extendee_commands ++ nested_commands
end)
end

def grpcurl_service(ctx) do
ctx
|> grpcurl_list_services()
|> Stream.unfold(fn
[] ->
nil

[{:service, service} | rest] = term ->
commands = grpcurl_describe_service(ctx, service)
{term, commands ++ rest}

[{:call, call} | rest] = term ->
commands = grpcurl_describe_call(ctx, call)
{term, commands ++ rest}

[{:type, type} | rest] = term ->
commands = grpcurl_describe_type(ctx, type)
{term, commands ++ rest}
end)
|> Enum.to_list()
|> List.flatten()
|> Enum.uniq()
|> Enum.sort()
end

defp grpcurl_list_services(%{host: host}) do
{result, 0} = System.cmd("grpcurl", ["-v", "-plaintext", host, "list"])

result
|> String.split("\n")
|> Enum.reject(&(&1 == ""))
|> Enum.map(&{:service, &1})
end

defp grpcurl_describe_service(%{host: host}, service) do
{result, 0} = System.cmd("grpcurl", ["-v", "-plaintext", host, "list", service])

result
|> String.split("\n")
|> Enum.reject(&(&1 == ""))
|> Enum.map(&{:call, &1})
end

defp grpcurl_describe_call(%{host: host}, call) do
{result, 0} = System.cmd("grpcurl", ["-v", "-plaintext", host, "describe", call])

~r/\((?: stream)? (?<symbol>[a-zA-Z0-9.]+) \)/
|> Regex.scan(result, capture: ["symbol"])
|> List.flatten()
|> Enum.map(&{:type, &1})
end

defp grpcurl_describe_type(%{host: host}, type) do
{result, 0} = System.cmd("grpcurl", ["-v", "-plaintext", host, "describe", type])

# we are grabbing the referenced types to we can fetch those too
# but some of them might be defined inside this file
# so we have to identify those and filter them back out

inline_declared_symbols =
~r/(?:message (?<name>\w+) {)|(?<close>})|(?<open>{)/
|> Regex.scan(result, capture: :all_but_first)
|> List.flatten()
|> Enum.reject(&(&1 == ""))
# replace first declaration with base type to get full names
|> then(fn [_base | rest] -> [type | rest] end)
|> remove_matched_parens()
|> Enum.reduce({[], []}, fn token, {path, inline_types} ->
case token do
"}" ->
[_ | path] = path
# End of current message — pop from path
{path, inline_types}

name ->
path = [name | path]
name = path |> Enum.reverse() |> Enum.join(".")
{path, [name | inline_types]}
end
end)
|> elem(1)

# now we can grab all the references, then reject the nested declarations
~r/ (?<symbol>\.[a-z]+[a-zA-Z0-9.]+) /
|> Regex.scan(result, capture: ["symbol"])
|> List.flatten()
|> Enum.reject(&Enum.member?(inline_declared_symbols, &1))
|> Enum.map(&{:type, &1})
end

# if we only match `message XYZ {` and `}`, we get extra `}` tokens from things like the
# oneof declarations. If we also match non-message `{` we can eliminate these intermeidate
# paren-blocks, which we do here for our needs
defp remove_matched_parens(["{", "}" | rest]) do
remove_matched_parens(rest)
end

defp remove_matched_parens([item | rest]) do
[item | remove_matched_parens(rest)]
end

defp remove_matched_parens(rest) do
rest
end
end