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

ahamez / protox / 0d2d5df473c2035009530f29b49a48e57378e364

03 Feb 2025 12:40PM UTC coverage: 94.042%. First build
0d2d5df473c2035009530f29b49a48e57378e364

push

github

ahamez
WIP

71 of 74 new or added lines in 6 files covered. (95.95%)

805 of 856 relevant lines covered (94.04%)

220572.78 hits per line

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

95.74
/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, required_fields, syntax, opts \\ []) do
8
    vars = %{
90✔
9
      acc: Macro.var(:acc, __MODULE__),
10
      acc_size: Macro.var(:acc_size, __MODULE__),
11
      msg: Macro.var(:msg, __MODULE__)
12
    }
13

14
    %{oneofs: oneofs, proto3_optionals: proto3_optionals, others: fields_without_oneofs} =
90✔
15
      Protox.Defs.split_oneofs(fields)
16

17
    top_level_encode_fun =
90✔
18
      make_top_level_encode_fun(oneofs, proto3_optionals ++ fields_without_oneofs)
19

20
    encode_oneof_funs = make_encode_oneof_funs(oneofs)
90✔
21
    encode_field_funs = make_encode_field_funs(fields, required_fields, syntax, vars)
90✔
22
    encode_unknown_fields_fun = make_encode_unknown_fields_fun(vars, opts)
90✔
23

24
    quote do
25
      _generator = unquote(make_generator(__ENV__))
26
      unquote(top_level_encode_fun)
27
      unquote_splicing(encode_oneof_funs)
28
      unquote_splicing(encode_field_funs)
29
      unquote(encode_unknown_fields_fun)
30
    end
31
  end
32

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

40
  defp make_encode_fun_body(ast) do
41
    quote do
42
      @spec encode(struct()) :: {:ok, iodata(), non_neg_integer()} | {:error, any()}
43
      def encode(msg) do
44
        _generator = unquote(make_generator(__ENV__))
45

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!(struct()) :: {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) do
83
    for {parent_name, children} <- oneofs do
90✔
84
      nil_clause =
5✔
85
        quote do
86
          nil -> acc
87
        end
88

89
      children_clauses_ast =
5✔
90
        Enum.flat_map(children, fn %Field{} = child_field ->
91
          encode_child_fun_name = make_encode_field_fun_name(child_field.name)
50✔
92

93
          quote do
94
            {unquote(child_field.name), _field_value} -> unquote(encode_child_fun_name)(acc, msg)
50✔
95
          end
96
        end)
97

98
      quote do
99
        defp unquote(make_encode_field_fun_name(parent_name))(acc, msg) do
100
          case msg.unquote(parent_name) do
101
            unquote(nil_clause ++ children_clauses_ast)
102
          end
103
        end
104
      end
105
    end
106
  end
107

108
  defp make_encode_field_funs(fields, required_fields, syntax, vars) do
109
    for %Field{name: name} = field <- fields do
90✔
110
      required = name in required_fields
975✔
111
      fun_name = make_encode_field_fun_name(name)
975✔
112
      fun_ast = make_encode_field_body(field, required, syntax, vars)
975✔
113

114
      quote do
115
        defp unquote(fun_name)({unquote(vars.acc), unquote(vars.acc_size)}, unquote(vars.msg)) do
975✔
116
          try do
117
            unquote(fun_ast)
118
          rescue
119
            ArgumentError ->
120
              reraise Protox.EncodingError.new(unquote(name), "invalid field value"),
121
                      __STACKTRACE__
122
          end
123
        end
124
      end
125
    end
126
  end
127

128
  defp make_encode_field_body(%Field{kind: %Scalar{}} = field, required, syntax, vars) do
129
    {key, key_size} = Protox.Encode.make_key_bytes(field.tag, field.type)
495✔
130
    var = quote do: unquote(vars.msg).unquote(field.name)
495✔
131
    encode_value_ast = get_encode_value_body(field.type, var)
495✔
132

133
    encode_value_clause =
495✔
134
      quote do
135
        {value_bytes, value_bytes_size} = unquote(encode_value_ast)
136

137
        {
138
          [unquote(key), value_bytes | unquote(vars.acc)],
495✔
139
          unquote(vars.acc_size) + unquote(key_size) + value_bytes_size
495✔
140
        }
