Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(v1.4) Fix mix task to pass pretty flag to custom codecs, add tests #677

Merged
merged 1 commit into from Jan 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
120 changes: 81 additions & 39 deletions lib/mix/tasks/absinthe.schema.json.ex
Expand Up @@ -6,7 +6,6 @@ defmodule Mix.Tasks.Absinthe.Schema.Json do
@shortdoc "Generate a schema.json file for an Absinthe schema"

@default_filename "./schema.json"
@default_codec_name "Poison"

@moduledoc """
Generate a schema.json file
Expand All @@ -15,79 +14,117 @@ defmodule Mix.Tasks.Absinthe.Schema.Json do

absinthe.schema.json [FILENAME] [OPTIONS]

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 schema. Default: As configured for `:absinthe` `:schema`
--json-codec Sets JSON Codec. Default: #{@default_codec_name}
--pretty Whether to pretty-print. Default: false
* `--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 and the default `#{@default_codec_name}` JSON codec:
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 and
the default `#{@default_codec_name}` JSON codec.
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, using the
default `#{@default_codec_name}` JSON codec, and pretty-printing:
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`:
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.

"""

@introspection_graphql Path.join([:code.priv_dir(:absinthe), "graphql", "introspection.graphql"])
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.Project.compile(argv)

{opts, args, _} = OptionParser.parse(argv)

schema = find_schema(opts)
json_codec = find_json(opts)
filename = args |> List.first() || @default_filename

{:ok, query} = File.read(@introspection_graphql)

case Absinthe.run(query, schema) do
{:ok, result} ->
create_directory(Path.dirname(filename))
content = json_codec.module.encode!(result, json_codec.opts)
create_file(filename, content, force: true)
opts = parse_options(argv)

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

defp find_json(opts) do
case Keyword.get(opts, :json_codec, Poison) do
module when is_atom(module) ->
%{module: module, opts: codec_opts(module, opts)}

other ->
other
@doc false
@spec generate_schema(Options.t()) :: String.t()
def generate_schema(%Options{
pretty: pretty,
schema: schema,
json_codec: json_codec
}) do
with {:ok, result} <- Absinthe.Schema.introspect(schema),
content <- json_codec.encode!(result, pretty: pretty) do
{:ok, content}
else
{:error, reason} -> {:error, reason}
error -> {:error, error}
end
end

defp codec_opts(Poison, opts) do
[pretty: Keyword.get(opts, :pretty, false)]
@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 codec_opts(_, _) do
[]
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
Expand All @@ -99,4 +136,9 @@ defmodule Mix.Tasks.Absinthe.Schema.Json do
[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
76 changes: 76 additions & 0 deletions test/mix/tasks/absinthe.schema.json_test.exs
@@ -0,0 +1,76 @@
defmodule Mix.Tasks.Absinthe.Schema.JsonTest do
use Absinthe.Case, async: true

alias Mix.Tasks.Absinthe.Schema.Json, as: Task

defmodule TestSchema do
use Absinthe.Schema

query do
field :item, :item
end

object :item do
description "A Basic Type"
field :id, :id
field :name, :string
end
end

defmodule TestEncoder do
def encode!(_map, opts) do
pretty_flag = Keyword.get(opts, :pretty, false)
pretty_string = if pretty_flag, do: "pretty", else: "ugly"
"test-encoder-#{pretty_string}"
end
end

@test_schema "Mix.Tasks.Absinthe.Schema.JsonTest.TestSchema"
@test_encoder "Mix.Tasks.Absinthe.Schema.JsonTest.TestEncoder"

describe "absinthe.schema.json" do
test "parses options" do
argv = ["output.json", "--schema", @test_schema, "--json-codec", @test_encoder, "--pretty"]

opts = Task.parse_options(argv)

assert opts.filename == "output.json"
assert opts.json_codec == TestEncoder
assert opts.pretty == true
assert opts.schema == TestSchema
end

test "provides default options" do
argv = ["--schema", @test_schema]

opts = Task.parse_options(argv)

assert opts.filename == "./schema.json"
assert opts.json_codec == Jason
assert opts.pretty == false
assert opts.schema == TestSchema
end

test "fails if no schema arg is provided" do
argv = []
catch_error(Task.parse_options(argv))
end

test "fails if codec hasn't been loaded" do
argv = ["--schema", @test_schema, "--json-codec", "UnloadedCodec"]
opts = Task.parse_options(argv)
catch_error(Task.generate_schema(opts))
end

test "can use a custom codec" do
argv = ["--schema", @test_schema, "--json-codec", @test_encoder, "--pretty"]

opts = Task.parse_options(argv)
{:ok, pretty_content} = Task.generate_schema(opts)
{:ok, ugly_content} = Task.generate_schema(%{opts | pretty: false})

assert pretty_content == "test-encoder-pretty"
assert ugly_content == "test-encoder-ugly"
end
end
end