Skip to content

Commit fec5bf1

Browse files
authored
Create Protobuf.Presence module (#403)
* Create Protobuf.Presence module Unifies presence tracking between encoders. Provides a public API for checking field presence * Add test for messages, fix proto3 * Improve docs
1 parent 8cbe462 commit fec5bf1

File tree

7 files changed

+356
-73
lines changed

7 files changed

+356
-73
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: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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+
For more information about field presence tracking rules, refer to the official
19+
[Field Presence docs](https://protobuf.dev/programming-guides/field_presence/).
20+
21+
22+
## Examples
23+
24+
# Non-optional proto3 field:
25+
Protobuf.Presence(%MyMessage{foo: 42}, :foo)
26+
#=> :present
27+
28+
Protobuf.Presence(%MyMessage{foo: 0}, :foo)
29+
#=> :maybe
30+
31+
Protobuf.Presence(%MyMessage{}, :foo)
32+
#=> :maybe
33+
34+
# Optional proto3 field:
35+
Protobuf.Presence(%MyMessage{bar: 42}, :bar)
36+
#=> :present
37+
38+
Protobuf.Presence(%MyMessage{bar: 0}, :bar)
39+
#=> :present
40+
41+
Protobuf.Presence(%MyMessage{}, :bar)
42+
#=> :not_present
43+
44+
"""
45+
@spec field_presence(message :: struct(), field :: atom()) :: :present | :not_present | :maybe
46+
def field_presence(%mod{} = message, field) do
47+
message_props = mod.__message_props__()
48+
transformed_message = transform_module(message, mod)
49+
fnum = Map.fetch!(message_props.field_tags, field)
50+
field_prop = Map.fetch!(message_props.field_props, fnum)
51+
value = get_oneof_value(transformed_message, message_props, field, field_prop)
52+
53+
transformed_value =
54+
case field_prop do
55+
%{embedded: true, type: mod} -> transform_module(value, mod)
56+
_ -> value
57+
end
58+
59+
get_field_presence(message_props.syntax, transformed_value, field_prop)
60+
end
61+
62+
defp get_oneof_value(message, message_props, field, field_prop) do
63+
case field_prop.oneof do
64+
nil ->
65+
Map.fetch!(message, field)
66+
67+
oneof_num ->
68+
{oneof_field, _} = Enum.find(message_props.oneof, fn {_name, tag} -> tag == oneof_num end)
69+
70+
case Map.fetch!(message, oneof_field) do
71+
{^field, value} -> value
72+
_ -> nil
73+
end
74+
end
75+
end
76+
77+
defp transform_module(message, module) do
78+
if transform_module = module.transform_module() do
79+
transform_module.encode(message, module)
80+
else
81+
message
82+
end
83+
end
84+
85+
# We probably want to make this public eventually, but it makes sense to hold
86+
# it until we add editions support, since we definitely don't want to add
87+
# `syntax` in a public API
88+
@doc false
89+
@spec get_field_presence(:proto2 | :proto3, term(), FieldProps.t()) :: :present | :not_present | :maybe
90+
def get_field_presence(syntax, value, field_prop)
91+
92+
# Repeated and maps are always implicit.
93+
def get_field_presence(_syntax, [], _prop) do
94+
:maybe
95+
end
96+
97+
def get_field_presence(_syntax, val, _prop) when is_map(val) do
98+
if map_size(val) == 0 do
99+
:maybe
100+
else
101+
:present
102+
end
103+
end
104+
105+
# For proto2 singular cardinality fields:
106+
#
107+
# - Non-one_of fields with default values have implicit presence
108+
# - Others have explicit presence
109+
def get_field_presence(:proto2, nil, _prop) do
110+
:not_present
111+
end
112+
113+
def get_field_presence(:proto2, value, %FieldProps{default: value, oneof: nil}) do
114+
:maybe
115+
end
116+
117+
def get_field_presence(:proto2, _value, _props) do
118+
:present
119+
end
120+
121+
# For proto3 singular cardinality fields:
122+
#
123+
# - Optional and Oneof fields have explicit presence tracking
124+
# - Other fields have implicit presence tracking
125+
def get_field_presence(:proto3, nil, %FieldProps{proto3_optional?: true}) do
126+
:not_present
127+
end
128+
129+
def get_field_presence(:proto3, _, %FieldProps{proto3_optional?: true}) do
130+
:present
131+
end
132+
133+
def get_field_presence(_syntax, value, %FieldProps{oneof: oneof}) when not is_nil(oneof) do
134+
if is_nil(value) do
135+
:not_present
136+
else
137+
:present
138+
end
139+
end
140+
141+
# Messages have explicit presence tracking in proto3
142+
def get_field_presence(:proto3, nil, _prop) do
143+
:not_present
144+
end
145+
146+
# Defaults for different field types: implicit presence means they are maybe set
147+
def get_field_presence(:proto3, 0, _prop) do
148+
:maybe
149+
end
150+
151+
def get_field_presence(:proto3, +0.0, _prop) do
152+
:maybe
153+
end
154+
155+
def get_field_presence(:proto3, "", _prop) do
156+
:maybe
157+
end
158+
159+
def get_field_presence(:proto3, false, _prop) do
160+
:maybe
161+
end
162+
163+
def get_field_presence(_syntax, value, %FieldProps{type: {:enum, enum_mod}}) do
164+
if enum_default?(enum_mod, value) do
165+
:maybe
166+
else
167+
:present
168+
end
169+
end
170+
171+
# Finally, everything else.
172+
def get_field_presence(_syntax, _val, _prop) do
173+
:present
174+
end
175+
176+
defp enum_default?(enum_mod, val) when is_atom(val), do: enum_mod.value(val) == 0
177+
defp enum_default?(_enum_mod, val) when is_integer(val), do: val == 0
178+
defp enum_default?(_enum_mod, list) when is_list(list), do: false
179+
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)