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(<>) defp byte_encoder(other), do: {:error, "'#{inspect(other)}' is not an integer"} defp octet_encoder(<<0::24, byte::8>>), do: {:ok, <>} defp octet_encoder(<>), do: {:error, "'#{other}' can't be encoded in 1 byte"} defp byte_decoder(<> <> 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