Repository: elixir-protobuf/protobuf
Pull request #124 (2020-07-11 04:49:15)

Refactor wire format encoder and decoder
This PR supersedes #121, adding more performance improvements and also refactoring some parts:

* Make property-based tests agnostic to implementation details by preventing direct access to private concerns.
* Extract conversion between Proto and Wire format into its own `Protobuf.Wire` module.
* Extract Varint encoding/decoding logic into its own `Protobuf.Wire.Varint` module.
* Streamline and better isolate group-skipping logic.

### Benchmark results

```
##### With input google_message3_5 #####
Name                                              ips        average  deviation         median         99th %
decode (refactor-1cfbf81-decode)             398.30 K        2.51 μs  ±5473.04%           1 μs           4 μs
decode (optimize-packed-b11d326-decode)      343.00 K        2.92 μs  ±5263.15%           1 μs           3 μs
decode (master-3428d3a-decode)               287.16 K        3.48 μs  ±4507.40%           2 μs           4 μs

Comparison: 
decode (refactor-1cfbf81-decode)             398.30 K
decode (optimize-packed-b11d326-decode)      343.00 K - 1.16x slower +0.40 μs
decode (master-3428d3a-decode)               287.16 K - 1.39x slower +0.97 μs

Memory usage statistics:

Name                                       Memory usage
decode (refactor-1cfbf81-decode)                  632 B
decode (optimize-packed-b11d326-decode)           920 B - 1.46x memory usage +288 B
decode (master-3428d3a-decode)                   1280 B - 2.03x memory usage +648 B

##### With input google_message3_4 #####
Name                                              ips        average  deviation         median         99th %
decode (refactor-1cfbf81-decode)             382.05 K        2.62 μs  ±5187.68%           1 μs           4 μs
decode (optimize-packed-b11d326-decode)      336.45 K        2.97 μs  ±4952.23%           1 μs           4 μs
decode (master-3428d3a-decode)               286.98 K        3.48 μs  ±4476.92%           2 μs           4 μs

Comparison: 
decode (refactor-1cfbf81-decode)             382.05 K
decode (optimize-packed-b11d326-decode)      336.45 K - 1.18x slower +0.46 μs
decode (master-3428d3a-decode)               286.98 K - 1.39x slower +0.97 μs

Memory usage statistics:

Name                                       Memory usage
decode (refactor-1cfbf81-decode)                  632 B
decode (optimize-packed-b11d326-decode)           920 B - 1.46x memory usage +288 B
decode (master-3428d3a-decode)                   1280 B - 2.03x memory usage +648 B

##### With input google_message3_3 #####
Name                                              ips        average  deviation         median         99th %
decode (refactor-1cfbf81-decode)             203.75 K        4.91 μs  ±2697.16%           3 μs           6 μs
decode (optimize-packed-b11d326-decode)      180.60 K        5.54 μs  ±2681.47%           3 μs           5 μs
decode (master-3428d3a-decode)               151.92 K        6.58 μs  ±2447.36%           3 μs           6 μs

Comparison: 
decode (refactor-1cfbf81-decode)             203.75 K
decode (optimize-packed-b11d326-decode)      180.60 K - 2.21x slower +3.03 μs
decode (master-3428d3a-decode)               151.92 K - 2.62x slower +4.07 μs

Memory usage statistics:

Name                                       Memory usage
decode (refactor-1cfbf81-decode)                1.32 KB
decode (optimize-packed-b11d326-decode)         1.88 KB - 3.05x memory usage +1.27 KB
decode (master-3428d3a-decode)                  2.52 KB - 4.09x memory usage +1.91 KB

##### With input google_message4 #####
Name                                              ips        average  deviation         median         99th %
decode (refactor-1cfbf81-decode)             159.32 K        6.28 μs  ±2074.80%           4 μs           8 μs
decode (optimize-packed-b11d326-decode)      143.57 K        6.97 μs  ±2091.18%           4 μs           6 μs
decode (master-3428d3a-decode)               121.78 K        8.21 μs  ±1876.36%           4 μs           9 μs

Comparison: 
decode (refactor-1cfbf81-decode)             159.32 K
decode (optimize-packed-b11d326-decode)      143.57 K - 2.77x slower +4.45 μs
decode (master-3428d3a-decode)               121.78 K - 3.27x slower +5.70 μs

Memory usage statistics:

Name                                       Memory usage
decode (refactor-1cfbf81-decode)                1.66 KB
decode (optimize-packed-b11d326-decode)         2.31 KB - 3.75x memory usage +1.70 KB
decode (master-3428d3a-decode)                  3.02 KB - 4.90x memory usage +2.41 KB

##### With input google_message1_proto2 #####
Name                                              ips        average  deviation         median         99th %
decode (refactor-1cfbf81-decode)              66.30 K       15.08 μs   ±924.67%           9 μs          22 μs
decode (optimize-packed-b11d326-decode)       60.27 K       16.59 μs   ±980.27%           8 μs          32 μs
decode (master-3428d3a-decode)                51.89 K       19.27 μs   ±883.01%           9 μs          37 μs

Comparison: 
decode (refactor-1cfbf81-decode)              66.30 K
decode (optimize-packed-b11d326-decode)       60.27 K - 6.61x slower +14.08 μs
decode (master-3428d3a-decode)                51.89 K - 7.68x slower +16.76 μs

Memory usage statistics:

Name                                       Memory usage
decode (refactor-1cfbf81-decode)                5.23 KB
decode (optimize-packed-b11d326-decode)         7.03 KB - 11.39x memory usage +6.41 KB
decode (master-3428d3a-decode)                  8.48 KB - 13.75x memory usage +7.87 KB

##### With input google_message1_proto3 #####
Name                                              ips        average  deviation         median         99th %
decode (refactor-1cfbf81-decode)              65.87 K       15.18 μs   ±921.78%           9 μs          23 μs
decode (optimize-packed-b11d326-decode)       64.86 K       15.42 μs  ±1047.99%           8 μs          16 μs
decode (master-3428d3a-decode)                54.42 K       18.38 μs   ±939.82%           9 μs          20 μs

Comparison: 
decode (refactor-1cfbf81-decode)              65.87 K
decode (optimize-packed-b11d326-decode)       64.86 K - 6.14x slower +12.91 μs
decode (master-3428d3a-decode)                54.42 K - 7.32x slower +15.87 μs

Memory usage statistics:

Name                                       Memory usage
decode (refactor-1cfbf81-decode)                5.23 KB
decode (optimize-packed-b11d326-decode)         7.03 KB - 11.39x memory usage +6.41 KB
decode (master-3428d3a-decode)                  8.48 KB - 13.75x memory usage +7.87 KB

##### With input google_message2 #####
Name                                              ips        average  deviation         median         99th %
decode (refactor-1cfbf81-decode)              1383.84        0.72 ms     ±9.27%        0.70 ms        0.98 ms
decode (optimize-packed-b11d326-decode)        853.24        1.17 ms     ±3.54%        1.17 ms        1.30 ms
decode (master-3428d3a-decode)                 687.49        1.45 ms     ±6.05%        1.45 ms        1.68 ms

Comparison: 
decode (refactor-1cfbf81-decode)              1383.84
decode (optimize-packed-b11d326-decode)        853.24 - 466.82x slower +1.17 ms
decode (master-3428d3a-decode)                 687.49 - 579.36x slower +1.45 ms

Memory usage statistics:

Name                                       Memory usage
decode (refactor-1cfbf81-decode)               22.90 KB
decode (optimize-packed-b11d326-decode)        23.74 KB - 38.47x memory usage +23.13 KB
decode (master-3428d3a-decode)                731.05 KB - 1184.48x memory usage +730.43 KB

##### With input google_message3_2 #####
Name                                              ips        average  deviation         median         99th %
decode (refactor-1cfbf81-decode)               106.70        9.37 ms     ±4.21%        9.32 ms       10.99 ms
decode (optimize-packed-b11d326-decode)         89.18       11.21 ms     ±4.47%       11.12 ms       12.69 ms
decode (master-3428d3a-decode)                  75.38       13.27 ms     ±2.65%       13.23 ms       14.03 ms

Comparison: 
decode (refactor-1cfbf81-decode)               106.70
decode (optimize-packed-b11d326-decode)         89.18 - 4466.43x slower +11.21 ms
decode (master-3428d3a-decode)                  75.38 - 5283.91x slower +13.26 ms

Memory usage statistics:

Name                                       Memory usage
decode (refactor-1cfbf81-decode)                2.34 MB
decode (optimize-packed-b11d326-decode)         2.88 MB - 4779.73x memory usage +2.88 MB
decode (master-3428d3a-decode)                  7.26 MB - 12046.59x memory usage +7.26 MB

##### With input google_message3_1 #####
Name                                              ips        average  deviation         median         99th %
decode (refactor-1cfbf81-decode)                0.141         7.10 s     ±3.53%         6.97 s         7.39 s
decode (optimize-packed-b11d326-decode)         0.139         7.17 s     ±1.68%         7.19 s         7.28 s
decode (master-3428d3a-decode)                  0.118         8.50 s     ±0.44%         8.52 s         8.53 s

Comparison: 
decode (refactor-1cfbf81-decode)                0.141
decode (optimize-packed-b11d326-decode)         0.139 - 2856588.76x slower +7.17 s
decode (master-3428d3a-decode)                  0.118 - 3386587.08x slower +8.50 s

Memory usage statistics:

Name                                       Memory usage
decode (refactor-1cfbf81-decode)                1.33 GB
decode (optimize-packed-b11d326-decode)         1.64 GB - 2792543.27x memory usage +1.64 GB
decode (master-3428d3a-decode)                  2.05 GB - 3479007.04x memory usage +2.05 GB

##### With input google_message1_proto3 #####
Name                                              ips        average  deviation         median         99th %
encode (optimize-packed-b11d326-encode)       19.06 K       52.48 μs  ±3762.13%          11 μs          20 μs
encode (master-3428d3a-encode)                18.78 K       53.24 μs  ±3800.24%          11 μs       22.69 μs
encode (refactor-1cfbf81-encode)              18.61 K       53.72 μs  ±3717.12%       11.90 μs       21.90 μs

Comparison: 
encode (optimize-packed-b11d326-encode)       19.06 K
encode (master-3428d3a-encode)                18.78 K - 1.01x slower +0.77 μs
encode (refactor-1cfbf81-encode)              18.61 K - 1.02x slower +1.25 μs

Memory usage statistics:

Name                                       Memory usage
encode (optimize-packed-b11d326-encode)         2.78 KB
encode (master-3428d3a-encode)                  2.78 KB - 1.00x memory usage +0 KB
encode (refactor-1cfbf81-encode)                2.78 KB - 1.00x memory usage +0 KB

##### With input google_message3_3 #####
Name                                              ips        average  deviation         median         99th %
encode (optimize-packed-b11d326-encode)       15.74 K       63.53 μs  ±3680.21%           4 μs          19 μs
encode (refactor-1cfbf81-encode)              13.16 K       75.98 μs  ±3388.69%        4.90 μs       12.90 μs
encode (master-3428d3a-encode)                12.60 K       79.36 μs  ±3500.22%           4 μs          30 μs

Comparison: 
encode (optimize-packed-b11d326-encode)       15.74 K
encode (refactor-1cfbf81-encode)              13.16 K - 1.45x slower +23.51 μs
encode (master-3428d3a-encode)                12.60 K - 1.51x slower +26.89 μs

Memory usage statistics:

Name                                       Memory usage
encode (optimize-packed-b11d326-encode)         1.82 KB
encode (refactor-1cfbf81-encode)                1.82 KB - 0.65x memory usage -0.96094 KB
encode (master-3428d3a-encode)                  1.82 KB - 0.65x memory usage -0.96094 KB

##### With input google_message1_proto2 #####
Name                                              ips        average  deviation         median         99th %
encode (master-3428d3a-encode)                11.49 K       87.02 μs  ±2973.81%          18 μs          26 μs
encode (optimize-packed-b11d326-encode)       11.13 K       89.84 μs  ±2914.48%          18 μs          24 μs
encode (refactor-1cfbf81-encode)              10.94 K       91.40 μs  ±2854.25%       18.90 μs       59.90 μs

Comparison: 
encode (master-3428d3a-encode)                11.49 K
encode (optimize-packed-b11d326-encode)       11.13 K - 1.71x slower +37.36 μs
encode (refactor-1cfbf81-encode)              10.94 K - 1.74x slower +38.92 μs

Memory usage statistics:

Name                                       Memory usage
encode (master-3428d3a-encode)                  7.48 KB
encode (optimize-packed-b11d326-encode)         7.48 KB - 2.69x memory usage +4.70 KB
encode (refactor-1cfbf81-encode)                7.48 KB - 2.69x memory usage +4.70 KB

##### With input google_message3_5 #####
Name                                              ips        average  deviation         median         99th %
encode (optimize-packed-b11d326-encode)        8.00 K      125.06 μs  ±2658.48%           2 μs           7 μs
encode (master-3428d3a-encode)                 7.71 K      129.73 μs  ±2857.39%           3 μs          10 μs
encode (refactor-1cfbf81-encode)               6.68 K      149.79 μs  ±2480.70%        3.90 μs       20.90 μs

Comparison: 
encode (optimize-packed-b11d326-encode)        8.00 K
encode (master-3428d3a-encode)                 7.71 K - 2.47x slower +77.25 μs
encode (refactor-1cfbf81-encode)               6.68 K - 2.85x slower +97.32 μs

Memory usage statistics:

Name                                       Memory usage
encode (optimize-packed-b11d326-encode)          1016 B
encode (master-3428d3a-encode)                   1016 B - 0.36x memory usage -1832 B
encode (refactor-1cfbf81-encode)                 1016 B - 0.36x memory usage -1832 B

##### With input google_message2 #####
Name                                              ips        average  deviation         median         99th %
encode (refactor-1cfbf81-encode)               5.29 K      188.96 μs  ±2165.60%        8.90 μs       31.90 μs
encode (optimize-packed-b11d326-encode)        5.17 K      193.27 μs  ±2114.16%           8 μs          49 μs
encode (master-3428d3a-encode)                 4.75 K      210.35 μs  ±2202.98%           8 μs          50 μs

Comparison: 
encode (refactor-1cfbf81-encode)               5.29 K
encode (optimize-packed-b11d326-encode)        5.17 K - 3.68x slower +140.79 μs
encode (master-3428d3a-encode)                 4.75 K - 4.01x slower +157.88 μs

Memory usage statistics:

Name                                       Memory usage
encode (refactor-1cfbf81-encode)                3.80 KB
encode (optimize-packed-b11d326-encode)         3.80 KB - 1.37x memory usage +1.02 KB
encode (master-3428d3a-encode)                  3.80 KB - 1.37x memory usage +1.02 KB

##### With input google_message3_4 #####
Name                                              ips        average  deviation         median         99th %
encode (optimize-packed-b11d326-encode)         92.84       10.77 ms   ±275.15%      0.0250 ms       91.76 ms
encode (refactor-1cfbf81-encode)                91.43       10.94 ms   ±280.27%      0.0474 ms       96.86 ms
encode (master-3428d3a-encode)                  90.93       11.00 ms   ±288.18%      0.0240 ms      109.00 ms

Comparison: 
encode (optimize-packed-b11d326-encode)         92.84
encode (refactor-1cfbf81-encode)                91.43 - 208.42x slower +10.88 ms
encode (master-3428d3a-encode)                  90.93 - 209.57x slower +10.94 ms

Memory usage statistics:

Name                                       Memory usage
encode (optimize-packed-b11d326-encode)         1.10 KB
encode (refactor-1cfbf81-encode)                1.10 KB - 0.40x memory usage -1.67969 KB
encode (master-3428d3a-encode)                  1.10 KB - 0.40x memory usage -1.67969 KB

##### With input google_message3_2 #####
Name                                              ips        average  deviation         median         99th %
encode (optimize-packed-b11d326-encode)         45.31       22.07 ms   ±171.20%        3.32 ms       98.83 ms
encode (refactor-1cfbf81-encode)                41.30       24.22 ms   ±164.22%        3.39 ms      100.80 ms
encode (master-3428d3a-encode)                  37.32       26.80 ms   ±166.62%        3.68 ms      151.63 ms

Comparison: 
encode (optimize-packed-b11d326-encode)         45.31
encode (refactor-1cfbf81-encode)                41.30 - 461.46x slower +24.16 ms
encode (master-3428d3a-encode)                  37.32 - 510.67x slower +26.75 ms

Memory usage statistics:

Name                                       Memory usage
encode (optimize-packed-b11d326-encode)         1.10 MB
encode (refactor-1cfbf81-encode)                1.10 MB - 404.75x memory usage +1.10 MB
encode (master-3428d3a-encode)                  1.10 MB - 404.76x memory usage +1.10 MB

##### With input google_message4 #####
Name                                              ips        average  deviation         median         99th %
encode (optimize-packed-b11d326-encode)          5.96      167.72 ms    ±77.42%      268.05 ms      322.36 ms
encode (refactor-1cfbf81-encode)                 5.84      171.25 ms    ±79.08%      283.25 ms      302.25 ms
encode (master-3428d3a-encode)                   5.62      177.79 ms    ±81.30%      277.33 ms      329.11 ms

Comparison: 
encode (optimize-packed-b11d326-encode)          5.96
encode (refactor-1cfbf81-encode)                 5.84 - 3263.49x slower +171.20 ms
encode (master-3428d3a-encode)                   5.62 - 3388.10x slower +177.74 ms

Memory usage statistics:

Name                                       Memory usage
encode (optimize-packed-b11d326-encode)         2.80 KB
encode (refactor-1cfbf81-encode)                2.80 KB - 1.01x memory usage +0.0234 KB
encode (master-3428d3a-encode)                  2.80 KB - 1.01x memory usage +0.0234 KB

##### With input google_message3_1 #####
Name                                              ips        average  deviation         median         99th %
encode (refactor-1cfbf81-encode)                 0.26         3.91 s    ±22.97%         3.46 s         5.42 s
encode (optimize-packed-b11d326-encode)          0.25         3.99 s    ±29.14%         3.27 s         5.82 s
encode (master-3428d3a-encode)                   0.24         4.17 s    ±20.84%         3.87 s         5.21 s

Comparison: 
encode (refactor-1cfbf81-encode)                 0.26
encode (optimize-packed-b11d326-encode)          0.25 - 76024.14x slower +3.99 s
encode (master-3428d3a-encode)                   0.24 - 79410.94x slower +4.17 s

Memory usage statistics:

Name                                       Memory usage
encode (refactor-1cfbf81-encode)              439.80 MB
encode (optimize-packed-b11d326-encode)       439.80 MB - 161924.52x memory usage +439.79 MB
encode (master-3428d3a-encode)                439.80 MB - 161924.85x memory usage +439.80 MB
```

