Current section
Files
Jump to
Current section
Files
lib/mix/tasks/censer.gen.graphql.function.ex
defmodule Mix.Tasks.Censer.Gen.Function.Docs do
@moduledoc false
@spec short_doc() :: String.t()
def short_doc do
"Generates Elixir function clauses with pattern-matching heads from a GraphQL query."
end
@spec example() :: String.t()
def example do
"mix censer.gen.function MyApp.UserContext priv/queries/get_user.graphql"
end
@spec long_doc() :: String.t()
def long_doc do
"""
#{short_doc()}
This task parses a `.graphql` file and uses `Igniter` to inject a corresponding
function into the specified target module.
The generated function's name is derived from the GraphQL operation name. The function
arguments are map patterns that match the exact shape of the query's selection set.
## Behavior
* **Module Creation:** If the target module does not exist, it will be created.
* **Idempotency:** If a function with the same name and arity (1) already exists
in the module, the task will skip injection to avoid overwriting existing logic.
* **Multiple Clauses:** If the GraphQL query implies multiple resulting shapes,
multiple function clauses will be generated.
## Usage
Pass the target module name and the path to your GraphQL file:
```sh
#{example()}
```
## Generated Code Shape
Given a GraphQL query file containing:
```graphql
query GetUser { user { id email } }
```
Censer will generate code similar to the following:
```elixir
defmodule MyApp.UserContext do
# ...
def handle_get_user(%{"user" => %{"id" => id, "email" => email}}) do
:ok
end
end
```
"""
end
end
defmodule Mix.Tasks.Censer.Gen.Function do
@shortdoc "#{__MODULE__.Docs.short_doc()}"
@moduledoc __MODULE__.Docs.long_doc()
use Igniter.Mix.Task
@impl Igniter.Mix.Task
def info(_argv, _parent) do
%Igniter.Mix.Task.Info{
positional: [:module, :graphql_file_path]
}
end
@impl Igniter.Mix.Task
def igniter(igniter) do
%{module: module, graphql_file_path: file_path} = igniter.args.positional
target_module = Igniter.Project.Module.parse(module)
if File.exists?(file_path) do
query = File.read!(file_path)
do_igniter(igniter, target_module, query)
else
Igniter.add_issue(igniter, "Query file not found at: #{file_path}")
end
end
defp do_igniter(igniter, target_module, query) do
{function_name, function_ast} = build_function_ast(query)
igniter
|> ensure_module_exists(target_module)
|> Igniter.Project.Module.find_and_update_module(target_module, fn zipper ->
# We skip if function exists to avoid messing up existing logic
case Igniter.Code.Function.move_to_def(zipper, function_name, 1) do
{:ok, _zipper} -> {:ok, zipper}
:error -> {:ok, Igniter.Code.Common.add_code(zipper, function_ast)}
end
end)
|> case do
{:ok, igni} -> igni
{:error, igni} -> Igniter.add_issue(igni, "Error generating function")
end
end
defp ensure_module_exists(igniter, module) do
case Igniter.Project.Module.module_exists(igniter, module) do
{true, new_igniter} ->
new_igniter
{false, new_igniter} ->
Igniter.Project.Module.create_module(new_igniter, module, "")
end
end
defp build_function_ast(query) do
{function_name, patterns} = Censer.Graphql.parse_and_build(query)
clauses =
Enum.map(patterns, fn pattern ->
quote do
def unquote(function_name)(unquote(pattern) = args) do
{:ok, args}
end
end
end)
fallback_clause =
quote do
def unquote(function_name)(unexpected_args) do
IO.inspect(unexpected_args)
{:error, :unexpected_match}
end
end
{function_name, {:__block__, [], clauses ++ [fallback_clause]}}
end
end