Skip to content

Instantly share code, notes, and snippets.

@maartenvanvliet
Created January 4, 2022 07:32
Show Gist options
  • Save maartenvanvliet/cc3005fdb5950059edc5e0e753bd7c0c to your computer and use it in GitHub Desktop.
Save maartenvanvliet/cc3005fdb5950059edc5e0e753bd7c0c to your computer and use it in GitHub Desktop.
Absinthe spec compliant int pipeline modifier
defmodule Absinthe.Type.BuiltIns.SpecCompliantInt do
use Absinthe.Schema.Notation
@moduledoc false
scalar :integer, name: "Int" do
description """
The `Int` scalar type represents non-fractional signed whole numeric
values between `-2^31` and `2^31 - 1` as outlined in the
[GraphQl spec](ttps://spec.graphql.org/October2021/#sec-Int).
"""
serialize &__MODULE__.serialize_int/1
parse parse_with([Absinthe.Blueprint.Input.Integer], &parse_int/1)
end
@max_int 2_147_483_647
@min_int -2_147_483_648
def serialize_int(value) when is_integer(value) and value >= @min_int and value <= @max_int do
value
end
defp parse_int(value) when is_integer(value) and value >= @min_int and value <= @max_int do
{:ok, value}
end
defp parse_int(_) do
:error
end
# Parse, supporting pulling values out of blueprint Input nodes
defp parse_with(node_types, coercion) do
fn
%{__struct__: str, value: value} ->
if Enum.member?(node_types, str) do
coercion.(value)
else
:error
end
%Absinthe.Blueprint.Input.Null{} ->
{:ok, nil}
other ->
coercion.(other)
end
end
end
defmodule Absinthe.Phase.Schema.SpecCompliantInt do
@moduledoc false
@behaviour Absinthe.Phase
alias Absinthe.Blueprint
def pipeline(pipeline) do
Absinthe.Pipeline.insert_after(pipeline, Absinthe.Phase.Schema.TypeImports, __MODULE__)
end
@spec run(Blueprint.t(), Keyword.t()) :: {:ok, Blueprint.t()}
def run(input, _options \\ []) do
node = Blueprint.prewalk(input, &handle_node/1)
{:ok, node}
end
defp handle_node(%Blueprint.Schema.ScalarTypeDefinition{identifier: :integer}) do
case ensure_compiled(Absinthe.Type.BuiltIns.SpecCompliantInt) do
{:module, module} ->
[types] = module.__absinthe_blueprint__.schema_definitions
Enum.find(types.type_definitions, &(&1.identifier == :integer))
{:error, reason} ->
raise reason
end
end
defp handle_node(node) do
node
end
# Elixir v1.12 includes a Code.ensure_compiled!/1 that tells
# the compiler it should only continue if the module is available.
# This gives the Elixir compiler more information to address
# deadlocks.
# TODO: Remove the else clause once we require Elixir v1.12+.
@compile {:no_warn_undefined, {Code, :ensure_compiled!, 1}}
@dialyzer {:nowarn_function, [ensure_compiled: 1]}
defp ensure_compiled(module) do
if function_exported?(Code, :ensure_compiled!, 1) do
{:module, Code.ensure_compiled!(module)}
else
Code.ensure_compiled(module)
end
end
end
defmodule Absinthe.Phase.Schema.SpecCompliantInt.Test do
use Absinthe.Case, async: true
defmodule Schema do
use Absinthe.Schema
@pipeline_modifier Absinthe.Phase.Schema.SpecCompliantInt
query do
field :parse, :integer do
arg :input, :integer
resolve fn args, _ ->
{:ok, args[:input]}
end
end
end
end
test "we can override the builtin scalars" do
assert {:ok,
%{
errors: [
%{
locations: [%{column: 34, line: 1}],
message: "Argument \"input\" has invalid value $input."
}
]
}} ==
Absinthe.run(
"query ($input: Integer!) { parse(input: $input) }",
Schema,
variables: %{
"input" => 9_007_199_254_740_991
}
)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment