diff --git a/lib/grpc_reflection/server.ex b/lib/grpc_reflection/server.ex index 7f7c1ec..b820ccd 100644 --- a/lib/grpc_reflection/server.ex +++ b/lib/grpc_reflection/server.ex @@ -22,12 +22,14 @@ defmodule GrpcReflection.Server do quote do @cfg {__MODULE__, unquote(services)} + alias GrpcReflection.Service + @doc """ Get the current list of configured services """ @spec list_services :: list(binary) def list_services do - GrpcReflection.Service.Agent.list_services(@cfg) + Service.list_services(@cfg) end @doc """ @@ -35,7 +37,7 @@ defmodule GrpcReflection.Server do """ @spec get_by_symbol(binary()) :: {:ok, GrpcReflection.descriptor_t()} | {:error, binary} def get_by_symbol(symbol) do - GrpcReflection.Service.Agent.get_by_symbol(@cfg, symbol) + Service.get_by_symbol(@cfg, symbol) end @doc """ @@ -43,7 +45,7 @@ defmodule GrpcReflection.Server do """ @spec get_by_filename(binary()) :: {:ok, GrpcReflection.descriptor_t()} | {:error, binary} def get_by_filename(filename) do - GrpcReflection.Service.Agent.get_by_filename(@cfg, filename) + Service.get_by_filename(@cfg, filename) end @doc """ @@ -51,7 +53,7 @@ defmodule GrpcReflection.Server do """ @spec get_extension_numbers_by_type(module()) :: {:ok, list(integer())} | {:error, binary} def get_extension_numbers_by_type(mod) do - GrpcReflection.Service.Agent.get_extension_numbers_by_type(@cfg, mod) + Service.get_extension_numbers_by_type(@cfg, mod) end @doc """ @@ -59,7 +61,7 @@ defmodule GrpcReflection.Server do """ @spec get_by_extension(binary()) :: {:ok, GrpcReflection.descriptor_t()} | {:error, binary} def get_by_extension(containing_type) do - GrpcReflection.Service.Agent.get_by_extension(@cfg, containing_type) + Service.get_by_extension(@cfg, containing_type) end @doc """ @@ -67,12 +69,9 @@ defmodule GrpcReflection.Server do """ @spec put_services(list(module())) :: :ok | {:error, binary()} def put_services(services) do - case GrpcReflection.Service.Builder.build_reflection_tree(services) do - %GrpcReflection.Service.Agent{} = state -> - GrpcReflection.Service.Agent.put_state(@cfg, state) - - err -> - err + case Service.build_reflection_tree(services) do + {:ok, state} -> Service.put_state(@cfg, state) + err -> err end end diff --git a/lib/grpc_reflection/service.ex b/lib/grpc_reflection/service.ex new file mode 100644 index 0000000..c6186f3 --- /dev/null +++ b/lib/grpc_reflection/service.ex @@ -0,0 +1,18 @@ +defmodule GrpcReflection.Service do + @moduledoc """ + Primary interface to internal reflection state and logic + """ + + alias GrpcReflection.Service.Agent + alias GrpcReflection.Service.Builder + + defdelegate build_reflection_tree(services), to: Builder + + defdelegate put_state(cfg, state), to: Agent + + defdelegate list_services(cfg), to: Agent + defdelegate get_by_symbol(cfg, symbol), to: Agent + defdelegate get_by_filename(cfg, filename), to: Agent + defdelegate get_by_extension(cfg, containing_type), to: Agent + defdelegate get_extension_numbers_by_type(cfg, mod), to: Agent +end diff --git a/lib/grpc_reflection/service/agent.ex b/lib/grpc_reflection/service/agent.ex index 88fa31b..c923fa4 100644 --- a/lib/grpc_reflection/service/agent.ex +++ b/lib/grpc_reflection/service/agent.ex @@ -6,66 +6,57 @@ defmodule GrpcReflection.Service.Agent do require Logger alias GrpcReflection.Service.Builder - alias GrpcReflection.Service.Lookup + alias GrpcReflection.Service.State - defstruct services: [], files: %{}, symbols: %{}, extensions: %{} - - @type descriptor_t :: GrpcReflection.Server.descriptor_t() @type cfg_t :: {atom(), list(atom)} - @type t :: %__MODULE__{ - services: list(module()), - files: %{optional(binary()) => descriptor_t()}, - symbols: %{optional(binary()) => descriptor_t()}, - extensions: %{optional(binary()) => list(integer())} - } def start_link(_, opts) do name = Keyword.get(opts, :name) services = Keyword.get(opts, :services) case Builder.build_reflection_tree(services) do - %__MODULE__{} = state -> + {:ok, state} -> Agent.start_link(fn -> state end, name: name) err -> Logger.error("Failed to build reflection tree: #{inspect(err)}") - Agent.start_link(fn -> %__MODULE__{} end, name: name) + Agent.start_link(fn -> %State{} end, name: name) end end @spec list_services(cfg_t()) :: list(binary) def list_services(cfg) do name = start_agent_on_first_call(cfg) - Agent.get(name, &Lookup.lookup_services/1) + Agent.get(name, &State.lookup_services/1) end - @spec get_by_symbol(cfg_t(), binary()) :: {:ok, descriptor_t()} | {:error, binary} + @spec get_by_symbol(cfg_t(), binary()) :: {:ok, State.descriptor_t()} | {:error, binary} def get_by_symbol(cfg, symbol) do name = start_agent_on_first_call(cfg) - Agent.get(name, &Lookup.lookup_symbol(symbol, &1)) + Agent.get(name, &State.lookup_symbol(symbol, &1)) end - @spec get_by_filename(cfg_t(), binary()) :: {:ok, descriptor_t()} | {:error, binary} + @spec get_by_filename(cfg_t(), binary()) :: {:ok, State.descriptor_t()} | {:error, binary} def get_by_filename(cfg, filename) do name = start_agent_on_first_call(cfg) - Agent.get(name, &Lookup.lookup_filename(filename, &1)) + Agent.get(name, &State.lookup_filename(filename, &1)) end - @spec get_by_extension(cfg_t(), binary()) :: {:ok, descriptor_t()} | {:error, binary} + @spec get_by_extension(cfg_t(), binary()) :: {:ok, State.descriptor_t()} | {:error, binary} def get_by_extension(cfg, containing_type) do name = start_agent_on_first_call(cfg) - Agent.get(name, &Lookup.lookup_extension(containing_type, &1)) + Agent.get(name, &State.lookup_extension(containing_type, &1)) end @spec get_extension_numbers_by_type(cfg_t(), binary()) :: {:ok, list(integer())} | {:error, binary} def get_extension_numbers_by_type(cfg, mod) do name = start_agent_on_first_call(cfg) - Agent.get(name, &Lookup.lookup_extension_numbers(mod, &1)) + Agent.get(name, &State.lookup_extension_numbers(mod, &1)) end - @spec put_state(cfg_t(), t()) :: :ok - def put_state(cfg, %__MODULE__{} = state) do + @spec put_state(cfg_t(), State.t()) :: :ok + def put_state(cfg, %State{} = state) do name = start_agent_on_first_call(cfg) Agent.update(name, fn _old_state -> state end) end diff --git a/lib/grpc_reflection/service/builder.ex b/lib/grpc_reflection/service/builder.ex index b6ec6ae..3278e0a 100644 --- a/lib/grpc_reflection/service/builder.ex +++ b/lib/grpc_reflection/service/builder.ex @@ -2,52 +2,40 @@ defmodule GrpcReflection.Service.Builder do @moduledoc false alias Google.Protobuf.FileDescriptorProto - alias GrpcReflection.Service.Agent + alias GrpcReflection.Service.State alias GrpcReflection.Service.Builder.Util - @type_message Map.fetch!(Google.Protobuf.FieldDescriptorProto.Type.mapping(), :TYPE_MESSAGE) - def build_reflection_tree(services) do - with :ok <- validate_services(services), - {%{files: _, symbols: _} = data, references} <- get_services_and_references(services) do - references - |> List.flatten() - |> Enum.uniq() - |> process_references(data) + with :ok <- Util.validate_services(services) do + services + |> process_services() + |> process_references() end end - defp validate_services(services) do - services - |> Enum.reject(fn service_mod -> - is_binary(service_mod.__meta__(:name)) and is_struct(service_mod.descriptor()) - end) - |> then(fn - [] -> :ok - _ -> {:error, "non-service module provided"} - end) - rescue - _ -> {:error, "non-service module provided"} - end + defp process_references(%State{} = state) do + # references is a growing set. Processing references can add new references + case State.get_missing_references(state) do + [] -> + {:ok, state} - defp get_services_and_references(services) do - Enum.reduce_while(services, {%Agent{services: services}, []}, fn service, {acc, refs} -> - case process_service(service) do - {:ok, %{files: files, symbols: symbols}, references} -> - {:cont, - {%{acc | files: Map.merge(files, acc.files), symbols: Map.merge(symbols, acc.symbols)}, - [refs | references]}} + missing_refs -> + missing_refs + |> Enum.reduce(state, &State.merge(&2, process_reference(&1))) + |> process_references() + end + end - {:error, reason} -> - {:halt, {:error, reason}} - end + defp process_services(services) do + Enum.reduce(services, State.new(services), fn service, state -> + State.merge(state, process_service(service)) end) end defp process_service(service) do descriptor = service.descriptor() service_name = service.__meta__(:name) - referenced_types = types_from_descriptor(descriptor) + referenced_types = Util.types_from_descriptor(descriptor) method_symbols = Enum.map( @@ -59,63 +47,24 @@ defmodule GrpcReflection.Service.Builder do payload = FileDescriptorProto.encode(unencoded_payload) response = %{file_descriptor_proto: [payload]} - root_symbols = + State.new() + |> State.add_symbols( method_symbols |> Enum.reduce(%{}, fn name, acc -> Map.put(acc, name, response) end) |> Map.put(service_name, response) - - root_files = %{(service_name <> ".proto") => response} - - {:ok, %Agent{files: root_files, symbols: root_symbols}, referenced_types} - rescue - _ -> {:error, "Couldn't process #{inspect(service)}"} - end - - defp process_references([], data), do: data - - defp process_references([reference | rest], data) do - if Map.has_key?(data.symbols, reference) do - process_references(rest, data) - else - {%{files: f, symbols: s, extensions: e}, references} = process_reference(reference) - - data = %{ - data - | files: Map.merge(data.files, f), - symbols: Map.merge(data.symbols, s), - extensions: Map.merge(data.extensions, e) - } - - references = (rest ++ references) |> List.flatten() |> Enum.uniq() - process_references(references, data) - end - end - - defp convert_symbol_to_module(symbol) do - symbol - |> then(fn - "." <> name -> name - name -> name - end) - |> String.split(".") - |> Enum.reverse() - |> then(fn - [m | segments] -> [m | Enum.map(segments, &Util.upcase_first/1)] - end) - |> Enum.reverse() - |> Enum.join(".") - |> then(fn name -> "Elixir." <> name end) - |> String.to_existing_atom() + ) + |> State.add_files(%{(service_name <> ".proto") => response}) + |> State.add_references(referenced_types) end defp process_reference(symbol) do symbol - |> convert_symbol_to_module() + |> Util.convert_symbol_to_module() |> then(fn mod -> descriptor = mod.descriptor() name = symbol - referenced_types = types_from_descriptor(descriptor) + referenced_types = Util.types_from_descriptor(descriptor) unencoded_payload = process_common(name, mod, descriptor) payload = FileDescriptorProto.encode(unencoded_payload) response = %{file_descriptor_proto: [payload]} @@ -128,8 +77,11 @@ defmodule GrpcReflection.Service.Builder do {root_extensions, root_files} = process_extensions(mod, symbol, extension_file, descriptor, root_files) - {%Agent{extensions: root_extensions, files: root_files, symbols: root_symbols}, - referenced_types} + State.new() + |> State.add_files(root_files) + |> State.add_symbols(root_symbols) + |> State.add_extensions(root_extensions) + |> State.add_references(referenced_types) end) end @@ -158,7 +110,7 @@ defmodule GrpcReflection.Service.Builder do name: extension_file, package: Util.package_from_name(symbol), dependency: [symbol <> ".proto"], - syntax: get_syntax(mod) + syntax: Util.get_syntax(mod) } {extension_numbers, extension_files} = @@ -174,9 +126,9 @@ defmodule GrpcReflection.Service.Builder do ) message_list = - for ext <- extension_files, is_message_descriptor?(ext) do + for ext <- extension_files, Util.is_message_descriptor?(ext) do ext.type_name - |> convert_symbol_to_module() + |> Util.convert_symbol_to_module() |> then(& &1.descriptor()) end @@ -191,17 +143,12 @@ defmodule GrpcReflection.Service.Builder do defp process_extensions(_, _, _, _), do: {:ignore, {nil, nil}} - defp is_message_descriptor?(%Google.Protobuf.FieldDescriptorProto{type: @type_message}), - do: true - - defp is_message_descriptor?(_), do: false - defp process_common(name, module, descriptor) do package = Util.package_from_name(name) dependencies = descriptor - |> types_from_descriptor() + |> Util.types_from_descriptor() |> Enum.map(fn name -> name <> ".proto" end) @@ -212,7 +159,7 @@ defmodule GrpcReflection.Service.Builder do name: name <> ".proto", package: package, dependency: dependencies, - syntax: get_syntax(module) + syntax: Util.get_syntax(module) } case descriptor do @@ -221,135 +168,4 @@ defmodule GrpcReflection.Service.Builder do %Google.Protobuf.EnumDescriptorProto{} -> %{response_stub | enum_type: [descriptor]} end end - - defp get_syntax(module) do - cond do - Keyword.has_key?(module.__info__(:functions), :__message_props__) -> - # this is a message type - case module.__message_props__().syntax do - :proto2 -> "proto2" - :proto3 -> "proto3" - end - - Keyword.has_key?(module.__info__(:functions), :__rpc_calls__) -> - # this is a service definition, grab a message and recurse - module.__rpc_calls__() - |> Enum.find(fn - {_, _, _} -> true - _ -> false - end) - |> then(fn - nil -> "proto2" - {_, {req, _}, _} -> get_syntax(req) - {_, _, {req, _}} -> get_syntax(req) - end) - - true -> - raise "Module #{inspect(module)} has neither rcp_calls nor __message_props__" - end - end - - defp types_from_descriptor(%Google.Protobuf.ServiceDescriptorProto{} = descriptor) do - descriptor.method - |> Enum.flat_map(fn method -> - [method.input_type, method.output_type] - end) - |> Enum.reject(&is_atom/1) - |> Enum.map(fn - "." <> symbol -> symbol - symbol -> symbol - end) - end - - defp types_from_descriptor(%Google.Protobuf.DescriptorProto{} = descriptor) do - (descriptor.field ++ Enum.flat_map(descriptor.nested_type, & &1.field)) - |> Enum.map(fn field -> - field.type_name - end) - |> Enum.reject(&is_nil/1) - |> Enum.map(fn - "." <> symbol -> symbol - symbol -> symbol - end) - end - - defp types_from_descriptor(%Google.Protobuf.EnumDescriptorProto{}) do - [] - end - - defmodule Util do - @moduledoc """ - Utility functions for the builder. - """ - @field_type_mapping Google.Protobuf.FieldDescriptorProto.Type.mapping() - @field_label_mapping Google.Protobuf.FieldDescriptorProto.Label.mapping() - - def package_from_name(service_name) do - service_name - |> String.split(".") - |> Enum.reverse() - |> then(fn [_ | rest] -> rest end) - |> Enum.reverse() - |> Enum.join(".") - end - - def upcase_first(<>), do: String.upcase(<>) <> rest - - def downcase_first(<>), - do: String.downcase(<>) <> rest - - # Generates a field descriptor from a field props struct. This function is compatible with proto2 only. - def convert_to_field_descriptor( - extendee, - %Protobuf.Extension.Props.Extension{field_props: field_props} - ) do - {type, type_name} = type_from_field_props(field_props) - - %Google.Protobuf.FieldDescriptorProto{ - name: field_props.name, - number: field_props.fnum, - label: label_from_field_props(field_props), - type: type, - type_name: type_name, - extendee: extendee - } - end - - # Google.Protobuf.FieldDescriptorProto.Label - defp label_from_field_props(%Protobuf.FieldProps{optional?: true}), - do: @field_label_mapping[:LABEL_OPTIONAL] - - defp label_from_field_props(%Protobuf.FieldProps{repeated?: true}), - do: @field_label_mapping[:LABEL_REPEATED] - - defp label_from_field_props(%Protobuf.FieldProps{required?: true}), - do: @field_label_mapping[:LABEL_REQUIRED] - - # Google.Protobuf.FieldDescriptorProto.Type - defp type_from_field_props(%Protobuf.FieldProps{type: type}) do - @field_type_mapping - |> Map.get(:"TYPE_#{type |> Atom.to_string() |> String.upcase()}", type) - |> then(fn type -> - cond do - is_integer(type) -> - {type, nil} - - is_atom(type) and Code.ensure_loaded?(type) and function_exported?(type, :descriptor, 0) -> - {@field_type_mapping[:TYPE_MESSAGE], get_pb_type_name(type)} - - true -> - raise("Unsupported type") - end - end) - end - - defp get_pb_type_name(type) do - {packs, [name]} = - type - |> Module.split() - |> Enum.split(-1) - - Enum.map_join(packs, ".", &downcase_first/1) <> "." <> name - end - end end diff --git a/lib/grpc_reflection/service/builder/util.ex b/lib/grpc_reflection/service/builder/util.ex new file mode 100644 index 0000000..cac8264 --- /dev/null +++ b/lib/grpc_reflection/service/builder/util.ex @@ -0,0 +1,157 @@ +defmodule GrpcReflection.Service.Builder.Util do + @moduledoc """ + Utility functions for the builder. + """ + @field_type_mapping Google.Protobuf.FieldDescriptorProto.Type.mapping() + @field_label_mapping Google.Protobuf.FieldDescriptorProto.Label.mapping() + + @type_message Map.fetch!(Google.Protobuf.FieldDescriptorProto.Type.mapping(), :TYPE_MESSAGE) + + def package_from_name(service_name) do + service_name + |> String.split(".") + |> Enum.reverse() + |> then(fn [_ | rest] -> rest end) + |> Enum.reverse() + |> Enum.join(".") + end + + def upcase_first(<>), do: String.upcase(<>) <> rest + + def downcase_first(<>), + do: String.downcase(<>) <> rest + + # Generates a field descriptor from a field props struct. This function is compatible with proto2 only. + def convert_to_field_descriptor( + extendee, + %Protobuf.Extension.Props.Extension{field_props: field_props} + ) do + {type, type_name} = type_from_field_props(field_props) + + %Google.Protobuf.FieldDescriptorProto{ + name: field_props.name, + number: field_props.fnum, + label: label_from_field_props(field_props), + type: type, + type_name: type_name, + extendee: extendee + } + end + + # Google.Protobuf.FieldDescriptorProto.Label + defp label_from_field_props(%Protobuf.FieldProps{optional?: true}), + do: @field_label_mapping[:LABEL_OPTIONAL] + + defp label_from_field_props(%Protobuf.FieldProps{repeated?: true}), + do: @field_label_mapping[:LABEL_REPEATED] + + defp label_from_field_props(%Protobuf.FieldProps{required?: true}), + do: @field_label_mapping[:LABEL_REQUIRED] + + # Google.Protobuf.FieldDescriptorProto.Type + defp type_from_field_props(%Protobuf.FieldProps{type: type}) do + @field_type_mapping + |> Map.get(:"TYPE_#{type |> Atom.to_string() |> String.upcase()}", type) + |> then(fn type -> + cond do + is_integer(type) -> + {type, nil} + + is_atom(type) and Code.ensure_loaded?(type) and function_exported?(type, :descriptor, 0) -> + {@field_type_mapping[:TYPE_MESSAGE], get_pb_type_name(type)} + + true -> + raise("Unsupported type") + end + end) + end + + defp get_pb_type_name(type) do + {packs, [name]} = + type + |> Module.split() + |> Enum.split(-1) + + Enum.map_join(packs, ".", &downcase_first/1) <> "." <> name + end + + def validate_services(services) do + invalid_services = + Enum.reject(services, fn service_mod -> + is_binary(service_mod.__meta__(:name)) and + is_struct(service_mod.descriptor()) + end) + + case invalid_services do + [] -> :ok + _ -> {:error, "non-service module provided"} + end + end + + def convert_symbol_to_module(symbol) do + symbol + |> then(fn + "." <> name -> name + name -> name + end) + |> String.split(".") + |> Enum.reverse() + |> then(fn + [m | segments] -> [m | Enum.map(segments, &upcase_first/1)] + end) + |> Enum.reverse() + |> Enum.join(".") + |> then(fn name -> "Elixir." <> name end) + |> String.to_existing_atom() + end + + def is_message_descriptor?(%Google.Protobuf.FieldDescriptorProto{type: @type_message}), + do: true + + def is_message_descriptor?(_), do: false + + def get_syntax(module) do + cond do + Keyword.has_key?(module.__info__(:functions), :__message_props__) -> + # this is a message type + case module.__message_props__().syntax do + :proto2 -> "proto2" + :proto3 -> "proto3" + end + + Keyword.has_key?(module.__info__(:functions), :__rpc_calls__) -> + # this is a service definition, grab a message and recurse + module.__rpc_calls__() + |> Enum.find(fn + {_, _, _} -> true + _ -> false + end) + |> then(fn + nil -> "proto2" + {_, {req, _}, _} -> get_syntax(req) + {_, _, {req, _}} -> get_syntax(req) + end) + + true -> + raise "Module #{inspect(module)} has neither rcp_calls nor __message_props__" + end + end + + def types_from_descriptor(%Google.Protobuf.ServiceDescriptorProto{} = descriptor) do + descriptor.method + |> Enum.flat_map(fn method -> [method.input_type, method.output_type] end) + |> Enum.reject(&is_atom/1) + |> Enum.map(&String.trim_leading(&1, ".")) + end + + def types_from_descriptor(%Google.Protobuf.DescriptorProto{} = descriptor) do + (descriptor.field ++ Enum.flat_map(descriptor.nested_type, & &1.field)) + |> Enum.map(fn field -> field.type_name end) + |> Enum.reject(&is_nil/1) + |> Enum.map(&String.trim_leading(&1, ".")) + end + + def types_from_descriptor(%Google.Protobuf.EnumDescriptorProto{}) do + [] + end +end diff --git a/lib/grpc_reflection/service/lookup.ex b/lib/grpc_reflection/service/lookup.ex deleted file mode 100644 index 525c1eb..0000000 --- a/lib/grpc_reflection/service/lookup.ex +++ /dev/null @@ -1,45 +0,0 @@ -defmodule GrpcReflection.Service.Lookup do - @moduledoc false - - alias GrpcReflection.Service.Agent - - def lookup_services(%Agent{services: services}) do - Enum.map(services, fn service_mod -> service_mod.__meta__(:name) end) - end - - def lookup_symbol("." <> symbol, state), do: lookup_symbol(symbol, state) - - def lookup_symbol(symbol, %Agent{symbols: symbols}) do - if Map.has_key?(symbols, symbol) do - {:ok, symbols[symbol]} - else - {:error, "symbol not found"} - end - end - - def lookup_filename(filename, %Agent{files: files}) do - if Map.has_key?(files, filename) do - {:ok, files[filename]} - else - {:error, "filename not found"} - end - end - - def lookup_extension(extendee, %Agent{files: files}) do - file = extendee <> "Extension.proto" - - if Map.has_key?(files, file) do - {:ok, files[file]} - else - {:error, "extension not found"} - end - end - - def lookup_extension_numbers(mod, %Agent{extensions: extensions}) do - if Map.has_key?(extensions, mod) do - {:ok, extensions[mod]} - else - {:error, "extension numbers not found"} - end - end -end diff --git a/lib/grpc_reflection/service/state.ex b/lib/grpc_reflection/service/state.ex new file mode 100644 index 0000000..209a4f1 --- /dev/null +++ b/lib/grpc_reflection/service/state.ex @@ -0,0 +1,100 @@ +defmodule GrpcReflection.Service.State do + @moduledoc false + + defstruct services: [], files: %{}, symbols: %{}, extensions: %{}, references: MapSet.new() + + @type descriptor_t :: GrpcReflection.Server.descriptor_t() + @type entry_t :: %{optional(binary()) => descriptor_t()} + + @type t :: %__MODULE__{ + services: list(module()), + files: entry_t(), + symbols: entry_t(), + extensions: %{optional(binary()) => list(integer())}, + references: MapSet.t(binary()) + } + + @spec new(list(module)) :: t() + def new(services \\ []), do: %__MODULE__{services: services} + + @spec merge(t(), t()) :: t() + def merge(%__MODULE__{} = state1, %__MODULE__{} = state2) do + %__MODULE__{ + services: Enum.uniq(state1.services ++ state2.services), + files: Map.merge(state1.files, state2.files), + symbols: Map.merge(state1.symbols, state2.symbols), + extensions: Map.merge(state1.extensions, state2.extensions), + references: MapSet.union(state1.references, state2.references) + } + end + + @spec add_files(t(), entry_t()) :: t() + def add_files(%__MODULE__{} = state, files) do + %{state | files: Map.merge(files, state.files)} + end + + @spec add_symbols(t(), entry_t()) :: t() + def add_symbols(%__MODULE__{} = state, symbols) do + %{state | symbols: Map.merge(symbols, state.symbols)} + end + + def add_extensions(%__MODULE__{} = state, extensions) do + %{state | extensions: Map.merge(extensions, state.extensions)} + end + + def add_references(%__MODULE__{} = state, refs) do + references = Enum.reduce(refs, state.references, &MapSet.put(&2, &1)) + %{state | references: references} + end + + def get_references(%__MODULE__{} = state), do: MapSet.to_list(state.references) + + def lookup_services(%__MODULE__{services: services}) do + Enum.map(services, fn service_mod -> service_mod.__meta__(:name) end) + end + + def lookup_symbol("." <> symbol, state), do: lookup_symbol(symbol, state) + + def lookup_symbol(symbol, %__MODULE__{symbols: symbols}) do + if Map.has_key?(symbols, symbol) do + {:ok, symbols[symbol]} + else + {:error, "symbol not found"} + end + end + + @doc """ + Get the list of refereneces that are not known symbols + """ + def get_missing_references(%__MODULE__{} = state) do + state.references + |> MapSet.to_list() + |> Enum.reject(&Map.has_key?(state.symbols, &1)) + end + + def lookup_filename(filename, %__MODULE__{files: files}) do + if Map.has_key?(files, filename) do + {:ok, files[filename]} + else + {:error, "filename not found"} + end + end + + def lookup_extension(extendee, %__MODULE__{files: files}) do + file = extendee <> "Extension.proto" + + if Map.has_key?(files, file) do + {:ok, files[file]} + else + {:error, "extension not found"} + end + end + + def lookup_extension_numbers(mod, %__MODULE__{extensions: extensions}) do + if Map.has_key?(extensions, mod) do + {:ok, extensions[mod]} + else + {:error, "extension numbers not found"} + end + end +end diff --git a/test/builder_util_test.exs b/test/builder/util_test.exs similarity index 97% rename from test/builder_util_test.exs rename to test/builder/util_test.exs index 24c81cb..3115f22 100644 --- a/test/builder_util_test.exs +++ b/test/builder/util_test.exs @@ -1,4 +1,4 @@ -defmodule GrpcReflection.UtilTest do +defmodule GrpcReflection.Service.Builder.UtilTest do @moduledoc false use ExUnit.Case diff --git a/test/builder_test.exs b/test/builder_test.exs index fe1020e..c226a11 100644 --- a/test/builder_test.exs +++ b/test/builder_test.exs @@ -3,12 +3,12 @@ defmodule GrpcReflection.BuilderTest do use ExUnit.Case - alias GrpcReflection.Service.Agent + alias GrpcReflection.Service.State alias GrpcReflection.Service.Builder test "supports all reflection types in proto3" do - tree = Builder.build_reflection_tree([TestserviceV3.TestService.Service]) - assert %Agent{services: [TestserviceV3.TestService.Service]} = tree + assert {:ok, tree} = Builder.build_reflection_tree([TestserviceV3.TestService.Service]) + assert %State{services: [TestserviceV3.TestService.Service]} = tree assert Map.keys(tree.files) == [ "google.protobuf.Any.proto", @@ -46,8 +46,8 @@ defmodule GrpcReflection.BuilderTest do end test "supports all reflection types in proto2" do - tree = Builder.build_reflection_tree([TestserviceV2.TestService.Service]) - assert %Agent{services: [TestserviceV2.TestService.Service]} = tree + assert {:ok, tree} = Builder.build_reflection_tree([TestserviceV2.TestService.Service]) + assert %State{services: [TestserviceV2.TestService.Service]} = tree assert Map.keys(tree.files) == [ "google.protobuf.Any.proto", @@ -86,8 +86,8 @@ defmodule GrpcReflection.BuilderTest do end test "handles an empty service" do - tree = Builder.build_reflection_tree([TestserviceV2.EmptyService.Service]) - assert %Agent{services: [TestserviceV2.EmptyService.Service]} = tree + assert {:ok, tree} = Builder.build_reflection_tree([TestserviceV2.EmptyService.Service]) + assert %State{services: [TestserviceV2.EmptyService.Service]} = tree (Map.values(tree.files) ++ Map.values(tree.symbols)) |> Enum.flat_map(&Map.get(&1, :file_descriptor_proto)) @@ -109,4 +109,10 @@ defmodule GrpcReflection.BuilderTest do ] end) end + + test "handles a non-service module" do + assert_raise UndefinedFunctionError, fn -> + Builder.build_reflection_tree([Enum]) + end + end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 1f95b20..6a0af57 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,8 +1 @@ -ExUnit.start() - -# include helloworld in tests -Application.put_env(:grpc_reflection, :services, [ - Helloworld.Greeter.Service, - Grpc.Reflection.V1.ServerReflection.Service, - Grpc.Reflection.V1alpha.ServerReflection.Service -]) +ExUnit.start(capture_log: true)