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

defmodule Mix.Tasks.Absinthe.Federation.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.federation.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.federation.schema.sdl
  Write to path `/path/to/schema.graphql` using the `MySchema` schema
      mix absinthe.federation.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)

    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 = Absinthe.Federation.remove_federated_types_pipeline(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)
    :ok
  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