defmodule Borsh.Encode do require Logger @moduledoc """ This module contains functions for encoding Elixir data structures into BORSH binary format. ## Usage ```elixir defmodule ParentStruct do @type t() :: %__MODULE__{first_name: String.t(), last_name: String.t(), age: integer} defstruct [ :first_name, :last_name, :age ] use Borsh, schema: [ first_name: :string, last_name: :string, age: :u8 ] end borsh_struct = %ParentStruct{first_name: "Boris", last_name: "Johnson", age: 58} Borsh.Encode.encode(borsh_struct) <<5, 0, 0, 0, 66, 111, 114, 105, 115, 7, 0, 0, 0, 74, 111, 104, 110, 115, 111, 110, 58>> ``` """ @doc """ Encodes structs according to the schema into the bitstring """ @spec borsh_encode(obj :: struct) :: bitstring() def borsh_encode(obj) do borsh_schema = obj.__struct__.borsh_schema() {_, res} = Enum.map_reduce( borsh_schema, [], fn schema_item, acc -> {schema_item, acc ++ [extract_encode_item(obj, schema_item)]} end ) res |> List.flatten() |> :erlang.list_to_binary() end # Encode defp extract_encode_item(obj, {key, format}) do value = Map.get(obj, key) encode_item(value, {key, format}) end # encoding list of structs defp encode_item(value, {_key, [:borsh]}), do: encode_list(value) defp encode_item(value, {_key, [{:borsh, _module}]}), do: encode_list(value) defp encode_item(value, {_key, _}) when is_list(value), do: encode_list(value) # keeping this for backward compatibility, we should use {:borsh, struct} instead of :borsh defp encode_item(value, {key, :borsh}) do Logger.warn( "Using :borsh format is deprecated, please use `#{key}: {:borsh, #{value.__struct__}}` instead of `#{key}: :borsh`" ) value.__struct__.borsh_encode(value) end # when this is a :borsh type with a struct name # we dont need struct name for encoding, but we'll need that for decoding defp encode_item(value, {_key, {:borsh, _}}) do value.__struct__.borsh_encode(value) end # encoding strings with fixed sizes: 32 bytes and 64 bytes, types `[32]` and `[64]` respectively: defp encode_item(<<_::size(256)>> = string_value, {key, [32]}) do encode_item(string_value, {key, :string}) end defp encode_item(_value, {key, [32]}), do: raise("Invalid string length for `#{key}`, must be 32 bytes") defp encode_item(<<_::size(512)>> = string_value, {key, [64]}) do encode_item(string_value, {key, :string}) end defp encode_item(_value, {key, [64]}), do: raise("Invalid string length for `#{key}`, must be 64 bytes") defp encode_item(value, {key, size}) when size in [:u8, :u16, :u32, :u64, :u128] and is_binary(value) do value |> String.to_integer() |> encode_item({key, size}) end defp encode_item(value, {_key, size}) when size in [:u8, :u16, :u32, :u64, :u128, :i8, :i16, :i32, :i64, :i128] do binarify(value, size) end defp encode_item(string_value, {_key, :string}) do # 4 bytes of the string length [ # 32-bits string length, string_value |> byte_size() |> binarify(:u32), # string itself string_value ] end defp encode_list(value) do [ # 4bytes binary length of the List value |> length() |> binarify(:u32), # encode each item in the list Enum.map(value, fn i -> i.__struct__.borsh_encode(i) end) ] end defp binarify(value, size) when size in [:i8, :i16, :i32, :i64, :i128] do size = convert_size(size) <> end defp binarify(value, size) when size in [:u8, :u16, :u32, :u64, :u128] do size = convert_size(size) <> end def convert_size(:u8), do: 8 def convert_size(:u16), do: 16 def convert_size(:u32), do: 32 def convert_size(:u64), do: 64 def convert_size(:u128), do: 128 def convert_size(:i8), do: 8 def convert_size(:i16), do: 16 def convert_size(:i32), do: 32 def convert_size(:i64), do: 64 def convert_size(:i128), do: 128 end