• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ahamez / protox / 65612d40bc356d808e24494fc4a7582368445548

11 Feb 2025 04:06PM UTC coverage: 94.755%. Remained the same
65612d40bc356d808e24494fc4a7582368445548

push

github

ahamez
refactor: inline encoding of oneof children

Benchmark show a slight speed increase for most of the cases

12 of 15 new or added lines in 1 file covered. (80.0%)

5 existing lines in 1 file now uncovered.

813 of 858 relevant lines covered (94.76%)

12569.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.67
/lib/protox/define_encoder.ex
1
defmodule Protox.DefineEncoder do
2
  @moduledoc false
3
  # Internal. Generates the encoder of a message.
4

5
  alias Protox.{Field, OneOf, Scalar}
6

7
  def define(fields, syntax, opts \\ []) do
8
    vars = %{
90✔
9
      acc: Macro.var(:acc, __MODULE__),
10
      acc_size: Macro.var(:acc_size, __MODULE__),
11
      child_field_value: Macro.var(:child_field_value, __MODULE__),
12
      msg: Macro.var(:msg, __MODULE__)
13
    }
14

15
    required_fields = get_required_fields(fields)
90✔
16

17
    %{oneofs: oneofs, proto3_optionals: proto3_optionals, others: fields_without_oneofs} =
90✔
18
      Protox.Defs.split_oneofs(fields)
19

20
    top_level_encode_fun =
90✔
21
      make_top_level_encode_fun(oneofs, proto3_optionals ++ fields_without_oneofs)
22

23
    encode_oneof_funs = make_encode_oneof_funs(oneofs, syntax, vars)
90✔
24
    encode_field_funs = make_encode_field_funs(fields, required_fields, syntax, vars)
90✔
25
    encode_unknown_fields_fun = make_encode_unknown_fields_fun(vars, opts)
90✔
26

27
    quote do
28
      unquote(top_level_encode_fun)
29
      unquote_splicing(encode_oneof_funs)
30
      unquote_splicing(encode_field_funs)
31
      unquote(encode_unknown_fields_fun)
32
    end
33
  end
34

35
  defp make_top_level_encode_fun(oneofs, fields) do
36
    quote(do: {_acc = [], _acc_size = 0})
37
    |> make_encode_oneof_fun(oneofs)
38
    |> make_encode_fun_field(fields)
39
    |> make_encode_fun_body()
90✔
40
  end
41

42
  defp make_encode_fun_body(ast) do
43
    quote do
44
      @spec encode(t()) :: {:ok, iodata(), non_neg_integer()} | {:error, any()}
45
      def encode(msg) do
46
        try do
47
          msg |> encode!() |> Tuple.insert_at(0, :ok)
48
        rescue
49
          e in [Protox.EncodingError, Protox.RequiredFieldsError] ->
50
            {:error, e}
51
        end
52
      end
53

54
      @spec encode!(t()) :: {iodata(), non_neg_integer()} | no_return()
55
      def encode!(msg), do: unquote(ast)
56
    end
57
  end
58

59
  defp make_encode_fun_field(ast, fields) do
60
    ast =
90✔
61
      Enum.reduce(fields, ast, fn %Protox.Field{} = field, ast_acc ->
62
        quote do
63
          unquote(ast_acc)
64
          |> unquote(make_encode_field_fun_name(field.name))(msg)
925✔
65
        end
66
      end)
67

68
    quote do
69
      unquote(ast) |> encode_unknown_fields(msg)
70
    end
71
  end
72

73
  defp make_encode_oneof_fun(ast, oneofs) do
74
    Enum.reduce(oneofs, ast, fn {parent_name, _children}, ast_acc ->
90✔
75
      quote do
76
        unquote(ast_acc)
77
        |> unquote(make_encode_field_fun_name(parent_name))(msg)
78
      end
79
    end)
80
  end
81

82
  defp make_encode_oneof_funs(oneofs, syntax, vars) do
83
    for {parent_name, children} <- oneofs do
90✔
84
      nil_clause =
5✔
85
        quote do
86
          nil -> {unquote(vars.acc), unquote(vars.acc_size)}
5✔
87
        end
88

