diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..01b60228 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/_build +/cover +/deps +/doc +erl_crash.dump +*.ez +*.beam diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f608e9e4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Alexandre Hamez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..29065ba8 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Protox + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed as: + + 1. Add `protox` to your list of dependencies in `mix.exs`: + + ```elixir + def deps do + [{:protox, "~> 0.1.0"}] + end + ``` + + 2. Ensure `protox` is started before your application: + + ```elixir + def application do + [applications: [:protox]] + end + ``` + diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 00000000..405452e0 --- /dev/null +++ b/config/config.exs @@ -0,0 +1,30 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Mix.Config module. +use Mix.Config + +# This configuration is loaded before any dependency and is restricted +# to this project. If another project depends on this project, this +# file won't be loaded nor affect the parent project. For this reason, +# if you want to provide default values for your application for +# 3rd-party users, it should be done in your "mix.exs" file. + +# You can configure for your application as: +# +# config :protox, key: :value +# +# And access this configuration in your application as: +# +# Application.get_env(:protox, :key) +# +# Or configure a 3rd-party app: +# +# config :logger, level: :info +# + +# It is also possible to import configuration files, relative to this +# directory. For example, you can emulate configuration per environment +# by uncommenting the line below and defining dev.exs, test.exs and such. +# Configuration from the imported file will override the ones defined +# here (which is why it is important to import them last). +# +# import_config "#{Mix.env}.exs" diff --git a/lib/protox.ex b/lib/protox.ex new file mode 100644 index 00000000..e90299fc --- /dev/null +++ b/lib/protox.ex @@ -0,0 +1,2 @@ +defmodule Protox do +end diff --git a/lib/protox/decode.ex b/lib/protox/decode.ex new file mode 100644 index 00000000..1f0a1828 --- /dev/null +++ b/lib/protox/decode.ex @@ -0,0 +1,175 @@ +defmodule Protox.Decode do + + require Protox.Util + + + alias Protox.{ + Enumeration, + Message, + Util, + } + import Util + + + def decode(bytes, defs) do + parse_key_value(bytes, defs, struct(defs.name)) + end + + + # -- Private + + + defp parse_key_value(<<>>, _, msg) do + msg + end + defp parse_key_value(bytes, defs, msg) do + {tag, wire_type, rest} = parse_key(bytes) + + field = defs.fields[tag] + {new_msg, new_rest} = if field do + {value, new_rest} = parse_value(rest, wire_type, field.type) + {set_field(msg, field, value), new_rest} + else + # TODO. Keep unknown bytes? + {msg, parse_unknown(wire_type, rest)} + end + + parse_key_value(new_rest, defs, new_msg) + end + + + # Get the key's tag and wire type. + defp parse_key(bytes) do + use Bitwise + {key, rest} = Varint.LEB128.decode(bytes) + {key >>> 3, key &&& 0b111, rest} + end + + + # Wire type 0: varint. + defp parse_value(bytes, 0, type) do + {value, rest} = Varint.LEB128.decode(bytes) + {varint_value(value, type), rest} + end + + + # Wire type 1: fixed 64-bit. + defp parse_value(<>, 1, :double) do + {value, rest} + end + defp parse_value(<>, 1, _) do + {value, rest} + end + + + # Wire type 2: length-delimited. + defp parse_value(bytes, 2, type) do + {len, new_bytes} = Varint.LEB128.decode(bytes) + <> = new_bytes + {parse_delimited(delimited, type), rest} + end + + + # Wire type 5: fixed 32-bit. + defp parse_value(<>, 5, :float) do + {value, rest} + end + defp parse_value(<>, 5, _) do + {value, rest} + end + + + defp parse_delimited(bytes, type) when is_primitive_varint(type) do + parse_repeated_varint([], bytes, type) + end + defp parse_delimited(bytes, type) when is_primitive_fixed(type) do + parse_repeated_fixed([], bytes, type) + end + defp parse_delimited(bytes, type) when type == :string or type == :bytes do + bytes + end + defp parse_delimited(bytes, type = %Message{}) do + decode(bytes, type) + end + + + defp parse_repeated_varint(acc, <<>>, _) do + Enum.reverse(acc) + end + defp parse_repeated_varint(acc, bytes, type) do + {value, rest} = Varint.LEB128.decode(bytes) + parse_repeated_varint([varint_value(value, type)|acc], rest, type) + end + + + defp parse_repeated_fixed(acc, <<>>, _) do + Enum.reverse(acc) + end + defp parse_repeated_fixed(acc, <>, ty) + when ty == :fixed64 or ty == :sfixed64 do + parse_repeated_fixed([value|acc], rest, ty) + end + defp parse_repeated_fixed(acc, <>, ty) + when ty == :fixed32 or ty == :sfixed32 do + parse_repeated_fixed([value|acc], rest, ty) + end + defp parse_repeated_fixed(acc, <>, :double) do + parse_repeated_fixed([value|acc], rest, :double) + end + defp parse_repeated_fixed(acc, <>, :float) do + parse_repeated_fixed([value|acc], rest, :float) + end + + + defp varint_value(value, :bool) , do: value == 1 + defp varint_value(value, :sint32) , do: Varint.Zigzag.decode(value) + defp varint_value(value, :sint64) , do: Varint.Zigzag.decode(value) + defp varint_value(value, :uint32) , do: value + defp varint_value(value, :uint64) , do: value + defp varint_value(value, e = %Enumeration{}), do: Map.get(e.members, value, value) + defp varint_value(value, :int32) do + <> = <> + res + end + defp varint_value(value, :int64) do + <> = <> + res + end + + + defp parse_unknown(0, bytes) , do: get_varint_bytes(bytes) + defp parse_unknown(1, <<_::64, rest::binary>>), do: rest + defp parse_unknown(5, <<_::32, rest::binary>>), do: rest + defp parse_unknown(2, bytes) do + {len, new_bytes} = Varint.LEB128.decode(bytes) + <<_::binary-size(len), rest::binary>> = new_bytes + rest + end + + + defp get_varint_bytes(<<0::1, _::7, rest::binary>>), do: rest + defp get_varint_bytes(<<1::1, _::7, rest::binary>>), do: get_varint_bytes(rest) + + + # Set the field correponding to `tag` in `msg` with `value`. + defp set_field(msg, field, value) do + {f, v} = case field.kind do + :map -> + previous = Map.fetch!(msg, field.name) + {field.name, Map.put(previous, value.key, value.value)} + + {:oneof, parent_field} -> + {parent_field, {field.name, value}} + + :repeated -> + previous = Map.fetch!(msg, field.name) + {field.name, previous ++ List.wrap(value)} + + :normal -> + {field.name, value} + end + + struct!(msg, [{f, v}]) + end + +end \ No newline at end of file diff --git a/lib/protox/default.ex b/lib/protox/default.ex new file mode 100644 index 00000000..41978772 --- /dev/null +++ b/lib/protox/default.ex @@ -0,0 +1,26 @@ +defmodule Protox.Default do + + alias Protox.{ + Enumeration, + Message, + } + + def default(:bool) , do: false + def default(:int32) , do: 0 + def default(:uint32) , do: 0 + def default(:int64) , do: 0 + def default(:uint64) , do: 0 + def default(:sint32) , do: 0 + def default(:sint64) , do: 0 + def default(:fixed64) , do: 0 + def default(:sfixed64) , do: 0 + def default(:fixed32) , do: 0 + def default(:sfixed32) , do: 0 + def default(:double) , do: 0 + def default(:float) , do: 0 + def default(:string) , do: "" + def default(:bytes) , do: <<>> + def default(e = %Enumeration{}), do: Map.fetch!(e.members, 0) + def default(%Message{}) , do: nil + +end diff --git a/lib/protox/defs.ex b/lib/protox/defs.ex new file mode 100644 index 00000000..361115a4 --- /dev/null +++ b/lib/protox/defs.ex @@ -0,0 +1,22 @@ +defmodule Protox.Field do + @enforce_keys [:name, :kind, :type] + defstruct name: nil, + kind: nil, + type: nil + # repeated: false, + # map: false, + # oneof: nil +end + +defmodule Protox.Message do + @enforce_keys [:name, :fields, :tags] + defstruct name: nil, + fields: %{}, + tags: %{} +end + +defmodule Protox.Enumeration do + @enforce_keys [:members] + defstruct members: %{}, + values: %{} +end diff --git a/lib/protox/encode.ex b/lib/protox/encode.ex new file mode 100644 index 00000000..51a0c7de --- /dev/null +++ b/lib/protox/encode.ex @@ -0,0 +1,179 @@ +defmodule Protox.Encode do + + alias Protox.{ + Default, + Enumeration, + Field, + Message, + Util, + } + import Util + use Bitwise + + + def encode(msg) do + defs = msg.__struct__.defs() + Enum.reduce( + defs.tags, + <<>>, + fn (tag, acc) -> + field = Map.fetch!(defs.fields, tag) + encode(field.kind, acc, msg, field, tag) + end) + end + + + # -- Private + + + defp encode(:map, acc, msg, field, tag) do + map = Map.fetch!(msg, field.name) + if map_size(map) == 0 do + acc + else + Enum.reduce( + map, + acc, + fn ({k, v}, acc) -> + # Creates a temporary message which acts as map entry. + # (https://developers.google.com/protocol-buffers/docs/proto3#backwards-compatibility) + msg = struct!(field.type.name, [{:key, k}, {:value, v}]) + value = encode(msg) + len = Varint.LEB128.encode(byte_size(value)) + key = make_key_bytes(tag, field.type) + <> + end) + end + end + + + defp encode(:repeated, acc, msg, field = %Field{type: ty}, tag) when is_primitive(ty) do + case Map.fetch!(msg, field.name) do + [] -> + acc + + values -> + key = Varint.LEB128.encode(tag <<< 3 ||| 2) + value = encode_packed(values, field) + <> + end + end + defp encode(:repeated, acc, msg, field, tag) do + case Map.fetch!(msg, field.name) do + [] -> + acc + + values -> + value = encode_repeated(values, field, tag, field.type) + <> + end + end + + + defp encode({:oneof, parent_field}, acc, msg, field, tag) do + name = field.name + + case Map.fetch!(msg, parent_field) do + nil -> + acc + + # The parent oneof field is set to the current field. + {^name, field_value} -> + key = make_key_bytes(tag, field.type) + value = encode_value(field_value, field) + <> + + _ -> + acc + end + end + + + defp encode(:normal, acc, msg, field, tag) do + field_value = Map.fetch!(msg, field.name) + if field_value == Default.default(field.type) do + acc + else + key = make_key_bytes(tag, field.type) + value = encode_value(field_value, field) + <> + end + end + + + defp make_key_bytes(tag, ty) do + Varint.LEB128.encode(make_key(tag, ty)) + end + + + defp make_key(tag, ty) when is_primitive_varint(ty) , do: tag <<< 3 + defp make_key(tag, %Enumeration{}) , do: tag <<< 3 + defp make_key(tag, ty) when is_primitive_fixed64(ty), do: tag <<< 3 ||| 1 + defp make_key(tag, ty) when is_delimited(ty) , do: tag <<< 3 ||| 2 + defp make_key(tag, %Message{}) , do: tag <<< 3 ||| 2 + defp make_key(tag, ty) when is_primitive_fixed32(ty), do: tag <<< 3 ||| 5 + + + defp encode_packed(values, field) do + bytes = Enum.reduce( + values, + <<>>, + fn (value, acc) -> + <> + end) + + <> + end + + + defp encode_repeated(values, field, tag, ty) do + Enum.reduce( + values, + <<>>, + fn (value, acc) -> + <> + end) + end + + + defp encode_value(true, _) do + <<1>> + end + defp encode_value(value, %Field{type: ty}) when ty == :sint32 or ty == :sint64 do + value + |> Varint.Zigzag.encode() + |> Varint.LEB128.encode() + end + defp encode_value(value, %Field{type: ty}) when ty == :int32 or ty == :int64 do + <> = <> + Varint.LEB128.encode(res) + end + defp encode_value(value, %Field{type: ty}) when is_primitive_varint(ty) do + Varint.LEB128.encode(value) + end + defp encode_value(value, %Field{type: :double}) do + <> + end + defp encode_value(value, %Field{type: :float}) do + <> + end + defp encode_value(value, %Field{type: ty}) when ty == :sfixed64 or ty == :fixed64 do + <> + end + defp encode_value(value, %Field{type: ty}) when ty == :sfixed32 or ty == :fixed32 do + <> + end + defp encode_value(value, %Field{type: ty}) when ty == :string or ty == :bytes do + len = Varint.LEB128.encode(byte_size(value)) + <> + end + defp encode_value(value, %Field{type: %Message{}}) do + encoded = encode(value) + len = byte_size(encoded) |> Varint.LEB128.encode() + <> + end + defp encode_value(value, %Field{type: e = %Enumeration{}}) do + Varint.LEB128.encode(Map.get(e.values, value, value)) + end + +end \ No newline at end of file diff --git a/lib/protox/util.ex b/lib/protox/util.ex new file mode 100644 index 00000000..b1d23144 --- /dev/null +++ b/lib/protox/util.ex @@ -0,0 +1,58 @@ +defmodule Protox.Util do + + defmacro is_primitive(type) do + quote do + unquote(type) == :int32 or unquote(type) == :int64 or\ + unquote(type) == :uint32 or unquote(type) == :uint64 or\ + unquote(type) == :sint32 or unquote(type) == :sint64 or\ + unquote(type) == :bool or unquote(type) == :float or\ + unquote(type) == :fixed32 or unquote(type) == :sfixed32 or\ + unquote(type) == :double or unquote(type) == :fixed64 or\ + unquote(type) == :sfixed32 + + end + end + + + defmacro is_primitive_varint(type) do + quote do + unquote(type) == :int32 or unquote(type) == :int64 or\ + unquote(type) == :uint32 or unquote(type) == :uint64 or\ + unquote(type) == :sint32 or unquote(type) == :sint64 or\ + unquote(type) == :bool or unquote(type) + end + end + + + defmacro is_primitive_fixed32(type) do + quote do + unquote(type) == :float or unquote(type) == :fixed32 or unquote(type) == :sfixed32 + end + end + + + defmacro is_primitive_fixed64(type) do + quote do + unquote(type) == :fixed64 or unquote(type) == :sfixed64 or unquote(type) == :double + end + end + + + defmacro is_primitive_fixed(type) do + quote do + # TODO. How can we factorize this? + unquote(type) == :fixed64 or unquote(type) == :sfixed64 or\ + unquote(type) == :double or unquote(type) == :float or\ + unquote(type) == :fixed32 or unquote(type) == :sfixed32 + end + end + + + defmacro is_delimited(type) do + quote do + unquote(type) == :string or unquote(type) == :bytes or\ + unquote(type) == Protox.Message + end + end + +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 00000000..2b109000 --- /dev/null +++ b/mix.exs @@ -0,0 +1,32 @@ +defmodule Protox.Mixfile do + + use Mix.Project + + def project do + [ + app: :protox, + version: "0.1.0", + elixir: "~> 1.3", + build_embedded: Mix.env == :prod, + start_permanent: Mix.env == :prod, + deps: deps(), + test_coverage: [tool: ExCoveralls], + ] + end + + def application do + [ + applications: [] + ] + end + + defp deps do + [ + {:credo, "~> 0.5.3"}, + {:dialyxir, "~> 0.4", only: [:dev], runtime: false}, + {:excoveralls, "~> 0.5", only: :test}, + {:varint, "~> 1.0"}, + ] + end + +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 00000000..f8aff8da --- /dev/null +++ b/mix.lock @@ -0,0 +1,13 @@ +%{"bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], []}, + "certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [:rebar3], []}, + "credo": {:hex, :credo, "0.5.3", "0c405b36e7651245a8ed63c09e2d52c2e2b89b6d02b1570c4d611e0fcbecf4a2", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, optional: false]}]}, + "dialyxir": {:hex, :dialyxir, "0.4.1", "236056d6acd25f740f336756c0f3b5dd6e2f0156074bc15f3b779aeee15390c8", [:mix], []}, + "excoveralls": {:hex, :excoveralls, "0.5.7", "5d26e4a7cdf08294217594a1b0643636accc2ad30e984d62f1d166f70629ff50", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]}, + "exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, optional: false]}]}, + "hackney": {:hex, :hackney, "1.6.3", "d489d7ca2d4323e307bedc4bfe684323a7bf773ecfd77938f3ee8074e488e140", [:mix, :rebar3], [{:certifi, "0.7.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, + "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, + "jsx": {:hex, :jsx, "2.8.1", "1453b4eb3615acb3e2cd0a105d27e6761e2ed2e501ac0b390f5bbec497669846", [:mix, :rebar3], []}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, + "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}, + "varint": {:hex, :varint, "1.1.0", "8c0774a56f49d9c3a0afc2d3375b6842360c77c1319ef32b024e4c86754f346d", [:mix], []}} diff --git a/test/defs.exs b/test/defs.exs new file mode 100644 index 00000000..361b0efe --- /dev/null +++ b/test/defs.exs @@ -0,0 +1,269 @@ +defmodule Sub do + + defstruct a: 0, + b: "", + c: 0, + d: 0, + e: 0, + f: 0, + g: [], + h: [], + i: [], + z: 0 + + + @spec encode(struct) :: binary + def encode(msg = %__MODULE__{}) do + Protox.Encode.encode(msg) + end + + + @spec decode(binary) :: struct + def decode(bytes) do + Protox.Decode.decode(bytes, __MODULE__.defs()) + end + + + def defs() do + %Protox.Message{ + name: __MODULE__, + fields: %{ + 1 => %Protox.Field{name: :a, kind: :normal, type: :int32}, + 2 => %Protox.Field{name: :b, kind: :normal, type: :string}, + 6 => %Protox.Field{name: :c, kind: :normal, type: :int64}, + 7 => %Protox.Field{name: :d, kind: :normal, type: :uint32}, + 8 => %Protox.Field{name: :e, kind: :normal, type: :uint64}, + 9 => %Protox.Field{name: :f, kind: :normal, type: :sint64}, + 13 => %Protox.Field{name: :g, kind: :repeated, type: :fixed64}, + 14 => %Protox.Field{name: :h, kind: :repeated, type: :sfixed32}, + 15 => %Protox.Field{name: :i, kind: :repeated, type: :double}, + 10001 => %Protox.Field{name: :z, kind: :normal, type: :sint32}, + }, + # Ordered by tag value. + tags: [ + 1, + 2, + 6, + 7, + 8, + 9, + 13, + 14, + 15, + 10001, + ] + } + end + +end + +#------------------------------------------------------------------------------------------------# + +defmodule Msg.MapFieldEntry_k do + + defstruct key: 0, + value: "" + + + @spec encode(struct) :: binary + def encode(msg = %__MODULE__{}) do + Protox.Encode.encode(msg) + end + + + @spec decode(binary) :: struct + def decode(bytes) do + Protox.Decode.decode(bytes, __MODULE__.defs()) + end + + + def defs() do + %Protox.Message{ + name: __MODULE__, + fields: %{ + 1 => %Protox.Field{name: :key, kind: :normal, type: :int32}, + 2 => %Protox.Field{name: :value, kind: :normal, type: :string}, + }, + tags: [ + 1, + 2, + ] + } + end +end + +#------------------------------------------------------------------------------------------------# + +defmodule Msg.MapFieldEntry_l do + + defstruct key: "", + value: 0.0 + + + @spec encode(struct) :: binary + def encode(msg = %__MODULE__{}) do + Protox.Encode.encode(msg) + end + + + @spec decode(binary) :: struct + def decode(bytes) do + Protox.Decode.decode(bytes, __MODULE__.defs()) + end + + + def defs() do + %Protox.Message{ + name: __MODULE__, + fields: %{ + 1 => %Protox.Field{name: :key, kind: :normal, type: :string}, + 2 => %Protox.Field{name: :value, kind: :normal, type: :double}, + }, + tags: [ + 1, + 2, + ] + } + end +end + +#------------------------------------------------------------------------------------------------# + +defmodule Msg do + + defstruct d: :FOO, + e: false, + f: nil, + g: [], + h: 0.0, + i: [], + j: [], + k: %{}, + l: %{}, + m: nil + + + @spec encode(struct) :: binary + def encode(msg = %__MODULE__{}) do + Protox.Encode.encode(msg) + end + + + @spec decode(binary) :: struct + def decode(bytes) do + Protox.Decode.decode(bytes, __MODULE__.defs()) + end + + + def defs() do + %Protox.Message{ + name: __MODULE__, + fields: %{ + 1 => %Protox.Field{name: :d, kind: :normal, type: %Protox.Enumeration{ + members: %{0 => :FOO, 1 => :BAR}, + values: %{FOO: 0, BAR: 1}, + } + }, + 2 => %Protox.Field{name: :e, kind: :normal, type: :bool}, + 3 => %Protox.Field{name: :f, kind: :normal, type: Sub.defs()}, + 4 => %Protox.Field{name: :g, kind: :repeated, type: :int32}, + 5 => %Protox.Field{name: :h, kind: :normal, type: :double}, + 6 => %Protox.Field{name: :i, kind: :repeated, type: :float}, + 7 => %Protox.Field{name: :j, kind: :repeated, type: Sub.defs()}, + 8 => %Protox.Field{name: :k, kind: :map, type: Msg.MapFieldEntry_k.defs()}, + 9 => %Protox.Field{name: :l, kind: :map, type: Msg.MapFieldEntry_l.defs()}, + 10 => %Protox.Field{name: :n, kind: {:oneof, :m}, type: :string}, + 11 => %Protox.Field{name: :o, kind: {:oneof, :m}, type: Sub.defs()}, + }, + # Ordered by tag value. + tags: [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + ], + } + end +end + +#-------------------------------------------------------------------------------------------------# + +defmodule MapFieldEntry_msg_map do + + defstruct key: "", + value: nil + + + @spec encode(struct) :: binary + def encode(msg = %__MODULE__{}) do + Protox.Encode.encode(msg) + end + + + @spec decode(binary) :: struct + def decode(bytes) do + Protox.Decode.decode(bytes, __MODULE__.defs()) + end + + + def defs() do + %Protox.Message{ + name: __MODULE__, + fields: %{ + 1 => %Protox.Field{name: :key, kind: :normal, type: :string}, + 2 => %Protox.Field{name: :value, kind: :normal, type: Msg.defs()}, + }, + tags: [ + 1, + 2, + ] + } + end + +end + +#-------------------------------------------------------------------------------------------------# + +defmodule Upper do + + defstruct msg: nil, + msg_map: %{} + + + @spec encode(struct) :: binary + def encode(msg = %__MODULE__{}) do + Protox.Encode.encode(msg) + end + + + @spec encode(binary) :: struct + def decode(bytes) do + Protox.Decode.decode(bytes, __MODULE__.defs()) + end + + + def defs() do + %Protox.Message{ + name: __MODULE__, + fields: %{ + 1 => %Protox.Field{name: :msg, kind: :normal, type: Msg.defs()}, + 2 => %Protox.Field{name: :msg_map, kind: :map, type: MapFieldEntry_msg_map.defs()}, + }, + # Ordered by tag value. + tags: [ + 1, + 2, + ] + } + end + +end + +#-------------------------------------------------------------------------------------------------# diff --git a/test/protox/decode_test.exs b/test/protox/decode_test.exs new file mode 100644 index 00000000..3eb91f8a --- /dev/null +++ b/test/protox/decode_test.exs @@ -0,0 +1,288 @@ +defmodule Protox.DecodeTest do + use ExUnit.Case + + setup do + { + :ok, + msg_defs: Msg.defs(), + sub_defs: Sub.defs(), + upper_defs: Upper.defs(), + } + end + + + test "Sub.a" do + bytes = <<8, 150, 1>> + assert Sub.decode(bytes) ==\ + %Sub{a: 150, b: "", z: 0} + end + + + test "Sub.a, negative" do + bytes = <<8, 234, 254, 255, 255, 255, 255, 255, 255, 255, 1>> + assert Sub.decode(bytes) ==\ + %Sub{a: -150, b: "", z: 0} + end + + + test "Sub.b" do + bytes = <<18, 7, 116, 101, 115, 116, 105, 110, 103>> + assert Sub.decode(bytes) ==\ + %Sub{a: 0, b: "testing", z: 0} + end + + + test "Sub.a; Sub.b" do + bytes = <<8, 150, 1, 18, 7, 116, 101, 115, 116, 105, 110, 103>> + assert Sub.decode(bytes) ==\ + %Sub{a: 150, b: "testing", z: 0} + end + + + test "Sub.a; Sub.b; Sub.z" do + bytes = <<8, 150, 1, 18, 7, 116, 101, 115, 116, 105, 110, 103, 136, 241, 4, 157, 156, 1>> + assert Sub.decode(bytes) ==\ + %Sub{a: 150, b: "testing", z: -9999} + end + + + test "Sub.b; Sub.a" do + bytes = <<18, 7, 116, 101, 115, 116, 105, 110, 103, 8, 150, 1>> + assert Sub.decode(bytes) ==\ + %Sub{a: 150, b: "testing", z: 0} + end + + + test "Sub, unknown tag (double)" do + bytes = <<8, 42, 25, 246, 40, 92, 143, 194, 53, 69, 64, 136, 241, 4, 83>> + assert Sub.decode(bytes) ==\ + %Sub{a: 42, b: "", z: -42} + end + + + test "Sub, unknown tag (embedded message)" do + bytes = <<8, 42, 34, 0, 136, 241, 4, 83>> + assert Sub.decode(bytes) ==\ + %Sub{a: 42, b: "", z: -42} + end + + + test "Sub, unknown tag (string)" do + bytes = <<8, 42, 42, 4, 121, 97, 121, 101, 136, 241, 4, 83>> + assert Sub.decode(bytes) ==\ + %Sub{a: 42, b: "", z: -42} + end + + + test "Sub, unknown tag (bytes)" do + bytes = <<8, 142, 26, 82, 4, 104, 101, 121, 33, 136, 241, 4, 19>> + assert Sub.decode(bytes) ==\ + %Sub{a: 3342, b: "", z: -10} + end + + + test "Sub, unknown tag (varint)" do + bytes = <<8, 142, 26, 82, 4, 104, 101, 121, 33, 88, 154, 5, 136, 241, 4, 19>> + assert Sub.decode(bytes) ==\ + %Sub{a: 3342, b: "", z: -10} + end + + + test "Sub, unknown tag (float)" do + bytes = <<8, 142, 26, 82, 4, 104, 101, 121, 33, 88, 154, 5, 101, 236, 81, 5, 66, 136, + 241, 4, 19>> + assert Sub.decode(bytes) ==\ + %Sub{a: 3342, b: "", z: -10} + end + + + test "Sub.c" do + bytes = <<48, 212, 253, 255, 255, 255, 255, 255, 255, 255, 1>> + assert Sub.decode(bytes) ==\ + %Sub{c: 0, b: "", c: -300, z: 0} + end + + + test "Sub.d; Sub.e" do + bytes = <<56, 133, 7, 64, 177, 3>> + assert Sub.decode(bytes) ==\ + %Sub{c: 0, b: "", c: 0, d: 901, e: 433, z: 0} + end + + + test "Sub.f" do + bytes = <<72, 213, 20>> + assert Sub.decode(bytes) ==\ + %Sub{c: 0, b: "", c: 0, d: 0, e: 0, f: -1323, z: 0} + end + + + test "Sub.g" do + bytes = <<18, 0, 48, 0, 56, 0, 64, 0, 72, 0, 106, 16, 1, 0, 0, 0, 0, 0, 0, 0, 254, 255, + 255, 255, 255, 255, 255, 255, 136, 241, 4, 0>> + assert Sub.decode(bytes) ==\ + %Sub{c: 0, b: "", c: 0, d: 0, e: 0, f: 0, g: [1,-2], h: [], i: [], z: 0} + end + + + test "Sub.h" do + bytes = <<18, 0, 48, 0, 56, 0, 64, 0, 72, 0, 106, 8, 0, 0, 0, 0, 0, 0, 0, 0, 114, 4, + 255, 255, 255, 255, 122, 16, 154, 153, 153, 153, 153, 153, 64, 64, 0, 0, 0, 0, + 0, 0, 70, 192, 136, 241, 4, 0>> + assert Sub.decode(bytes) ==\ + %Sub{c: 0, b: "", c: 0, d: 0, e: 0, f: 0, g: [0], h: [-1], i: [33.2, -44.0], z: 0} + end + + + test "Sub.g; Sub.h; Sub.i" do + bytes = <<18, 0, 48, 0, 56, 0, 64, 0, 72, 0, 114, 8, 255, 255, 255, 255, 254, 255, 255, + 255, 136, 241, 4, 0>> + assert Sub.decode(bytes) ==\ + %Sub{c: 0, b: "", c: 0, d: 0, e: 0, f: 0, g: [], h: [-1,-2], i: [], z: 0} + end + + + test "Msg.Sub.a" do + bytes = <<26, 3, 8, 150, 1>> + assert Msg.decode(bytes) ==\ + %Msg{d: :FOO, e: false, f: %Sub{a: 150, b: "", z: 0}, g: [], h: 0.0, i: [], j: [], + k: %{}} + end + + + test "Msg.Sub.a; Msg.Sub.b" do + bytes = <<26, 12, 8, 150, 1, 18, 7, 116, 101, 115, 116, 105, 110, 103>> + assert Msg.decode(bytes) ==\ + %Msg{d: :FOO, e: false, f: %Sub{a: 150, b: "testing", z: 0}, g: [], h: 0.0, i: [], + j: [], k: %{}} + end + + + test "Msg.g" do + bytes = <<34, 6, 3, 142, 2, 158, 167, 5>> + assert Msg.decode(bytes) ==\ + %Msg{d: :FOO, e: false, f: nil, g: [3, 270, 86942], h: 0.0, i: [], j: [], k: %{}} + end + + + test "Msg.g (unpacked)" do + bytes = <<32, 1, 32, 2, 32, 3>> + assert Msg.decode(bytes) ==\ + %Msg{d: :FOO, e: false, f: nil, g: [1, 2, 3], h: 0.0, i: [], j: [], k: %{}} + end + + + test "Msg.Sub.a; Msg.Sub.b; Msg.g" do + bytes = <<26, 12, 8, 150, 1, 18, 7, 116, 101, 115, 116, 105, 110, 103, 34, 6, 3, 142, + 2, 158, 167, 5>> + assert Msg.decode(bytes) ==\ + %Msg{d: :FOO, e: false, f: %Sub{a: 150, b: "testing", z: 0}, g: [3, 270, 86942], + h: 0.0, i: [], j: [], k: %{}} + end + + + test "Msg.e" do + bytes = <<16, 1>> + assert Msg.decode(bytes) ==\ + %Msg{d: :FOO, e: true, f: nil, g: [], h: 0.0, i: [], j: [], k: %{}} + end + + + test "Msg.h" do + bytes = <<41, 246, 40, 92, 143, 194, 181, 64, 192>> + assert Msg.decode(bytes) ==\ + %Msg{d: :FOO, e: false, f: nil, g: [], h: -33.42, i: [], j: [], k: %{}} + end + + + test "Msg.i" do + bytes = <<50, 8, 0, 0, 128, 63, 0, 0, 0, 64>> + assert Msg.decode(bytes) ==\ + %Msg{d: :FOO, e: false, f: nil, g: [], h: 0.0, i: [1.0, 2.0], j: [], k: %{}} + end + + + test "Msg.d" do + bytes = <<8, 1>> + assert Msg.decode(bytes) ==\ + %Msg{d: :BAR, e: false, f: nil, g: [], h: 0.0, i: [], j: [], k: %{}} + end + + + test "Msg.d, unknown enum entry" do + bytes = <<8, 2>> + assert Msg.decode(bytes) ==\ + %Msg{d: 2, e: false, f: nil, g: [], h: 0.0, i: [], j: [], k: %{}} + end + + + test "Msg.j" do + bytes = <<58, 3, 8, 146, 6, 58, 5, 18, 3, 102, 111, 111>> + assert Msg.decode(bytes) ==\ + %Msg{d: :FOO, e: false, f: nil, g: [], h: 0.0, i: [], j: [%Sub{a: 786}, %Sub{b: "foo"}], + k: %{}} + end + + + test "Msg.k" do + bytes = <<66, 7, 8, 2, 18, 3, 98, 97, 114, 66, 7, 8, 1, 18, 3, 102, 111, 111>> + assert Msg.decode(bytes) ==\ + %Msg{d: :FOO, e: false, f: nil, g: [], h: 0.0, i: [], j: [], + k: %{1 => "foo", 2 => "bar"}} + end + + + test "Msg.l" do + bytes = <<74, 14, 10, 3, 98, 97, 114, 17, 0, 0, 0, 0, 0, 0, 240, 63, 74, 14, 10, 3, 102, + 111, 111, 17, 154, 153, 153, 153, 153, 153, 69, 64>> + assert Msg.decode(bytes) ==\ + %Msg{d: :FOO, e: false, f: nil, g: [], h: 0.0, i: [], j: [], + k: %{}, l: %{"bar" => 1.0, "foo" => 43.2}} + end + + + test "Msg.m, empty" do + bytes = "" + assert Msg.decode(bytes) ==\ + %Msg{d: :FOO, e: false, f: nil, g: [], h: 0.0, i: [], j: [], k: %{}, l: %{}, m: nil} + end + + + test "Msg.m, string" do + bytes = <<82, 3, 98, 97, 114>> + assert Msg.decode(bytes) ==\ + %Msg{d: :FOO, e: false, f: nil, g: [], h: 0.0, i: [], j: [], + k: %{}, l: %{}, m: {:n, "bar"}} + end + + + test "Msg.m, Sub" do + bytes = <<90, 2, 8, 42>> + assert Msg.decode(bytes) ==\ + %Msg{d: :FOO, e: false, f: nil, g: [], h: 0.0, i: [], j: [], + k: %{}, l: %{}, m: {:o, %Sub{a: 42}}} + end + + + test "Upper.msg.f" do + bytes = <<10, 4, 26, 2, 8, 42>> + assert Upper.decode(bytes) ==\ + %Upper{msg: %Msg{d: :FOO, e: false, f: %Sub{a: 42}, g: [], h: 0.0, i: [], j: []}} + end + + + test "Upper.msg_map" do + bytes = <<18, 9, 10, 3, 102, 111, 111, 18, 2, 8, 1, 18, 9, 10, 3, 98, 97, 122, 18, 2, 16, 1>> + assert Upper.decode(bytes) ==\ + %Upper{ + msg: nil, + msg_map: %{ + "foo" => %Msg{d: :BAR, e: false, f: nil, g: [], h: 0.0, i: [], j: [], + k: %{}, l: %{}, m: nil}, + "baz" => %Msg{d: :FOO, e: true, f: nil, g: [], h: 0.0, i: [], j: [], + k: %{}, l: %{}, m: nil}, + } + } + end + +end diff --git a/test/protox/encode_test.exs b/test/protox/encode_test.exs new file mode 100644 index 00000000..4cfaa151 --- /dev/null +++ b/test/protox/encode_test.exs @@ -0,0 +1,136 @@ +defmodule Protox.EncodeTest do + use ExUnit.Case + + test "empty", %{} do + assert Sub.encode(%Sub{}) == <<>> + end + + + test "Sub.a" do + assert Sub.encode(%Sub{a: 150}) + == <<8, 150, 1>> + end + + + test "Sub.b" do + assert Sub.encode(%Sub{b: "testing"}) + == <<18, 7, 116, 101, 115, 116, 105, 110, 103>> + end + + + test "Sub.z" do + assert Protox.Encode.encode(%Sub{z: -20}) + == <<136, 241, 4, 39>> + end + + + test "Msg.d, :FOO" do + assert Protox.Encode.encode(%Msg{d: :FOO}) + == <<>> + end + + + test "Msg.d, :BAR" do + assert Protox.Encode.encode(%Msg{d: :BAR}) + == <<8, 1>> + end + + + test "Msg.d, unknown value" do + assert Protox.Encode.encode(%Msg{d: 99}) + == "\bc" + end + + + test "Msg.e, false" do + assert Protox.Encode.encode(%Msg{e: false}) + == <<>> + end + + + test "Msg.e, true" do + assert Protox.Encode.encode(%Msg{e: true}) + == <<16, 1>> + end + + + test "Msg.f, empty" do + assert Protox.Encode.encode(%Msg{f: %Sub{}}) + == <<26, 0>> + end + + + test "Msg.f.a" do + assert Protox.Encode.encode(%Msg{f: %Sub{a: 150}}) + == <<26, 3, 8, 150, 1>> + end + + + test "Msg.g, empty" do + assert Protox.Encode.encode(%Msg{g: []}) + == <<>> + end + + + test "Msg.g (negative)" do + assert Protox.Encode.encode(%Msg{g: [1, 2, -3]}) + == <<34, 12, 1, 2, 253, 255, 255, 255, 255, 255, 255, 255, 255, 1>> + end + + + test "Msg.g" do + assert Protox.Encode.encode(%Msg{g: [1, 2, 3]}) + == <<34, 3, 1, 2, 3>> + end + + + test "Msg.h, empty" do + assert Protox.Encode.encode(%Msg{h: 0.0}) + == <<>> + end + + + test "Msg.h" do + assert Protox.Encode.encode(%Msg{h: -43.2}) + == <<41, 154, 153, 153, 153, 153, 153, 69, 192>> + end + + + test "Msg.i" do + assert Protox.Encode.encode(%Msg{i: [2.3, -4.2]}) + == <<50, 8, 51, 51, 19, 64, 102, 102, 134, 192>> + end + + + test "Msg.j" do + assert Protox.Encode.encode(%Msg{j: [%Sub{a: 42}, %Sub{b: "foo"}]}) + == <<58, 2, 8, 42, 58, 5, 18, 3, 102, 111, 111>> + end + + + test "Msg.k" do + assert Protox.Encode.encode(%Msg{k: %{1 => "foo", 2 => "bar"}}) + == <<66, 7, 8, 1, 18, 3, 102, 111, 111, 66, 7, 8, 2, 18, 3, 98, 97, 114>> + end + + + test "Msg.l" do + assert %Msg{l: %{"bar" => 1.0, "foo" => 43.2}} + |> Protox.Encode.encode() + == <<74, 14, 10, 3, 98, 97, 114, 17, 0, 0, 0, 0, 0, 0, 240, 63, 74, 14, 10, 3, 102, + 111, 111, 17, 154, 153, 153, 153, 153, 153, 69, 64>> + end + + + test "Msg.m, string" do + assert %Msg{m: {:n, "bar"}} |> Protox.Encode.encode() + == <<82, 3, 98, 97, 114>> + end + + + test "Msg.m, Sub" do + assert %Msg{m: {:o, %Sub{a: 42}}} |> Protox.Encode.encode() + == <<90, 2, 8, 42>> + end + +end diff --git a/test/protox_test.exs b/test/protox_test.exs new file mode 100644 index 00000000..8845b779 --- /dev/null +++ b/test/protox_test.exs @@ -0,0 +1,4 @@ +defmodule ProtoxTest do + use ExUnit.Case + doctest Protox +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 00000000..d55897f6 --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +Code.load_file("test/defs.exs")