whatyouhide: We're merging this and we'll fix the compilation warning later on before releasing a new version.

Patch:
diff --git a/lib/protobuf/decoder.ex b/lib/protobuf/decoder.ex
index 34270af..d034242 100644
--- a/lib/protobuf/decoder.ex
+++ b/lib/protobuf/decoder.ex
@@ -1,473 +1,213 @@
 defmodule Protobuf.Decoder do
   @moduledoc false
-  import Protobuf.WireTypes
-  import Bitwise, only: [bsl: 2, bsr: 2, band: 2]
-  require Logger
-
-  @max_bits 64
-  @mask64 bsl(1, @max_bits) - 1
-
-  alias Protobuf.DecodeError
-
-  @spec decode(binary, atom) :: any
-  def decode(data, module) do
-    kvs = raw_decode_key(data, [], [])
-    msg_props = module.__message_props__()
-    struct = build_struct(kvs, msg_props, module.new())
-    reverse_repeated(struct, msg_props.repeated_fields)
-  end
-
-  @doc false
-  def decode_raw(data) do
-    raw_decode_key(data, [], [])
-  end
-
-  @doc false
-  # For performance
-  defmacro decode_type_m(type, key, val) do
-    quote do
-      case unquote(type) do
-        :int32 ->
-          <<n::signed-integer-32>> = <<unquote(val)::32>>
-          n
-
-        :string ->
-          unquote(val)
-
-        :bytes ->
-          unquote(val)
-
-        :int64 ->
-          <<n::signed-integer-64>> = <<unquote(val)::64>>
-          n
-
-        :uint32 ->
-          <<n::unsigned-integer-32>> = <<unquote(val)::32>>
-          n
-
-        :uint64 ->
-          <<n::unsigned-integer-64>> = <<unquote(val)::64>>
-          n
-
-        :bool ->
-          unquote(val) != 0
-
-        :sint32 ->
-          decode_zigzag(unquote(val))
-
-        :sint64 ->
-          decode_zigzag(unquote(val))
-
-        :fixed64 ->
-          <<n::little-64>> = unquote(val)
-          n
-
-        :sfixed64 ->
-          <<n::little-signed-64>> = unquote(val)
-          n
-
-        :double ->
-          case unquote(val) do
-            <<n::little-float-64>> ->
-              n
-
-            # little endianness
-            # should be 0b0_11111111111_000000000...
-            # should be 0b1_11111111111_000000000...
-            <<0, 0, 0, 0, 0, 0, 0b1111::4, 0::4, 0b01111111::8>> ->
-              :infinity
-
-            <<0, 0, 0, 0, 0, 0, 0b1111::4, 0::4, 0b11111111::8>> ->
-              :negative_infinity
-
-            <<a::48, 0b1111::4, b::4, _::1, 0b1111111::7>> when a != 0 or b != 0 ->
-              :nan
-          end
-
-        :fixed32 ->
-          <<n::little-32>> = unquote(val)
-          n
-
-        :sfixed32 ->
-          <<n::little-signed-32>> = unquote(val)
-          n
-
-        :float ->
-          case unquote(val) do
-            <<n::little-float-32>> ->
-              n
-
-            # little endianness
-            # should be 0b0_11111111_000000000...
-            <<0, 0, 0b1000_0000::8, 0b01111111::8>> ->
-              :infinity
-
-            # little endianness
-            # should be 0b1_11111111_000000000...
-            <<0, 0, 0b1000_0000::8, 0b11111111::8>> ->
-              :negative_infinity
 
-            # should be 0b*_11111111_not_zero...
-            <<a::16, 1::1, b::7, _::1, 0b1111111::7>> when a != 0 or b != 0 ->
-              :nan
-          end
-
-        {:enum, enum_type} ->
-          try do
-            enum_type.key(unquote(val))
-          rescue
-            FunctionClauseError ->
-              Logger.warn(
-                "unknown enum value #{unquote(val)} when decoding for #{inspect(unquote(type))}"
-              )
-
-              unquote(val)
-          end
+  import Protobuf.{Wire.Types, Wire.Varint}
+  import Bitwise, only: [bsr: 2, band: 2]
 
-        _ ->
-          raise DecodeError,
-            message: "can't decode type #{unquote(type)} for field #{unquote(key)}"
-      end
-    end
-  end
+  alias Protobuf.{DecodeError, Wire}
 
-  defp build_struct([tag, wire, val | rest], %{field_props: f_props} = msg_props, struct) do
-    case f_props do
-      %{
-        ^tag =>
-          %{
-            wire_type: ^wire,
-            repeated?: is_repeated,
-            map?: is_map,
-            type: type,
-            oneof: oneof,
-            name_atom: name_atom,
-            embedded?: embedded
-          } = prop
-      } ->
-        key = if oneof, do: oneof_field(prop, msg_props), else: name_atom
-
-        struct =
-          if embedded do
-            embedded_msg = decode(val, type)
-            val = if is_map, do: %{embedded_msg.key => embedded_msg.value}, else: embedded_msg
-            val = if oneof, do: {name_atom, val}, else: val
-
-            val = merge_embedded_value(struct, key, val, is_repeated)
-
-            Map.put(struct, key, val)
-          else
-            val = decode_type_m(type, key, val)
-            val = if oneof, do: {name_atom, val}, else: val
-
-            val =
-              if is_repeated do
-                merge_simple_repeated_value(struct, key, val)
-              else
-                val
-              end
-
-            Map.put(struct, key, val)
-          end
-
-        build_struct(rest, msg_props, struct)
-
-      %{^tag => %{packed?: true} = f_prop} ->
-        struct = put_packed_field(struct, f_prop, val)
-        build_struct(rest, msg_props, struct)
-
-      %{^tag => %{wire_type: wire2} = f_prop} ->
-        raise DecodeError,
-              "wrong wire_type for #{prop_display(f_prop)}: got #{wire}, want #{wire2}"
-
-      _ ->
-        struct = try_decode_extension(struct, tag, wire, val)
-        build_struct(rest, msg_props, struct)
-    end
-  end
-
-  defp build_struct([], _, struct) do
-    struct
-  end
+  require Logger
 
-  defp merge_embedded_value(struct, key, val, is_repeated) do
-    case struct do
-      %{^key => nil} ->
-        if is_repeated, do: [val], else: val
+  @compile {:inline,
+            decode_field: 3, skip_varint: 4, skip_delimited: 4, reverse_repeated: 2, field_key: 2}
 
-      %{^key => value} ->
-        if is_repeated, do: [val | value], else: Map.merge(value, val)
+  @spec decode(binary, atom) :: any
+  def decode(bin, module) do
+    props = module.__message_props__()
 
-      _ ->
-        if is_repeated, do: [val], else: val
-    end
+    bin
+    |> build_message(module.new(), props)
+    |> reverse_repeated(props.repeated_fields)
   end
 
-  defp merge_simple_repeated_value(struct, key, val) do
-    case struct do
-      %{^key => nil} ->
-        [val]
+  defp build_message(<<>>, message, _props), do: message
 
-      %{^key => value} ->
-        [val | value]
-
-      _ ->
-        [val]
-    end
+  defp build_message(<<bin::bits>>, message, props) do
+    decode_field(bin, message, props)
   end
 
-  @doc false
-  def decode_varint(bin, type \\ :key) do
-    raw_decode_varint(bin, [], type, [])
+  decoder :defp, :decode_field, [:message, :props] do
+    field_number = bsr(value, 3)
+    wire_type = band(value, 7)
+    handle_field(rest, field_number, wire_type, message, props)
   end
 
-  defp raw_decode_key(<<>>, result, []), do: Enum.reverse(result)
-
-  defp raw_decode_key(<<bin::bits>>, result, groups) do
-    raw_decode_varint(bin, result, :key, groups)
+  defp handle_field(<<bin::bits>>, field_number, wire_start_group(), message, props) do
+    skip_field(bin, message, props, [field_number])
   end
 
