Skip to content

Commit e0f7de6

Browse files
committed
Create Protobuf.Presence module
Unifies presence tracking between encoders. Provides a public API for checking field presence
1 parent 8cbe462 commit e0f7de6

File tree

6 files changed

+310
-71
lines changed

6 files changed

+310
-71
lines changed

lib/protobuf/encoder.ex

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -77,35 +77,22 @@ defmodule Protobuf.Encoder do
7777
"Got error when encoding #{inspect(struct_mod)}##{prop.name_atom}: #{Exception.format(:error, error)}"
7878
end
7979

80-
defp skip_field?(_syntax, [], _prop), do: true
81-
defp skip_field?(_syntax, val, _prop) when is_map(val), do: map_size(val) == 0
82-
defp skip_field?(:proto2, nil, %FieldProps{optional?: optional?}), do: optional?
83-
defp skip_field?(:proto2, value, %FieldProps{default: value, oneof: nil}), do: true
84-
85-
defp skip_field?(:proto3, val, %FieldProps{proto3_optional?: true}),
86-
do: is_nil(val)
87-
88-
defp skip_field?(:proto3, nil, _prop), do: true
89-
defp skip_field?(:proto3, 0, %FieldProps{oneof: nil}), do: true
90-
defp skip_field?(:proto3, +0.0, %FieldProps{oneof: nil}), do: true
91-
defp skip_field?(:proto3, "", %FieldProps{oneof: nil}), do: true
92-
defp skip_field?(:proto3, false, %FieldProps{oneof: nil}), do: true
93-
94-
defp skip_field?(:proto3, value, %FieldProps{type: {:enum, enum_mod}, oneof: nil}) do
95-
enum_props = enum_mod.__message_props__()
96-
%{name_atom: name_atom, name: name} = enum_props.field_props[0]
97-
value == name_atom or value == name
80+
defp skip_field?(syntax, value, field_prop) do
81+
case Protobuf.Presence.get_field_presence(syntax, value, field_prop) do
82+
:present -> false
83+
# Proto2 required isn't skipped even if not present
84+
:maybe -> not(syntax == :proto2 && field_prop.required?)
85+
:not_present -> not(syntax == :proto2 && field_prop.required?)
86+
end
9887
end
9988

100-
defp skip_field?(_syntax, _val, _prop), do: false
101-
10289
defp do_encode_field(
10390
:normal,
10491
val,
10592
syntax,
10693
%FieldProps{encoded_fnum: fnum, type: type, repeated?: repeated?} = prop
10794
) do
108-
if skip_field?(syntax, val, prop) or skip_enum?(syntax, val, prop) do
95+
if skip_field?(syntax, val, prop) do
10996
:skip
11097
else
11198
iodata = apply_or_map(val, repeated?, &[fnum | Wire.encode(type, &1)])
@@ -143,7 +130,7 @@ defmodule Protobuf.Encoder do
143130
end
144131

145132
defp do_encode_field(:packed, val, syntax, %FieldProps{type: type, encoded_fnum: fnum} = prop) do
146-
if skip_field?(syntax, val, prop) or skip_enum?(syntax, val, prop) do
133+
if skip_field?(syntax, val, prop) do
147134
:skip
148135
else
149136
encoded = Enum.map(val, &Wire.encode(type, &1))
@@ -198,20 +185,6 @@ defmodule Protobuf.Encoder do
198185
defp apply_or_map(val, _repeated? = true, func), do: Enum.map(val, func)
199186
defp apply_or_map(val, _repeated? = false, func), do: func.(val)
200187

201-
defp skip_enum?(:proto2, _value, _prop), do: false
202-
defp skip_enum?(:proto3, _value, %FieldProps{proto3_optional?: true}), do: false
203-
defp skip_enum?(_syntax, _value, %FieldProps{enum?: false}), do: false
204-
205-
defp skip_enum?(_syntax, _value, %FieldProps{enum?: true, oneof: oneof}) when not is_nil(oneof),
206-
do: false
207-
208-
defp skip_enum?(_syntax, _value, %FieldProps{required?: true}), do: false
209-
defp skip_enum?(_syntax, value, %FieldProps{type: type}), do: enum_default?(type, value)
210-
211-
defp enum_default?({:enum, enum_mod}, val) when is_atom(val), do: enum_mod.value(val) == 0
212-
defp enum_default?({:enum, _enum_mod}, val) when is_integer(val), do: val == 0
213-
defp enum_default?({:enum, _enum_mod}, list) when is_list(list), do: false
214-
215188
# Returns a map of %{field_name => field_value} from oneofs. For example, if you have:
216189
# oneof body {
217190
# string a = 1;

