Current section
Files
Jump to
Current section
Files
lib/codec.ex
defmodule Codex do
defstruct encode: nil, decode: nil
@type type :: any
@type remaining_bits :: bitstring
@type encode_result :: {:ok, bitstring} | {:error, Sting.t()}
@type decode_result :: {:ok, type, remaining_bits} | {:error, Sting.t(), remaining_bits}
@type encoder :: (type -> encode_result)
@type decoder :: (bitstring -> decode_result)
@spec create(encoder, decoder) :: %Codex{}
def create(encode, decode) when is_function(encode, 1) and is_function(decode, 1) do
%Codex{encode: encode, decode: decode}
end
@spec encode(type, %Codex{}) :: encode_result
def encode(value, %Codex{encode: encode}) when is_function(encode, 1), do: encode.(value)
def encode(_value, %Codex{encode: _encode}), do: nil
@spec decode(bitstring, %Codex{}) :: decode_result
def decode(bits, %Codex{decode: decode}) when is_function(decode, 1), do: decode.(bits)
def decode(_bits, %Codex{decode: _decode}), do: nil
@spec convert(%Codex{}, (type -> type), (type -> type)) :: %Codex{}
def convert(%Codex{} = codec, convert_to, convert_from) do
create(&convert_encoder(codec, convert_from, &1), &convert_decoder(codec, convert_to, &1))
end
defp convert_encoder(codec, convert_from, a), do: a |> convert_from.() |> codec.encode.()
defp convert_decoder(codec, convert_to, a) do
with {:ok, a, bits} <- codec.decode.(a) do
{:ok, convert_to.(a), bits}
end
end
@spec combine(%Codex{}, %Codex{}) :: %Codex{}
def combine(%Codex{} = codec1, %Codex{} = codec2) do
codecs = [codec1, codec2]
create(&combine_encoder(codecs, &1), &combine_decoder(codecs, &1))
end
defp combine_encoder([codec1, codec2], {a, b}) do
with {:ok, a_bits} <- codec1.encode.(a),
{:ok, b_bits} <- codec2.encode.(b) do
{:ok, a_bits <> b_bits}
end
end
defp combine_decoder([codec1, codec2], bits) do
with {:ok, a, bits} <- codec1.decode.(bits),
{:ok, b, bits} <- codec2.decode.(bits) do
{:ok, {a, b}, bits}
end
end
@spec byte() :: %Codex{}
def byte(), do: create(&byte_encoder/1, &byte_decoder/1)
defp byte_encoder(integer) when is_integer(integer), do: octet_encoder(<<integer::32>>)
defp byte_encoder(other), do: {:error, "'#{inspect(other)}' is not an integer"}
defp octet_encoder(<<0::24, byte::8>>), do: {:ok, <<byte::8>>}
defp octet_encoder(<<other::32>>), do: {:error, "'#{other}' can't be encoded in 1 byte"}
defp byte_decoder(<<byte::8>> <> rest), do: {:ok, byte, rest}
@spec bytes(non_neg_integer) :: %Codex{}
def bytes(count), do: list_of(count, byte())
@spec list_of(non_neg_integer, %Codex{}) :: %Codex{}
def list_of(count, _codec) when count < 0, do: raise("list_of count must be >= 0")
def list_of(0, _codec), do: empty()
def list_of(count, codec), do: codec |> cons(list_of(count - 1, codec))
@spec cons(%Codex{}, %Codex{}) :: %Codex{}
def cons(codec, list_codec) do
codec
|> combine(list_codec)
|> convert(
fn {head, rest} -> [head | rest] end,
fn [head | rest] -> {head, rest} end
)
end
@spec empty() :: %Codex{}
def empty(), do: create(fn _ -> {:ok, <<>>} end, fn bits -> {:ok, [], bits} end)
def test(value, codec) do
value
|> encode(codec)
|> elem(1)
|> IO.inspect()
|> decode(codec)
|> elem(1)
|> IO.inspect()
end
end