-  defp raw_decode_varint(<<0::1, x::7, rest::bits>>, result, type, groups) do
-    raw_handle_varint(type, rest, result, x, groups)
+  defp handle_field(<<_bin::bits>>, closing, wire_end_group(), _message, _props) do
+    msg = "closing group #{inspect(closing)} but no groups are open"
+    raise Protobuf.DecodeError, message: msg
   end
 
-  defp raw_decode_varint(<<1::1, x0::7, 0::1, x1::7, rest::bits>>, result, type, groups) do
-    val = bsl(x1, 7) + x0
-    raw_handle_varint(type, rest, result, val, groups)
+  defp handle_field(<<bin::bits>>, field_number, wire_varint(), message, props) do
+    decode_varint(bin, field_number, message, props)
   end
 
-  defp raw_decode_varint(
-         <<1::1, x0::7, 1::1, x1::7, 0::1, x2::7, rest::bits>>,
-         result,
-         type,
-         groups
-       ) do
-    val = bsl(x2, 14) + bsl(x1, 7) + x0
-    raw_handle_varint(type, rest, result, val, groups)
+  defp handle_field(<<bin::bits>>, field_number, wire_delimited(), message, props) do
+    decode_delimited(bin, field_number, message, props)
   end
 
-  defp raw_decode_varint(
-         <<1::1, x0::7, 1::1, x1::7, 1::1, x2::7, 0::1, x3::7, rest::bits>>,
-         result,
-         type,
-         groups
-       ) do
-    val = bsl(x3, 21) + bsl(x2, 14) + bsl(x1, 7) + x0
-    raw_handle_varint(type, rest, result, val, groups)
+  defp handle_field(<<bin::bits>>, field_number, wire_32bits(), message, props) do
+    <<value::bits-32, rest::bits>> = bin
+    handle_value(rest, field_number, wire_32bits(), value, message, props)
   end
 
-  defp raw_decode_varint(
-         <<1::1, x0::7, 1::1, x1::7, 1::1, x2::7, 1::1, x3::7, 0::1, x4::7, rest::bits>>,
-         result,
-         type,
-         groups
-       ) do
-    val = bsl(x4, 28) + bsl(x3, 21) + bsl(x2, 14) + bsl(x1, 7) + x0
-    raw_handle_varint(type, rest, result, val, groups)
+  defp handle_field(<<bin::bits>>, field_number, wire_64bits(), message, props) do
+    <<value::bits-64, rest::bits>> = bin
+    handle_value(rest, field_number, wire_64bits(), value, message, props)
   end
 
-  defp raw_decode_varint(
-         <<1::1, x0::7, 1::1, x1::7, 1::1, x2::7, 1::1, x3::7, 1::1, x4::7, 0::1, x5::7,
-           rest::bits>>,
-         result,
-         type,
-         groups
-       ) do
-    val = bsl(x5, 35) + bsl(x4, 28) + bsl(x3, 21) + bsl(x2, 14) + bsl(x1, 7) + x0
-    raw_handle_varint(type, rest, result, val, groups)
+  defp handle_field(_bin, _field_number, _wire_type, _message, _props) do
+    raise Protobuf.DecodeError, message: "cannot decode binary data"
   end
 
-  defp raw_decode_varint(
-         <<1::1, x0::7, 1::1, x1::7, 1::1, x2::7, 1::1, x3::7, 1::1, x4::7, 1::1, x5::7, 0::1,
-           x6::7, rest::bits>>,
-         result,
-         type,
-         groups
-       ) do
-    val = bsl(x6, 42) + bsl(x5, 35) + bsl(x4, 28) + bsl(x3, 21) + bsl(x2, 14) + bsl(x1, 7) + x0
-    raw_handle_varint(type, rest, result, val, groups)
-  end
+  decoder :defp, :skip_field, [:message, :props, :groups] do
+    field_number = bsr(value, 3)
+    wire_type = band(value, 7)
 
-  defp raw_decode_varint(
-         <<1::1, x0::7, 1::1, x1::7, 1::1, x2::7, 1::1, x3::7, 1::1, x4::7, 1::1, x5::7, 1::1,
-           x6::7, 0::1, x7::7, rest::bits>>,
-         result,
-         type,
-         groups
-       ) do
-    val =
-      bsl(x7, 49) + bsl(x6, 42) + bsl(x5, 35) + bsl(x4, 28) + bsl(x3, 21) + bsl(x2, 14) +
-        bsl(x1, 7) + x0
-
-    raw_handle_varint(type, rest, result, val, groups)
-  end
+    case wire_type do
+      wire_start_group() ->
+        skip_field(rest, message, props, [field_number | groups])
 
-  defp raw_decode_varint(
-         <<1::1, x0::7, 1::1, x1::7, 1::1, x2::7, 1::1, x3::7, 1::1, x4::7, 1::1, x5::7, 1::1,
-           x6::7, 1::1, x7::7, 0::1, x8::7, rest::bits>>,
-         result,
-         type,
-         groups
-       ) do
-    val =
-      bsl(x8, 56) + bsl(x7, 49) + bsl(x6, 42) + bsl(x5, 35) + bsl(x4, 28) + bsl(x3, 21) +
-        bsl(x2, 14) + bsl(x1, 7) + x0
-
-    raw_handle_varint(type, rest, result, val, groups)
-  end
+      wire_end_group() ->
+        case groups do
+          [^field_number] ->
+            build_message(rest, message, props)
 
-  defp raw_decode_varint(
-         <<1::1, x0::7, 1::1, x1::7, 1::1, x2::7, 1::1, x3::7, 1::1, x4::7, 1::1, x5::7, 1::1,
-           x6::7, 1::1, x7::7, 1::1, x8::7, 0::1, x9::7, rest::bits>>,
-         result,
-         type,
-         groups
-       ) do
-    val =
-      bsl(x9, 63) + bsl(x8, 56) + bsl(x7, 49) + bsl(x6, 42) + bsl(x5, 35) + bsl(x4, 28) +
-        bsl(x3, 21) + bsl(x2, 14) + bsl(x1, 7) + x0
-
-    val = band(val, @mask64)
-    raw_handle_varint(type, rest, result, val, groups)
-  end
+          [^field_number | groups] ->
+            skip_field(rest, message, props, groups)
 
-  defp raw_decode_varint(_, _, _, _) do
-    raise Protobuf.DecodeError, message: "cannot decode binary data"
-  end
+          [group | _] ->
+            msg = "closing group #{inspect(field_number)} but group #{inspect(group)} is open"
+            raise Protobuf.DecodeError, message: msg
+        end
 
-  defp raw_handle_varint(:key, <<bin::bits>>, result, key, groups) do
-    tag = bsr(key, 3)
-    wire_type = band(key, 7)
-    raw_handle_key(wire_type, tag, groups, bin, result)
-  end
+      wire_varint() ->
+        skip_varint(rest, message, props, groups)
 
-  defp raw_handle_varint(:value, <<>>, result, val, []), do: Enum.reverse([val | result])
+      wire_delimited() ->
+        skip_delimited(rest, message, props, groups)
 
-  defp raw_handle_varint(:value, <<bin::bits>>, result, val, []) do
-    raw_decode_varint(bin, [val | result], :key, [])
-  end
+      wire_32bits() ->
+        <<_skip::bits-32, rest::bits>> = rest
+        skip_field(rest, message, props, groups)
 
-  defp raw_handle_varint(:value, <<bin::bits>>, result, _val, groups) do
-    raw_decode_varint(bin, result, :key, groups)
+      wire_64bits() ->
+        <<_skip::bits-64, rest::bits>> = rest
+        skip_field(rest, message, props, groups)
+    end
   end
 
-  defp raw_handle_varint(:bytes_len, <<bin::bits>>, result, len, []) do
-    <<bytes::bytes-size(len), rest::bits>> = bin
-    raw_decode_key(rest, [bytes | result], [])
+  decoder :defp, :skip_varint, [:message, :props, :groups] do
+    _ = value
+    skip_field(rest, message, props, groups)
   end
 
-  defp raw_handle_varint(:bytes_len, <<bin::bits>>, result, len, groups) do
-    <<_bytes::bytes-size(len), rest::bits>> = bin
-    raw_decode_key(rest, result, groups)
+  decoder :defp, :skip_delimited, [:message, :props, :groups] do
+    <<_skip::bytes-size(value), rest::bits>> = rest
+    skip_field(rest, message, props, groups)
   end
 
-  defp raw_handle_varint(:packed, <<>>, result, val, _groups), do: [val | result]
-
-  defp raw_handle_varint(:packed, <<bin::bits>>, result, val, groups) do
-    raw_decode_varint(bin, [val | result], :packed, groups)
+  decoder :defp, :decode_varint, [:field_number, :message, :props] do
+    handle_value(rest, field_number, wire_varint(), value, message, props)
   end
 
-  defp raw_handle_key(wire_start_group(), opening, groups, <<bin::bits>>, result) do
-    raw_decode_key(bin, result, [opening | groups])
+  decoder :defp, :decode_delimited, [:field_number, :message, :props] do
+    <<bytes::bytes-size(value), rest::bits>> = rest
+    handle_value(rest, field_number, wire_delimited(), bytes, message, props)
   end
 
-  defp raw_handle_key(wire_end_group(), closing, [closing | groups], <<bin::bits>>, result) do
-    raw_decode_key(bin, result, groups)
-  end
+  defp handle_value(<<bin::bits>>, field_number, wire_type, value, message, props) do
+    case props.field_props do
+      %{^field_number => %{packed?: true} = prop} ->
+        key = prop.name_atom
+        current_value = Map.get(message, key)
+        new_value = value_for_packed(value, current_value, prop)
+        new_message = Map.put(message, key, new_value)
+        build_message(bin, new_message, props)
 
-  defp raw_handle_key(wire_end_group(), closing, [], _bin, _result) do
-    raise(Protobuf.DecodeError,
-      message: "closing group #{inspect(closing)} but no groups are open"
-    )
-  end
+      %{^field_number => %{wire_type: ^wire_type} = prop} ->
+        key = field_key(prop, props)
+        current_value = Map.get(message, key)
+        new_value = value_for_field(value, current_value, prop)
+        new_message = Map.put(message, key, new_value)
+        build_message(bin, new_message, props)
 
-  defp raw_handle_key(wire_end_group(), closing, [open | _], _bin, _result) do
-    raise(Protobuf.DecodeError,
-      message: "closing group #{inspect(closing)} but group #{inspect(open)} is open"
-    )
-  end
+      %{^field_number => %{wire_type: wanted, name: field}} ->
+        message = "wrong wire_type for #{field}: got #{wire_type}, want #{wanted}"
+        raise DecodeError, message: message
 
-  defp raw_handle_key(wire_type, tag, [], <<bin::bits>>, result) do
-    raw_decode_value(wire_type, bin, [wire_type, tag | result], [])
-  end
+      %{} ->
+        %mod{} = message
 
-  defp raw_handle_key(wire_type, _tag, groups, <<bin::bits>>, result) do
-    raw_decode_value(wire_type, bin, result, groups)
-  end
+        new_message =
+          case Protobuf.Extension.get_extension_props_by_tag(mod, field_number) do
+            {ext_mod, %{field_props: prop}} ->
+              current_value = Protobuf.Extension.get(message, ext_mod, prop.name_atom, nil)
+              new_value = value_for_field(value, current_value, prop)
+              Protobuf.Extension.put(mod, message, ext_mod, prop.name_atom, new_value)
 
-  @doc false
-  def raw_decode_value(wire, bin, result, groups \\ [])
+            _ ->
+              message
+          end
 
-  def raw_decode_value(wire_varint(), <<bin::bits>>, result, groups) do
-    raw_decode_varint(bin, result, :value, groups)
+        build_message(bin, new_message, props)
+    end
   end
 
-  def raw_decode_value(wire_delimited(), <<bin::bits>>, result, groups) do
-    raw_decode_varint(bin, result, :bytes_len, groups)
-  end
+  defp value_for_field(value, current, %{embedded?: false} = prop) do
+    val = Wire.to_proto(prop.type, value)
+    val = if prop.oneof, do: {prop.name_atom, val}, else: val
 
-  def raw_decode_value(wire_32bits(), <<n::32, rest::bits>>, result, []) do
-    raw_decode_key(rest, [<<n::32>> | result], [])
+    case {current, prop.repeated?} do
+      {nil, true} -> [val]
+      {current, true} -> [val | current]
+      _ -> val
+    end
   end
 
-  def raw_decode_value(wire_32bits(), <<_n::32, rest::bits>>, result, groups) do
-    raw_decode_key(rest, result, groups)
-  end
+  defp value_for_field(bin, current, %{embedded?: true} = prop) do
+    embedded_msg = decode(bin, prop.type)
+    val = if prop.map?, do: %{embedded_msg.key => embedded_msg.value}, else: embedded_msg
+    val = if prop.oneof, do: {prop.name_atom, val}, else: val
 
-  def raw_decode_value(wire_64bits(), <<n::64, rest::bits>>, result, []) do
-    raw_decode_key(rest, [<<n::64>> | result], [])
+    case {current, prop.repeated?} do
+      {nil, true} -> [val]
+      {nil, false} -> val
+      {current, true} -> [val | current]
+      {current, false} -> Map.merge(current, val)
+    end
   end
 
