lib/mix/tasks/schema.ex

defmodule Mix.Tasks.Absinthe.Gen.Schema do
  use Mix.Task

  alias Mix.AbsintheGeneratorUtils

  @shortdoc "Generates an absinthe schema"

  @moduledoc """
  Generates an Absinthe Schema

  ### Options

  #{NimbleOptions.docs(AbsintheGenerator.Schema.definitions())}

  ### Specifying Middleware
  To specify middleware we can utilize the following syntax

  ```bash
  pre_middleware:mutation:AuthMiddleware post_middleware:all:ChangesetErrorFormatter
  ```

  Middleware can be set for `mutation`, `query`, `subscription` or `all` and can
  also be set to either run pre or post resolution using `pre_middleware` or `post_middleware`


  ### Example

  ```bash
  mix absinthe.gen.schema pre_middleware:mutation:MyMiddlwareModule post_middleware:all:MyAllMiddleware
    --app-name MyApp
    --query test
    --query user
    --mutation user
    --mutation session
    --type MyApp
    --moduledoc "this is the test"
    --data-source "EctoSchemas.Cats"
  ```
  """

  @middleware_regex ~r/(pre_middleware|post_middleware):(mutation|query|subscription|all):[a-zA-Z_]+/

  def run(args) do
    AbsintheGeneratorUtils.ensure_not_in_umbrella!("absinthe.gen.schema")

    {args, extra_args} = AbsintheGeneratorUtils.parse_path_opts(args, [
      path: :string,
      app_name: :string,
      moduledoc: :string,

      type: :keep,
      query: :keep,
      mutation: :keep,
      subscription: :keep,

      data_source: :keep
    ])

    parsed_middleware = extra_args
      |> validate_middleware_string
      |> parse_middleware

    path = Keyword.get(args, :path, "./lib/#{Macro.underscore(args[:app_name])}_web/schema.ex")

    args
      |> AbsintheGeneratorUtils.collect_arguments([:query, :mutation, :subscription, :type, :data_source])
      |> Map.merge(parsed_middleware)
      |> serialize_to_schema_struct
      |> AbsintheGenerator.Schema.run
      |> AbsintheGeneratorUtils.write_template(path)
  end

  defp validate_middleware_string(middleware_args) do
    middleware_string = Enum.join(middleware_args, " ")

    if middleware_string === "" or Regex.match?(@middleware_regex, middleware_string) do
      middleware_args
    else
      Mix.raise("""
      \n
      Middleware format doesn't match what's expected, please make sure it matches the following Regex:

        #{inspect @middleware_regex}

      Example:

        pre_middleware:mutation:MyMiddlewareModule
        pre_middleware:query:MyMiddlewareModule
        pre_middleware:all:MyMiddlewareModule
      """)
    end
  end

  defp parse_middleware(extra_args) do
    middleware_acc = %{
      pre_middleware: [],
      post_middleware: []
    }

    Enum.reduce(extra_args, middleware_acc, fn (arg_string, acc) ->
      [
        middleware_type,
        middleware_query_type,
        middleware_module
      ] = String.split(arg_string, ":")

      middleware_type = String.to_atom(middleware_type)

      middleware = [%{
        type: middleware_query_type,
        module: middleware_module
      }]

      Map.update(acc, middleware_type, middleware, &(&1 ++ middleware))
    end)
  end

  defp serialize_to_schema_struct(params) do
    data_sources = params
      |> Map.get(:data_source, [])
      |> Enum.map(&struct!(AbsintheGenerator.Schema.DataSource, %{source: &1, query: nil}))

    pre_middleware = params
      |> Map.get(:pre_middleware, [])
      |> serialize_middleware

    post_middleware = params
      |> Map.get(:post_middleware, [])
      |> serialize_middleware

    %AbsintheGenerator.Schema{
      app_name: params[:app_name],
      moduledoc: params[:moduledoc],
      queries: params[:query] || [],
      mutations: params[:mutation] || [],
      subscriptions: params[:subscription] || [],
      types: params[:type] || [],
      data_sources: data_sources,
      post_middleware: post_middleware,
      pre_middleware: pre_middleware
    }
  end

  defp serialize_middleware(middleware_params) do
    middleware_params
      |> Enum.group_by(&(&1.module), &(&1.type))
      |> Enum.map(fn {module, types} -> %{types: types, module: module} end)
      |> Enum.map(&struct!(AbsintheGenerator.Schema.Middleware, &1))
  end
end