diff --git a/README.md b/README.md index 9acf84b..2960cb9 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,41 @@ defimpl CBOR.Encoder, for: Money do end ``` +## Custom Decoding + +If you want to decode something that is not supported out of the box you can add a custom tag decoder function with the `tag_decoder` option into `CBOR.decode/1`. The function should take in a `CBOR.Tag` struct and convert the value based on the tag. An example for decoding Tuples and Atoms with a custom tags is shown below. + +```elixir +# Tag 50 represents Tuples, tag 51 represents Atoms. Tag numbers chosen arbitrarily. +defmodule TupleDecoder do + def tag_decoder(tag_struct) do + case tag_struct.tag do + 50 -> + List.to_tuple(tag_struct.value) + 51 -> + String.to_atom(tag_struct.value) + _ -> + tag_struct + end + end +end + +iex(1)> bin_tuple = CBOR.encode(%CBOR.Tag{tag: 50, value: {%CBOR.Tag{tag: 51, value: "atom"}, %CBOR.Tag{tag: 50, value: {"nested_tuple", 1, 2}}}}) + +iex(2)> CBOR.decode(bin_tuple, tag_decoder: TupleDecoder.tag_decoder) +{:ok, {:atom, {"nested_tuple", 1, 2}}, ""} + +iex(3)> CBOR.decode(bin_tuple) +{:ok, + %CBOR.Tag{ + tag: 50, + value: [ + %CBOR.Tag{tag: 51, value: "atom"}, + %CBOR.Tag{tag: 50, value: ["nested_tuple", 1, 2]} + ] + }, ""} +``` + ### Documentation Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) diff --git a/lib/cbor.ex b/lib/cbor.ex index b2527f7..f9bf45d 100644 --- a/lib/cbor.ex +++ b/lib/cbor.ex @@ -92,12 +92,15 @@ defmodule CBOR do iex> CBOR.encode(%{"a" => 1, "b" => [2, 3]}) <<162, 97, 97, 1, 97, 98, 130, 2, 3>> + iex> CBOR.encode(%CBOR.Tag{tag: 50, value: {1, 2, %CBOR.Tag{tag: 50, value: {"nested_tuple", 1, 2}}}}) + <<216, 50, 131, 1, 2, 216, 50, 131, 108, 110, 101, 115, 116, 101, 100, 95, 116, 117, 112, 108, 101, 1, 2>> """ @spec encode(any()) :: binary() def encode(value), do: CBOR.Encoder.encode_into(value, <<>>) @doc """ - Converts a CBOR encoded binary into native elixir data structures + Converts a CBOR encoded binary into native elixir data structures. Allows passing in a custom tag decoder that takes in a CBOR.Tag + struct and outputs a converted value. ## Examples @@ -110,22 +113,32 @@ defmodule CBOR do iex> CBOR.decode(<<162, 97, 97, 1, 97, 98, 130, 2, 3>>) {:ok, %{"a" => 1, "b" => [2, 3]}, ""} + iex(1)> tuple_decoder = fn(tag_struct) -> case tag_struct.tag, do: (50 -> List.to_tuple(tag_struct.value); _ -> tag_struct) end + iex(2)> CBOR.decode(<<216, 50, 131, 1, 2, 216, 50, 131, 108, 110, 101, 115, 116, 101, 100, 95, 116, 117, 112, 108, 101, 1, 2>>, tag_decoder: tuple_decoder) + {:ok, {1, 2, {"nested_tuple", 1, 2}}, ""} + + iex> CBOR.decode(<<216, 50, 131, 1, 2, 216, 50, 131, 108, 110, 101, 115, 116, 101, 100, 95, 116, 117, 112, 108, 101, 1, 2>>) + {:ok, + %CBOR.Tag{ + tag: 50, + value: [1, 2, %CBOR.Tag{tag: 50, value: ["nested_tuple", 1, 2]}] + }, ""} """ @spec decode(binary()) :: {:ok, any(), binary()} | {:error, atom} - def decode(binary) do + def decode(binary, opts \\ []) do try do - perform_decoding(binary) + perform_decoding(binary, opts) rescue FunctionClauseError -> {:error, :cbor_function_clause_error} end end - defp perform_decoding(binary) when is_binary(binary) do - case CBOR.Decoder.decode(binary) do + defp perform_decoding(binary, opts) when is_binary(binary) do + case CBOR.Decoder.decode(binary, opts) do {value, rest} -> {:ok, value, rest} _other -> {:error, :cbor_decoder_error} end end - defp perform_decoding(_value), do: {:error, :cannot_decode_non_binary_values} + defp perform_decoding(_value, _opts), do: {:error, :cannot_decode_non_binary_values} end diff --git a/lib/cbor/decoder.ex b/lib/cbor/decoder.ex index ed6519c..76bbb98 100644 --- a/lib/cbor/decoder.ex +++ b/lib/cbor/decoder.ex @@ -1,26 +1,26 @@ defmodule CBOR.Decoder do - def decode(binary) do - decode(binary, header(binary)) + def decode(binary, opts) do + decode(binary, header(binary), opts) end - def decode(_binary, {mt, :indefinite, rest}) do + def decode(_binary, {mt, :indefinite, rest}, opts) do case mt do 2 -> mark_as_bytes(decode_string_indefinite(rest, 2, [])) 3 -> decode_string_indefinite(rest, 3, []) - 4 -> decode_array_indefinite(rest, []) - 5 -> decode_map_indefinite(rest, %{}) + 4 -> decode_array_indefinite(rest, [], opts) + 5 -> decode_map_indefinite(rest, %{}, opts) end end - def decode(bin, {mt, value, rest}) do + def decode(bin, {mt, value, rest}, opts) do case mt do 0 -> {value, rest} 1 -> {-value - 1, rest} 2 -> mark_as_bytes(decode_string(rest, value)) 3 -> decode_string(rest, value) - 4 -> decode_array(value, rest) - 5 -> decode_map(value, rest) - 6 -> decode_other(value, decode(rest)) + 4 -> decode_array(value, rest, opts) + 5 -> decode_map(value, rest, opts) + 6 -> decode_other(value, decode(rest, opts), opts) 7 -> decode_float(bin, value, rest) end end @@ -65,38 +65,38 @@ defmodule CBOR.Decoder do end end - defp decode_array(0, rest), do: {[], rest} - defp decode_array(len, rest), do: decode_array(len, [], rest) - defp decode_array(0, acc, bin), do: {Enum.reverse(acc), bin} - defp decode_array(len, acc, bin) do - {value, bin_rest} = decode(bin) - decode_array(len - 1, [value|acc], bin_rest) + defp decode_array(0, rest, _opts), do: {[], rest} + defp decode_array(len, rest, opts), do: decode_array(len, [], rest, opts) + defp decode_array(0, acc, bin, _opts), do: {Enum.reverse(acc), bin} + defp decode_array(len, acc, bin, opts) do + {value, bin_rest} = decode(bin, opts) + decode_array(len - 1, [value|acc], bin_rest, opts) end - defp decode_array_indefinite(<<0xff, new_rest::binary>>, acc) do + defp decode_array_indefinite(<<0xff, new_rest::binary>>, acc, _opts) do {Enum.reverse(acc), new_rest} end - defp decode_array_indefinite(rest, acc) do - {value, new_rest} = decode(rest) - decode_array_indefinite(new_rest, [value | acc]) + defp decode_array_indefinite(rest, acc, opts) do + {value, new_rest} = decode(rest, opts) + decode_array_indefinite(new_rest, [value | acc], opts) end - defp decode_map(0, rest), do: {%{}, rest} - defp decode_map(len, rest), do: decode_map(len, %{}, rest) - defp decode_map(0, acc, bin), do: {acc, bin} - defp decode_map(len, acc, bin) do - {key, key_rest} = decode(bin) - {value, bin_rest} = decode(key_rest) + defp decode_map(0, rest, _opts), do: {%{}, rest} + defp decode_map(len, rest, opts), do: decode_map(len, %{}, rest, opts) + defp decode_map(0, acc, bin, _opts), do: {acc, bin} + defp decode_map(len, acc, bin, opts) do + {key, key_rest} = decode(bin, opts) + {value, bin_rest} = decode(key_rest, opts) - decode_map(len - 1, Map.put(acc, key, value), bin_rest) + decode_map(len - 1, Map.put(acc, key, value), bin_rest, opts) end - defp decode_map_indefinite(<<0xff, new_rest::binary>>, acc), do: {acc, new_rest} - defp decode_map_indefinite(rest, acc) do - {key, key_rest} = decode(rest) - {value, new_rest} = decode(key_rest) - decode_map_indefinite(new_rest, Map.put(acc, key, value)) + defp decode_map_indefinite(<<0xff, new_rest::binary>>, acc, _opts), do: {acc, new_rest} + defp decode_map_indefinite(rest, acc, opts) do + {key, key_rest} = decode(rest, opts) + {value, new_rest} = decode(key_rest, opts) + decode_map_indefinite(new_rest, Map.put(acc, key, value), opts) end defp decode_float(bin, value, rest) do @@ -125,7 +125,13 @@ defmodule CBOR.Decoder do end end - defp decode_other(value, {inner, rest}), do: {decode_tag(value, inner), rest} + defp decode_other(tag, {value, rest}, opts) do + decoded_tag = decode_tag(tag, value) + case opts == [] do + true -> {decoded_tag, rest} + false -> {Keyword.get(opts, :tag_decoder).(decoded_tag), rest} + end + end def decode_non_finite(0, 0), do: %CBOR.Tag{tag: :float, value: :inf} def decode_non_finite(1, 0), do: %CBOR.Tag{tag: :float, value: :"-inf"} diff --git a/lib/cbor/tag.ex b/lib/cbor/tag.ex index 994c9a2..1b60844 100644 --- a/lib/cbor/tag.ex +++ b/lib/cbor/tag.ex @@ -1,4 +1,4 @@ defmodule CBOR.Tag do @enforce_keys [:tag, :value] defstruct [:tag, :value] -end \ No newline at end of file +end diff --git a/test/cbor/decoder_test.exs b/test/cbor/decoder_test.exs index 5cde477..8881f62 100644 --- a/test/cbor/decoder_test.exs +++ b/test/cbor/decoder_test.exs @@ -8,7 +8,7 @@ defmodule CBOR.DecoderTest do test "too little data" do assert_raise(FunctionClauseError, fn -> - CBOR.Decoder.decode("") == 1 + CBOR.Decoder.decode("", []) == 1 end) end