-  def raw_decode_value(wire_64bits(), <<_n::64, rest::bits>>, result, groups) do
-    raw_decode_key(rest, result, groups)
-  end
+  defp value_for_packed(bin, current, prop) do
+    acc = current || []
 
-  def raw_decode_value(_, _, _, _) do
-    raise Protobuf.DecodeError, message: "cannot decode binary data"
+    case prop.wire_type do
+      wire_varint() -> decode_varints(bin, acc)
+      wire_32bits() -> decode_fixed32(bin, prop.type, acc)
+      wire_64bits() -> decode_fixed64(bin, prop.type, acc)
+    end
   end
 
-  # packed
-  defp put_packed_field(msg, %{wire_type: wire_type, type: type, name_atom: key}, bin) do
-    acc =
-      case msg do
-        %{^key => value} when is_list(value) -> value
-        %{} -> []
-      end
-
-    value =
-      case wire_type do
-        wire_varint() -> raw_decode_varint(bin, acc, :packed, [])
-        wire_32bits() -> decode_fixed32(bin, type, key, acc)
-        wire_64bits() -> decode_fixed64(bin, type, key, acc)
-      end
-
-    Map.put(msg, key, value)
-  end
+  defp decode_varints(<<>>, acc), do: acc
 
-  @dialyzer {:nowarn_function, decode_fixed32: 4}
-  defp decode_fixed32(<<n::bits-32, bin::bits>>, type, key, acc) do
-    decode_fixed32(bin, type, key, [decode_type_m(type, key, n) | acc])
+  decoder :defp, :decode_varints, [:acc] do
+    decode_varints(rest, [value | acc])
   end
 
-  defp decode_fixed32(<<>>, _, _, acc), do: acc
-
-  @dialyzer {:nowarn_function, decode_fixed64: 4}
-  defp decode_fixed64(<<n::bits-64, bin::bits>>, type, key, acc) do
-    decode_fixed64(bin, type, key, [decode_type_m(type, key, n) | acc])
+  defp decode_fixed32(<<n::bits-32, bin::bits>>, type, acc) do
+    decode_fixed32(bin, type, [Wire.to_proto(type, n) | acc])
   end
 
-  defp decode_fixed64(<<>>, _, _, acc), do: acc
+  defp decode_fixed32(<<>>, _, acc), do: acc
 
-  @doc false
-  @spec decode_zigzag(integer) :: integer
-  def decode_zigzag(n) when band(n, 1) == 0, do: bsr(n, 1)
-  def decode_zigzag(n) when band(n, 1) == 1, do: -bsr(n + 1, 1)
-
-  defp prop_display(prop) do
-    prop.name
+  defp decode_fixed64(<<n::bits-64, bin::bits>>, type, acc) do
+    decode_fixed64(bin, type, [Wire.to_proto(type, n) | acc])
   end
 
-  defp reverse_repeated(msg, []), do: msg
+  defp decode_fixed64(<<>>, _, acc), do: acc
 
   defp reverse_repeated(msg, [h | t]) do
     case msg do
@@ -479,49 +219,12 @@ defmodule Protobuf.Decoder do
     end
   end
 
-  defp oneof_field(%{oneof: oneof}, %{oneof: oneofs}) do
-    {field, ^oneof} = Enum.at(oneofs, oneof)
-    field
-  end
-
-  defp try_decode_extension(%mod{} = struct, tag, wire, val) do
-    case Protobuf.Extension.get_extension_props_by_tag(mod, tag) do
-      {ext_mod,
-       %{
-         field_props: %{
-           wire_type: ^wire,
-           repeated?: is_repeated,
-           type: type,
-           name_atom: name_atom,
-           embedded?: embedded
-         }
-       }} ->
-        val =
-          if embedded do
-            embedded_msg = decode(val, type)
-            merge_embedded_value(struct, name_atom, embedded_msg, is_repeated)
-          else
-            val = decode_type_m(type, name_atom, val)
-
-            if is_repeated do
-              merge_simple_repeated_value(struct, name_atom, val)
-            else
-              val
-            end
-          end
-
-        key = {ext_mod, name_atom}
-
-        case struct do
-          %{__pb_extensions__: pb_ext} ->
-            Map.put(struct, :__pb_extensions__, Map.put(pb_ext, key, val))
+  defp reverse_repeated(msg, []), do: msg
 
-          _ ->
-            Map.put(struct, :__pb_extensions__, %{key => val})
-        end
+  defp field_key(%{oneof: nil, name_atom: field_key}, _message_props), do: field_key
 
-      _ ->
-        struct
-    end
+  defp field_key(%{oneof: oneof_number}, %{oneof: oneofs}) do
+    {field_key, ^oneof_number} = Enum.at(oneofs, oneof_number)
+    field_key
   end
 end
diff --git a/lib/protobuf/encoder.ex b/lib/protobuf/encoder.ex
index 9e66c2b..dde6752 100644
--- a/lib/protobuf/encoder.ex
+++ b/lib/protobuf/encoder.ex
@@ -1,9 +1,9 @@
 defmodule Protobuf.Encoder do
   @moduledoc false
-  import Protobuf.WireTypes
-  import Bitwise, only: [bsr: 2, band: 2, bsl: 2, bor: 2]
+  import Protobuf.Wire.Types
+  import Bitwise, only: [bsl: 2, bor: 2]
 
-  alias Protobuf.{MessageProps, FieldProps}
+  alias Protobuf.{FieldProps, MessageProps, Wire, Wire.Varint}
 
   @spec encode(atom, map | struct, keyword) :: iodata
   def encode(mod, msg, opts) do
@@ -98,7 +98,7 @@ defmodule Protobuf.Encoder do
   @spec encode_field(atom, any, FieldProps.t()) :: iodata
   defp encode_field(:normal, val, %{encoded_fnum: fnum, type: type, repeated?: is_repeated}) do
     repeated_or_not(val, is_repeated, fn v ->
-      [fnum | encode_type(type, v)]
+      [fnum | Wire.from_proto(type, v)]
     end)
   end
 
@@ -114,14 +114,14 @@ defmodule Protobuf.Encoder do
       # so that oneof {:atom, v} can be encoded
       encoded = encode(type, v, iolist: true)
       byte_size = IO.iodata_length(encoded)
-      [fnum | encode_varint(byte_size)] ++ encoded
+      [fnum | Varint.encode(byte_size)] ++ encoded
     end)
   end
 
   defp encode_field(:packed, val, %{type: type, encoded_fnum: fnum}) do
-    encoded = Enum.map(val, fn v -> encode_type(type, v) end)
+    encoded = Enum.map(val, fn v -> Wire.from_proto(type, v) end)
     byte_size = IO.iodata_length(encoded)
-    [fnum | encode_varint(byte_size)] ++ encoded
+    [fnum | Varint.encode(byte_size)] ++ encoded
   end
 
   @spec class_field(map) :: atom
@@ -143,77 +143,10 @@ defmodule Protobuf.Encoder do
     fnum
     |> bsl(3)
     |> bor(wire_type)
-    |> encode_varint()
+    |> Varint.encode()
     |> IO.iodata_to_binary()
   end
 
-  @doc false
-  @spec encode_type(atom, any) :: iodata
-  def encode_type(:int32, n) when n >= -0x80000000 and n <= 0x7FFFFFFF, do: encode_varint(n)
-
-  def encode_type(:int64, n) when n >= -0x8000000000000000 and n <= 0x7FFFFFFFFFFFFFFF,
-    do: encode_varint(n)
-
-  def encode_type(:string, n), do: encode_type(:bytes, n)
-  def encode_type(:uint32, n) when n >= 0 and n <= 0xFFFFFFFF, do: encode_varint(n)
-  def encode_type(:uint64, n) when n >= 0 and n <= 0xFFFFFFFFFFFFFFFF, do: encode_varint(n)
-  def encode_type(:bool, true), do: encode_varint(1)
-  def encode_type(:bool, false), do: encode_varint(0)
-  def encode_type({:enum, type}, n) when is_atom(n), do: n |> type.value() |> encode_varint()
-  def encode_type({:enum, _}, n), do: encode_varint(n)
-  def encode_type(:float, :infinity), do: [0, 0, 128, 127]
-  def encode_type(:float, :negative_infinity), do: [0, 0, 128, 255]
-  def encode_type(:float, :nan), do: [0, 0, 192, 127]
-  def encode_type(:float, n), do: <<n::32-float-little>>
-  def encode_type(:double, :infinity), do: [0, 0, 0, 0, 0, 0, 240, 127]
-  def encode_type(:double, :negative_infinity), do: [0, 0, 0, 0, 0, 0, 240, 255]
-  def encode_type(:double, :nan), do: [1, 0, 0, 0, 0, 0, 248, 127]
-  def encode_type(:double, n), do: <<n::64-float-little>>
-
-  def encode_type(:bytes, n) do
-    len = n |> IO.iodata_length() |> encode_varint()
-    len ++ n
-  end
-
-  def encode_type(:sint32, n) when n >= -0x80000000 and n <= 0x7FFFFFFF,
-    do: n |> encode_zigzag |> encode_varint
-
-  def encode_type(:sint64, n) when n >= -0x8000000000000000 and n <= 0x7FFFFFFFFFFFFFFF,
-    do: n |> encode_zigzag |> encode_varint
-
-  def encode_type(:fixed64, n) when n >= 0 and n <= 0xFFFFFFFFFFFFFFFF, do: <<n::64-little>>
-
-  def encode_type(:sfixed64, n) when n >= -0x8000000000000000 and n <= 0x7FFFFFFFFFFFFFFF,
-    do: <<n::64-signed-little>>
-
-  def encode_type(:fixed32, n) when n >= 0 and n <= 0xFFFFFFFF, do: <<n::32-little>>
-
-  def encode_type(:sfixed32, n) when n >= -0x80000000 and n <= 0x7FFFFFFF,
-    do: <<n::32-signed-little>>
-
-  def encode_type(type, n) do
-    raise Protobuf.TypeEncodeError, message: "#{inspect(n)} is invalid for type #{type}"
-  end
-
-  @spec encode_zigzag(integer) :: integer
-  defp encode_zigzag(val) when val >= 0, do: val * 2
-  defp encode_zigzag(val) when val < 0, do: val * -2 - 1
-
-  @doc false
-  @spec encode_varint(integer) :: iolist
-  def encode_varint(n) when n < 0 do
-    <<n::64-unsigned-native>> = <<n::64-signed-native>>
-    encode_varint(n)
-  end
-
-  def encode_varint(n) when n <= 127 do
-    [n]
-  end
-
-  def encode_varint(n) do
-    [<<1::1, band(n, 127)::7>> | encode_varint(bsr(n, 7))]
-  end
-
   @doc false
   @spec wire_type(atom) :: integer
   def wire_type(:int32), do: wire_varint()
