lib/borsh.ex

defmodule Borsh do
  @moduledoc """
  BORSH, binary serializer for security-critical projects.

  Borsh stands for "Binary Object Representation Serializer for Hashing".
  It is meant to be used in security-critical projects as it prioritizes consistency, safety, speed;
  and comes with a strict specification.
  In short, Borsh is a non self-describing binary serialization format.
  It is designed to serialize any objects to canonical and deterministic set of bytes.

  General principles of Borsh serialization:

  - Integers are encoded in little-endian format.
  - The size of dynamic containers (such as hash maps and hash sets) is written as a 32-bit unsigned integer before the values.
  - All unordered containers are ordered lexicographically by key, with a tie breaker of the value.
  - Structs are serialized in the order of their fields.
  - Enums are serialized by storing the ordinal as an 8-bit unsigned integer, followed by the data contained within the enum value (if present).

  This is Elixir implementation of the Borsh serializer and deserializer.
  Official specification: https://github.com/near/borsh#specification

  ## Usage

    ```elixir
      use Borsh,
        schema: [
          signer_id: :string,
          public_key: :borsh,
          nonce: :u64,
          receiver_id: :string,
          block_hash: [32],
          actions: [:borsh]
        ]
    ```

  ### Options

  `schema`:
    Borsh schema itself, structure of fields for serialisation with serialisation formats.

  ### Borsh literal formats

  `:string` - the type represents the string representation of a value. When using the Borsh serialization format, it is encoded as-is, with a 4-byte little-endian header indicating the number of bytes in the string.

  `:borsh` - Struct of the borsh-ed module. The serializer will take this struct and executes struct's module `.borsh_encode` against this struct and assign binary result to the literal.

  `[:borsh]` - Enum of borsh-ed structs. Each element of this list of `:borsh` struct must have a Borsh schema

  `:u64` - Unsigned integer 64-bit size. There are also `:u8`, `:u16`, `:u32` and `:u128`

  `[32]` or `[64]` - A string with 32/64 chars length.

  """

  defmacro __using__(opts) do
    schema = opts[:schema]

    quote do
      @doc """
       The `borsh_schema` function returns the value of the `schema` variable,
       which is expected to be a data structure that describes the
       layout of the struct's data when serialized.
      """
      def borsh_schema do
        unquote(schema)
      end

      @doc """
      The borsh_encode function takes a single argument, `obj`, which should be a struct.
      It calls the borsh_encode function from the `Borsh.Encode` module and passes `obj` as an argument.
      It returns the result of this function call, which is expected to be a bitstring representing the _serialized_ data.
      """
      @spec borsh_encode(obj :: struct) :: bitstring()
      def borsh_encode(obj) do
        Borsh.Encode.borsh_encode(obj)
      end

      @doc """
      The `borsh_decode` function takes a single argument, `bs`, which should be a bitstring.
      It calls the `borsh_decode` function from the `Borsh.Decode` module and passes `bs` and the
      current module's name as arguments.
      It returns the result of this function call, which is expected to be a struct containing the _deserialized_ data.
      """
      @spec borsh_decode(bs :: bitstring()) :: struct()
      def borsh_decode(bs) do
        Borsh.Decode.borsh_decode(bs, __MODULE__)
      end
    end
  end
end