lib/mix/tasks/absinthe.schema.sdl.ex

defmodule Mix.Tasks.Absinthe.Schema.Sdl do
  use Mix.Task
  import Mix.Generator

  @shortdoc "Generate a schema.graphql file for an Absinthe schema"

  @default_filename "./schema.graphql"

  @moduledoc """
  Generate a `schema.graphql` file.

  ## Usage

      mix absinthe.schema.sdl [OPTIONS] [FILENAME]

  ## Options

  * `--schema` - The name of the `Absinthe.Schema` module defining the schema to be generated.
     Default: As [configured](https://hexdocs.pm/mix/Mix.Config.html) for `:absinthe` `:schema`

  ## Examples

  Write to default path `#{@default_filename}` using the `:schema` configured for the `:absinthe` application:

      mix absinthe.schema.sdl

  Write to path `/path/to/schema.graphql` using the `MySchema` schema

      mix absinthe.schema.sdl --schema MySchema /path/to/schema.graphql

  """

  defmodule Options do
    @moduledoc false
    defstruct filename: nil, schema: nil

    @type t() :: %__MODULE__{
            filename: String.t(),
            schema: module()
          }
  end

  @impl Mix.Task
  def run(argv) do
    Application.ensure_all_started(:absinthe)

    Mix.Task.run("loadpaths", argv)
    Mix.Task.run("compile", argv)

    opts = parse_options(argv)

    # Start Absinthe.Schema if using persistent term provider
    if opts.schema.__absinthe_schema_provider__() == Absinthe.Schema.PersistentTerm do
      Supervisor.start_link([{Absinthe.Schema, opts.schema}], strategy: :one_for_one)
    end

    case generate_schema(opts) do
      {:ok, content} -> write_schema(content, opts.filename)
      {:error, error} -> raise error
    end
  end

  def generate_schema(%Options{schema: schema}) do
    pipeline =
      schema
      |> Absinthe.Pipeline.for_schema(prototype_schema: schema.__absinthe_prototype_schema__())
      |> Absinthe.Pipeline.upto({Absinthe.Phase.Schema.Validation.Result, pass: :final})
      |> Absinthe.Schema.apply_modifiers(schema)

    with {:ok, blueprint, _phases} <-
           Absinthe.Pipeline.run(
             schema.__absinthe_blueprint__(),
             pipeline
           ) do
      {:ok, inspect(blueprint, pretty: true)}
    else
      _ -> {:error, "Failed to render schema"}
    end
  end

  defp write_schema(content, filename) do
    create_directory(Path.dirname(filename))
    create_file(filename, content, force: true)
  end

  def parse_options(argv) do
    {opts, args, _} = OptionParser.parse(argv, strict: [schema: :string])

    %Options{
      filename: args |> List.first() || @default_filename,
      schema: find_schema(opts)
    }
  end

  defp find_schema(opts) do
    case Keyword.get(opts, :schema, Application.get_env(:absinthe, :schema)) do
      nil ->
        raise "No --schema given or :schema configured for the :absinthe application"

      value ->
        [value] |> Module.safe_concat()
    end
  end
end