diff --git a/lib/protobuf/wire.ex b/lib/protobuf/wire.ex
new file mode 100644
index 0000000..6d614d0
--- /dev/null
+++ b/lib/protobuf/wire.ex
@@ -0,0 +1,136 @@
+defmodule Protobuf.Wire do
+  @moduledoc """
+  Utilities to convert data from wire format to protobuf and back.
+  """
+
+  alias Protobuf.Wire.{Varint, Zigzag}
+
+  require Logger
+
+  @type proto_type ::
+          :int32
+          | :int64
+          | :fixed32
+          | :fixed64
+          | :uint32
+          | :uint64
+          | :sfixed32
+          | :sfixed64
+          | :sint32
+          | :sint64
+          | :float
+          | :double
+          | :bool
+          | :string
+          | :bytes
+          | {:enum, any}
+
+  @type proto_float :: :infinity | :negative_infinity | :nan | float
+
+  @type proto_value :: binary | integer | bool | proto_float | atom
+
+  @sint32_range -0x80000000..0x7FFFFFFF
+  @sint64_range -0x8000000000000000..0x7FFFFFFFFFFFFFFF
+  @uint32_range 0..0xFFFFFFFF
+  @uint64_range 0..0xFFFFFFFFFFFFFFFF
+
+  @spec from_proto(proto_type, proto_value) :: iodata
+  # Returns improper list, but still valid iodata.
+  def from_proto(type, binary) when type in [:string, :bytes] do
+    len = binary |> IO.iodata_length() |> Varint.encode()
+    len ++ binary
+  end
+
+  def from_proto(:int32, n) when n in @sint32_range, do: Varint.encode(n)
+  def from_proto(:int64, n) when n in @sint64_range, do: Varint.encode(n)
+  def from_proto(:uint32, n) when n in @uint32_range, do: Varint.encode(n)
+  def from_proto(:uint64, n) when n in @uint64_range, do: Varint.encode(n)
+
+  def from_proto(:bool, true), do: Varint.encode(1)
+  def from_proto(:bool, false), do: Varint.encode(0)
+
+  def from_proto({:enum, enum}, key) when is_atom(key), do: Varint.encode(enum.value(key))
+  def from_proto({:enum, _}, n) when is_integer(n), do: Varint.encode(n)
+
+  def from_proto(:float, :infinity), do: [0, 0, 128, 127]
+  def from_proto(:float, :negative_infinity), do: [0, 0, 128, 255]
+  def from_proto(:float, :nan), do: [0, 0, 192, 127]
+  def from_proto(:float, n), do: <<n::32-float-little>>
+
+  def from_proto(:double, :infinity), do: [0, 0, 0, 0, 0, 0, 240, 127]
+  def from_proto(:double, :negative_infinity), do: [0, 0, 0, 0, 0, 0, 240, 255]
+  def from_proto(:double, :nan), do: [1, 0, 0, 0, 0, 0, 248, 127]
+  def from_proto(:double, n), do: <<n::64-float-little>>
+
+  def from_proto(:sint32, n) when n in @sint32_range, do: Varint.encode(Zigzag.encode(n))
+  def from_proto(:sint64, n) when n in @sint64_range, do: Varint.encode(Zigzag.encode(n))
+  def from_proto(:fixed32, n) when n in @uint32_range, do: <<n::32-little>>
+  def from_proto(:fixed64, n) when n in @uint64_range, do: <<n::64-little>>
+  def from_proto(:sfixed32, n) when n in @sint32_range, do: <<n::32-signed-little>>
+  def from_proto(:sfixed64, n) when n in @sint64_range, do: <<n::64-signed-little>>
+
+  def from_proto(type, n) do
+    raise Protobuf.TypeEncodeError, message: "#{inspect(n)} is invalid for type #{type}"
+  end
+
+  @spec to_proto(proto_type, binary | integer) :: proto_value
+  def to_proto(type, val) when type in [:string, :bytes], do: val
+
+  def to_proto(:int32, val) do
+    <<n::signed-integer-32>> = <<val::32>>
+    n
+  end
+
+  def to_proto(:int64, val) do
+    <<n::signed-integer-64>> = <<val::64>>
+    n
+  end
+
+  def to_proto(:uint32, val) do
+    <<n::unsigned-integer-32>> = <<val::32>>
+    n
+  end
+
+  def to_proto(:uint64, val) do
+    <<n::unsigned-integer-64>> = <<val::64>>
+    n
+  end
+
+  def to_proto(:bool, val), do: val != 0
+
+  def to_proto({:enum, enum}, val) do
+    enum.key(val)
+  rescue
+    FunctionClauseError ->
+      Logger.warn("unknown enum value #{val} when decoding for #{inspect(enum)}")
+      val
+  end
+
+  def to_proto(:float, <<n::little-float-32>>), do: n
+  # little endianness, should be 0b0_11111111_000000000...
+  def to_proto(:float, <<0, 0, 0b1000_0000::8, 0b01111111::8>>), do: :infinity
+  # little endianness, should be 0b1_11111111_000000000...
+  def to_proto(:float, <<0, 0, 0b1000_0000::8, 0b11111111::8>>), do: :negative_infinity
+  # should be 0b*_11111111_not_zero...
+  def to_proto(:float, <<a::16, 1::1, b::7, _::1, 0b1111111::7>>) when a != 0 or b != 0,
+    do: :nan
+
+  def to_proto(:double, <<n::little-float-64>>), do: n
+  # little endianness, should be 0b0_11111111111_000000000...
+  def to_proto(:double, <<0::48, 0b1111::4, 0::4, 0b01111111::8>>), do: :infinity
+  # little endianness, should be 0b1_11111111111_000000000...
+  def to_proto(:double, <<0::48, 0b1111::4, 0::4, 0b11111111::8>>), do: :negative_infinity
+
+  def to_proto(:double, <<a::48, 0b1111::4, b::4, _::1, 0b1111111::7>>) when a != 0 or b != 0,
+    do: :nan
+
+  def to_proto(type, val) when type in [:sint32, :sint64], do: Zigzag.decode(val)
+  def to_proto(:fixed32, <<n::little-32>>), do: n
+  def to_proto(:fixed64, <<n::little-64>>), do: n
+  def to_proto(:sfixed32, <<n::little-signed-32>>), do: n
+  def to_proto(:sfixed64, <<n::little-signed-64>>), do: n
+
+  def to_proto(type, val) do
+    raise Protobuf.DecodeError, message: "can't decode #{inspect(val)} into type #{type}"
+  end
+end
diff --git a/lib/protobuf/wire_types.ex b/lib/protobuf/wire/types.ex
similarity index 86%
rename from lib/protobuf/wire_types.ex
rename to lib/protobuf/wire/types.ex
index 6a1f6be..de77832 100644
--- a/lib/protobuf/wire_types.ex
+++ b/lib/protobuf/wire/types.ex
@@ -1,4 +1,4 @@
-defmodule Protobuf.WireTypes do
+defmodule Protobuf.Wire.Types do
   @moduledoc false
 
   defmacro wire_varint, do: 0
diff --git a/lib/protobuf/wire/varint.ex b/lib/protobuf/wire/varint.ex
new file mode 100644
index 0000000..78ed520
--- /dev/null
+++ b/lib/protobuf/wire/varint.ex
@@ -0,0 +1,208 @@
+defmodule Protobuf.Wire.Varint do
+  @moduledoc """
+  Varint encoding and decoding utilities.
+
+  https://developers.google.com/protocol-buffers/docs/encoding#varints
+
+  For performance reasons, varint decoding must be built through a macro, so that binary
+  match contexts are reused and no new large binaries get allocated. You can define your
+  own varint decoders with the `decoder` macro, which generates function heads for up to
+  10-bytes long varint-encoded data.
+
+      defmodule VarintDecoders do
+        import Protobuf.Wire.Varint
+
+        decoder :def, :decode_and_sum, [:plus] do
+          {:ok, value + plus, rest}
+        end
+
+        def decode_all(<<bin::bits>>), do: decode_all(bin, [])
+
+        defp decode_all(<<>>, acc), do: acc
+
+        decoder :defp, :decode_all, [:acc] do
+          decode_all(rest, [value | acc])
+        end
+      end
+
+      iex> VarintDecoders.decode_and_sum(<<35>>, 7)
+      {:ok, 42, ""}
+
+      iex> VarintDecoders.decode_all("abcd asdf")
+      [102, 100, 115, 97, 32, 100, 99, 98, 97]
+
+  Refer to [efficiency guide](http://www1.erlang.org/doc/efficiency_guide/binaryhandling.html)
+  for more on efficient binary handling.
+
+  Encoding on the other hand is simpler. It takes an integer and returns an iolist with its
+  varint representation:
+
+      iex> Protobuf.Wire.Varint.encode(35)
+      [35]
+
+      iex> Protobuf.Wire.Varint.encode(1_234_567)
+      [<<135>>, <<173>>, 75]
+  """
+  use Bitwise
+
+  @max_bits 64
+  @mask64 bsl(1, @max_bits) - 1
+
+  @varints [
+    {
+      quote(do: <<0::1, value::7>>),
+      quote(do: value)
+    },
+    {
+      quote(do: <<1::1, x0::7, 0::1, x1::7>>),
+      quote(do: x0 + bsl(x1, 7))
+    },
+    {
+      quote(do: <<1::1, x0::7, 1::1, x1::7, 0::1, x2::7>>),
+      quote(do: x0 + bsl(x1, 7) + bsl(x2, 14))
+    },
+    {
+      quote(do: <<1::1, x0::7, 1::1, x1::7, 1::1, x2::7, 0::1, x3::7>>),
+      quote(do: x0 + bsl(x1, 7) + bsl(x2, 14) + bsl(x3, 21))
+    },
+    {
+      quote(do: <<1::1, x0::7, 1::1, x1::7, 1::1, x2::7, 1::1, x3::7, 0::1, x4::7>>),
+      quote(do: x0 + bsl(x1, 7) + bsl(x2, 14) + bsl(x3, 21) + bsl(x4, 28))
+    },
+    {
+      quote do
+        <<1::1, x0::7, 1::1, x1::7, 1::1, x2::7, 1::1, x3::7, 1::1, x4::7, 0::1, x5::7>>
+      end,
+      quote do
+        x0 +
+          bsl(x1, 7) +
+          bsl(x2, 14) +
+          bsl(x3, 21) +
+          bsl(x4, 28) +
+          bsl(x5, 35)
+      end
+    },
+    {
+      quote do
+        <<1::1, x0::7, 1::1, x1::7, 1::1, x2::7, 1::1, x3::7, 1::1, x4::7, 1::1, x5::7, 0::1,
+          x6::7>>
+      end,
+      quote do
+        x0 +
+          bsl(x1, 7) +
+          bsl(x2, 14) +
+          bsl(x3, 21) +
+          bsl(x4, 28) +
+          bsl(x5, 35) +
+          bsl(x6, 42)
+      end
+    },
+    {
+      quote do
+        <<1::1, x0::7, 1::1, x1::7, 1::1, x2::7, 1::1, x3::7, 1::1, x4::7, 1::1, x5::7, 1::1,
+          x6::7, 0::1, x7::7>>
+      end,
+      quote do
+        x0 +
+          bsl(x1, 7) +
+          bsl(x2, 14) +
+          bsl(x3, 21) +
+          bsl(x4, 28) +
+          bsl(x5, 35) +
+          bsl(x6, 42) +
+          bsl(x7, 49)
+      end
+    },
+    {
+      quote do
+        <<1::1, x0::7, 1::1, x1::7, 1::1, x2::7, 1::1, x3::7, 1::1, x4::7, 1::1, x5::7, 1::1,
+          x6::7, 1::1, x7::7, 0::1, x8::7>>
+      end,
+      quote do
+        x0 +
+          bsl(x1, 7) +
+          bsl(x2, 14) +
+          bsl(x3, 21) +
+          bsl(x4, 28) +
+          bsl(x5, 35) +
+          bsl(x6, 42) +
+          bsl(x7, 49) +
+          bsl(x8, 56)
+      end
+    },
+    {
+      quote do
+        <<1::1, x0::7, 1::1, x1::7, 1::1, x2::7, 1::1, x3::7, 1::1, x4::7, 1::1, x5::7, 1::1,
+          x6::7, 1::1, x7::7, 1::1, x8::7, 0::1, x9::7>>
+      end,
+      quote do
+        band(
+          x0 +
+            bsl(x1, 7) +
+            bsl(x2, 14) +
+            bsl(x3, 21) +
+            bsl(x4, 28) +
+            bsl(x5, 35) +
+            bsl(x6, 42) +
+            bsl(x7, 49) +
+            bsl(x8, 56) +
+            bsl(x9, 63),
+          unquote(@mask64)
+        )
+      end
+    }
+  ]
+
+  defmacro decoder(kind, name, args \\ [], do: block) do
+    def_success(kind, name, args, block) ++ [def_failure(kind, name, args)]
+  end
+
+  defp def_success(kind, name, args, block) do
+    args = Enum.map(args, fn arg -> {arg, [line: 1], nil} end)
+
+    for {pattern, expression} <- @varints do
+      head = quote(do: unquote(name)(<<unquote(pattern), rest::bits>>, unquote_splicing(args)))
+
+      body =
+        quote do
+          var!(value) = unquote(expression)
+          var!(rest) = rest
+          unquote(block)
+        end
+
+      quote do
+        case unquote(kind) do
+          :def -> def unquote(head), do: unquote(body)
+          :defp -> defp unquote(head), do: unquote(body)
+        end
+      end
+    end
+  end
+
+  defp def_failure(kind, name, args) do
+    args = Enum.map(args, fn _ -> {:_, [line: 1], nil} end)
+    head = quote(do: unquote(name)(<<_::bits>>, unquote_splicing(args)))
+    body = quote(do: raise(Protobuf.DecodeError, message: "cannot decode binary data"))
+
+    quote do
+      case unquote(kind) do
+        :def -> def unquote(head), do: unquote(body)
+        :defp -> defp unquote(head), do: unquote(body)
+      end
+    end
+  end
+
+  @spec encode(integer) :: iolist
+  def encode(n) when n < 0 do
+    <<n::64-unsigned-native>> = <<n::64-signed-native>>
+    encode(n)
+  end
+
+  def encode(n) when n <= 127 do
+    [n]
+  end
+
+  def encode(n) do
+    [<<1::1, band(n, 127)::7>> | encode(bsr(n, 7))]
+  end
+end
diff --git a/lib/protobuf/wire/zigzag.ex b/lib/protobuf/wire/zigzag.ex
new file mode 100644
index 0000000..53932c6
--- /dev/null
+++ b/lib/protobuf/wire/zigzag.ex
@@ -0,0 +1,13 @@
+defmodule Protobuf.Wire.Zigzag do
+  @moduledoc false
+
+  use Bitwise, skip_operators: true
+
+  @spec encode(integer) :: integer
+  def encode(n) when n >= 0, do: n * 2
+  def encode(n) when n < 0, do: n * -2 - 1
+
+  @spec decode(integer) :: integer
+  def decode(n) when band(n, 1) == 0, do: bsr(n, 1)
+  def decode(n) when band(n, 1) == 1, do: -bsr(n + 1, 1)
+end


Test patch:
diff --git a/test/pbt/encode_decode_type_test.exs b/test/pbt/encode_decode_type_test.exs
index 3bde730..9dbb88e 100644
--- a/test/pbt/encode_decode_type_test.exs
+++ b/test/pbt/encode_decode_type_test.exs
@@ -1,21 +1,24 @@
 defmodule Protobuf.EncodeDecodeTypeTest.PropertyGenerator do
-  alias Protobuf.{Encoder}
-
-  require Logger
-  import Protobuf.Decoder
+  def decode(type, bin) do
+    bin
+    |> TestMsg.Scalars.decode()
+    |> Map.fetch!(type)
+  end
 
