Skip to content

Commit

Permalink
Auto generate mutation and query code sample (#41)
Browse files Browse the repository at this point in the history
* Rename root mutation type in SchemaTest

* Move schema test fixture into function

* Reduce duplication in test by using module attributes

* Add markdown helper function to create GQL operation text block

* Add test for more than one argument for operation text block

* Handle operation with no arguments

* Add gql code block generation to multi_page and single_page modules

* Update markdown helper to print out fields that are returned

* Gather and send operation details in single- and multi-page markdown modules

* Use Enum.map_join/3 instead of Enum.map/2 with Enum.join/2

* Update check for empty list of fields

* Add config.md back in

* Add typespecs and doc for OperationDetailsHelpers functions

* Change function name to graphql_operation_code_block

* Add doc and spec for MarkdownHelpers.graphql_operation_code_block/1

* Add specs for functsion in MarkdownHelpers

* Correct test definition

* Remove Elixir 1.13 and 1.14 workflows

---------

Co-authored-by: Clifton McIntosh <[email protected]>
  • Loading branch information
cliftonmcintosh and Clifton McIntosh authored Jun 24, 2024
1 parent ee525d1 commit 1199a7c
Show file tree
Hide file tree
Showing 9 changed files with 743 additions and 122 deletions.
9 changes: 1 addition & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,8 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-22.04, ubuntu-20.04]
elixir_version: [1.13.3, 1.14.3, 1.15, 1.16]
elixir_version: [1.15, 1.16]
otp_version: [24, 25, 26]
exclude:
- otp_version: 25
elixir_version: 1.13.3
- otp_version: 26
elixir_version: 1.13.3
- otp_version: 26
elixir_version: 1.14.3
steps:
- uses: actions/checkout@v4

Expand Down
1 change: 0 additions & 1 deletion guides/config.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
## Local Development configuration

This section describes the available configuration when working with Graphql Markdown

113 changes: 113 additions & 0 deletions lib/graphql_markdown/markdown_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ defmodule GraphqlMarkdown.MarkdownHelpers do
@moduledoc """
A set of helpers to generate proper markdown easily
"""
alias GraphqlMarkdown.OperationDetailsHelpers

@spec header(String.t(), non_neg_integer(), boolean()) :: String.t()
def header(text, level, capitalize \\ false)

def header(text, level, true) do
Expand All @@ -12,6 +15,7 @@ defmodule GraphqlMarkdown.MarkdownHelpers do
"#{String.duplicate("#", level)} #{text}"
end

@spec list(String.t(), non_neg_integer(), boolean()) :: String.t()
def list(text, level, capitalize \\ false)

def list(text, level, true) do
Expand All @@ -22,6 +26,7 @@ defmodule GraphqlMarkdown.MarkdownHelpers do
"#{String.duplicate(" ", level * 2)}* #{text}"
end

@spec anchor(String.t(), String.t() | nil) :: String.t()
def anchor(text, anchor_text \\ nil) do
case anchor_text do
nil ->
Expand All @@ -32,6 +37,7 @@ defmodule GraphqlMarkdown.MarkdownHelpers do
end
end

@spec link(String.t(), String.t() | nil) :: String.t()
def link(text, url \\ nil) do
case url do
nil ->
Expand All @@ -42,18 +48,22 @@ defmodule GraphqlMarkdown.MarkdownHelpers do
end
end

@spec default_value(any()) :: String.t()
def default_value(nil), do: ""

def default_value(defaultValue) do
"The default value is `#{defaultValue}`"
end

@spec code(String.t()) :: String.t()
def code(text), do: "`#{text}`"

@spec new_line() :: String.t()
def new_line do
"\n"
end

