lib/absinthe_sdl.ex

defmodule AbsintheSdl do
  import AbsintheSdl.Utils

  @moduledoc """
  Convert the json output of an introspection query into Graphql SDL syntax.

  ## Example
  ```
  AbsintheSdl.encode!(Jason.decode!("swapi.json"))
  ```

  Can be used to convert an Absinthe schema to SDL by using AbsintheSdl as the JSON
  encoder.

  ## Example
  ```
  mix absinthe.schema.json --schema MySchema --json-codec AbsintheSdl
  ```

  """
  @default_scalars [
    "Boolean",
    "ID",
    "String",
    "Float",
    "Int"
  ]
  @doc """
  Partial implementation of JSON codec, enough to satisfy Absinthe when passing
  in AbsintheSdl as codec.

  The schema passed in is the elixir representation of the json-result of an
  introspection query.
  """
  def encode!(schema, _opts \\ []) do
    schema =
      schema
      |> schema_from_data()
      |> sorted_objects()

    root_nodes(schema) <> type_nodes(schema)
  end

  defp schema_from_data(%{"data" => %{"__schema" => schema}}), do: schema
  defp schema_from_data(%{data: %{"__schema" => schema}}), do: schema

  defp type_nodes(%{"types" => types}) do
    types
    |> Enum.sort_by(fn %{"name" => name} -> name end)
    |> Enum.map_join(fn type ->
      type_node(type)
    end)
  end

  defp type_node(%{"name" => name}) when name in @default_scalars, do: ""
  # Skip reserved names
  defp type_node(%{"name" => "__" <> _}), do: ""

  defp type_node(type) do
    """
    #{type_description(type)}
    #{type(type)} #{type_name(type)}#{with_interfaces(type)} #{type_fields(type)}
    """
  end

  defp type_name(%{"name" => name}), do: name

  defp with_interfaces(%{"interfaces" => nil}), do: ""
  defp with_interfaces(%{"interfaces" => []}), do: ""

  defp with_interfaces(%{"interfaces" => interfaces}) do
    interfaces = interfaces |> Enum.map_join(", ", fn %{"name" => name} -> name end)
    " implements " <> interfaces
  end

  defp type(%{"kind" => "INTERFACE"}), do: "interface"
  defp type(%{"kind" => "UNION"}), do: "union"
  defp type(%{"kind" => "ENUM"}), do: "enum"
  defp type(%{"kind" => "SCALAR"}), do: "scalar"
  defp type(%{"kind" => "INPUT_OBJECT"}), do: "input"
  defp type(%{"kind" => "OBJECT"}), do: "type"

  defp field_description(%{"description" => nil}), do: ""

  defp field_description(%{"description" => description}) do
    "\"\"\"#{description}\"\"\"\n  "
  end

  defp type_description(%{"description" => nil}), do: ""

  defp type_description(%{"description" => description}) do
    "\n\"\"\"#{description}\"\"\""
  end

  defp type_fields(%{"kind" => "UNION", "possibleTypes" => possible_types}) do
    types =
      possible_types
      |> Enum.map_join(" | ", fn %{"name" => name} ->
        name
      end)

    "= #{types}"
  end

  defp type_fields(%{"kind" => "INPUT_OBJECT", "inputFields" => input_fields}) do
    map_fields(input_fields, &field_node/1)
  end

  defp type_fields(%{"kind" => "ENUM", "enumValues" => enum_values}) do
    map_fields(enum_values, &enum_value/1)
  end

  defp type_fields(%{"kind" => "SCALAR"}), do: ""
  defp type_fields(%{"fields" => nil}), do: ""

  defp type_fields(%{"fields" => fields}) do
    map_fields(fields, &field_node/1)
  end

  defp enum_value(field) do
    """
      #{field_description(field)}#{field["name"]}#{field_deprecated(field)}\
    """
  end

  defp field_node(field) do
    """
      #{field_description(field)}#{field["name"]}#{field_args(field)}: #{
      field_type(field["type"])
    }#{field_default_value(field)}#{field_deprecated(field)}\
    """
  end

  defp field_args(%{"args" => []}), do: ""

  defp field_args(%{"args" => args}) do
    args
    |> Enum.map_join(", ", fn arg ->
      "#{arg["name"]}: #{field_type(arg["type"])}#{field_default_value(arg)}"
    end)
    |> decorate("(", ")")
  end

  defp field_args(_), do: ""

  defp field_default_value(%{"defaultValue" => nil}), do: ""
  defp field_default_value(%{"defaultValue" => value}), do: " = #{value}"
  defp field_default_value(_), do: ""

  defp field_deprecated(%{"isDeprecated" => false}), do: ""

  defp field_deprecated(%{"isDeprecated" => true, "deprecationReason" => reason}) do
    " @deprecated" <> field_deprecation_reason(reason)
  end

  defp field_deprecated(_), do: ""

  defp field_deprecation_reason(nil), do: ""

  defp field_deprecation_reason(reason) do
    "(reason: \"#{reason}\")"
  end

  defp field_type(%{"kind" => "LIST", "ofType" => type}),
    do: type |> field_type |> decorate("[", "]")

  defp field_type(%{"kind" => "NON_NULL", "ofType" => type}), do: field_type(type) <> "!"
  defp field_type(%{"kind" => _, "name" => name, "ofType" => _}), do: name

  defp root_nodes(schema) do
    """
    schema {
    #{query_type(schema)}#{mutation_type(schema)}#{subscription_type(schema)}\
    }
    """
  end

  defp query_type(%{"queryType" => %{"name" => name}}) do
    "  query: #{name}\n"
  end

  defp query_type(_), do: ""

  defp mutation_type(%{"mutationType" => %{"name" => name}}) do
    "  mutation: #{name}\n"
  end

  defp mutation_type(_), do: ""

  defp subscription_type(%{"subscriptionType" => %{"name" => name}}) do
    "  subscription: #{name}\n"
  end

  defp subscription_type(_), do: ""

  defp sorted_objects(value)

  defp sorted_objects(map) when is_map(map) do
    for {key, val} <- map, into: %{}, do: {key, sorted_objects(val)}
  end

  defp sorted_objects(list) when is_list(list) do
    list
    |> Enum.sort_by(&list_sorting_value/1)
    |> Enum.map(&sorted_objects/1)
  end

  defp sorted_objects(value), do: value

  defp list_sorting_value(%{name: name}), do: name
  defp list_sorting_value(%{"name" => name}), do: name
  defp list_sorting_value(value), do: value
end