141
      end
142

143
    case syntax do
495✔
144
      :proto2 ->
145
        if required do
175✔
146
          quote do
147
            case unquote(vars.msg).unquote(field.name) do
10✔
148
              nil -> raise Protox.RequiredFieldsError.new([unquote(field.name)])
10✔
149
              _ -> unquote(encode_value_clause)
150
            end
151
          end
152
        else
153
          quote do
154
            case unquote(var) do
155
              nil -> {unquote(vars.acc), unquote(vars.acc_size)}
165✔
156
              _ -> unquote(encode_value_clause)
157
            end
158
          end
159
        end
160

161
      :proto3 ->
162
        quote do
163
          # Use == rather than pattern match for float comparison
164
          if unquote(var) == unquote(field.kind.default_value) do
320✔
165
            {unquote(vars.acc), unquote(vars.acc_size)}
320✔
166
          else
167
            unquote(encode_value_clause)
168
          end
169
        end
170
    end
171
  end
172

173
  # Generate the AST to encode child `field.name` of a oneof
174
  defp make_encode_field_body(
175
         %Field{kind: %OneOf{}} = field,
176
         _required,
177
         _syntax,
178
         vars
179
       ) do
180
    {key, key_size} = Protox.Encode.make_key_bytes(field.tag, field.type)
50✔
181
    var = Macro.var(:child_field_value, __MODULE__)
50✔
182
    encode_value_ast = get_encode_value_body(field.type, var)
50✔
183

184
    case field.label do
50✔
185
      :proto3_optional ->
186
        quote do
187
          case unquote(vars.msg).unquote(field.name) do
×
188
            nil ->
NEW
189
              {unquote(vars.acc), unquote(vars.acc_size)}
×
190

191
            unquote(var) ->
192
              {value_bytes, value_bytes_size} = unquote(encode_value_ast)
193

194
              {
NEW
195
                [unquote(key), value_bytes | unquote(vars.acc)],
×
NEW
196
                unquote(vars.acc_size) + unquote(key_size) + value_bytes_size
×
197
              }
198
          end
199
        end
200

201
      _ ->
202
        # The dispatch on the correct child is performed by the parent encoding function,
203
        # this is why we don't check if the child is set.
204
        quote do
205
          {_, unquote(var)} = unquote(vars.msg).unquote(field.kind.parent)
50✔
206
          {value_bytes, value_bytes_size} = unquote(encode_value_ast)
207

208
          {
209
            [unquote(key), value_bytes | unquote(vars.acc)],
50✔
210
            unquote(vars.acc_size) + unquote(key_size) + value_bytes_size
50✔
211
          }
212
        end
213
    end
214
  end
215

216
  # TODO: repeated packed are always made of scalar, exploit this?
217
  defp make_encode_field_body(%Field{kind: :packed} = field, _required, _syntax, vars) do
218
    {key_bytes, key_size} = Protox.Encode.make_key_bytes(field.tag, :packed)
145✔
219
    encode_packed_ast = make_encode_packed_body(field.type, vars)
145✔
220

221
    quote do
222
      _generator = unquote(make_generator(__ENV__))
223

224
      case unquote(vars.msg).unquote(field.name) do
145✔
225
        [] ->
226
          {unquote(vars.acc), unquote(vars.acc_size)}
145✔
227

228
        values ->
229
          {packed_bytes, packed_size} = unquote(encode_packed_ast)
230

231
          {
232
            [unquote(key_bytes), packed_bytes | unquote(vars.acc)],
145✔
233
            unquote(vars.acc_size) + unquote(key_size) + packed_size
145✔
234
          }
235
      end
236
    end
237
  end
238

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

242
    quote do
243
      _generator = unquote(make_generator(__ENV__))
244

245
      case unquote(vars.msg).unquote(field.name) do
190✔
246
        [] -> {unquote(vars.acc), unquote(vars.acc_size)}
190✔
247
        values -> unquote(encode_repeated_ast)
248
      end
249
    end
250
  end
251

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

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

258
    {map_key_type, map_value_type} = field.type
95✔
259

260
    k_var = Macro.var(:k, __MODULE__)
95✔
261
    v_var = Macro.var(:v, __MODULE__)
95✔
262

263
    encode_map_key_ast = get_encode_value_body(map_key_type, k_var)