89
      children_clauses_ast =
5✔
90
        Enum.flat_map(children, fn %Field{} = child_field ->
91
          encode_child_body = make_encode_field_body(child_field, false, syntax, vars)
50✔
92

93
          quote do
94
            {unquote(child_field.name), unquote(vars.child_field_value)} ->
50✔
95
              unquote(encode_child_body)
96
          end
97
        end)
98

99
      quote do
100
        defp unquote(make_encode_field_fun_name(parent_name))(
101
               {unquote(vars.acc), unquote(vars.acc_size)},
5✔
102
               msg
103
             ) do
104
          case msg.unquote(parent_name) do
105
            unquote(nil_clause ++ children_clauses_ast)
106
          end
107
        end
108
      end
109
    end
110
  end
111

112
  defp make_encode_field_funs(fields, required_fields, syntax, vars) do
113
    fields =
90✔
114
      Enum.reject(fields, fn
NEW
115
        %Field{label: :proto3_optional, kind: %OneOf{}} -> false
×
116
        %Field{kind: %OneOf{}} -> true
50✔
117
        _ -> false
925✔
118
      end)
119

120
    for %Field{name: name} = field <- fields do
90✔
121
      required = name in required_fields
925✔
122
      fun_name = make_encode_field_fun_name(name)
925✔
123
      fun_ast = make_encode_field_body(field, required, syntax, vars)
925✔
124

125
      quote do
126
        defp unquote(fun_name)({unquote(vars.acc), unquote(vars.acc_size)}, unquote(vars.msg)) do
925✔
127
          try do
128
            unquote(fun_ast)
129
          rescue
130
            ArgumentError ->
131
              reraise Protox.EncodingError.new(unquote(name), "invalid field value"),
132
                      __STACKTRACE__
133
          end
134
        end
135
      end
136
    end
137
  end
138

139
  defp make_encode_field_body(%Field{kind: %Scalar{}} = field, required, syntax, vars) do
140
    {key, key_size} = Protox.Encode.make_key_bytes(field.tag, field.type)
495✔
141
    var = quote do: unquote(vars.msg).unquote(field.name)
495✔
142
    encode_value_ast = get_encode_value_body(field.type, var)
495✔
143

144
    encode_value_clause =
495✔
145
      quote do
146
        {value_bytes, value_bytes_size} = unquote(encode_value_ast)
147

148
        {
149
          [unquote(key), value_bytes | unquote(vars.acc)],
495✔
150
          unquote(vars.acc_size) + unquote(key_size) + value_bytes_size
495✔
151
        }
152
      end
153

154
    case {syntax, required} do
495✔
155
      {:proto2, _required = true} ->
156
        quote do
157
          case unquote(vars.msg).unquote(field.name) do
10✔
158
            nil -> raise Protox.RequiredFieldsError.new([unquote(field.name)])
10✔
159
            _ -> unquote(encode_value_clause)
160
          end
161
        end
162

163
      {:proto2, _required = false} ->
164
        quote do
165
          case unquote(var) do
166
            nil -> {unquote(vars.acc), unquote(vars.acc_size)}
165✔
167
            _ -> unquote(encode_value_clause)
168
          end
169
        end
170

171
      {:proto3, _required} ->
172
        quote do
173
          # Use == rather than pattern match for float comparison
174
          if unquote(var) == unquote(field.kind.default_value) do
320✔
175
            {unquote(vars.acc), unquote(vars.acc_size)}
320✔
176
          else
177
            unquote(encode_value_clause)
178
          end
179
        end
180
    end
181
  end
182

183
  defp make_encode_field_body(
184
         %Field{label: :proto3_optional, kind: %OneOf{}} = field,
185
         _required,
186
         _syntax,
187
         vars
188
       ) do
UNCOV
189
    {key, key_size} = Protox.Encode.make_key_bytes(field.tag, field.type)
×
UNCOV
190
    var = Macro.var(:child_field_value, __MODULE__)
×
UNCOV
191
    encode_value_ast = get_encode_value_body(field.type, var)
×
192

193
    quote do
NEW
194
      case unquote(vars.msg).unquote(field.name) do
×
195
        nil ->