lib/protobuf/json/encode.ex

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ defmodule Protobuf.JSON.Encode do
162162
defp encode_regular_fields(struct, %{field_props: field_props, syntax: syntax}, opts) do
163163
for {_field_num, %{name_atom: name, oneof: nil} = prop} <- field_props,
164164
%{^name => value} = struct,
165-
emit?(syntax, prop, value) || opts[:emit_unpopulated] do
165+
emit?(syntax, prop, value, opts[:emit_unpopulated]) do
166166
encode_field(prop, value, opts)
167167
end
168168
end
@@ -301,18 +301,17 @@ defmodule Protobuf.JSON.Encode do
301301
defp maybe_repeat(%{repeated?: false}, val, fun), do: fun.(val)
302302
defp maybe_repeat(%{repeated?: true}, val, fun), do: Enum.map(val, fun)
303303

304-
defp emit?(:proto2, %{default: value}, value), do: false
305-
defp emit?(:proto2, %{optional?: true}, val), do: not is_nil(val)
306-
defp emit?(:proto3, %{proto3_optional?: true}, val), do: not is_nil(val)
307-
defp emit?(_syntax, _prop, +0.0), do: false
308-
defp emit?(_syntax, _prop, nil), do: false
309-
defp emit?(_syntax, _prop, 0), do: false
310-
defp emit?(_syntax, _prop, false), do: false
311-
defp emit?(_syntax, _prop, []), do: false
312-
defp emit?(_syntax, _prop, ""), do: false
313-
defp emit?(_syntax, _prop, %{} = map) when map_size(map) == 0, do: false
314-
defp emit?(_syntax, %{type: {:enum, enum}}, key) when is_atom(key), do: enum.value(key) != 0
315-
defp emit?(_syntax, _prop, _value), do: true
304+
defp emit?(_syntax, _field_prop, _value, true = _emit_unpopulated?) do
305+
true
306+
end
307+
308+
defp emit?(syntax, field_prop, value, _emit_unpopulated?) do
309+
case Protobuf.Presence.get_field_presence(syntax, value, field_prop) do
310+
:present -> true
311+
:maybe -> false
312+
:not_present -> false
313+
end
314+
end
316315

317316
defp transform_module(message, module) do
318317
if transform_module = module.transform_module() do

