Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SDL Improvements #731

Merged
merged 13 commits into from
May 31, 2019
Merged
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ elixir:
- 1.8.0
notifications:
recipients:
- vincefoley@gmail.com
- brwcodes@gmail.com
- ben.wilson@cargosense.com
otp_release:
- 20.0
- 21.0
- 22.0
script:
- mix format --check-formatted
- MIX_ENV=test mix local.hex --force && MIX_ENV=test mix do deps.get, test
19 changes: 16 additions & 3 deletions lib/absinthe/blueprint/directive.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ defmodule Absinthe.Blueprint.Directive do
# Added by phases
schema_node: nil,
flags: %{},
errors: []
errors: [],
__reference__: nil,
__private__: []
]

@type t :: %__MODULE__{
Expand All @@ -21,7 +23,9 @@ defmodule Absinthe.Blueprint.Directive do
source_location: nil | Blueprint.SourceLocation.t(),
schema_node: nil | Absinthe.Type.Directive.t(),
flags: Blueprint.flags_t(),
errors: [Phase.Error.t()]
errors: [Phase.Error.t()],
__reference__: nil,
__private__: []
}

@spec expand(t, Blueprint.node_t()) :: {t, map}
Expand All @@ -31,7 +35,15 @@ defmodule Absinthe.Blueprint.Directive do

def expand(%__MODULE__{schema_node: type} = directive, node) do
args = Blueprint.Input.Argument.value_map(directive.arguments)
Absinthe.Type.function(type, :expand).(args, node)

case Absinthe.Type.function(type, :expand) do
nil ->
# Directive is a no-op
node

expansion when is_function(expansion) ->
expansion.(args, node)
end
end

