Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ use Mix.Config

config :logger, level: :debug

# Results will be ordered lists of tuples instead of maps
# config :absinthe, ordered: true

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
Expand Down
2 changes: 1 addition & 1 deletion lib/absinthe.ex
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ defmodule Absinthe do
root_value: term,
operation_name: String.t,
analyze_complexity: boolean,
max_complexity: non_neg_integer | :infinity
max_complexity: non_neg_integer | :infinity,
]

@type run_result :: {:ok, result_t} | {:error, String.t}
Expand Down
2 changes: 1 addition & 1 deletion lib/absinthe/middleware.ex
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ defmodule Absinthe.Middleware do
**It is important to note that we are matching for the `:query`, `:subscription` or
`:mutation` identifier types. We do this because the middleware function will be
called for each field in the schema. It is also important to provide a fallback so
that the default `Absinthe.Middleware.MapGet` is configured.**
that the default `Absinthe.Middleware.MapGet` or `Absinthe.Middleware.OrdMapGet` is configured.**

## Main Points

Expand Down
2 changes: 1 addition & 1 deletion lib/absinthe/middleware/map_get.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ defmodule Absinthe.Middleware.MapGet do
def call(%{source: source} = res, key) do
%{res | state: :resolved, value: Map.get(source, key)}
end
end
end
11 changes: 11 additions & 0 deletions lib/absinthe/middleware/ord_map_get.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule Absinthe.Middleware.OrdMapGet do
@moduledoc """
This is middleware for ordered results.
"""

@behaviour Absinthe.Middleware

def call(%{source: source} = res, key) do
%{res | state: :resolved, value: OrdMap.get(OrdMap.new(source), key)}
end
end
120 changes: 120 additions & 0 deletions lib/absinthe/phase/document/ordered_result.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
defmodule Absinthe.Phase.Document.OrderedResult do

@moduledoc false

# Produces data fit for external encoding from annotated value tree
# Preserves fields order

alias Absinthe.{Blueprint, Phase, Type}
use Absinthe.Phase

@spec run(Blueprint.t | Phase.Error.t, Keyword.t) :: {:ok, map}
def run(%Blueprint{} = bp, _options \\ []) do
{:ok, %{bp | result: process(bp)}}
end

defp process(blueprint) do
result = case blueprint.execution do
%{validation_errors: [], result: nil} ->
:execution_failed
%{validation_errors: [], result: result} ->
{:ok, field_data(result.fields, [])}
%{validation_errors: errors} ->
{:validation_failed, errors}
end
format_result(result)
end

defp format_result(:execution_failed) do
%{data: nil}
end
defp format_result({:ok, {data, []}}) do
%{data: data}
end
defp format_result({:ok, {data, errors}}) do
errors = errors |> Enum.uniq |> Enum.map(&format_error/1)
%{data: data, errors: errors}
end
defp format_result({:validation_failed, errors}) do
errors = errors |> Enum.uniq |> Enum.map(&format_error/1)
%{errors: errors}
end
defp format_result({:parse_failed, error}) do
%{errors: [format_error(error)]}
end

defp data(%{errors: [_|_] = field_errors}, errors), do: {nil, field_errors ++ errors}

# Leaf
defp data(%{value: nil}, errors), do: {nil, errors}
defp data(%{value: value, emitter: emitter}, errors) do
value =
case Type.unwrap(emitter.schema_node.type) do
%Type.Scalar{} = schema_node ->
Type.Scalar.serialize(schema_node, value)
%Type.Enum{} = schema_node ->
Type.Enum.serialize(schema_node, value)
end
{value, errors}
end

# Object
defp data(%{fields: fields}, errors), do: field_data(fields, errors)

# List
defp data(%{values: values}, errors), do: list_data(values, errors)

defp list_data(fields, errors, acc \\ [])
defp list_data([], errors, acc), do: {:lists.reverse(acc), errors}
defp list_data([%{errors: []} = field | fields], errors, acc) do
{value, errors} = data(field, errors)
list_data(fields, errors, [value | acc])
end
defp list_data([%{errors: errs} | fields], errors, acc) when length(errs) > 0 do
list_data(fields, errs ++ errors, acc)
end