-  def decode_type(wire, type, bin) do
-    [n] = raw_decode_value(wire, bin, [])
-    decode_type_m(type, :fake_key, n)
+  def encode(type, val) do
+    [{type, val}]
+    |> TestMsg.Scalars.new!()
+    |> Protobuf.Encoder.encode(iolist: false)
   end
 
-  defmacro make_property(gen_func, field_type, wire_type) do
+  defmacro make_property(gen_func, field_type) do
     quote do
-      property unquote(Atom.to_string(field_type)) <> " roundtrip" do
+      property "#{unquote(field_type)} roundtrip" do
         check all n <- unquote(gen_func) do
-          iodata = Encoder.encode_type(unquote(field_type), n)
-          bin = IO.iodata_to_binary(iodata)
-          assert n == decode_type(unquote(wire_type), unquote(field_type), bin)
+          field_type = unquote(field_type)
+          bin = encode(field_type, n)
+
+          assert n == decode(field_type, bin)
         end
       end
     end
@@ -24,28 +27,16 @@ defmodule Protobuf.EncodeDecodeTypeTest.PropertyGenerator do
   # Since float point is not precise, make canonical value before doing PBT
   # ref: http://hypothesis.works/articles/canonical-serialization/
   # and try 0.2 here: https://www.h-schmidt.net/FloatConverter/IEEE754.html
-  defmacro make_canonical_property(gen_func, field_type, wire_type) do
+  defmacro make_canonical_property(gen_func, field_type) do
     quote do
-      property unquote(Atom.to_string(field_type)) <> " canonical roundtrip" do
+      property "#{unquote(field_type)} canonical roundtrip" do
         check all n <- unquote(gen_func) do
-          encoded_val =
-            unquote(field_type)
-            |> Encoder.encode_type(n)
-            |> IO.iodata_to_binary()
-
-          canonical_val =
-            decode_type(
-              unquote(wire_type),
-              unquote(field_type),
-              encoded_val
-            )
-
-          bin =
-            unquote(field_type)
-            |> Encoder.encode_type(canonical_val)
-            |> IO.iodata_to_binary()
+          field_type = unquote(field_type)
+          encoded_val = encode(field_type, n)
+          canonical_val = decode(field_type, encoded_val)
+          bin = encode(field_type, canonical_val)
 
-          assert canonical_val == decode_type(unquote(wire_type), unquote(field_type), bin)
+          assert canonical_val == decode(field_type, bin)
         end
       end
     end
@@ -74,18 +65,18 @@ defmodule Protobuf.EncodeDecodeTypeTest do
     map(integer(), &abs/1)
   end
 
-  make_property(integer(), :int32, 0)
-  make_property(large_integer(), :int64, 0)
-  make_property(uint32_gen(), :uint32, 0)
-  make_property(uint64_gen(), :uint64, 0)
-  make_property(integer(), :sint32, 0)
-  make_property(large_integer(), :sint64, 0)
+  make_property(integer(), :int32)
+  make_property(large_integer(), :int64)
+  make_property(uint32_gen(), :uint32)
+  make_property(uint64_gen(), :uint64)
+  make_property(integer(), :sint32)
+  make_property(large_integer(), :sint64)
 
-  make_property(boolean(), :bool, 0)
+  make_property(boolean(), :bool)
 
-  make_property(natural_number(), :fixed64, 1)
-  make_property(large_integer(), :sfixed64, 1)
+  make_property(natural_number(), :fixed64)
+  make_property(large_integer(), :sfixed64)
 
-  make_canonical_property(float(), :double, 1)
-  make_canonical_property(float(), :float, 5)
+  make_canonical_property(float(), :double)
+  make_canonical_property(float(), :float)
 end
diff --git a/test/pbt/encode_decode_varint_test.exs b/test/pbt/encode_decode_varint_test.exs
index ed04d04..8d1d25d 100644
--- a/test/pbt/encode_decode_varint_test.exs
+++ b/test/pbt/encode_decode_varint_test.exs
@@ -2,13 +2,18 @@ defmodule Protobuf.EncodeDecodeVarintTest do
   use ExUnit.Case, async: true
   use ExUnitProperties
 
-  alias Protobuf.{Encoder, Decoder}
+  import Protobuf.Wire.Varint
+
+  decoder :defp, :decode do
+    "" = rest
+    value
+  end
 
   property "varint roundtrip" do
     check all n <- large_integer_gen() do
-      iodata = Encoder.encode_varint(n)
+      iodata = encode(n)
       bin = IO.iodata_to_binary(iodata)
-      [n] = Decoder.decode_varint(bin, :value)
+      n = decode(bin)
       assert <<n::signed-64>> == <<n::64>>
     end
   end
@@ -17,7 +22,7 @@ defmodule Protobuf.EncodeDecodeVarintTest do
     negative_large_integer_gen = map(large_integer_gen(), &(-abs(&1)))
 
     check all n <- negative_large_integer_gen do
-      assert IO.iodata_length(Encoder.encode_varint(n)) == 10
+      assert IO.iodata_length(encode(n)) == 10
     end
   end
 
diff --git a/test/protobuf/decode/decode_type_test.exs b/test/protobuf/decode/decode_type_test.exs
deleted file mode 100644
index d554ca4..0000000
--- a/test/protobuf/decode/decode_type_test.exs
+++ /dev/null
@@ -1,103 +0,0 @@
-defmodule Protobuf.Decoder.DecodeTypeTest do
-  use ExUnit.Case, async: true
-
-  require Logger
-  import Protobuf.Decoder
-
-  def decode_type(type, val) do
-    decode_type_m(type, :fake_key, val)
-  end
-
-  test "decode_type/2 varint" do
-    assert 42 == decode_type(:int32, 42)
-  end
-
-  test "decode_type/2 int64" do
-    assert -1 == decode_type(:int64, -1)
-  end
-
-  test "decode_type/2 string" do
-    assert "a" = decode_type(:string, "a")
-  end
-
-  test "decode_type/3 min sint32" do
-    assert -2_147_483_648 == decode_type(:sint32, 4_294_967_295)
-  end
-
-  test "decode_type/3 max sint32" do
-    assert 2_147_483_647 == decode_type(:sint32, 4_294_967_294)
-  end
-
-  test "decode_type/3 min sint64" do
-    assert -9_223_372_036_854_775_808 == decode_type(:sint64, 18_446_744_073_709_551_615)
-  end
-
-  test "decode_type/3 max sint64" do
-    assert 9_223_372_036_854_775_807 == decode_type(:sint64, 18_446_744_073_709_551_614)
-  end
-
-  test "decode_type/3 bool works" do
-    assert true == decode_type(:bool, 1)
-    assert false == decode_type(:bool, 0)
-  end
-
-  test "decode_type/3 a fixed64" do
-    assert 8_446_744_073_709_551_615 ==
-             decode_type(:fixed64, <<255, 255, 23, 118, 251, 220, 56, 117>>)
-  end
-
-  test "decode_type/3 max fixed64" do
-    assert 18_446_744_073_709_551_615 ==
-             decode_type(:fixed64, <<255, 255, 255, 255, 255, 255, 255, 255>>)
-  end
-
-  test "decode_type/3 min sfixed64" do
-    assert -9_223_372_036_854_775_808 == decode_type(:sfixed64, <<0, 0, 0, 0, 0, 0, 0, 128>>)
-  end
-
-  test "decode_type/3 max sfixed64" do
-    assert 9_223_372_036_854_775_807 ==
-             decode_type(:sfixed64, <<255, 255, 255, 255, 255, 255, 255, 127>>)
-  end
-
-  test "decode_type/3 min double" do
-    assert 5.0e-324 == decode_type(:double, <<1, 0, 0, 0, 0, 0, 0, 0>>)
-  end
-
-  test "decode_type/3 max double" do
-    assert 1.7976931348623157e308 ==
-             decode_type(:double, <<255, 255, 255, 255, 255, 255, 239, 127>>)
-  end
-
-  test "decode_type/3 string" do
-    assert "testing" == decode_type(:string, <<116, 101, 115, 116, 105, 110, 103>>)
-  end
-
-  test "decode_type/3 bytes" do
-    assert <<42, 43, 44, 45>> == decode_type(:bytes, <<42, 43, 44, 45>>)
-  end
-
-  test "decode_type/3 fixed32" do
-    assert 4_294_967_295 == decode_type(:fixed32, <<255, 255, 255, 255>>)
-  end
-
-  test "decode_type/3 sfixed32" do
-    assert 2_147_483_647 == decode_type(:sfixed32, <<255, 255, 255, 127>>)
-  end
-
-  test "decode_type/3 float" do
-    assert 3.4028234663852886e38 == decode_type(:float, <<255, 255, 127, 127>>)
-  end
-
-  test "decode_type/3 float infinity,-infinity,nan" do
-    assert :infinity == decode_type(:float, <<0, 0, 128, 127>>)
-    assert :negative_infinity == decode_type(:float, <<0, 0, 128, 255>>)
-    assert :nan == decode_type(:float, <<0, 0, 192, 127>>)
-  end
-
-  test "decode_type/3 double infinity,-infinity,nan" do
-    assert :infinity == decode_type(:double, <<0, 0, 0, 0, 0, 0, 240, 127>>)
-    assert :negative_infinity == decode_type(:double, <<0, 0, 0, 0, 0, 0, 240, 255>>)
-    assert :nan == decode_type(:double, <<1, 0, 0, 0, 0, 0, 248, 127>>)
-  end
-end
diff --git a/test/protobuf/decode/decode_varint_test.exs b/test/protobuf/decode/decode_varint_test.exs
deleted file mode 100644
index 19df349..0000000
--- a/test/protobuf/decode/decode_varint_test.exs
+++ /dev/null
@@ -1,58 +0,0 @@
-defmodule Protobuf.Decoder.DecodeVarintTest do
-  use ExUnit.Case, async: true
-
-  import Protobuf.Decoder
-  require Logger
-
-  def decode_type(type, val) do
-    decode_type_m(type, :fake_key, val)
-  end
-
-  test "decode_varint 300" do
-    assert [300] == decode_varint(<<0b1010110000000010::16>>, :value)
-  end
-
-  test "decode_varint 150" do
-    assert [1, 0, 150] == decode_varint(<<8, 150, 01>>)
-  end
-
-  test "decode_varint zero value(int, bool, enum)" do
-    assert [] == decode_raw(<<>>)
-  end
-
-  test "decode_varint+decode_type min int32" do
-    assert [1, 0, val = 18_446_744_071_562_067_968] ==
-             decode_varint(<<8, 128, 128, 128, 128, 248, 255, 255, 255, 255, 1>>)
-
-    assert -2_147_483_648 == decode_type(:int32, val)
-  end
-
-  test "decode_varint max int32" do
-    [1, 0, 2_147_483_647] = decode_varint(<<8, 255, 255, 255, 255, 7>>)
-  end
-
-  test "decode_varint+decode_type min int64" do
-    [1, 0, val] = decode_varint(<<8, 128, 128, 128, 128, 128, 128, 128, 128, 128, 1>>)
-    assert -9_223_372_036_854_775_808 == decode_type(:int64, val)
-  end
-
-  test "decode_varint max int64" do
-    [1, 0, val] = decode_varint(<<8, 255, 255, 255, 255, 255, 255, 255, 255, 127>>)
-    assert 9_223_372_036_854_775_807 == decode_type(:int64, val)
-  end
-
-  test "decode_varint max uint32" do
-    [1, 0, val] = decode_varint(<<8, 255, 255, 255, 255, 15>>)
-    assert 4_294_967_295 == decode_type(:uint32, val)
-  end
-
-  test "decode_varint max uint64" do
-    [1, 0, val] = decode_varint(<<8, 255, 255, 255, 255, 255, 255, 255, 255, 255, 1>>)
-    assert 18_446_744_073_709_551_615 == decode_type(:uint64, val)
-  end
-
-  test "decode_varint true/enum_1" do
-    [1, 0, val = 1] = decode_varint(<<8, 1>>)
-    assert true === decode_type(:bool, val)
-  end
-end
diff --git a/test/protobuf/decoder_test.exs b/test/protobuf/decoder_test.exs
index 4d72935..00ba7da 100644
--- a/test/protobuf/decoder_test.exs
+++ b/test/protobuf/decoder_test.exs
@@ -80,15 +80,16 @@ defmodule Protobuf.DecoderTest do
   test "decodes enum type" do
     struct = Decoder.decode(<<88, 1>>, TestMsg.Foo)
     assert struct == TestMsg.Foo.new(j: :A)
+
     struct = Decoder.decode(<<88, 2>>, TestMsg.Foo)
     assert struct == TestMsg.Foo.new(j: :B)
   end
 
-  test "decodes unknown enum type" do
+  test "decodes unknown enum value" do
     assert ExUnit.CaptureLog.capture_log(fn ->
              struct = Decoder.decode(<<88, 3>>, TestMsg.Foo)
              assert struct == TestMsg.Foo.new(j: 3)
-           end) =~ ~r/unknown enum value 3 when decoding for {:enum, TestMsg.EnumFoo}/
+           end) =~ ~r/unknown enum value 3 when decoding for TestMsg\.EnumFoo/
   end
 
   test "decodes map type" do
