defmodule Absinthe.Type do @moduledoc false alias __MODULE__ alias Absinthe.Schema @type function_identifier :: {module, any} @type function_ref :: {:ref, module, function_identifier} # ALL TYPES @type_modules [ Type.Scalar, Type.Object, Type.Interface, Type.Union, Type.Enum, Type.InputObject, Type.List, Type.NonNull ] @typedoc "The types that can be custom-built in a schema" @type custom_t :: Type.Scalar.t() | Type.Object.t() | Type.Field.t() | Type.Interface.t() | Type.Union.t() | Type.Enum.t() | Type.InputObject.t() @typedoc "All the possible types" @type t :: custom_t | wrapping_t @typedoc "A type identifier" @type identifier_t :: atom @typedoc "A type reference" @type reference_t :: identifier_t | binary | t def function(type, key) do case Map.fetch!(type, key) do {:ref, module, identifier} -> module.__absinthe_function__(identifier, key) function -> function end end @doc false # this is just for debugging def expand(%module{} = type) do module.functions() |> Enum.reduce(type, fn :middleware, type -> type attr, type -> Map.put(type, attr, Absinthe.Type.function(type, attr)) end) end @doc "Lookup a custom metadata field on a type" @spec meta(custom_t, atom) :: nil | any def meta(%{__private__: store}, key) do get_in(store, [:meta, key]) end @doc "Return all custom metadata on a type" @spec meta(custom_t) :: map def meta(%{__private__: store}) do Keyword.get(store, :meta, []) |> Enum.into(%{}) end @doc "Determine if a struct matches one of the types" @spec type?(any) :: boolean def type?(%{__struct__: mod}) when mod in @type_modules, do: true def type?(_), do: false @doc "Determine whether a field/argument is deprecated" @spec deprecated?(Type.Field.t() | Type.Argument.t()) :: boolean def deprecated?(%{deprecation: nil}), do: false def deprecated?(%{deprecation: _}), do: true def equal?(%{name: name}, %{name: name}), do: true def equal?(_, _), do: false def built_in?(type) do type.definition |> built_in_module?() end def built_in_module?(module) do module |> Module.split() |> Enum.take(3) |> Module.concat() == Absinthe.Type.BuiltIns end # INPUT TYPES @input_type_modules [Type.Scalar, Type.Enum, Type.InputObject, Type.List, Type.NonNull] @typedoc "These types may be used as input types for arguments and directives." @type input_t :: Type.Scalar.t() | Type.Enum.t() | Type.InputObject.t() | Type.List.t() | Type.NonNull.t() @doc "Determine if a term is an input type" @spec input_type?(any) :: boolean def input_type?(term) do term |> named_type |> do_input_type? end defp do_input_type?(%{__struct__: mod}) when mod in @input_type_modules, do: true defp do_input_type?(_), do: false # OBJECT TYPE @doc "Determine if a term is an object type" @spec object_type?(any) :: boolean def object_type?(%Type.Object{}), do: true def object_type?(_), do: false @doc "Resolve a type for a value from an interface (if necessary)" @spec resolve_type(t, any) :: t def resolve_type(%{resolve_type: resolver}, value), do: resolver.(value) def resolve_type(type, _value), do: type # TYPE WITH FIELDS @doc "Determine if a type has fields" @spec fielded?(any) :: boolean def fielded?(%{fields: _}), do: true def fielded?(_), do: false # OUTPUT TYPES @output_type_modules [Type.Scalar, Type.Object, Type.Interface, Type.Union, Type.Enum] @typedoc "These types may be used as output types as the result of fields." @type output_t :: Type.Scalar.t() | Type.Object.t() | Type.Interface.t() | Type.Union.t() | Type.Enum.t() @doc "Determine if a term is an output type" @spec output_type?(any) :: boolean def output_type?(term) do term |> named_type |> do_output_type? end defp do_output_type?(%{__struct__: mod}) when mod in @output_type_modules, do: true defp do_output_type?(_), do: false # LEAF TYPES @leaf_type_modules [Type.Scalar, Type.Enum] @typedoc "These types may describe types which may be leaf values." @type leaf_t :: Type.Scalar.t() | Type.Enum.t() @doc "Determine if a term is a leaf type" @spec leaf_type?(any) :: boolean def leaf_type?(term) do term |> named_type |> do_leaf_type? end defp do_leaf_type?(%{__struct__: mod}) when mod in @leaf_type_modules, do: true defp do_leaf_type?(_), do: false # COMPOSITE TYPES @composite_type_modules [Type.Object, Type.Interface, Type.Union] @typedoc "These types may describe the parent context of a selection set." @type composite_t :: Type.Object.t() | Type.Interface.t() | Type.Union.t() @doc "Determine if a term is a composite type" @spec composite_type?(any) :: boolean def composite_type?(%{__struct__: mod}) when mod in @composite_type_modules, do: true def composite_type?(_), do: false # ABSTRACT TYPES @abstract_type_modules [Type.Interface, Type.Union] @typedoc "These types may describe the parent context of a selection set." @type abstract_t :: Type.Interface.t() | Type.Union.t() @doc "Determine if a term is an abstract type" @spec abstract?(any) :: boolean def abstract?(%{__struct__: mod}) when mod in @abstract_type_modules, do: true def abstract?(_), do: false # NULLABLE TYPES # @nullable_type_modules [Type.Scalar, Type.Object, Type.Interface, Type.Union, Type.Enum, Type.InputObject, Type.List] @typedoc "These types can all accept null as a value." @type nullable_t :: Type.Scalar.t() | Type.Object.t() | Type.Interface.t() | Type.Union.t() | Type.Enum.t() | Type.InputObject.t() | Type.List.t() @doc "Unwrap the underlying nullable type or return unmodified" # nullable_t is a subset of t, but broken out for clarity @spec nullable(any) :: nullable_t | t def nullable(%Type.NonNull{of_type: nullable}), do: nullable def nullable(term), do: term @doc "Determine if a type is non null" @spec non_null?(t) :: boolean def non_null?(%Type.NonNull{}), do: true def non_null?(_), do: false # NAMED TYPES @named_type_modules [ Type.Scalar, Type.Object, Type.Interface, Type.Union, Type.Enum, Type.InputObject ] @typedoc "These named types do not include modifiers like Absinthe.Type.List or Absinthe.Type.NonNull." @type named_t :: Type.Scalar.t() | Type.Object.t() | Type.Interface.t() | Type.Union.t() | Type.Enum.t() | Type.InputObject.t() @doc "Determine the underlying named type, if any" @spec named_type(any) :: nil | named_t def named_type(%{__struct__: mod, of_type: unmodified}) when mod in [Type.List, Type.NonNull] do named_type(unmodified) end def named_type(%{__struct__: mod} = term) when mod in @named_type_modules, do: term def named_type(_), do: nil @doc "Determine if a type is named" @spec named?(t) :: boolean def named?(%{name: _}), do: true def named?(_), do: false # WRAPPERS @wrapping_modules [Type.List, Type.NonNull] @typedoc "A type wrapped in a List on NonNull" @type wrapping_t :: Type.List.t() | Type.NonNull.t() @spec wrapped?(t) :: boolean def wrapped?(%{__struct__: mod}) when mod in @wrapping_modules, do: true def wrapped?(_), do: false @doc "Unwrap a type from a List or NonNull" @spec unwrap(custom_t | wrapping_t | map) :: reference_t | map | nil def unwrap(%{of_type: t}), do: unwrap(t) def unwrap(type), do: type @doc "Unwrap a type from NonNull" @spec unwrap_non_null(Type.NonNull.t()) :: reference_t @spec unwrap_non_null(type) :: type when type: custom_t | Type.List.t() def unwrap_non_null(%Type.NonNull{of_type: t}), do: unwrap_non_null(t) def unwrap_non_null(type), do: type @doc """ Get the GraphQL name for a (possibly wrapped) type, expanding any references if necessary using the provided schema. """ @spec name(reference_t, Schema.t()) :: String.t() def name(ref, schema) do expanded = expand(ref, schema) name(expanded) end @doc """ Get the GraphQL name for a (possibly wrapped) type. Note: Use `name/2` if the provided type reference needs to be expanded to resolve any atom type references. """ @spec name(wrapping_t | t) :: String.t() def name(%Type.NonNull{of_type: contents}) do name(contents) <> "!" end def name(%Type.List{of_type: contents}) do "[" <> name(contents) <> "]" end def name(%{name: name}) do name end @doc "Expand any atom type references inside a List or NonNull" @spec expand(reference_t, Schema.t()) :: wrapping_t | t def expand(ref, schema) when is_atom(ref) do schema.__absinthe_lookup__(ref) end def expand(%{of_type: contents} = ref, schema) do %{ref | of_type: expand(contents, schema)} end def expand(type, _) do type end # INTROSPECTION TYPE @spec introspection?(t) :: boolean def introspection?(%{name: "__" <> _}) do true end def introspection?(_) do false end # VALUE TYPE @spec value_type(t, Schema.t()) :: Type.t() def value_type(%Type.Field{} = node, schema) do Type.expand(node.type, schema) end def value_type(type, schema) do Type.expand(type, schema) end # VALID TYPE def valid_input?(%Type.NonNull{}, nil) do false end def valid_input?(%Type.NonNull{of_type: internal_type}, value) do valid_input?(internal_type, value) end def valid_input?(_type, nil) do true end def valid_input?(%{parse: parse}, value) do case parse.(value) do {:ok, _} -> true :error -> false end end def valid_input?(_, _) do true end end