lib/json_stream_encoder.ex

defmodule JsonStreamEncoder do
  @moduledoc """
  Primary API for encoding streaming JSON documents.
  """

  @type io_stream :: IO.device()
  @opaque streamer :: %JsonStreamEncoder.CompactStreamer{} | %JsonStreamEncoder.IndentedStreamer{}
  @type key :: atom | binary | number | nonempty_charlist | boolean
  @type indentation :: boolean | binary
  @type encodeable :: Jason.Encoder.t

  @spec new(io_stream) :: streamer
  @doc """
    Create a new streamer without indentation.
  """
  def new(io_stream) do
    JsonStreamEncoder.CompactStreamer.new(io_stream)
  end

  @spec new(io_stream, indentation) :: streamer
  @doc """
    Create a new streamer with the given indentation settings.

    Pass `false` for no indentation, `true` for default indentation (two spaces), or
    a `binary` representing the desired indentation string.
  """
  def new(io_stream, false) do
    JsonStreamEncoder.CompactStreamer.new(io_stream)
  end

  def new(io_stream, true) do
    JsonStreamEncoder.IndentedStreamer.new(io_stream)
  end

  def new(io_stream, indent_value) do
    JsonStreamEncoder.IndentedStreamer.new(io_stream, indent_value)
  end

@doc """
Encode an array into the stream, using the contents of the function to set elements.

## Examples

    ary(streamer, fn(stream) ->
      stream |> val(1) |> val("2")
    end)
    #=> [1,"2"]

"""
  @spec ary(streamer, (streamer -> streamer)) :: streamer
  def ary(streamer, fn_ary) do
    fn_ary.(streamer |> ary_start) |> ary_end
  end

@doc """
Start encoding an array.  Always pair with `ary_end/1`.

More often you will want to use `ary/2`.

## Examples

    streamer |> ary_start() |> val(1) |> val("2") |> ary_end()
    #=> [1,"2"]

"""
  @spec ary_start(streamer) :: streamer
  def ary_start(streamer) do
    streamer.__struct__.ary_start(streamer)
  end

@doc """
End an array you previously started encoding with `ary_start/1`.
"""
  @spec ary_end(streamer) :: streamer
  def ary_end(streamer) do
    streamer.__struct__.ary_end(streamer)
  end

@doc """
Encode an object into the stream, using the contents of the function to set key/value pairs.

## Examples

    obj(streamer, fn(stream) ->
      stream |> kv(1,"2")
    end)
    #=> {"1": "2"}

"""
  @spec obj(streamer, (streamer -> streamer)) :: streamer
  def obj(streamer, fn_obj) do
    fn_obj.(streamer |> obj_start) |> obj_end
  end

@doc """
Start encoding an object.  Always pair with `obj_end/1`.

More often you will want to use `obj/2`.

## Examples

    streamer |> obj_start() |> kv(1,"2") |> obj_end()
    #=> {"1": "2"}

"""
  @spec obj_start(streamer) :: streamer
  def obj_start(streamer) do
    streamer.__struct__.obj_start(streamer)
  end

@doc """
End an object you previously started encoding with `obj_start/1`.
"""
  @spec obj_end(streamer) :: streamer
  def obj_end(streamer) do
    streamer.__struct__.obj_end(streamer)
  end

@doc """
Encode a key for an object.

Always follow this with a `val/2`, `obj/2`, or `ary/2` - otherwise your JSON won't make any sense.
"""
  @spec key(streamer, key) :: streamer
  def key(streamer, k) do
    streamer.__struct__.key(streamer, k)
  end

  @doc """
    Encode a value.

    Usable anywhere you would expect a JSON value:
    - The root of a document
    - After a key
    - As an array element
  """
  @spec val(streamer, encodeable) :: streamer
  def val(streamer, v) do
    streamer.__struct__.val(streamer, v)
  end

  @doc """
    Encode a key-value pair for an object.

    You should really only use this if your value is simple, otherwise use `key/2` in combination with other functions.
  """
  @spec kv(streamer, key, encodeable) :: streamer
  def kv(streamer, k, v) do
    streamer |> key(k) |> val(v)
  end
end