diff --git a/test/protobuf/encoder/encode_type_test.exs b/test/protobuf/encoder/encode_type_test.exs
deleted file mode 100644
index 6261e86..0000000
--- a/test/protobuf/encoder/encode_type_test.exs
+++ /dev/null
@@ -1,226 +0,0 @@
-defmodule Protobuf.Encoder.DecodeTypeTest do
-  use ExUnit.Case, async: true
-
-  alias Protobuf.Encoder
-  alias Protobuf.Decoder
-  require Logger
-  import Protobuf.Decoder
-
-  test "encode_type/2 varint" do
-    assert encode(:int32, 42) == <<42>>
-  end
-
-  test "encode_type/2 min int32" do
-    assert encode(:int32, -2_147_483_648) ==
-             <<128, 128, 128, 128, 248, 255, 255, 255, 255, 1>>
-  end
-
-  test "encode_type/2 min int64" do
-    assert encode(:int64, -9_223_372_036_854_775_808) ==
-             <<128, 128, 128, 128, 128, 128, 128, 128, 128, 1>>
-  end
-
-  test "encode_type/3 min sint32" do
-    assert encode(:sint32, -2_147_483_648) == <<255, 255, 255, 255, 15>>
-  end
-
-  test "encode_type/3 max sint32" do
-    assert encode(:sint32, 2_147_483_647) == <<254, 255, 255, 255, 15>>
-  end
-
-  test "encode_type/3 min sint64" do
-    assert encode(:sint64, -9_223_372_036_854_775_808) ==
-             <<255, 255, 255, 255, 255, 255, 255, 255, 255, 1>>
-  end
-
-  test "encode_type/3 max sint64" do
-    assert encode(:sint64, 9_223_372_036_854_775_807) ==
-             <<254, 255, 255, 255, 255, 255, 255, 255, 255, 1>>
-  end
-
-  test "encode_type/3 bool false" do
-    assert encode(:bool, false) == <<0>>
-  end
-
-  test "encode_type/3 bool true" do
-    assert encode(:bool, true) == <<1>>
-  end
-
-  test "encode_type/3 a fixed64" do
-    assert encode(:fixed64, 8_446_744_073_709_551_615) ==
-             <<255, 255, 23, 118, 251, 220, 56, 117>>
-  end
-
-  test "encode_type/3 max fixed64" do
-    assert encode(:fixed64, 18_446_744_073_709_551_615) ==
-             <<255, 255, 255, 255, 255, 255, 255, 255>>
-  end
-
-  test "encode_type/3 min sfixed64" do
-    assert encode(:sfixed64, -9_223_372_036_854_775_808) ==
-             <<0, 0, 0, 0, 0, 0, 0, 128>>
-  end
-
-  test "encode_type/3 max sfixed64" do
-    assert encode(:sfixed64, 9_223_372_036_854_775_807) ==
-             <<255, 255, 255, 255, 255, 255, 255, 127>>
-  end
-
-  test "encode_type/3 min double" do
-    assert encode(:double, 5.0e-324) == <<1, 0, 0, 0, 0, 0, 0, 0>>
-  end
-
-  test "encode_type/3 max double" do
-    assert encode(:double, 1.7976931348623157e308) ==
-             <<255, 255, 255, 255, 255, 255, 239, 127>>
-  end
-
-  test "encode_type/3 int as double" do
-    assert encode(:double, -9_223_372_036_854_775_808) ==
-             <<0, 0, 0, 0, 0, 0, 224, 195>>
-  end
-
-  test "encode_type/3 string" do
-    assert encode(:string, "testing") == <<7, 116, 101, 115, 116, 105, 110, 103>>
-  end
-
-  test "encode_type/3 bytes" do
-    assert encode(:bytes, <<42, 43, 44, 45>>) == <<4, 42, 43, 44, 45>>
-  end
-
-  test "encode_type/3 fixed32" do
-    assert encode(:fixed32, 4_294_967_295) == <<255, 255, 255, 255>>
-  end
-
-  test "encode_type/3 sfixed32" do
-    assert encode(:sfixed32, 2_147_483_647) == <<255, 255, 255, 127>>
-  end
-
-  test "encode_type/3 float" do
-    assert encode(:float, 3.4028234663852886e38) == <<255, 255, 127, 127>>
-  end
-
-  test "encode_type/3 int as float" do
-    assert encode(:float, 3) == <<0, 0, 64, 64>>
-  end
-
-  test "encode_type/3 float infinity/-infinity/nan" do
-    Enum.each([:infinity, :negative_infinity, :nan], fn f ->
-      bin = encode(:float, f)
-      assert f == Decoder.decode_type_m(:float, :fake, bin)
-    end)
-  end
-
-  test "encode_type/3 double infinity/-infinity/nan" do
-    Enum.each([:infinity, :negative_infinity, :nan], fn f ->
-      bin = encode(:double, f)
-      assert f == Decoder.decode_type_m(:double, :fake, bin)
-    end)
-  end
-
-  test "encode_type/2 wrong uint32" do
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:uint32, 12_345_678_901_234_567_890)
-    end
-
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:uint32, -1)
-    end
-  end
-
-  test "encode_type/2 wrong uint64" do
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:uint64, 184_467_440_737_095_516_150)
-    end
-
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:uint64, -1)
-    end
-  end
-
-  test "encode_type/2 wrong fixed32" do
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:fixed32, 12_345_678_901_234_567_890)
-    end
-
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:fixed32, -1)
-    end
-  end
-
-  test "encode_type/2 wrong fixed64" do
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:fixed64, 184_467_440_737_095_516_150)
-    end
-
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:fixed64, -1)
-    end
-  end
-
-  test "encode_type/2 wrong int32" do
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:int32, 2_147_483_648)
-    end
-
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:int32, -2_147_483_649)
-    end
-  end
-
-  test "encode_type/2 wrong int64" do
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:int64, 184_467_440_737_095_516_150)
-    end
-
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:int64, -184_467_440_737_095_516_150)
-    end
-  end
-
-  test "encode_type/2 wrong sint32" do
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:sint32, 2_147_483_648)
-    end
-
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:sint32, -2_147_483_649)
-    end
-  end
-
-  test "encode_type/2 wrong sint64" do
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:sint64, 184_467_440_737_095_516_150)
-    end
-
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:sint64, -184_467_440_737_095_516_150)
-    end
-  end
-
-  test "encode_type/2 wrong sfixed32" do
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:sfixed32, 2_147_483_648)
-    end
-
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:sfixed32, -2_147_483_649)
-    end
-  end
-
-  test "encode_type/2 wrong sfixed64" do
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:sfixed64, 184_467_440_737_095_516_150)
-    end
-
-    assert_raise Protobuf.TypeEncodeError, fn ->
-      encode(:sfixed64, -184_467_440_737_095_516_150)
-    end
-  end
-
-  defp encode(type, value) do
-    type
-    |> Encoder.encode_type(value)
-    |> IO.iodata_to_binary()
-  end
-end
diff --git a/test/protobuf/encoder/encode_varint_test.exs b/test/protobuf/encoder/encode_varint_test.exs
deleted file mode 100644
index 23c4be6..0000000
--- a/test/protobuf/encoder/encode_varint_test.exs
+++ /dev/null
@@ -1,51 +0,0 @@
-defmodule Protobuf.Encoder.DecodeVarintTest do
-  use ExUnit.Case, async: true
-
-  alias Protobuf.Encoder
-
-  test "encode_varint 300" do
-    assert encode(300) == <<0b1010110000000010::16>>
-  end
-
-  test "encode_varint 150" do
-    assert encode(150) == <<150, 1>>
-  end
-
-  test "encode_varint 0" do
-    assert encode(0) == <<0>>
-  end
-
-  test "encode_varint/2 min int32" do
-    assert encode(-2_147_483_648) ==
-             <<128, 128, 128, 128, 248, 255, 255, 255, 255, 1>>
-  end
-
-  test "encode_varint max int32" do
-    assert encode(2_147_483_647) == <<255, 255, 255, 255, 7>>
-  end
-
-  test "encode_varint/2 min int64" do
-    assert encode(-9_223_372_036_854_775_808) ==
-             <<128, 128, 128, 128, 128, 128, 128, 128, 128, 1>>
-  end
-
-  test "encode_varint max int64" do
-    assert encode(9_223_372_036_854_775_807) ==
-             <<255, 255, 255, 255, 255, 255, 255, 255, 127>>
-  end
-
-  test "encode_varint max uint32" do
-    assert encode(4_294_967_295) == <<255, 255, 255, 255, 15>>
-  end
-
-  test "encode_varint max uint64" do
-    assert encode(18_446_744_073_709_551_615) ==
-             <<255, 255, 255, 255, 255, 255, 255, 255, 255, 1>>
-  end
-
-  defp encode(varint) do
-    varint
-    |> Encoder.encode_varint()
-    |> IO.iodata_to_binary()
-  end
-end
diff --git a/test/protobuf/encoder_validation_test.exs b/test/protobuf/encoder_validation_test.exs
index bdc124c..160af8e 100644
--- a/test/protobuf/encoder_validation_test.exs
+++ b/test/protobuf/encoder_validation_test.exs
@@ -1,8 +1,6 @@
 defmodule Protobuf.EncoderTest.Validation do
   use ExUnit.Case, async: true
 