defp field_data(fields, errors, acc \\ [])
defp field_data([], errors, acc) do
{OrdMap.new(:lists.reverse(acc)), errors}
end
defp field_data([%Absinthe.Resolution{} = res | _], _errors, _acc) do
raise """
Found unresolved resolution struct!

You probably forgot to run the resolution phase again.

#{inspect res}
"""
end
defp field_data([field | fields], errors, acc) do
{value, errors} = data(field, errors)
field_data(fields, errors, [{field_name(field.emitter), value} | acc])
end

defp field_name(%{alias: nil, name: name}), do: name
defp field_name(%{alias: name}), do: name
defp field_name(%{name: name}), do: name

defp format_error(%Phase.Error{locations: []} = error) do
error_object = %{message: error.message}
Map.merge(error.extra, error_object)
end
defp format_error(%Phase.Error{} = error) do
error_object = %{
message: error.message,
locations: Enum.flat_map(error.locations, &format_location/1),
}
error_object = case error.path do
[] -> error_object
path -> Map.put(error_object, :path, path)
end
Map.merge(Map.new(error.extra), error_object)
end

defp format_location(%{line: line, column: col}) do
[%{line: line || 0, column: col || 0}]
end
defp format_location(_), do: []

end
222 changes: 109 additions & 113 deletions lib/absinthe/phase/document/result.ex
Original file line number Diff line number Diff line change
@@ -1,117 +1,113 @@
defmodule Absinthe.Phase.Document.Result do

@moduledoc false

# Produces data fit for external encoding from annotated value tree

alias Absinthe.{Blueprint, Phase, Type}
use Absinthe.Phase

@spec run(Blueprint.t | Phase.Error.t, Keyword.t) :: {:ok, map}
def run(%Blueprint{} = bp, _options \\ []) do
{:ok, %{bp | result: process(bp)}}
end

defp process(blueprint) do
result = case blueprint.execution do
%{validation_errors: [], result: nil} ->
:execution_failed
%{validation_errors: [], result: result} ->
{:ok, field_data(result.fields, [])}
%{validation_errors: errors} ->
{:validation_failed, errors}

@moduledoc false

# Produces data fit for external encoding from annotated value tree

alias Absinthe.{Blueprint, Phase, Type}
use Absinthe.Phase

@spec run(Blueprint.t | Phase.Error.t, Keyword.t) :: {:ok, map}
def run(%Blueprint{} = bp, _options \\ []) do
result = Map.merge(bp.result, process(bp))
{:ok, %{bp | result: result}}
end
format_result(result)
end

defp format_result(:execution_failed) do
%{data: nil}
end
defp format_result({:ok, {data, []}}) do
%{data: data}
end
defp format_result({:ok, {data, errors}}) do
errors = errors |> Enum.uniq |> Enum.map(&format_error/1)
%{data: data, errors: errors}
end
defp format_result({:validation_failed, errors}) do
errors = errors |> Enum.uniq |> Enum.map(&format_error/1)
%{errors: errors}
end
defp format_result({:parse_failed, error}) do
%{errors: [format_error(error)]}
end

defp data(%{errors: [_|_] = field_errors}, errors), do: {nil, field_errors ++ errors}

# Leaf
defp data(%{value: nil}, errors), do: {nil, errors}
defp data(%{value: value, emitter: emitter}, errors) do
value =
case Type.unwrap(emitter.schema_node.type) do
%Type.Scalar{} = schema_node ->
Type.Scalar.serialize(schema_node, value)
%Type.Enum{} = schema_node ->
Type.Enum.serialize(schema_node, value)

defp process(blueprint) do
result = case blueprint.execution do
%{validation_errors: [], result: result} ->
{:ok, data(result, [])}
%{validation_errors: errors} ->
{:validation_failed, errors}
end
{value, errors}
end

# Object
defp data(%{fields: fields}, errors), do: field_data(fields, errors)

# List
defp data(%{values: values}, errors), do: list_data(values, errors)

defp list_data(fields, errors, acc \\ [])
defp list_data([], errors, acc), do: {:lists.reverse(acc), errors}
defp list_data([%{errors: []} = field | fields], errors, acc) do
{value, errors} = data(field, errors)
list_data(fields, errors, [value | acc])
end
defp list_data([%{errors: errs} | fields], errors, acc) when length(errs) > 0 do
list_data(fields, errs ++ errors, acc)
end

