diff --git a/lib/mix/tasks/absinthe.schema.json.ex b/lib/mix/tasks/absinthe.schema.json.ex index 4627506b3f..c036dc98fb 100644 --- a/lib/mix/tasks/absinthe.schema.json.ex +++ b/lib/mix/tasks/absinthe.schema.json.ex @@ -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 @@ -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 @@ -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 diff --git a/test/mix/tasks/absinthe.schema.json_test.exs b/test/mix/tasks/absinthe.schema.json_test.exs new file mode 100644 index 0000000000..0e067e52a2 --- /dev/null +++ b/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