Input, Output, ByteAccess

In order to allow for seamless integration in all kinds of application environments borer abstracts over decoding input, encoding output as well as general “chunks of bytes” with three additional type classes Input[T], Output[T] and ByteAccess[T].

Input

For decoding borer happily consumes any type for which an Input.Provider[T] is implicitly available, which is responsible for constructing an Input from or around T:

source/**
 * Responsible for converting an instance of [[T]]
 * to a respective [[Input]] instance.
 */
trait Provider[T]:
  type Bytes
  def byteAccess: ByteAccess[Bytes]
  def apply(value: T): Input[Bytes]

Currently borer comes with predefined Input implementations for these types:

  • Array[Byte]
  • java.nio.ByteBuffer
  • java.io.InputStream
  • java.io.File
  • akka.util.ByteString (with the borer-compat-akka module)
  • pekko.util.ByteString (with the borer-compat-pekko module)
  • scodec.bits.ByteVector (with the borer-compat-scodec module)
  • Iterator[T], provided that there is an Input.Provider[T] available implicitly

The latter is a great way to consume large input in a streaming fashion, without having to load everything into memory at once. The FromFileInput, for example, relies on it to parse large files as an iteration of chunks.

The Input trait isn’t particularly hard to implement, especially since it merely has to support single-pass access to the underlying bytes with minimal buffering and without random access.

Output

On the encoding side borer can either produce any type T for which an Output.ToTypeProvider[T] is available, or “push” the output into a value of type T if an Output.ToValueProvider[T] is available:

source/**
 * Responsible for providing an Output that produces instances of [[T]].
 */
trait ToTypeProvider[T]:
  type Out <: Output { type Result = T }
  def apply(bufferSize: Int, allowBufferCaching: Boolean): Out

/**
 * Responsible for providing an Output that outputs into the given value [[T]].
 */
trait ToValueProvider[T]:
  type Out <: Output { type Result = T }
  def apply(value: T, bufferSize: Int, allowBufferCaching: Boolean): Out

Currently borer comes with predefined Output implementations for these types:

  • Array[Byte]
  • java.nio.ByteBuffer
  • java.io.OutputStream (to a an existing instance)
  • java.io.File (to a an existing instance)
  • akka.util.ByteString (with the borer-compat-akka module)
  • pekko.util.ByteString (with the borer-compat-pekko module)
  • scodec.bits.ByteVector (with the borer-compat-scodec module)

The Output trait isn’t hard to implement as it simply writes out all bytes in a single pass.

Here are a few examples to illustrate the top-level output API:

sourceimport io.bullet.borer.Cbor

val value = List("foo", "bar", "baz") // example value

// encodes into a new byte array instance,
// relies on the `Output.ToTypeProvider[Array[Byte]]` type class instance
Cbor.encode(value).to[Array[Byte]].result ==> hex"9f63666f6f636261726362617aff"

// same as above but slightly more convenient
Cbor.encode(value).toByteArray ==> hex"9f63666f6f636261726362617aff"

// encodes into an existing file,
// relies on the `Output.ToValueProvider[File]` type class instance
val file = new java.io.File(filename)
Cbor.encode(value).to(file).result ==> file

ByteAccess

Unfortunately Scala (and the whole JVM eco-system) has no single, versatile abstraction for a “chunk of bytes” that fits the needs of all applications. In order to remain open to the preferences of the application borer also abstracts over “chunks of bytes” by allowing the use of any type T, for which a ByteAccess[T] is available.

Currently borer comes with predefined ByteAccess implementations for these types:

  • Array[Byte]
  • java.nio.ByteBuffer
  • akka.util.ByteString (with the borer-compat-akka module)
  • pekko.util.ByteString (with the borer-compat-pekko module)
  • scodec.bits.ByteVector (with the borer-compat-scodec module)