NEW
196
          {unquote(vars.acc), unquote(vars.acc_size)}
×
197

198
        unquote(var) ->
199
          {value_bytes, value_bytes_size} = unquote(encode_value_ast)
200

201
          {
UNCOV
202
            [unquote(key), value_bytes | unquote(vars.acc)],
×
UNCOV
203
            unquote(vars.acc_size) + unquote(key_size) + value_bytes_size
×
204
          }
205
      end
206
    end
207
  end
208

209
  defp make_encode_field_body(
210
         %Field{kind: %OneOf{}} = field,
211
         _required,
212
         _syntax,
213
         vars
214
       ) do
215
    {key, key_size} = Protox.Encode.make_key_bytes(field.tag, field.type)
50✔
216
    encode_value_ast = get_encode_value_body(field.type, vars.child_field_value)
50✔
217

218
    # The dispatch on the correct child is performed by the parent encoding function,
219
    # this is why we don't check if the child is set.
220
    quote do
221
      {value_bytes, value_bytes_size} = unquote(encode_value_ast)
222

223
      {
224
        [unquote(key), value_bytes | unquote(vars.acc)],
50✔
225
        unquote(vars.acc_size) + unquote(key_size) + value_bytes_size
50✔
226
      }
227
    end
228
  end
229

230
  defp make_encode_field_body(%Field{kind: :packed} = field, _required, _syntax, vars) do
231
    {key_bytes, key_size} = Protox.Encode.make_key_bytes(field.tag, :packed)
145✔
232
    encode_packed_ast = make_encode_packed_body(field.type)
145✔
233

234
    quote do
235
      case unquote(vars.msg).unquote(field.name) do
145✔
236
        [] ->
237
          {unquote(vars.acc), unquote(vars.acc_size)}
145✔
238

239
        values ->
240
          {packed_bytes, packed_size} = unquote(encode_packed_ast)
241

242
          {
243
            [unquote(key_bytes), packed_bytes | unquote(vars.acc)],
145✔
244
            unquote(vars.acc_size) + unquote(key_size) + packed_size
145✔
245
          }
246
      end
247
    end
248
  end
249

250
  defp make_encode_field_body(%Field{kind: :unpacked} = field, _required, _syntax, vars) do
251
    encode_repeated_ast = make_encode_repeated_body(field.tag, field.type)
190✔
252

253
    quote do
254
      case unquote(vars.msg).unquote(field.name) do
190✔
255
        [] ->
256
          {unquote(vars.acc), unquote(vars.acc_size)}
190✔
257

258
        values ->
259
          {value_bytes, value_size} = unquote(encode_repeated_ast)
260
          {[value_bytes | unquote(vars.acc)], unquote(vars.acc_size) + value_size}
190✔
261
      end
262
    end
263
  end
264

265
  defp make_encode_field_body(%Field{kind: :map} = field, _required, _syntax, vars) do
266
    # Each key/value entry of a map has the same layout as a message.
267
    # https://developers.google.com/protocol-buffers/docs/proto3#backwards-compatibility
268

269
    {field_key, field_key_size} = Protox.Encode.make_key_bytes(field.tag, :map_entry)
95✔
270

271
    {map_key_type, map_value_type} = field.type
95✔
272

273
    k_var = Macro.var(:k, __MODULE__)
95✔
274
    v_var = Macro.var(:v, __MODULE__)
95✔
275

276
    encode_map_key_ast = get_encode_value_body(map_key_type, k_var)
95✔
277
    encode_map_value_ast = get_encode_value_body(map_value_type, v_var)
95✔
278

279
    {k_key_bytes, k_key_size} = Protox.Encode.make_key_bytes(1, map_key_type)
95✔
280
    {v_key_bytes, v_key_size} = Protox.Encode.make_key_bytes(2, map_value_type)
95✔
281
    keys_len = k_key_size + v_key_size
95✔
282

283
    quote do
284
      map = Map.fetch!(unquote(vars.msg), unquote(field.name))
95✔
285

286
      if map_size(map) == 0 do
287
        {unquote(vars.acc), unquote(vars.acc_size)}
95✔
288
      else
