|
| 1 | +defmodule GrpcReflection.Service.Builder.Synthesizer do |
| 2 | + @moduledoc false |
| 3 | + |
| 4 | + # Builds a FieldDescriptorProto from a Protobuf.FieldProps. |
| 5 | + # Note: oneof_index is NOT set here for proto3_optional fields — those synthetic |
| 6 | + # oneof indices are assigned in message_descriptor/1 where all fields are visible. |
| 7 | + # syntax must be passed explicitly when building from message context; proto3 repeated |
| 8 | + # scalars have packed?: true in FieldProps but must NOT set options.packed (implicit). |
| 9 | + def field_descriptor_from_props(%Protobuf.FieldProps{} = props, syntax \\ :proto3) do |
| 10 | + {type, type_name} = resolve_type(props) |
| 11 | + |
| 12 | + %Google.Protobuf.FieldDescriptorProto{ |
| 13 | + name: props.name, |
| 14 | + number: props.fnum, |
| 15 | + json_name: props.json_name, |
| 16 | + type: type, |
| 17 | + type_name: type_name, |
| 18 | + label: resolve_label(props), |
| 19 | + default_value: encode_default(props.default), |
| 20 | + oneof_index: props.oneof, |
| 21 | + options: field_options(props, syntax), |
| 22 | + proto3_optional: props.proto3_optional?, |
| 23 | + extendee: nil |
| 24 | + } |
| 25 | + end |
| 26 | + |
| 27 | + def message_descriptor(module) do |
| 28 | + props = module.__message_props__() |
| 29 | + |
| 30 | + fields = |
| 31 | + props.ordered_tags |
| 32 | + |> Enum.map(&props.field_props[&1]) |
| 33 | + |> Enum.map(&field_descriptor_from_props(&1, props.syntax)) |
| 34 | + |> assign_proto3_optional_oneof_indices(length(props.oneof)) |
| 35 | + |
| 36 | + real_oneof_decls = |
| 37 | + Enum.map(props.oneof, fn {name, _tag} -> |
| 38 | + %Google.Protobuf.OneofDescriptorProto{name: Atom.to_string(name)} |
| 39 | + end) |
| 40 | + |
| 41 | + synthetic_oneof_decls = |
| 42 | + fields |
| 43 | + |> Enum.filter(& &1.proto3_optional) |
| 44 | + |> Enum.map(fn f -> |
| 45 | + %Google.Protobuf.OneofDescriptorProto{name: "_#{f.name}"} |
| 46 | + end) |
| 47 | + |
| 48 | + extension_ranges = |
| 49 | + (props.extension_range || []) |
| 50 | + |> Enum.map(fn {start, stop} -> |
| 51 | + %Google.Protobuf.DescriptorProto.ExtensionRange{start: start, end: stop} |
| 52 | + end) |
| 53 | + |
| 54 | + short_name = module.full_name() |> String.split(".") |> List.last() |
| 55 | + |
| 56 | + %Google.Protobuf.DescriptorProto{ |
| 57 | + name: short_name, |
| 58 | + field: fields, |
| 59 | + oneof_decl: real_oneof_decls ++ synthetic_oneof_decls, |
| 60 | + extension_range: extension_ranges, |
| 61 | + # nested_type is always [] in synthesized descriptors. All message types — including |
| 62 | + # map-entry types and any inline nested message definitions in the original .proto — |
| 63 | + # are discovered via field traversal in builder.ex and emitted as separate files. |
| 64 | + # This differs from protoc output (which embeds nested types in the parent descriptor) |
| 65 | + # but is handled correctly by standard reflection clients. |
| 66 | + nested_type: [], |
| 67 | + enum_type: [] |
| 68 | + } |
| 69 | + end |
| 70 | + |
| 71 | + def enum_descriptor(module) do |
| 72 | + short_name = module.full_name() |> String.split(".") |> List.last() |
| 73 | + |
| 74 | + # mapping() is a map so has no intrinsic order; __message_props__.ordered_tags is |
| 75 | + # numerically sorted. Neither preserves proto declaration order, so we sort by value. |
| 76 | + # This differs from protoc output for enums with non-monotonic declaration order |
| 77 | + # (e.g. negative sentinel values declared after positive ones), but reflection |
| 78 | + # clients look up enum values by number, not position, so this is safe in practice. |
| 79 | + values = |
| 80 | + module.__message_props__().ordered_tags |
| 81 | + |> Enum.map(fn tag -> |
| 82 | + props = module.__message_props__().field_props[tag] |
| 83 | + %Google.Protobuf.EnumValueDescriptorProto{name: props.name, number: props.fnum} |
| 84 | + end) |
| 85 | + |
| 86 | + %Google.Protobuf.EnumDescriptorProto{name: short_name, value: values} |
| 87 | + end |
| 88 | + |
| 89 | + def service_descriptor(module) do |
| 90 | + service_name = module.__meta__(:name) |> String.split(".") |> List.last() |
| 91 | + |
| 92 | + methods = |
| 93 | + module.__rpc_calls__() |
| 94 | + |> Enum.map(fn |
| 95 | + {method, {req, req_stream}, {resp, resp_stream}} -> |
| 96 | + build_method_descriptor(method, req, req_stream, resp, resp_stream) |
| 97 | + |
| 98 | + {method, {req, req_stream}, {resp, resp_stream}, _opts} -> |
| 99 | + build_method_descriptor(method, req, req_stream, resp, resp_stream) |
| 100 | + end) |
| 101 | + |
| 102 | + %Google.Protobuf.ServiceDescriptorProto{name: service_name, method: methods} |
| 103 | + end |
| 104 | + |
| 105 | + defp resolve_label(%Protobuf.FieldProps{required?: true}), do: :LABEL_REQUIRED |
| 106 | + defp resolve_label(%Protobuf.FieldProps{repeated?: true}), do: :LABEL_REPEATED |
| 107 | + defp resolve_label(%Protobuf.FieldProps{map?: true}), do: :LABEL_REPEATED |
| 108 | + defp resolve_label(_), do: :LABEL_OPTIONAL |
| 109 | + |
| 110 | + defp resolve_type(%Protobuf.FieldProps{enum?: true, type: {:enum, mod}}) do |
| 111 | + {:TYPE_ENUM, "." <> mod.full_name()} |
| 112 | + end |
| 113 | + |
| 114 | + defp resolve_type(%Protobuf.FieldProps{embedded?: true, type: mod}) when is_atom(mod) do |
| 115 | + {:TYPE_MESSAGE, "." <> mod.full_name()} |
| 116 | + end |
| 117 | + |
| 118 | + defp resolve_type(%Protobuf.FieldProps{type: type}) do |
| 119 | + {:"TYPE_#{type |> Atom.to_string() |> String.upcase()}", nil} |
| 120 | + end |
| 121 | + |
| 122 | + # proto3 repeated scalars are implicitly packed — reflection clients infer this from |
| 123 | + # syntax, so options.packed must not be set (matches real protoc output). |
| 124 | + defp field_options(props, syntax) do |
| 125 | + packed = props.packed? == true and syntax == :proto2 |
| 126 | + deprecated = props.deprecated? == true |
| 127 | + |
| 128 | + case {packed, deprecated} do |
| 129 | + {false, false} -> |
| 130 | + nil |
| 131 | + |
| 132 | + {packed, deprecated} -> |
| 133 | + %Google.Protobuf.FieldOptions{packed: packed || nil, deprecated: deprecated || nil} |
| 134 | + end |
| 135 | + end |
| 136 | + |
| 137 | + defp encode_default(nil), do: nil |
| 138 | + defp encode_default(v) when is_binary(v), do: v |
| 139 | + defp encode_default(v) when is_boolean(v), do: to_string(v) |
| 140 | + defp encode_default(v) when is_integer(v), do: Integer.to_string(v) |
| 141 | + defp encode_default(v) when is_float(v), do: Float.to_string(v) |
| 142 | + defp encode_default(v) when is_atom(v), do: Atom.to_string(v) |
| 143 | + |
| 144 | + # proto3_optional fields have no oneof in __message_props__ (the runtime doesn't need |
| 145 | + # the synthetic oneof) but the wire format and reflection clients expect oneof_index to |
| 146 | + # point at a synthetic "_fieldname" oneof entry appended after any real oneofs. |
| 147 | + defp assign_proto3_optional_oneof_indices(fields, real_oneof_count) do |
| 148 | + fields |
| 149 | + |> Enum.map_reduce(0, fn field, counter -> |
| 150 | + if field.proto3_optional do |
| 151 | + {%{field | oneof_index: real_oneof_count + counter}, counter + 1} |
| 152 | + else |
| 153 | + {field, counter} |
| 154 | + end |
| 155 | + end) |
| 156 | + |> elem(0) |
| 157 | + end |
| 158 | + |
| 159 | + defp build_method_descriptor(method, req, req_stream, resp, resp_stream) do |
| 160 | + %Google.Protobuf.MethodDescriptorProto{ |
| 161 | + name: Atom.to_string(method), |
| 162 | + input_type: "." <> req.full_name(), |
| 163 | + output_type: "." <> resp.full_name(), |
| 164 | + client_streaming: req_stream, |
| 165 | + server_streaming: resp_stream |
| 166 | + } |
| 167 | + end |
| 168 | +end |
0 commit comments