Skip to content

Commit

Permalink
cherry pick fix for mix task (#677)
Browse files Browse the repository at this point in the history
  • Loading branch information
hadfieldn authored and benwilson512 committed Jan 27, 2019
1 parent 7c58750 commit 3233ada
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 39 deletions.
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

0 comments on commit 3233ada

Please sign in to comment.