defp field_data(fields, errors, acc \\ [])
defp field_data([], errors, acc), do: {Map.new(acc), errors}
defp field_data([%Absinthe.Resolution{} = res | _], _errors, _acc) do
raise """
Found unresolved resolution struct!

You probably forgot to run the resolution phase again.

#{inspect res}
"""
end
defp field_data([field | fields], errors, acc) do
{value, errors} = data(field, errors)
field_data(fields, errors, [{field_name(field.emitter), value} | acc])
end

defp field_name(%{alias: nil, name: name}), do: name
defp field_name(%{alias: name}), do: name
defp field_name(%{name: name}), do: name

defp format_error(%Phase.Error{locations: []} = error) do
error_object = %{message: error.message}
Map.merge(error.extra, error_object)
end
defp format_error(%Phase.Error{} = error) do
error_object = %{
message: error.message,
locations: Enum.flat_map(error.locations, &format_location/1),
}
error_object = case error.path do
[] -> error_object
path -> Map.put(error_object, :path, path)
format_result(result)
end
Map.merge(Map.new(error.extra), error_object)
end

defp format_location(%{line: line, column: col}) do
[%{line: line || 0, column: col || 0}]
end
defp format_location(_), do: []

end

defp format_result(:execution_failed) do
%{data: nil}
end
defp format_result({:ok, {data, []}}) do
%{data: data}
end
defp format_result({:ok, {data, errors}}) do
errors = errors |> Enum.uniq |> Enum.map(&format_error/1)
%{data: data, errors: errors}
end
defp format_result({:validation_failed, errors}) do
errors = errors |> Enum.uniq |> Enum.map(&format_error/1)
%{errors: errors}
end
defp format_result({:parse_failed, error}) do
%{errors: [format_error(error)]}
end

defp data(%{errors: [_|_] = field_errors}, errors), do: {nil, field_errors ++ errors}

# Leaf
defp data(%{value: nil}, errors), do: {nil, errors}
defp data(%{value: value, emitter: emitter}, errors) do
value =
case Type.unwrap(emitter.schema_node.type) do
%Type.Scalar{} = schema_node ->
Type.Scalar.serialize(schema_node, value)
%Type.Enum{} = schema_node ->
Type.Enum.serialize(schema_node, value)
end
{value, errors}
end

# Object
defp data(%{fields: fields}, errors), do: field_data(fields, errors)

# List
defp data(%{values: values}, errors), do: list_data(values, errors)

defp list_data(fields, errors, acc \\ [])
defp list_data([], errors, acc), do: {:lists.reverse(acc), errors}
defp list_data([%{errors: errs} = field | fields], errors, acc) do
{value, errors} = data(field, errors)
list_data(fields, errs ++ errors, [value | acc])
end

defp field_data(fields, errors, acc \\ [])
defp field_data([], errors, acc), do: {Map.new(acc), errors}
defp field_data([%Absinthe.Resolution{} = res | _], _errors, _acc) do
raise """
Found unresolved resolution struct!

You probably forgot to run the resolution phase again.

#{inspect res}
"""
end
defp field_data([field | fields], errors, acc) do
{value, errors} = data(field, errors)
field_data(fields, errors, [{field_name(field.emitter), value} | acc])
end

defp field_name(%{alias: nil, name: name}), do: name
defp field_name(%{alias: name}), do: name
defp field_name(%{name: name}), do: name

defp format_error(%Phase.Error{locations: []} = error) do
error_object = %{message: error.message}
Map.merge(error.extra, error_object)
end
defp format_error(%Phase.Error{} = error) do
error_object = %{
message: error.message,
locations: Enum.flat_map(error.locations, &format_location/1),
}
error_object = case error.path do
[] -> error_object
path -> Map.put(error_object, :path, path)
end
Map.merge(Map.new(error.extra), error_object)
end

defp format_location(%{line: line, column: col}) do
[%{line: line || 0, column: col || 0}]
end
defp format_location(_), do: []

end
Loading