defmodule Absinthe.Federation.Schema.EntityUnion do @moduledoc false alias Absinthe.Blueprint alias Absinthe.Blueprint.Schema.UnionTypeDefinition alias Absinthe.Blueprint.TypeReference.Name alias Absinthe.Schema.Notation alias Absinthe.Type require Protocol def build(blueprint) do %UnionTypeDefinition{ __reference__: Notation.build_reference(__ENV__), description: "a union of all types that use the @key directive", identifier: :_entity, module: __MODULE__, name: "_Entity", types: types(blueprint), resolve_type: &__MODULE__.resolve_type/2 } end def resolve_type(map, resolution) do Absinthe.Federation.Schema.EntityUnion.Resolver.resolve_type(map, resolution) end defp types(node) do {_node, types} = Blueprint.postwalk(node, [], &collect_types/2) types end defp collect_types( %{name: name, __private__: _private} = node, types ) do if is_object_type?(node) and has_key_directive?(node) do {node, [%Name{name: name} | types]} else {node, types} end end defp collect_types(node, acc), do: {node, acc} defp has_key_directive?(node) do meta = Type.meta(node) has_meta_key = Map.has_key?(meta, :key_fields) node_directives = Map.get(node, :directives, []) has_key_directive = Enum.any?(node_directives, &is_key_directive?/1) has_meta_key or has_key_directive end defp is_key_directive?(%{name: "key"} = _directive), do: true defp is_key_directive?(_directive), do: false defp is_object_type?(%Absinthe.Blueprint.Schema.ObjectTypeDefinition{}), do: true defp is_object_type?(_), do: false end defprotocol Absinthe.Federation.Schema.EntityUnion.Resolver do @fallback_to_any true def resolve_type(map, resolution) end defimpl Absinthe.Federation.Schema.EntityUnion.Resolver, for: Any do alias Absinthe.Adapter.LanguageConventions def resolve_type(%struct_name{} = data, resolution) do typename = struct_name |> Module.split() |> List.last() inner_resolve_type(data, typename, resolution) end def resolve_type(%{__typename: typename} = data, resolution) do inner_resolve_type(data, typename, resolution) end def resolve_type(%{"__typename" => typename} = data, resolution) do inner_resolve_type(data, typename, resolution) end defp inner_resolve_type(data, typename, resolution) do type = Absinthe.Schema.lookup_type(resolution.schema, typename) case type do %{resolve_type: resolve_type} when not is_nil(resolve_type) -> resolver = Absinthe.Type.function(type, :resolve_type) resolver.(data, resolution) _type -> to_internal_name(typename, resolution.adapter) end end defp to_internal_name(name, adapter) when is_nil(adapter) do name |> LanguageConventions.to_internal_name(:type) |> String.to_existing_atom() end defp to_internal_name(name, adapter) when is_atom(adapter) do if adapter_has_to_internal_name_modifier?(adapter) do name |> adapter.to_internal_name(:type) |> String.to_existing_atom() else to_internal_name(name, nil) end end defp adapter_has_to_internal_name_modifier?(adapter) do Keyword.get(adapter.__info__(:functions), :to_internal_name) == 2 end end