289
        Enum.reduce(
290
          map,
291
          {unquote(vars.acc), unquote(vars.acc_size)},
95✔
292
          fn {unquote(k_var), unquote(v_var)}, {unquote(vars.acc), unquote(vars.acc_size)} ->
95✔
293
            {k_value_bytes, k_value_len} = unquote(encode_map_key_ast)
294
            {v_value_bytes, v_value_len} = unquote(encode_map_value_ast)
295

296
            len = unquote(keys_len) + k_value_len + v_value_len
297
            {len_varint, len_varint_size} = Protox.Varint.encode(len)
298

299
            unquote(vars.acc) = [
95✔
300
              <<unquote(field_key), len_varint::binary, unquote(k_key_bytes)>>,
301
              k_value_bytes,
302
              unquote(v_key_bytes),
303
              v_value_bytes
304
              | unquote(vars.acc)
95✔
305
            ]
306

307
            {
308
              unquote(vars.acc),
95✔
309
              unquote(vars.acc_size) + unquote(field_key_size + keys_len) + k_value_len +
95✔
310
                v_value_len + len_varint_size
311
            }
312
          end
313
        )
314
      end
315
    end
316
  end
317

318
  defp make_encode_unknown_fields_fun(vars, opts) do
319
    unknown_fields_name = Keyword.fetch!(opts, :unknown_fields_name)
90✔
320

321
    quote do
322
      defp encode_unknown_fields({unquote(vars.acc), unquote(vars.acc_size)}, msg) do
90✔
323
        Enum.reduce(
324
          msg.unquote(unknown_fields_name),
325
          {unquote(vars.acc), unquote(vars.acc_size)},
90✔
326
          fn {tag, wire_type, bytes}, {unquote(vars.acc), unquote(vars.acc_size)} ->
90✔
327
            case wire_type do
328
              0 ->
329
                {key_bytes, key_size} = Protox.Encode.make_key_bytes(tag, :int32)
330

331
                {
332
                  [unquote(vars.acc), <<key_bytes::binary, bytes::binary>>],
90✔
333
                  unquote(vars.acc_size) + key_size + byte_size(bytes)
90✔
334
                }
335

336
              1 ->
337
                {key_bytes, key_size} = Protox.Encode.make_key_bytes(tag, :double)
338

339
                {
340
                  [unquote(vars.acc), <<key_bytes::binary, bytes::binary>>],
90✔
341
                  unquote(vars.acc_size) + key_size + byte_size(bytes)
90✔
342
                }
343

344
              2 ->
345
                {len_bytes, len_size} = bytes |> byte_size() |> Protox.Varint.encode()
346
                {key_bytes, key_size} = Protox.Encode.make_key_bytes(tag, :packed)
347

348
                {
349
                  [unquote(vars.acc), <<key_bytes::binary, len_bytes::binary, bytes::binary>>],
90✔
350
                  unquote(vars.acc_size) + key_size + len_size + byte_size(bytes)
90✔
351
                }
352

353
              5 ->
354
                {key_bytes, key_size} = Protox.Encode.make_key_bytes(tag, :float)
355

356
                {
357
                  [unquote(vars.acc), <<key_bytes::binary, bytes::binary>>],
90✔
358
                  unquote(vars.acc_size) + key_size + byte_size(bytes)
90✔
359
                }
360
            end
361
          end
362
        )
363
      end
364
    end
365
  end
366

367
  defp make_encode_packed_body(type) do
368
    value_var = Macro.var(:value, __MODULE__)
145✔
369
    encode_value_ast = get_encode_value_body(type, value_var)
145✔
370

371
    quote do
372
      {value_bytes, value_size} =
373
        Enum.reduce(
374
          values,
375
          {_local_acc = [], _local_acc_size = 0},
376
          fn unquote(value_var), {local_acc, local_acc_size} ->
377
            {value_bytes, value_bytes_size} = unquote(encode_value_ast)
378

379
            {
380
              [value_bytes | local_acc],
381
              local_acc_size + value_bytes_size
382
            }
383
          end
384
        )
385

386
      {value_size_bytes, value_size_size} = Protox.Varint.encode(value_size)
387

388
      {[value_size_bytes, Enum.reverse(value_bytes)], value_size + value_size_size}