@doc """
Expand All @@ -45,6 +57,7 @@ defmodule Absinthe.Blueprint.Directive do
def placement(%Blueprint.Document.Fragment.Inline{}), do: :inline_fragment
def placement(%Blueprint.Document.Operation{}), do: :operation_definition
def placement(%Blueprint.Schema.SchemaDefinition{}), do: :schema
def placement(%Blueprint.Schema.SchemaDeclaration{}), do: :schema
def placement(%Blueprint.Schema.ScalarTypeDefinition{}), do: :scalar
def placement(%Blueprint.Schema.ObjectTypeDefinition{}), do: :object
def placement(%Blueprint.Schema.FieldDefinition{}), do: :field_definition
Expand Down
27 changes: 27 additions & 0 deletions lib/absinthe/blueprint/schema/schema_declaration.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
defmodule Absinthe.Blueprint.Schema.SchemaDeclaration do
@moduledoc false

alias Absinthe.Blueprint

defstruct description: nil,
module: nil,
field_definitions: [],
directives: [],
source_location: nil,
# Added by phases
flags: %{},
errors: [],
__reference__: nil,
__private__: []

@type t :: %__MODULE__{
description: nil | String.t(),
module: nil | module(),
directives: [Blueprint.Directive.t()],
field_definitions: [Blueprint.Schema.FieldDefinition.t()],
source_location: nil | Blueprint.SourceLocation.t(),
# Added by phases
flags: Blueprint.flags_t(),
errors: [Absinthe.Phase.Error.t()]
}
end
31 changes: 31 additions & 0 deletions lib/absinthe/language/schema_declaration.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
defmodule Absinthe.Language.SchemaDeclaration do
@moduledoc false

alias Absinthe.{Blueprint, Language}

defstruct description: nil,
directives: [],
fields: [],
loc: %{line: nil}

@type t :: %__MODULE__{
description: nil | String.t(),
directives: [Language.Directive.t()],
fields: [Language.FieldDefinition.t()],
loc: Language.loc_t()
}

defimpl Blueprint.Draft do
def convert(node, doc) do
%Blueprint.Schema.SchemaDeclaration{
description: node.description,
field_definitions: Absinthe.Blueprint.Draft.convert(node.fields, doc),
directives: Absinthe.Blueprint.Draft.convert(node.directives, doc),
source_location: source_location(node)
}
end

defp source_location(%{loc: nil}), do: nil
defp source_location(%{loc: loc}), do: Blueprint.SourceLocation.at(loc)
end
end
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Absinthe.Phase.Validation.KnownDirectives do
defmodule Absinthe.Phase.Document.Validation.KnownDirectives do
@moduledoc false

alias Absinthe.{Blueprint, Phase}
Expand Down Expand Up @@ -64,7 +64,7 @@ defmodule Absinthe.Phase.Validation.KnownDirectives do
defp error_unknown(node) do
%Phase.Error{
phase: __MODULE__,
message: "Unknown directive.",
message: "Unknown directive `#{node.name}'.",
locations: [node.source_location]
}
end
Expand All @@ -75,7 +75,7 @@ defmodule Absinthe.Phase.Validation.KnownDirectives do

%Phase.Error{
phase: __MODULE__,
message: "May not be used on #{placement_name}.",
message: "Directive `#{node.name}' may not be used on #{placement_name}.",
locations: [node.source_location]
}
end
Expand Down
115 changes: 115 additions & 0 deletions lib/absinthe/phase/schema/apply_declaration.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
defmodule Absinthe.Phase.Schema.ApplyDeclaration do
@moduledoc false

use Absinthe.Phase
alias Absinthe.Blueprint

@type operation :: :query | :mutation | :subscription

@type root_mappings :: %{operation() => Blueprint.TypeReference.Name.t()}

def run(blueprint, _opts) do
blueprint = process(blueprint)
{:ok, blueprint}
end

# Apply schema declaration to each schema definition
@spec process(blueprint :: Blueprint.t()) :: Blueprint.t()
defp process(blueprint = %Blueprint{}) do
%{
blueprint
| schema_definitions: Enum.map(blueprint.schema_definitions, &process_schema_definition/1)
}
end

# Strip the schema declaration out of the schema's type definitions and apply it
@spec process_schema_definition(schema_definition :: Blueprint.Schema.SchemaDefinition.t()) ::
Blueprint.Schema.SchemaDefinition.t()
defp process_schema_definition(schema_definition) do
{declarations, type_defs} =
Enum.split_with(
schema_definition.type_definitions,
&match?(%Blueprint.Schema.SchemaDeclaration{}, &1)
)

# Remove declaration
schema_definition = %{schema_definition | type_definitions: type_defs}

case declarations do
[declaration] ->
root_mappings =
declaration
|> extract_root_mappings

%{
schema_definition
| type_definitions:
Enum.map(schema_definition.type_definitions, &maybe_mark_root(&1, root_mappings))
}

[] ->
schema_definition

[_first | extra_declarations] ->
extra_declarations
|> Enum.reduce(schema_definition, fn declaration, acc ->
acc
|> put_error(error(declaration))
end)
end
end

# Generate an error for extraneous schema declarations
@spec error(declaration :: Blueprint.Schema.SchemaDeclaration.t()) :: Absinthe.Phase.Error.t()
defp error(declaration) do
%Absinthe.Phase.Error{
message:
"More than one schema declaration found. Only one instance of `schema' should be present in SDL.",
locations: [declaration.__reference__.location],
phase: __MODULE__
}
end

# Extract the declared root type names
@spec extract_root_mappings(declaration :: Blueprint.Schema.SchemaDeclaration.t()) ::
root_mappings()
defp extract_root_mappings(declaration) do
for field_def <- declaration.field_definitions,
field_def.identifier in ~w(query mutation subscription)a,
into: %{} do
{field_def.identifier, field_def.type}
end
end

# If the type definition is declared as a root type, set the identifier appropriately
@spec maybe_mark_root(type_def :: Blueprint.Schema.node_t(), root_mappings :: root_mappings()) ::
Blueprint.Schema.node_t()
defp maybe_mark_root(%Blueprint.Schema.ObjectTypeDefinition{} = type_def, root_mappings) do
case operation_root_identifier(type_def, root_mappings) do
nil ->
type_def

identifier ->
%{type_def | identifier: identifier}
end
end

defp maybe_mark_root(type_def, _root_mappings), do: type_def

# Determine which, if any, root identifier should be applied to an object type definitiona
@spec operation_root_identifier(
type_def :: Blueprint.Schema.ObjectTypeDefinition.t(),
root_mappings :: root_mappings()
) :: nil | operation()
defp operation_root_identifier(type_def, root_mappings) do
match_name = type_def.name

Enum.find_value(root_mappings, fn
{ident, %{name: ^match_name}} ->
ident

_ ->
false
end)
end
end
54 changes: 54 additions & 0 deletions lib/absinthe/phase/schema/attach_directives.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
defmodule Absinthe.Phase.Schema.AttachDirectives do
@moduledoc false

# Expand all directives in the document.
#
# Note that no validation occurs in this phase.

use Absinthe.Phase
alias Absinthe.Blueprint

@spec run(Blueprint.t(), Keyword.t()) :: {:ok, Blueprint.t()}
def run(input, _options \\ []) do
node = Blueprint.prewalk(input, &handle_node(&1, input.schema))
{:ok, node}
end

@spec handle_node(node :: Blueprint.Directive.t(), schema :: Absinthe.Schema.t()) ::
Blueprint.Directive.t()
defp handle_node(%Blueprint.Directive{} = node, schema) do
schema_node = Enum.find(available_directives(schema), &(&1.name == node.name))
%{node | schema_node: schema_node}
end

@spec handle_node(node :: Blueprint.node_t(), schema :: Absinthe.Schema.t()) ::
Blueprint.node_t()
defp handle_node(node, _schema) do
node
end

defp available_directives(schema) do
schema.sdl_directives(builtins())
end

def builtins do
[
%Absinthe.Type.Directive{
name: "deprecated",
locations: [:field_definition, :input_field_definition, :argument_definition],
expand: &expand_deprecate/2
}
]
end

@doc """
Add a deprecation (with an optional reason) to a node.
"""
@spec expand_deprecate(
arguments :: %{optional(:reason) => String.t()},
node :: Blueprint.node_t()
) :: Blueprint.node_t()
def expand_deprecate(arguments, node) do
%{node | deprecation: %Absinthe.Type.Deprecation{reason: arguments[:reason]}}
end
end
Loading