95✔
264
    encode_map_value_ast = get_encode_value_body(map_value_type, v_var)
95✔
265

266
    {k_key_bytes, k_key_size} = Protox.Encode.make_key_bytes(1, map_key_type)
95✔
267
    {v_key_bytes, v_key_size} = Protox.Encode.make_key_bytes(2, map_value_type)
95✔
268
    keys_len = k_key_size + v_key_size
95✔
269

270
    quote do
271
      _generator = unquote(make_generator(__ENV__))
272

273
      map = Map.fetch!(unquote(vars.msg), unquote(field.name))
95✔
274

275
      if map_size(map) == 0 do
276
        {unquote(vars.acc), unquote(vars.acc_size)}
95✔
277
      else
278
        Enum.reduce(
279
          map,
280
          {unquote(vars.acc), unquote(vars.acc_size)},
95✔
281
          fn {unquote(k_var), unquote(v_var)}, {unquote(vars.acc), unquote(vars.acc_size)} ->
95✔
282
            {k_value_bytes, k_value_len} = unquote(encode_map_key_ast)
283
            {v_value_bytes, v_value_len} = unquote(encode_map_value_ast)
284

285
            len = unquote(keys_len) + k_value_len + v_value_len
286
            {len_varint, len_varint_size} = Protox.Varint.encode(len)
287

288
            unquote(vars.acc) = [
95✔
289
              <<unquote(field_key), len_varint::binary, unquote(k_key_bytes)>>,
290
              k_value_bytes,
291
              unquote(v_key_bytes),
292
              v_value_bytes
293
              | unquote(vars.acc)
95✔
294
            ]
295

296
            {
297
              unquote(vars.acc),
95✔
298
              unquote(vars.acc_size) + unquote(field_key_size + keys_len) + k_value_len +
95✔
299
                v_value_len + len_varint_size
300
            }
301
          end
302
        )
303
      end
304
    end
305
  end
306

307
  defp make_encode_unknown_fields_fun(vars, opts) do
308
    unknown_fields_name = Keyword.fetch!(opts, :unknown_fields_name)
90✔
309

310
    quote do
311
      defp encode_unknown_fields({unquote(vars.acc), unquote(vars.acc_size)}, msg) do
90✔
312
        _generator = unquote(make_generator(__ENV__))
313

314
        Enum.reduce(
315
          msg.unquote(unknown_fields_name),
316
          {unquote(vars.acc), unquote(vars.acc_size)},
90✔
317
          fn {tag, wire_type, bytes}, {unquote(vars.acc), unquote(vars.acc_size)} ->
90✔
318
            case wire_type do
319
              0 ->
320
                {key_bytes, key_size} = Protox.Encode.make_key_bytes(tag, :int32)
321

322
                {
323
                  [unquote(vars.acc), <<key_bytes::binary, bytes::binary>>],
90✔
324
                  unquote(vars.acc_size) + key_size + byte_size(bytes)
90✔
325
                }
326

327
              1 ->
328
                {key_bytes, key_size} = Protox.Encode.make_key_bytes(tag, :double)
329

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

335
              2 ->
336
                {len_bytes, len_size} = bytes |> byte_size() |> Protox.Varint.encode()
337
                {key_bytes, key_size} = Protox.Encode.make_key_bytes(tag, :packed)
338

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

344
              5 ->
345
                {key_bytes, key_size} = Protox.Encode.make_key_bytes(tag, :float)
346

347
                {
348
                  [unquote(vars.acc), <<key_bytes::binary, bytes::binary>>],
90✔
349
                  unquote(vars.acc_size) + key_size + byte_size(bytes)
90✔
350
                }
351
            end
352
          end
353
        )
354
      end
355
    end
356
  end
357

358
  # TODO, for fixed*, we know the size of the value, maybe we can exploit this?
359
  defp make_encode_packed_body(type, vars) do
360
    value_var = Macro.var(:value, __MODULE__)
145✔
361
    encode_value_ast = get_encode_value_body(type, value_var)
145✔
362

363
    quote do
364
      _generator = unquote(make_generator(__ENV__))
365

366
      {value_bytes, value_size} =