-  import Protobuf.Encoder
-
   @valid_vals %{
     int32: -32,
     int64: -64,
@@ -33,7 +31,7 @@ defmodule Protobuf.EncoderTest.Validation do
     assert_invalid = fn type, others ->
       Enum.each(other_types(others), fn {invalid, err_type} ->
         assert_raise err_type, fn ->
-          encode_type(type, invalid)
+          Protobuf.Wire.from_proto(type, invalid)
         end
       end)
     end
diff --git a/test/protobuf/wire/varint_test.exs b/test/protobuf/wire/varint_test.exs
new file mode 100644
index 0000000..c3a6044
--- /dev/null
+++ b/test/protobuf/wire/varint_test.exs
@@ -0,0 +1,107 @@
+defmodule Protobuf.Wire.VarintTest do
+  use ExUnit.Case, async: true
+  doctest Protobuf.Wire.Varint
+
+  alias Protobuf.Wire.Varint
+
+  describe "encode/1" do
+    test "300" do
+      assert encode(300) == <<0b10101100, 0b00000010>>
+    end
+
+    test "150" do
+      assert encode(150) == <<150, 1>>
+    end
+
+    test "0" do
+      assert encode(0) == <<0>>
+    end
+
+    test "1" do
+      assert encode(1) == <<1>>
+    end
+
+    test "min int32" do
+      assert encode(-2_147_483_648) == <<128, 128, 128, 128, 248, 255, 255, 255, 255, 1>>
+    end
+
+    test "max int32" do
+      assert encode(2_147_483_647) == <<255, 255, 255, 255, 7>>
+    end
+
+    test "min int64" do
+      assert encode(-9_223_372_036_854_775_808) ==
+               <<128, 128, 128, 128, 128, 128, 128, 128, 128, 1>>
+    end
+
+    test "max int64" do
+      assert encode(9_223_372_036_854_775_807) ==
+               <<255, 255, 255, 255, 255, 255, 255, 255, 127>>
+    end
+
+    test "max uint32" do
+      assert encode(4_294_967_295) == <<255, 255, 255, 255, 15>>
+    end
+
+    test "max uint64" do
+      assert encode(18_446_744_073_709_551_615) ==
+               <<255, 255, 255, 255, 255, 255, 255, 255, 255, 1>>
+    end
+
+    defp encode(n) do
+      n
+      |> Varint.encode()
+      |> IO.iodata_to_binary()
+    end
+  end
+
+  describe "decode/1" do
+    require Varint
+
+    Varint.decoder(:defp, :decode, do: {value, rest})
+
+    test "300" do
+      assert {300, ""} == decode(<<0b1010110000000010::16>>)
+    end
+
+    test "150" do
+      assert {150, ""} == decode(<<150, 01>>)
+    end
+
+    test "0" do
+      assert {0, ""} == decode(<<0>>)
+    end
+
+    test "1" do
+      assert {1, ""} == decode(<<1>>)
+    end
+
+    test "min int32" do
+      {val, ""} = decode(<<128, 128, 128, 128, 248, 255, 255, 255, 255, 1>>)
+      assert <<-2_147_483_648::signed-32>> == <<val::32>>
+    end
+
+    test "max int32" do
+      assert {2_147_483_647, ""} == decode(<<255, 255, 255, 255, 7>>)
+    end
+
+    test "min int64" do
+      {val, ""} = decode(<<128, 128, 128, 128, 128, 128, 128, 128, 128, 1>>)
+      assert <<-9_223_372_036_854_775_808::signed-64>> == <<val::64>>
+    end
+
+    test "max int64" do
+      assert {9_223_372_036_854_775_807, ""} ==
+               decode(<<255, 255, 255, 255, 255, 255, 255, 255, 127>>)
+    end
+
+    test "max uint32" do
+      assert {4_294_967_295, ""} == decode(<<255, 255, 255, 255, 15>>)
+    end
+
+    test "max uint64" do
+      assert {18_446_744_073_709_551_615, ""} ==
+               decode(<<255, 255, 255, 255, 255, 255, 255, 255, 255, 1>>)
+    end
+  end
+end
diff --git a/test/protobuf/wire_test.exs b/test/protobuf/wire_test.exs
new file mode 100644
index 0000000..3710f76
--- /dev/null
+++ b/test/protobuf/wire_test.exs
@@ -0,0 +1,382 @@
+defmodule Protobuf.WireTest do
+  use ExUnit.Case, async: true
+
+  alias Protobuf.Wire
+
+  describe "from_proto/2" do
+    test "varint" do
+      assert encode(:int32, 42) == <<42>>
+    end
+
+    test "min int32" do
+      assert encode(:int32, -2_147_483_648) ==
+               <<128, 128, 128, 128, 248, 255, 255, 255, 255, 1>>
+    end
+
+    test "min int64" do
+      assert encode(:int64, -9_223_372_036_854_775_808) ==
+               <<128, 128, 128, 128, 128, 128, 128, 128, 128, 1>>
+    end
+
+    test "min sint32" do
+      assert encode(:sint32, -2_147_483_648) == <<255, 255, 255, 255, 15>>
+    end
+
+    test "max sint32" do
+      assert encode(:sint32, 2_147_483_647) == <<254, 255, 255, 255, 15>>
+    end
+
+    test "min sint64" do
+      assert encode(:sint64, -9_223_372_036_854_775_808) ==
+               <<255, 255, 255, 255, 255, 255, 255, 255, 255, 1>>
+    end
+
+    test "max sint64" do
+      assert encode(:sint64, 9_223_372_036_854_775_807) ==
+               <<254, 255, 255, 255, 255, 255, 255, 255, 255, 1>>
+    end
+
+    test "bool false" do
+      assert encode(:bool, false) == <<0>>
+    end
+
+    test "bool true" do
+      assert encode(:bool, true) == <<1>>
+    end
+
+    test "enum atom and alias" do
+      assert encode({:enum, TestMsg.EnumFoo}, :C) == <<4>>
+      assert encode({:enum, TestMsg.EnumFoo}, :D) == <<4>>
+    end
+
+    test "enum known and unknown integer" do
+      assert encode({:enum, TestMsg.EnumFoo}, 1) == <<1>>
+      assert encode({:enum, TestMsg.EnumFoo}, 5) == <<5>>
+    end
+
+    test "a fixed64" do
+      assert encode(:fixed64, 8_446_744_073_709_551_615) ==
+               <<255, 255, 23, 118, 251, 220, 56, 117>>
+    end
+
+    test "max fixed64" do
+      assert encode(:fixed64, 18_446_744_073_709_551_615) ==
+               <<255, 255, 255, 255, 255, 255, 255, 255>>
+    end
+
+    test "min sfixed64" do
+      assert encode(:sfixed64, -9_223_372_036_854_775_808) ==
+               <<0, 0, 0, 0, 0, 0, 0, 128>>
+    end
+
+    test "max sfixed64" do
+      assert encode(:sfixed64, 9_223_372_036_854_775_807) ==
+               <<255, 255, 255, 255, 255, 255, 255, 127>>
+    end
+
+    test "min double" do
+      assert encode(:double, 5.0e-324) == <<1, 0, 0, 0, 0, 0, 0, 0>>
+    end
+
+    test "max double" do
+      assert encode(:double, 1.7976931348623157e308) == <<255, 255, 255, 255, 255, 255, 239, 127>>
+    end
+
+    test "int as double" do
+      assert encode(:double, -9_223_372_036_854_775_808) == <<0, 0, 0, 0, 0, 0, 224, 195>>
+    end
+
+    test "string" do
+      assert encode(:string, "testing") == <<7, 116, 101, 115, 116, 105, 110, 103>>
+    end
+
+    test "bytes" do
+      assert encode(:bytes, <<42, 43, 44, 45>>) == <<4, 42, 43, 44, 45>>
+    end
+
+    test "fixed32" do
+      assert encode(:fixed32, 4_294_967_295) == <<255, 255, 255, 255>>
+    end
+
+    test "sfixed32" do
+      assert encode(:sfixed32, 2_147_483_647) == <<255, 255, 255, 127>>
+    end
+
+    test "float" do
+      assert encode(:float, 3.4028234663852886e38) == <<255, 255, 127, 127>>
+    end
+
+    test "int as float" do
+      assert encode(:float, 3) == <<0, 0, 64, 64>>
+    end
+
+    test "float infinity/-infinity/nan" do
+      Enum.each([:infinity, :negative_infinity, :nan], fn f ->
+        bin = encode(:float, f)
+        assert f == Wire.to_proto(:float, bin)
+      end)
+    end
+
+    test "double infinity, -infinity, nan" do
+      Enum.each([:infinity, :negative_infinity, :nan], fn f ->
+        bin = encode(:double, f)
+        assert f == Wire.to_proto(:double, bin)
+      end)
+    end
+
+    test "wrong uint32" do
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:uint32, 12_345_678_901_234_567_890)
+      end
+
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:uint32, -1)
+      end
+    end
+
+    test "wrong uint64" do
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:uint64, 184_467_440_737_095_516_150)
+      end
+
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:uint64, -1)
+      end
+    end
+
+    test "wrong fixed32" do
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:fixed32, 12_345_678_901_234_567_890)
+      end
+
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:fixed32, -1)
+      end
+    end
+
+    test "wrong fixed64" do
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:fixed64, 184_467_440_737_095_516_150)
+      end
+
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:fixed64, -1)
+      end
+    end
+
+    test "wrong int32" do
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:int32, 2_147_483_648)
+      end
+
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:int32, -2_147_483_649)
+      end
+    end
+
+    test "wrong int64" do
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:int64, 184_467_440_737_095_516_150)
+      end
+
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:int64, -184_467_440_737_095_516_150)
+      end
+    end
+
+    test "wrong sint32" do
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:sint32, 2_147_483_648)
+      end
+
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:sint32, -2_147_483_649)
+      end
+    end
+
+    test "wrong sint64" do
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:sint64, 184_467_440_737_095_516_150)
+      end
+
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:sint64, -184_467_440_737_095_516_150)
+      end
+    end
+
+    test "wrong sfixed32" do
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:sfixed32, 2_147_483_648)
+      end
+
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:sfixed32, -2_147_483_649)
+      end
+    end
+
+    test "wrong sfixed64" do
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:sfixed64, 184_467_440_737_095_516_150)
+      end
+
+      assert_raise Protobuf.TypeEncodeError, fn ->
+        encode(:sfixed64, -184_467_440_737_095_516_150)
+      end
+    end
+
+    defp encode(type, value) do
+      type
+      |> Wire.from_proto(value)
+      |> IO.iodata_to_binary()
+    end
+  end
+
+  describe "to_proto/2" do
+    test "varint" do
+      assert 42 == Wire.to_proto(:int32, 42)
+    end
+
+    test "int64" do
+      assert -1 == Wire.to_proto(:int64, -1)
+    end
+
+    test "min int32" do
+      assert -2_147_483_648 == Wire.to_proto(:int32, 18_446_744_071_562_067_968)
+    end
+
+    test "max int32" do
+      assert -2_147_483_647 == Wire.to_proto(:int32, 18_446_744_071_562_067_969)
+    end
+
+    test "min int64" do
+      assert -9_223_372_036_854_775_808 == Wire.to_proto(:int64, 9_223_372_036_854_775_808)
+    end
+
+    test "max int64" do
+      assert 9_223_372_036_854_775_807 == Wire.to_proto(:int64, 9_223_372_036_854_775_807)
+    end
+
+    test "min sint32" do
+      assert -2_147_483_648 == Wire.to_proto(:sint32, 4_294_967_295)
+    end
+
+    test "max sint32" do
+      assert 2_147_483_647 == Wire.to_proto(:sint32, 4_294_967_294)
+    end
+
+    test "min sint64" do
+      assert -9_223_372_036_854_775_808 == Wire.to_proto(:sint64, 18_446_744_073_709_551_615)
+    end
+
+    test "max sint64" do
+      assert 9_223_372_036_854_775_807 == Wire.to_proto(:sint64, 18_446_744_073_709_551_614)
+    end
+
+    test "max uint32" do
+      assert 4_294_967_295 == Wire.to_proto(:uint32, 4_294_967_295)
+    end
+
+    test "max uint64" do
+      assert 9_223_372_036_854_775_807 == Wire.to_proto(:uint64, 9_223_372_036_854_775_807)
+    end
+
+    test "bool works" do
+      assert true == Wire.to_proto(:bool, 1)
+      assert false == Wire.to_proto(:bool, 0)
+    end
+
+    test "enum known and unknown integer" do
+      assert :A == Wire.to_proto({:enum, TestMsg.EnumFoo}, 1)
+
+      assert ExUnit.CaptureLog.capture_log(fn ->
+               assert 5 == Wire.to_proto({:enum, TestMsg.EnumFoo}, 5)
+             end) =~ ~r/unknown enum value 5 when decoding for TestMsg\.EnumFoo/
+    end
+
+    test "a fixed64" do
+      assert 8_446_744_073_709_551_615 ==
+               Wire.to_proto(:fixed64, <<255, 255, 23, 118, 251, 220, 56, 117>>)
+    end
+
+    test "max fixed64" do
+      assert 18_446_744_073_709_551_615 ==
+               Wire.to_proto(:fixed64, <<255, 255, 255, 255, 255, 255, 255, 255>>)
+    end
+
+    test "min sfixed64" do
+      assert -9_223_372_036_854_775_808 == Wire.to_proto(:sfixed64, <<0, 0, 0, 0, 0, 0, 0, 128>>)
+    end
+
+    test "max sfixed64" do
+      assert 9_223_372_036_854_775_807 ==
+               Wire.to_proto(:sfixed64, <<255, 255, 255, 255, 255, 255, 255, 127>>)
+    end
+
+    test "min double" do
+      assert 5.0e-324 == Wire.to_proto(:double, <<1, 0, 0, 0, 0, 0, 0, 0>>)
+    end
+
+    test "max double" do
+      assert 1.7976931348623157e308 ==
+               Wire.to_proto(:double, <<255, 255, 255, 255, 255, 255, 239, 127>>)
+    end
+
+    test "string" do
+      assert "testing" == Wire.to_proto(:string, <<116, 101, 115, 116, 105, 110, 103>>)
+    end
+
+    test "bytes" do
+      assert <<42, 43, 44, 45>> == Wire.to_proto(:bytes, <<42, 43, 44, 45>>)
+    end
+
+    test "fixed32" do
+      assert 4_294_967_295 == Wire.to_proto(:fixed32, <<255, 255, 255, 255>>)
+    end
+
+    test "sfixed32" do
+      assert 2_147_483_647 == Wire.to_proto(:sfixed32, <<255, 255, 255, 127>>)
+    end
+
+    test "float" do
+      assert 3.4028234663852886e38 == Wire.to_proto(:float, <<255, 255, 127, 127>>)
+    end
+
+    test "float infinity, -infinity, nan" do
+      assert :infinity == Wire.to_proto(:float, <<0, 0, 128, 127>>)
+      assert :negative_infinity == Wire.to_proto(:float, <<0, 0, 128, 255>>)
+      assert :nan == Wire.to_proto(:float, <<0, 0, 192, 127>>)
+    end
+
+    test "double infinity, -infinity, nan" do
+      assert :infinity == Wire.to_proto(:double, <<0, 0, 0, 0, 0, 0, 240, 127>>)
+      assert :negative_infinity == Wire.to_proto(:double, <<0, 0, 0, 0, 0, 0, 240, 255>>)
+      assert :nan == Wire.to_proto(:double, <<1, 0, 0, 0, 0, 0, 248, 127>>)
+    end
+
+    test "mismatching fixed-length sizes" do
+      msg = "can't decode <<0, 0, 0>> into type fixed32"
+
+      assert_raise Protobuf.DecodeError, msg, fn ->
+        Wire.to_proto(:fixed32, <<0, 0, 0>>)
+      end
+
+      msg = "can't decode <<0, 0, 0, 0, 0>> into type fixed32"
+
+      assert_raise Protobuf.DecodeError, msg, fn ->
+        Wire.to_proto(:fixed32, <<0, 0, 0, 0, 0>>)
+      end
+
+      msg = "can't decode <<0, 0, 0, 0, 0, 0, 0>> into type fixed64"
+
+      assert_raise Protobuf.DecodeError, msg, fn ->
+        Wire.to_proto(:fixed64, <<0, 0, 0, 0, 0, 0, 0>>)
+      end
+
+      msg = "can't decode <<0, 0, 0, 0, 0, 0, 0, 0, 0>> into type fixed64"
+
+      assert_raise Protobuf.DecodeError, msg, fn ->
+        Wire.to_proto(:fixed64, <<0, 0, 0, 0, 0, 0, 0, 0, 0>>)
+      end
+    end
+  end
+end
diff --git a/test/support/doctest.ex b/test/support/doctest.ex
index ea34e8e..37050ef 100644
--- a/test/support/doctest.ex
+++ b/test/support/doctest.ex
@@ -17,3 +17,19 @@ defmodule Car do
   field :color, 1, type: Color, enum: true
   field :top_speed, 2, type: :float, json_name: "topSpeed"
 end
+
+defmodule VarintDecoders do
+  import Protobuf.Wire.Varint
+
+  decoder :def, :decode_and_sum, [:plus] do
+    {:ok, value + plus, rest}
+  end
+
+  def decode_all(<<bin::bits>>), do: decode_all(bin, [])
+
+  defp decode_all(<<>>, acc), do: acc
+
+  decoder :defp, :decode_all, [:acc] do
+    decode_all(rest, [value | acc])
+  end
+end