lib/protobuf/presence.ex

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
defmodule Protobuf.Presence do
2+
@moduledoc """
3+
Helpers for determining Protobuf field presence.
4+
"""
5+
6+
alias Protobuf.FieldProps
7+
8+
@doc """
9+
Returns whether a field or oneof is present, not present, or maybe present
10+
11+
`:present` and `:not present` mean that a field is **explicitly** present or not,
12+
respectively.
13+
14+
Some values may be implicitly present. For example, lists in `repeated` fields
15+
always have implicit presence. In these cases, if the presence is ambiguous,
16+
returns `:maybe`.
17+
"""
18+
@spec field_presence(Protobuf.Message.t(), atom()) :: :present | :not_present | :maybe
19+
def field_presence(%mod{} = message, field) do
20+
message_props = mod.__message_props__()
21+
transformed_message = transform_module(message, mod)
22+
fnum = Map.fetch!(message_props.field_tags, field)
23+
field_prop = Map.fetch!(message_props.field_props, fnum)
24+
value = get_oneof_value(transformed_message, message_props, field, field_prop)
25+
26+
transformed_value =
27+
case field_prop do
28+
%{embedded: true, type: mod} -> transform_module(value, mod)
29+
_ -> value
30+
end
31+
32+
get_field_presence(message_props.syntax, transformed_value, field_prop)
33+
end
34+
35+
defp get_oneof_value(message, message_props, field, field_prop) do
36+
case field_prop.oneof do
37+
nil ->
38+
Map.fetch!(message, field)
39+
40+
oneof_num ->
41+
{oneof_field, _} = Enum.find(message_props.oneof, fn {_name, tag} -> tag == oneof_num end)
42+
43+
case Map.fetch!(message, oneof_field) do
44+
{^field, value} -> value
45+
_ -> nil
46+
end
47+
end
48+
end
49+
50+
defp transform_module(message, module) do
51+
if transform_module = module.transform_module() do
52+
transform_module.encode(message, module)
53+
else
54+
message
55+
end
56+
end
57+
58+
# We probably want to make this public eventually, but it makes sense to hold
59+
# it until we add editions support, since we definitely don't want to add
60+
# `syntax` in a public API
61+
@doc false
62+
@spec get_field_presence(:proto2 | :proto3, term(), FieldProps.t()) :: :present | :not_present | :maybe
63+
def get_field_presence(syntax, value, field_prop)
64+
65+
# Repeated and maps are always implicit.
66+
def get_field_presence(_syntax, [], _prop) do
67+
:maybe
68+
end
69+
70+
def get_field_presence(_syntax, val, _prop) when is_map(val) do
71+
if map_size(val) == 0 do
72+
:maybe
73+
else
74+
:present
75+
end
76+
end
77+
78+
# For proto2 singular cardinality fields:
79+
#
80+
# - Non-one_of fields with default values have implicit presence
81+
# - Others have explicit presence
82+
def get_field_presence(:proto2, nil, _prop) do
83+
:not_present
84+
end
85+
86+
def get_field_presence(:proto2, value, %FieldProps{default: value, oneof: nil}) do
87+
:maybe
88+
end
89+
90+
def get_field_presence(:proto2, _value, _props) do
91+
:present
92+
end
93+
94+
# For proto3 singular cardinality fields:
95+
#
96+
# - Optional and Oneof fields have explicit presence tracking
97+
# - Other fields have implicit presence tracking
98+
def get_field_presence(:proto3, nil, %FieldProps{proto3_optional?: true}) do
99+
:not_present
100+
end
101+
102+
def get_field_presence(:proto3, _, %FieldProps{proto3_optional?: true}) do
103+
:present
104+
end
105+
106+
def get_field_presence(_syntax, value, %FieldProps{oneof: oneof}) when not is_nil(oneof) do
107+
if is_nil(value) do
108+
:not_present
109+
else
110+
:present
111+
end
112+
end
113+
114+
# Defaults for different field types: implicit presence means they are maybe set
115+
def get_field_presence(:proto3, nil, _prop) do
116+
:maybe
117+
end
118+
119+
def get_field_presence(:proto3, 0, _prop) do
120+
:maybe
121+
end
122+
123+
def get_field_presence(:proto3, +0.0, _prop) do
124+
:maybe
125+
end
126+
127+
def get_field_presence(:proto3, "", _prop) do
128+
:maybe
129+
end
130+
131+
def get_field_presence(:proto3, false, _prop) do
132+
:maybe
133+
end
134+
135+
def get_field_presence(_syntax, value, %FieldProps{type: {:enum, enum_mod}}) do
136+
if enum_default?(enum_mod, value) do
137+
:maybe
138+
else
139+
:present
140+
end
141+
end
142+
143+
# Finally, everything else.
144+
def get_field_presence(_syntax, _val, _prop) do
145+
:present
146+
end
147+
148+
defp enum_default?(enum_mod, val) when is_atom(val), do: enum_mod.value(val) == 0
149+
defp enum_default?(_enum_mod, val) when is_integer(val), do: val == 0
150+
defp enum_default?(_enum_mod, list) when is_list(list), do: false
151+
end

lib/protobuf/text.ex

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -176,28 +176,14 @@ defmodule Protobuf.Text do
176176
end
177177
end
178178

179-
# Copied from Protobuf.Encoder. Should it be shared?
180-
defp skip_field?(_syntax, [], _prop), do: true
181-
defp skip_field?(_syntax, val, _prop) when is_map(val), do: map_size(val) == 0
182-
defp skip_field?(:proto2, nil, %FieldProps{optional?: optional?}), do: optional?
183-
defp skip_field?(:proto2, value, %FieldProps{default: value, oneof: nil}), do: true
184-
defp skip_field?(:proto3, val, %FieldProps{proto3_optional?: true}), do: is_nil(val)
185-
186-
defp skip_field?(:proto3, nil, _prop), do: true
187-
defp skip_field?(:proto3, 0, %FieldProps{oneof: nil}), do: true
188-
defp skip_field?(:proto3, +0.0, %FieldProps{oneof: nil}), do: true
189-
defp skip_field?(:proto3, "", %FieldProps{oneof: nil}), do: true
190-
defp skip_field?(:proto3, false, %FieldProps{oneof: nil}), do: true
191-
192-
# This is actually new. Should it be ported to Protobuf.Encoder?
193-
defp skip_field?(:proto3, value, %FieldProps{type: {:enum, enum_mod}, oneof: nil}) do
194-
enum_props = enum_mod.__message_props__()
195-
%{name_atom: name_atom, name: name, json_name: name_json} = enum_props.field_props[0]
196-
197-
value == name_atom or value == name or value == name_json
179+
defp skip_field?(syntax, value, field_prop) do
180+
case Protobuf.Presence.get_field_presence(syntax, value, field_prop) do
181+
:present -> false
182+
# Proto2 required isn't skipped even if not present
183+
:maybe -> not(syntax == :proto2 && field_prop.required?)
184+
:not_present -> not(syntax == :proto2 && field_prop.required?)
185+
end
198186
end
199187

200-
defp skip_field?(_, _, _), do: false
201-
202188
defp inspect_opts(), do: %Inspect.Opts{limit: :infinity}
203189
end

0 commit comments

Comments
 (0)