defmodule Mix.Tasks.Absinthe.Schema.Json do
require Logger
use Mix.Task
import Mix.Generator
@shortdoc "Generate a schema.json file for an Absinthe schema"
@default_filename "./schema.json"
@moduledoc """
Generate a `schema.json` file
## Usage
mix absinthe.schema.json [OPTIONS] [FILENAME]
The JSON codec to be used needs to be included in your `mix.exs` dependencies. If using the default codec,
see the Jason [installation instructions](https://hexdocs.pm/jason).
## 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`
* `--json-codec` - Codec to use to generate the JSON file (see [Custom Codecs](#module-custom-codecs)).
Default: [`Jason`](https://hexdocs.pm/jason/)
* `--pretty` - Whether to pretty-print.
Default: `false`
## Examples
Write to default path `#{@default_filename}` using the `:schema` configured for the `:absinthe` application:
mix absinthe.schema.json
Write to default path `#{@default_filename}` using the `MySchema` schema:
mix absinthe.schema.json --schema MySchema
Write to path `/path/to/schema.json` using the `MySchema` schema, with pretty-printing:
mix absinthe.schema.json --schema MySchema --pretty /path/to/schema.json
Write to default path `#{@default_filename}` using the `MySchema` schema and a custom JSON codec, `MyCodec`:
mix absinthe.schema.json --schema MySchema --json-codec MyCodec
## Custom Codecs
Any module that provides `encode!/2` can be used as a custom codec:
encode!(value, options)
* `value` will be provided as a Map containing the generated schema.
* `options` will be a keyword list with a `:pretty` boolean, indicating whether the user requested pretty-printing.
The function should return a string to be written to the output file.
"""
defmodule Options do
@moduledoc false
defstruct filename: nil, schema: nil, json_codec: nil, pretty: false
@type t() :: %__MODULE__{
filename: String.t(),
schema: module(),
json_codec: module(),
pretty: boolean()
}
end
@doc "Callback implementation for `Mix.Task.run/1`, which receives a list of command-line args."
@spec run(argv :: [binary()]) :: any()
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
@doc false
@spec generate_schema(Options.t()) :: {:error, binary()} | {:ok, String.t()}
def generate_schema(%Options{
pretty: pretty,
schema: schema,
json_codec: json_codec
}) do
with {:ok, result} <- Absinthe.Schema.introspect(schema) do
content = json_codec.encode!(result, pretty: pretty)
{:ok, content}
else
{:error, reason} -> {:error, reason}
error -> {:error, error}
end
end
@doc false
@spec parse_options([String.t()]) :: Options.t()
def parse_options(argv) do
parse_options = [strict: [schema: :string, json_codec: :string, pretty: :boolean]]
{opts, args, _} = OptionParser.parse(argv, parse_options)
%Options{
filename: args |> List.first() || @default_filename,
schema: find_schema(opts),
json_codec: json_codec_as_atom(opts),
pretty: Keyword.get(opts, :pretty, false)
}
end
defp json_codec_as_atom(opts) do
opts
|> Keyword.fetch(:json_codec)
|> case do
{:ok, codec} -> Module.concat([codec])
_ -> Jason
end
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
defp write_schema(content, filename) do
create_directory(Path.dirname(filename))
create_file(filename, content, force: true)
end
end