389
    end
390
  end
391

392
  defp make_encode_repeated_body(tag, type) do
393
    {key_bytes, key_bytes_sz} = Protox.Encode.make_key_bytes(tag, type)
190✔
394
    value_var = Macro.var(:value, __MODULE__)
190✔
395
    encode_value_ast = get_encode_value_body(type, value_var)
190✔
396

397
    quote do
398
      {value_bytes, value_size} =
399
        Enum.reduce(
400
          values,
401
          {_local_acc = [], _local_acc_size = 0},
402
          fn unquote(value_var), {local_acc, local_acc_size} ->
403
            {value_bytes, value_bytes_size} = unquote(encode_value_ast)
404

405
            {
406
              [value_bytes, unquote(key_bytes) | local_acc],
407
              local_acc_size + unquote(key_bytes_sz) + value_bytes_size
408
            }
409
          end
410
        )
411

412
      {Enum.reverse(value_bytes), value_size}
413
    end
414
  end
415

416
  defp get_encode_value_body({:message, _}, value_var) do
417
    quote do
418
      Protox.Encode.encode_message(unquote(value_var))
419
    end
420
  end
421

422
  defp get_encode_value_body({:enum, enum}, value_var) do
423
    quote do
424
      unquote(value_var) |> unquote(enum).encode() |> Protox.Encode.encode_enum()
425
    end
426
  end
427

428
  defp get_encode_value_body(:bool, value_var) do
429
    quote(do: Protox.Encode.encode_bool(unquote(value_var)))
430
  end
431

432
  defp get_encode_value_body(:bytes, value_var) do
433
    quote(do: Protox.Encode.encode_bytes(unquote(value_var)))
434
  end
435

436
  defp get_encode_value_body(:string, value_var) do
437
    quote(do: Protox.Encode.encode_string(unquote(value_var)))
438
  end
439

440
  defp get_encode_value_body(:int32, value_var) do
441
    quote(do: Protox.Encode.encode_int32(unquote(value_var)))
442
  end
443

444
  defp get_encode_value_body(:int64, value_var) do
445
    quote(do: Protox.Encode.encode_int64(unquote(value_var)))
446
  end
447

448
  defp get_encode_value_body(:uint32, value_var) do
449
    quote(do: Protox.Encode.encode_uint32(unquote(value_var)))
450
  end
451

452
  defp get_encode_value_body(:uint64, value_var) do
453
    quote(do: Protox.Encode.encode_uint64(unquote(value_var)))
454
  end
455

456
  defp get_encode_value_body(:sint32, value_var) do
457
    quote(do: Protox.Encode.encode_sint32(unquote(value_var)))
458
  end
459

460
  defp get_encode_value_body(:sint64, value_var) do
461
    quote(do: Protox.Encode.encode_sint64(unquote(value_var)))
462
  end
463

464
  defp get_encode_value_body(:fixed32, value_var) do
465
    quote(do: Protox.Encode.encode_fixed32(unquote(value_var)))
466
  end
467

468
  defp get_encode_value_body(:fixed64, value_var) do
469
    quote(do: Protox.Encode.encode_fixed64(unquote(value_var)))
470
  end
471

472
  defp get_encode_value_body(:sfixed32, value_var) do
473
    quote(do: Protox.Encode.encode_sfixed32(unquote(value_var)))
474
  end
475

476
  defp get_encode_value_body(:sfixed64, value_var) do
477
    quote(do: Protox.Encode.encode_sfixed64(unquote(value_var)))
478
  end
479

480
  defp get_encode_value_body(:float, value_var) do
481
    quote(do: Protox.Encode.encode_float(unquote(value_var)))
482
  end
483

484
  defp get_encode_value_body(:double, value_var) do
485
    quote(do: Protox.Encode.encode_double(unquote(value_var)))
486
  end
487

488
  defp make_encode_field_fun_name(field) when is_atom(field) do
489
    String.to_atom("encode_#{field}")
1,860✔
490
  end
491

492
  defp get_required_fields(fields) do
493
    for %Field{label: :required, name: name} <- fields, do: name
90✔
494
  end
495
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc