lib/graphql_document/selection.ex

defmodule GraphQLDocument.Selection do
  @moduledoc """
  A [Selection](http://spec.graphql.org/October2021/#Selection) is
  the list of
  [Fields](http://spec.graphql.org/October2021/#sec-Language.Fields) or
  [Fragments](http://spec.graphql.org/October2021/#sec-Language.Fragments)
  to be returned in an object.
  """

  alias GraphQLDocument.{Field, Fragment}

  @typedoc """
  """
  @type t :: Field.t() | Fragment.spread() | Fragment.inline()

  @doc ~S'''
  Returns the list of selections in
  a [SelectionSet](http://spec.graphql.org/October2021/#SelectionSet) as iodata
  to be inserted into a Document.

  ### Examples

      iex> render([lightsaber: [:color, :style]], 1) |> IO.iodata_to_binary()
      """
       {
        lightsaber {
          color
          style
        }
      }\
      """

      iex> render(
      ...>   [
      ...>     invoices: {
      ...>       [customer: "123456"],
      ...>       [
      ...>         :id,
      ...>         :total,
      ...>         items: ~w(description amount),
      ...>         payments: {[after: "2021-01-01", posted: true], ~w(amount date)}
      ...>       ]
      ...>     }
      ...>   ],
      ...>   1
      ...> )
      ...> |> IO.iodata_to_binary()
      """
       {
        invoices(customer: \"123456\") {
          id
          total
          items {
            description
            amount
          }
          payments(after: \"2021-01-01\", posted: true) {
            amount
            date
          }
        }
      }\
      """

      iex> render([foo: :bar], 1)
      ** (ArgumentError) Expected a field; received {:foo, :bar}

      iex> render([foo: [:bar]], 0)
      ** (ArgumentError) indent_level must be at least 1; received 0

  '''
  @spec render([t], integer) :: iolist
  def render(selections, indent_level)
      when (is_list(selections) or is_map(selections)) and indent_level > 0 do
    indent = List.duplicate("  ", indent_level)

    rendered =
      selections
      |> Enum.map(fn
        {:..., fragment} ->
          [
            indent,
            Fragment.render(fragment, indent_level + 1)
          ]

        field ->
          [
            indent,
            Field.render(field, indent_level + 1)
          ]
      end)
      |> Enum.intersperse(?\n)

    if Enum.any?(selections) do
      [
        ?\s,
        ?{,
        ?\n,
        rendered,
        ?\n,
        List.duplicate("  ", indent_level - 1),
        ?}
      ]
    else
      []
    end
  end

  def render(_selection, indent_level) when indent_level < 1 do
    raise ArgumentError,
      message: "indent_level must be at least 1; received #{inspect(indent_level)}"
  end

  def render(selections, _indent_level) do
    raise ArgumentError,
      message: """
      Expected a list of fields.

      Received: `#{inspect(selections)}`
      Did you forget to enclose it in a list?
      """
  end
end