Skip to content

Commit

Permalink
SDL Improvements (#731)
Browse files Browse the repository at this point in the history
* Build SDL version of Things test schema

* Use Absinthe.Case consistently

* Start work on schema directives

* Extensible SDL directives

* Expand schema directives

* Add resolvers to SDL schema

* Support no-op directives

* Add directive placement for schema declarations

* Work on hydrate rename

* Fix callback

* Typos!

* Formatting

* Update travis
  • Loading branch information
bruce authored May 31, 2019
1 parent fc171a8 commit 5af2f70
Show file tree
Hide file tree
Showing 112 changed files with 994 additions and 384 deletions.
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

0 comments on commit 5af2f70

Please sign in to comment.