367
        Enum.reduce(
368
          values,
369
          {_local_acc = [], _local_acc_size = 0},
370
          fn unquote(value_var), {local_acc, local_acc_size} ->
371
            {value_bytes, value_bytes_size} = unquote(encode_value_ast)
372

373
            {
374
              [value_bytes | local_acc],
375
              local_acc_size + value_bytes_size
376
            }
377
          end
378
        )
379

380
      value_bytes = Enum.reverse(value_bytes) |> :binary.list_to_bin()
381
      {value_size_bytes, value_size_size} = Protox.Varint.encode(value_size)
382

383
      {
384
        [<<value_size_bytes::binary, value_bytes::binary>> | unquote(vars.acc)],
145✔
385
        unquote(vars.acc_size) + value_size + value_size_size
145✔
386
      }
387
    end
388
  end
389

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

395
    quote do
396
      _generator = unquote(make_generator(__ENV__))
397

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
      value_bytes = Enum.reverse(value_bytes)
413

414
      {
415
        [value_bytes | unquote(vars.acc)],
190✔
416
        unquote(vars.acc_size) + value_size
190✔
417
      }
418
    end
419
  end
420

421
  defp get_encode_value_body({:message, _}, value_var) do
422
    quote do
423
      Protox.Encode.encode_message(unquote(value_var))
424
    end
425
  end
426

427
  defp get_encode_value_body({:enum, enum}, value_var) do
428
    quote do
429
      unquote(value_var) |> unquote(enum).encode() |> Protox.Encode.encode_enum()
430
    end
431
  end
432

433
  defp get_encode_value_body(:bool, value_var) do
434
    quote(do: Protox.Encode.encode_bool(unquote(value_var)))
435
  end
436

437
  defp get_encode_value_body(:bytes, value_var) do
438
    quote(do: Protox.Encode.encode_bytes(unquote(value_var)))
439
  end
440

441
  defp get_encode_value_body(:string, value_var) do
442
    quote(do: Protox.Encode.encode_string(unquote(value_var)))
443
  end
444

445
  defp get_encode_value_body(:int32, value_var) do
446
    quote(do: Protox.Encode.encode_int32(unquote(value_var)))
447
  end
448

449
  defp get_encode_value_body(:int64, value_var) do
450
    quote(do: Protox.Encode.encode_int64(unquote(value_var)))
451
  end
452

453
  defp get_encode_value_body(:uint32, value_var) do
454
    quote(do: Protox.Encode.encode_uint32(unquote(value_var)))
455
  end
456

457
  defp get_encode_value_body(:uint64, value_var) do
458
    quote(do: Protox.Encode.encode_uint64(unquote(value_var)))
459
  end
460

461
  defp get_encode_value_body(:sint32, value_var) do
462
    quote(do: Protox.Encode.encode_sint32(unquote(value_var)))
463
  end
464

465
  defp get_encode_value_body(:sint64, value_var) do
466
    quote(do: Protox.Encode.encode_sint64(unquote(value_var)))
467
  end
468

469
  defp get_encode_value_body(:fixed32, value_var) do
470
    quote(do: Protox.Encode.encode_fixed32(unquote(value_var)))
471
  end
472

473
  defp get_encode_value_body(:fixed64, value_var) do
474
    quote(do: Protox.Encode.encode_fixed64(unquote(value_var)))
475
  end
476

477
  defp get_encode_value_body(:sfixed32, value_var) do
478
    quote(do: Protox.Encode.encode_sfixed32(unquote(value_var)))
479
  end
480

481
  defp get_encode_value_body(:sfixed64, value_var) do
482
    quote(do: Protox.Encode.encode_sfixed64(unquote(value_var)))
483
  end
484

485
  defp get_encode_value_body(:float, value_var) do
486
    quote(do: Protox.Encode.encode_float(unquote(value_var)))
487
  end
488

489
  defp get_encode_value_body(:double, value_var) do
490
    quote(do: Protox.Encode.encode_double(unquote(value_var)))
491
  end
492

493
  defp make_encode_field_fun_name(field) when is_atom(field) do
494
    String.to_atom("encode_#{field}")
1,960✔
495
  end
496

497
  defp make_generator(%Macro.Env{} = env) do
498
    {fun_name, _fun_arity} = env.function
1,035✔
499
    "#{fun_name}:#{env.line}"
1,035✔
500
  end
501
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