@spec table(list(), list()) :: String.t()
def table(fields, rows) do
headers =
Enum.join(
Expand All @@ -79,4 +89,107 @@ defmodule GraphqlMarkdown.MarkdownHelpers do

headers <> new_line() <> data
end

@doc """
Generates a code block for a GraphQL operation. Only the top-level fields returned are represented in the code block.
When one of the fields returned is an object, the object's fields are not included in the code block.
Example:
```gql
mutation RefreshIdToken($refreshToken: String!) {
refreshIdToken(refreshToken: $refreshToken) {
idToken
userSsoDetails {
}
}
}
```
In this example, the `idToken` field is a scalar, so it is included in the code block.
In this example the `userSsoDetails` field is an object, so its fields are not included in the code block.
"""
@spec graphql_operation_code_block(OperationDetailsHelpers.graphql_operation_details()) ::
String.t()
def graphql_operation_code_block(operation_details) do
%{
operation_name: operation_name,
operation_type: operation_type,
arguments: args,
return_type: return_type
} =
operation_details

capitalized_operation_name = capitalize_operation_name(operation_name)

arguments_types = argument_types_string(args)
arguments = operation_arguments_string(args)

return_fields = returned_fields(return_type)

"""
```gql
#{operation_type} #{capitalized_operation_name}#{arguments_types} {
#{operation_name}#{arguments} {#{return_fields}
}
}
```
"""
end

@spec capitalize_operation_name(String.t()) :: String.t()
defp capitalize_operation_name(operation_name) do
<<first_grapheme::utf8, rest::binary>> = operation_name
String.capitalize(<<first_grapheme::utf8>>) <> rest
end

@spec argument_types_string([OperationDetailsHelpers.argument()]) :: String.t()
defp argument_types_string([]), do: ""

defp argument_types_string(args) do
arg_types =
Enum.map_join(args, ", ", fn arg ->
arg_type = arg.type
arg_name = arg.name

type_suffix =
if arg.required, do: "!", else: ""

"$#{arg_name}: #{arg_type}#{type_suffix}"
end)

"(#{arg_types})"
end

@spec operation_arguments_string([OperationDetailsHelpers.argument()]) :: String.t()
defp operation_arguments_string([]), do: ""

defp operation_arguments_string(args) do
arguments_string =
Enum.map_join(args, ", ", fn arg ->
"#{arg.name}: $#{arg.name}"
end)

"(#{arguments_string})"
end

@spec returned_fields(OperationDetailsHelpers.return_type()) :: String.t()
defp returned_fields(%{kind: "SCALAR"}), do: ""

defp returned_fields(%{kind: "OBJECT"} = return_type) do
fields = Map.get(return_type, :fields, [])

return_values =
for field <- fields do
if field.type == "OBJECT" do
"#{field.name} {\n }"
else
field.name
end
end

case return_values do
[_ | _] -> "\n " <> Enum.join(return_values, "\n ")
_ -> ""
end
end
end
26 changes: 21 additions & 5 deletions lib/graphql_markdown/multi_page.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule GraphqlMarkdown.MultiPage do
Multi page generator from Graphql to Markdown
"""
alias GraphqlMarkdown.MarkdownHelpers
alias GraphqlMarkdown.OperationDetailsHelpers
alias GraphqlMarkdown.Renderer
alias GraphqlMarkdown.Schema

Expand All @@ -17,7 +18,13 @@ defmodule GraphqlMarkdown.MultiPage do
case Renderer.start_link(name: section, filename: filename) do
{:ok, _pid} ->
generate_title(section, options)
generate_section(section, Map.get(schema_details, String.to_existing_atom(section)))

generate_section(
section,
Map.get(schema_details, String.to_existing_atom(section)),
schema_details
)

Renderer.save(String.to_existing_atom(section))
filename

Expand All @@ -33,15 +40,15 @@ defmodule GraphqlMarkdown.MultiPage do
render_newline(type)
end

def generate_section(type, []) do
def generate_section(type, [], _schema_details) do
render(type, "None")
end

def generate_section(type, nil) do
def generate_section(type, nil, _schema_details) do
render(type, "None")
end

def generate_section(type, %{"fields" => fields} = _details)
def generate_section(type, %{"fields" => fields} = _details, schema_details)
when type in ["queries", "mutations"] do
Enum.each(fields, fn field ->
render(type, MarkdownHelpers.header(field["name"], 2))
Expand All @@ -61,10 +68,19 @@ defmodule GraphqlMarkdown.MultiPage do
data = generate_data(field["args"])
render(type, MarkdownHelpers.table([field: {}, description: {}], data))
render_newline(type)

gql_code_markdown =
type
|> OperationDetailsHelpers.generate_operation_details(field, schema_details)
|> MarkdownHelpers.graphql_operation_code_block()

render(type, gql_code_markdown)

render_newline(type)
end)
end

def generate_section(type, details) do
def generate_section(type, details, _schema_details) do
input_kind = Schema.input_kind()
scalar_kind = Schema.scalar_kind()
enum_kind = Schema.enum_kind()
Expand Down
108 changes: 108 additions & 0 deletions lib/graphql_markdown/operation_details_helpers.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
defmodule GraphqlMarkdown.OperationDetailsHelpers do
@moduledoc """
A set of helpers to generate query and mutation details.
"""

alias GraphqlMarkdown.Schema

@type argument :: %{
name: String.t(),
type: String.t(),
required: boolean()
}

@type field :: %{
name: String.t(),
type: String.t()
}

@type return_type :: %{
name: String.t(),
kind: String.t(),
fields: [field()]
}

@type graphql_operation_details :: %{
operation_type: String.t(),
operation_name: String.t(),
arguments: [argument()],
return_type: return_type()
}

@doc """
Creates a map with the details of a query or mutation. The details created include
the operation type, operation name, arguments, and return type.
"""
@spec generate_operation_details(String.t(), map(), GraphqlMarkdown.Schema.t()) ::
graphql_operation_details()
def generate_operation_details(type, field, schema_details) do
operation_type =
case type do
"queries" -> "query"
"mutations" -> "mutation"
end

arguments = operation_arguments(field["args"])

operation_details = %{
operation_type: operation_type,
operation_name: field["name"],
arguments: arguments,
return_type: return_fields(field["type"], schema_details)
}

operation_details
end

@spec operation_arguments([map()]) :: [argument()]
defp operation_arguments(args) do
Enum.map(args, fn arg ->
arg_type = arg["type"]
type = Schema.field_type(arg_type)
required = arg_type["kind"] == "NON_NULL"

%{
name: arg["name"],
type: type,
required: required
}
end)
end

@spec return_fields(map(), GraphqlMarkdown.Schema.t()) :: return_type()
defp return_fields(%{"name" => name, "kind" => "OBJECT"}, schema_details) do
object_fields =
schema_details
|> Map.get(:objects, [])
|> Enum.find(fn object -> object["name"] == name end)
|> Map.get("fields", [])
|> Enum.map(fn field ->
field_name = field["name"]
type = return_field_type(field)
%{name: field_name, type: type}
end)

%{
name: name,
kind: "OBJECT",
fields: object_fields
}
end

defp return_fields(return_type, _schema_details) do
%{
name: return_type["name"],
kind: return_type["kind"],
fields: []
}
end

@spec return_field_type(map()) :: String.t()
defp return_field_type(%{"type" => %{"kind" => "NON_NULL"}} = field) do
get_in(field, ["type", "ofType", "kind"])
end

defp return_field_type(field) do
get_in(field, ["type", "kind"])
end
end
Loading

0 comments on commit 1199a7c

Please sign in to comment.