Unfortunately, there is no way to define this at the input_object level. However, there are a few different ways to handle parameter transformation.
1. Change your business logic
You can just change the contracts within your service layer to handle an object instead of an array.
# Inside of some module
def my_functon(attrs, other, params) do
formatted_attrs = case attrs do
%{range: %{start: startrange, end: endrange}} ->
%{attrs | range: [startrange, endrange]}
_ ->
attrs
end
# ...
end
2. Handle the input object where it is passed by args in the schema
You can transform the arguments when they are submitted
# Some graphql definition
field(:my_field, :my_object) do
arg(:range, non_null(:numrange))
resolve(fn parent, %{range: %{start: rstart, end: rend}} = args, ctx ->
new_args = %{args | range: [rstart, rend]}
SomeModule.my_function(new_attrs, parent, ctx)
end)
end
3. Create a middleware
You can create an Absinthe.Middleware to transform arguments when they are submitted
defmodule MyApp.NumrangeTransform do
@behaviour Absinthe.Middleware
@impl Absinthe.Middleware
def call(%Absinthe.Resolution{arguments: args} = res, opts) do
field = Keyword.fetch!(opts, :field)
new_args = case Map.get(args, field) do
%{start: rstart, end: rend} ->
Map.put(args, field, [rstart, rend])
_ ->
args
end
%{res | arguments: new_args}
end
end
Then in your schema definition:
field(:my_field, :type) do
middleware(MyApp.NumrangeTransform, field: :range)
arg(:range, :numrange)
# ...
end
The Middleware will transform the args for you without having to write transformation logic everywhere
4. Create a custom scalar type
custom scalar types can be defined in Absinthe:
# In some schema definition
scalar :numrange, name: "NumRange" do
description("A number range of integers m..n")
serialize([rstart, rend]) when is_integer(rstart) and is_integer(rend) do
rstart <> ".." <> rend
end
parse(&do_parse/1)
defp do_parse(%Absinthe.Blueprint.Input.String{value: range_str}) do
with [s_str, e_str] <- String.split(range_str, ".."),
{rstart, _} <- Integer.parse(s_str),
{rend, _} <- Integer.parse(e_str) do
{:ok, [rstart, rend]}
else
_ -> :error
end
end
def do_parse(%Absinthe.Blueprint.Input.Null{}), do: {:ok, nil}
def do_parse(_), do: :error
end
Then it is added somewhere in the schema
field(:my_field, :type) do
arg(:range, non_null(:numrange))
# ...
end
And the GraphQL looks something like this:
query SomeQuery {
myField(range:"1..3")
}
This is probably the least attractive option as it creates a non-standard way of both presenting and accepting number ranges for any front-end application. However, if this is not a public API that is accessed by third-party applications, there should be no issue with doing it.
Conclusion
There are many ways to define and handle parameter transformation in your input arguments. There may be other solutions that I haven't mentioned. You could probably do something pretty crazy by writing a custom Absinthe.Phase, but that is a complex endeavour that would likely be too heavy-handed for something this simple.