defmodule Absinthe.Schema.Notation do @moduledoc """ This module contains macros used to build GraphQL types. See `Absinthe.Schema` for a rough overview of schema building from scratch. """ alias Absinthe.Utils alias Absinthe.Type alias Absinthe.Schema.Notation.Scope defmacro __using__(opts \\ []) do import_opts = opts |> Keyword.put(:only, :macros) quote do import Absinthe.Resolution.Helpers, only: [async: 1, batch: 3] import unquote(__MODULE__), unquote(import_opts) Module.register_attribute __MODULE__, :absinthe_definitions, accumulate: true Module.register_attribute(__MODULE__, :absinthe_descriptions, accumulate: true) @before_compile unquote(__MODULE__).Writer @desc nil end end Module.register_attribute(__MODULE__, :placement, accumulate: true) @doc false # Return a quote that records the current @desc value for a given identifier def desc_attribute_recorder(identifier) do quote do @absinthe_descriptions {unquote(identifier), @desc} @desc nil end end # OBJECT @placement {:object, [toplevel: true]} @doc """ Define an object type. Adds an `Absinthe.Type.Object` to your schema. ## Placement #{Utils.placement_docs(@placement)} ## Examples Basic definition: ``` object :car do # ... end ``` Providing a custom name: ``` object :car, name: "CarType" do # ... end ``` """ defmacro object(identifier, attrs \\ [], [do: block]) do __CALLER__ |> recordable!(:object, @placement[:object]) |> record_object!(identifier, attrs, block) desc_attribute_recorder(identifier) end def record_object!(env, identifier, attrs, block) do scope(env, :object, identifier, attrs, block) end @placement {:interfaces, [under: :object]} @doc """ Declare implemented interfaces for an object. See also `interface/1`, which can be used for one interface, and `interface/3`, used to define interfaces themselves. ## Placement #{Utils.placement_docs(@placement)} ## Examples ``` object :car do interfaces [:vehicle, :branded] # ... end ``` """ defmacro interfaces(ifaces) when is_list(ifaces) do __CALLER__ |> recordable!(:interfaces, @placement[:interfaces]) |> record_interfaces!(ifaces) end @doc false # Record a list of implemented interfaces in the current scope def record_interfaces!(env, ifaces) do Enum.each(ifaces, &record_interface!(env, &1)) :ok end @placement {:resolve, [under: [:field]]} @doc """ Mark a field as deprecated In most cases you can simply pass the deprecate: "message" attribute. However when using the block form of a field it can be nice to also use this macro. ## Placement #{Utils.placement_docs(@placement)} ## Examples ``` field :foo, :string do deprecate "Foo will no longer be supported" end ``` This is how to deprecate other things ``` field :foo, :string do arg :bar, :integer, deprecate: "This isn't supported either" end enum :colors do value :red value :blue, deprecate: "This isn't supported" end ``` """ defmacro deprecate(msg) do __CALLER__ |> recordable!(:deprecate, @placement[:deprecate]) |> record_deprecate!(msg) end @doc false # Record a deprecation in the current scope def record_deprecate!(env, msg) do Scope.put_attribute(env.module, :deprecate, msg) :ok end @doc """ Declare an implemented interface for an object. Adds an `Absinthe.Type.Interface` to your schema. See also `interfaces/1`, which can be used for multiple interfaces, and `interface/3`, used to define interfaces themselves. ## Examples ``` object :car do interface :vehicle # ... end ``` """ @placement {:interface_attribute, [under: :object]} defmacro interface(identifier) do __CALLER__ |> recordable!(:interface_attribute, @placement[:interface_attribute], as: "`interface` (as an attribute)") |> record_interface!(identifier) end @doc false # Record an implemented interface in the current scope def record_interface!(env, identifier) do Scope.put_attribute(env.module, :interfaces, identifier, accumulate: true) Scope.recorded!(env.module, :attr, :interface) :ok end # INTERFACES @placement {:interface, [toplevel: :true]} @doc """ Define an interface type. Adds an `Absinthe.Type.Interface` to your schema. Also see `interface/1` and `interfaces/1`, which declare that an object implements one or more interfaces. ## Placement #{Utils.placement_docs(@placement)} ## Examples ``` interface :vehicle do field :wheel_count, :integer end object :rally_car do field :wheel_count, :integer interface :vehicle end ``` """ defmacro interface(identifier, attrs \\ [], [do: block]) do __CALLER__ |> recordable!(:interface, @placement[:interface]) |> record_interface!(identifier, attrs, block) desc_attribute_recorder(identifier) end @doc false # Record an interface type def record_interface!(env, identifier, attrs, block) do scope(env, :interface, identifier, attrs, block) end @placement {:resolve_type, [under: [:interface, :union]]} @doc """ Define a type resolver for a union or interface. See also: * `Absinthe.Type.Interface` * `Absinthe.Type.Union` ## Placement #{Utils.placement_docs(@placement)} ## Examples ``` interface :entity do # ... resolve_type fn %{employee_count: _}, _ -> :business %{age: _}, _ -> :person end end ``` """ defmacro resolve_type(func_ast) do __CALLER__ |> recordable!(:resolve_type, @placement[:resolve_type]) |> record_resolve_type!(func_ast) end @doc false # Record a type resolver in the current scope def record_resolve_type!(env, func_ast) do Scope.put_attribute(env.module, :resolve_type, func_ast) Scope.recorded!(env.module, :attr, :resolve_type) :ok end # FIELDS @placement {:field, [under: [:input_object, :interface, :object]]} @doc """ Defines a GraphQL field See `field/4` """ defmacro field(identifier, [do: block]) do __CALLER__ |> recordable!(:field, @placement[:field]) |> record_field!(identifier, [], block) end defmacro field(identifier, attrs) when is_list(attrs) do __CALLER__ |> recordable!(:field, @placement[:field]) |> record_field!(identifier, attrs, nil) end defmacro field(identifier, type) do __CALLER__ |> recordable!(:field, @placement[:field]) |> record_field!(identifier, [type: type], nil) end @doc """ Defines a GraphQL field See `field/4` """ defmacro field(identifier, attrs, [do: block]) when is_list(attrs) do __CALLER__ |> recordable!(:field, @placement[:field]) |> record_field!(identifier, attrs, block) end defmacro field(identifier, type, [do: block]) do __CALLER__ |> recordable!(:field, @placement[:field]) |> record_field!(identifier, [type: type], block) end defmacro field(identifier, type, attrs) do __CALLER__ |> recordable!(:field, @placement[:field]) |> record_field!(identifier, Keyword.put(attrs, :type, type), nil) end @doc """ Defines a GraphQL field. ## Placement #{Utils.placement_docs(@placement)} `query`, `mutation`, and `subscription` are all objects under the covers, and thus you'll find `field` definitions under those as well. ## Examples ``` field :id, :id field :age, :integer, description: "How old the item is" field :name, :string do description "The name of the item" end field :location, type: :location ``` """ defmacro field(identifier, type, attrs, [do: block]) do __CALLER__ |> recordable!(:field, @placement[:field]) |> record_field!(identifier, Keyword.put(attrs, :type, type), block) end @doc false # Record a field in the current scope def record_field!(env, identifier, attrs, block) do scope(env, :field, identifier, attrs, block) end @placement {:resolve, [under: [:field]]} @doc """ Defines a resolve function for a field Specify a 2 or 3 arity function to call when resolving a field. You can either hard code a particular anonymous function, or have a function call that returns a 2 or 3 arity anonymous function. See examples for more information. Note that when using a hard coded anonymous function, the function will not capture local variables. ### 3 Arity Functions The first argument to the function is the parent entity. ``` { user(id: 1) { name } } ``` A resolution function on the `name` field would have the result of the `user(id: 1)` field as its first argument. Top level fields have the `root_value` as their first argument. Unless otherwise specified, this defaults to an empty map. The second argument to the resolution function is the field arguments. The final argument is an `Absinthe.Resolution` struct, which includes information like the `context` and other execution data. ### 2 Arity Function Exactly the same as the 3 arity version, but without the first argument (the parent entity) ## Placement #{Utils.placement_docs(@placement)} ## Examples ``` query do field :person, :person do resolve &Person.resolve/2 end end ``` ``` query do field :person, :person do resolve fn %{id: id}, _ -> {:ok, Person.find(id)} end end end ``` ``` query do field :person, :person do resolve lookup(:person) end end def lookup(:person) do fn %{id: id}, _ -> {:ok, Person.find(id)} end end ``` """ defmacro resolve(func_ast) do __CALLER__ |> recordable!(:resolve, @placement[:resolve]) |> record_resolve!(func_ast) end @doc false # Record a resolver in the current scope def record_resolve!(env, func_ast) do Scope.put_attribute(env.module, :resolve, func_ast) Scope.recorded!(env.module, :attr, :resolve) :ok end @placement {:is_type_of, [under: [:object]]} @doc """ ## Placement #{Utils.placement_docs(@placement)} """ defmacro is_type_of(func_ast) do __CALLER__ |> recordable!(:is_type_of, @placement[:is_type_of]) |> record_is_type_of!(func_ast) end @doc false # Record a type checker in the current scope def record_is_type_of!(env, func_ast) do Scope.put_attribute(env.module, :is_type_of, func_ast) Scope.recorded!(env.module, :attr, :is_type_of) :ok end @placement {:arg, [under: [:directive, :field]]} # ARGS @doc """ Add an argument. ## Placement #{Utils.placement_docs(@placement)} ## Examples ``` field do arg :size, :integer arg :name, :string, description: "The desired name" end ``` """ defmacro arg(identifier, type, attrs) do __CALLER__ |> recordable!(:arg, @placement[:arg]) |> record_arg!(identifier, Keyword.put(attrs, :type, type), nil) end @doc """ Add an argument. See `arg/3` """ defmacro arg(identifier, attrs) when is_list(attrs) do __CALLER__ |> recordable!(:arg, @placement[:arg]) |> record_arg!(identifier, attrs, nil) end defmacro arg(identifier, type) do __CALLER__ |> recordable!(:arg, @placement[:arg]) |> record_arg!(identifier, [type: type], nil) end @doc false # Record an argument in the current scope def record_arg!(env, identifier, attrs, block) do scope(env, :arg, identifier, attrs, block) end # SCALARS @placement {:scalar, [toplevel: true]} @doc """ Define a scalar type A scalar type requires `parse/1` and `serialize/1` functions. ## Placement #{Utils.placement_docs(@placement)} ## Examples ``` scalar :time, description: "ISOz time" do parse &Timex.parse(&1.value, "{ISOz}") serialize &Timex.format!(&1, "{ISOz}") end ``` """ defmacro scalar(identifier, attrs, [do: block]) do __CALLER__ |> recordable!(:scalar, @placement[:scalar]) |> record_scalar!(identifier, attrs, block) desc_attribute_recorder(identifier) end @doc """ Defines a scalar type See `scalar/3` """ defmacro scalar(identifier, [do: block]) do __CALLER__ |> recordable!(:scalar, @placement[:scalar]) |> record_scalar!(identifier, [], block) desc_attribute_recorder(identifier) end defmacro scalar(identifier, attrs) do __CALLER__ |> recordable!(:scalar, @placement[:scalar]) |> record_scalar!(identifier, attrs, nil) desc_attribute_recorder(identifier) end @doc false # Record a scalar type def record_scalar!(env, identifier, attrs, block) do scope(env, :scalar, identifier, attrs, block) end @placement {:serialize, [under: [:scalar]]} @doc """ Defines a serialization function for a `scalar` type The specified `serialize` function is used on outgoing data. It should simply return the desired external representation. ## Placement #{Utils.placement_docs(@placement)} """ defmacro serialize(func_ast) do __CALLER__ |> recordable!(:serialize, @placement[:serialize]) |> record_serialize!(func_ast) end @doc false # Record a serialize function in the current scope def record_serialize!(env, func_ast) do Scope.put_attribute(env.module, :serialize, func_ast) Scope.recorded!(env.module, :attr, :serialize) :ok end @placement {:private, [under: [:field, :object, :input_object, :enum, :scalar, :interface, :union]]} @doc false defmacro private(owner, key, value) do __CALLER__ |> recordable!(:private, @placement[:private]) |> record_private!(owner, key, value) end @placement {:meta, [under: [:field, :object, :input_object, :enum, :scalar, :interface, :union]]} @doc """ Defines a metadata key/value pair for a custom type. """ defmacro meta(key, value) do __CALLER__ |> recordable!(:meta, @placement[:meta]) |> record_private!(:meta, key, value) end @doc false # Record a private value def record_private!(env, raw_owner, raw_key, raw_value) do [owner, key, value] = Enum.map([raw_owner, raw_key, raw_value], &Macro.expand(&1, env)) new_attrs = Scope.current(env.module).attrs |> Keyword.put_new(:__private__, []) |> update_in([:__private__, owner], &List.wrap(&1)) |> put_in([:__private__, owner, key], value) Scope.put_attribute(env.module, :__private__, new_attrs[:__private__]) :ok end @placement {:parse, [under: [:scalar]]} @doc """ Defines a parse function for a `scalar` type The specified `parse` function is used on incoming data to transform it into an elixir datastructure. It should return `{:ok, value}` or `{:error, reason}` ## Placement #{Utils.placement_docs(@placement)} """ defmacro parse(func_ast) do __CALLER__ |> recordable!(:parse, @placement[:parse]) |> record_parse!(func_ast) [] end @doc false # Record a parse function in the current scope def record_parse!(env, func_ast) do Scope.put_attribute(env.module, :parse, func_ast) Scope.recorded!(env.module, :attr, :parse) :ok end # DIRECTIVES @placement {:directive, [toplevel: true]} @doc """ Defines a directive ## Placement #{Utils.placement_docs(@placement)} ## Examples ``` directive :mydirective do arg :if, non_null(:boolean), description: "Skipped when true." on Language.FragmentSpread on Language.Field on Language.InlineFragment instruction fn %{if: true} -> :skip _ -> :include end end ``` """ defmacro directive(identifier, attrs \\ [], [do: block]) do __CALLER__ |> recordable!(:directive, @placement[:directive]) |> record_directive!(identifier, attrs, block) desc_attribute_recorder(identifier) end @doc false # Record a directive def record_directive!(env, identifier, attrs, block) do scope(env, :directive, identifier, attrs, block) end @placement {:on, [under: :directive]} @doc """ Declare a directive as operating an a AST node type See `directive/2` ## Placement #{Utils.placement_docs(@placement)} """ defmacro on(ast_node) do __CALLER__ |> recordable!(:on, @placement[:on]) |> record_locations!(ast_node) end @doc false # Record directive AST nodes in the current scope def record_locations!(env, ast_node) do ast_node |> List.wrap |> Enum.each(fn value -> Scope.put_attribute( env.module, :locations, value, accumulate: true ) Scope.recorded!(env.module, :attr, :locations) end) :ok end @placement {:instruction, [under: :directive]} @doc """ Calculate the instruction for a directive ## Placement #{Utils.placement_docs(@placement)} """ defmacro instruction(func_ast) do __CALLER__ |> recordable!(:instruction, @placement[:instruction]) |> record_instruction!(func_ast) end @doc false # Record a directive instruction function in the current scope def record_instruction!(env, func_ast) do Scope.put_attribute(env.module, :instruction, func_ast) Scope.recorded!(env.module, :attr, :instruction) :ok end @placement {:expand, [under: :directive]} @doc """ Define the expansion for a directive ## Placement #{Utils.placement_docs(@placement)} """ defmacro expand(func_ast) do __CALLER__ |> recordable!(:expand, @placement[:expand]) |> record_expand!(func_ast) end @doc false # Record a directive expand function in the current scope def record_expand!(env, func_ast) do Scope.put_attribute(env.module, :expand, func_ast) Scope.recorded!(env.module, :attr, :expand) :ok end # INPUT OBJECTS @placement {:input_object, [toplevel: true]} @doc """ Defines an input object See `Absinthe.Type.InputObject` ## Placement #{Utils.placement_docs(@placement)} ## Examples ``` input_object :contact_input do field :email, non_null(:string) end ``` """ defmacro input_object(identifier, attrs \\ [], [do: block]) do __CALLER__ |> recordable!(:input_object, @placement[:input_object]) |> record_input_object!(identifier, attrs, block) desc_attribute_recorder(identifier) end @doc false # Record an input object type def record_input_object!(env, identifier, attrs, block) do scope(env, :input_object, identifier, attrs, block) end # UNIONS @placement {:union, [toplevel: true]} @doc """ Defines a union type See `Absinthe.Type.Union` ## Placement #{Utils.placement_docs(@placement)} ## Examples ``` union :search_result do description "A search result" types [:person, :business] resolve_type fn %Person{}, _ -> :person %Business{}, _ -> :business end end ``` """ defmacro union(identifier, attrs \\ [], [do: block]) do __CALLER__ |> recordable!(:union, @placement[:union]) |> record_union!(identifier, attrs, block) desc_attribute_recorder(identifier) end @doc false # Record a union type def record_union!(env, identifier, attrs, block) do scope(env, :union, identifier, attrs, block) end @placement {:types, [under: [:union]]} @doc """ Defines the types possible under a union type See `union/3` ## Placement #{Utils.placement_docs(@placement)} """ defmacro types(types) do __CALLER__ |> recordable!(:types, @placement[:types]) |> record_types!(types) end @doc false # Record a list of member types for a union in the current scope def record_types!(env, types) do Scope.put_attribute(env.module, :types, List.wrap(types)) Scope.recorded!(env.module, :attr, :types) :ok end # ENUMS @placement {:enum, [toplevel: true]} @doc """ Defines an enum type ## Placement #{Utils.placement_docs(@placement)} ## Examples Handling `RED`, `GREEN`, `BLUE` values from the query document: ``` enum :color do value :red value :green value :blue end ``` A given query document might look like: ```graphql { foo(color: RED) } ``` Internally you would get an argument in elixir that looks like: ```elixir %{color: :red} ``` If your return value is an enum, it will get serialized out as: ```json {"color": "RED"} ``` You can provide custom value mappings. Here we use `r`, `g`, `b` values: ``` enum :color do value :red, as: "r" value :green, as: "g" value :blue, as: "b" end ``` """ defmacro enum(identifier, attrs, [do: block]) do __CALLER__ |> recordable!(:enum, @placement[:enum]) |> record_enum!(identifier, attrs, block) desc_attribute_recorder(identifier) end @doc """ Defines an enum type See `enum/3` """ defmacro enum(identifier, [do: block]) do __CALLER__ |> recordable!(:enum, @placement[:enum]) |> record_enum!(identifier, [], block) desc_attribute_recorder(identifier) end defmacro enum(identifier, attrs) do __CALLER__ |> recordable!(:enum, @placement[:enum]) |> record_enum!(identifier, attrs, nil) desc_attribute_recorder(identifier) end @doc false # Record an enum type def record_enum!(env, identifier, attrs, block) do scope(env, :enum, identifier, attrs, block) end @placement {:value, [under: [:enum]]} @doc """ Defines a value possible under an enum type See `enum/3` ## Placement #{Utils.placement_docs(@placement)} """ defmacro value(identifier, raw_attrs \\ []) do __CALLER__ |> recordable!(:value, @placement[:value]) |> record_value!(identifier, raw_attrs) end @doc false # Record an enum value in the current scope def record_value!(env, identifier, raw_attrs) do attrs = raw_attrs |> Keyword.put(:value, Keyword.get(raw_attrs, :as, identifier)) |> Keyword.delete(:as) |> add_description(env) Scope.put_attribute(env.module, :values, {identifier, attrs}, accumulate: true) Scope.recorded!(env.module, :attr, :value) :ok end # GENERAL ATTRIBUTES @placement {:description, [toplevel: false]} @doc """ Defines a description This macro adds a description to any other macro which takes a block. Note that you can also specify a description by using `@desc` above any item that can take a description attribute. ## Placement #{Utils.placement_docs(@placement)} """ defmacro description(text) do __CALLER__ |> recordable!(:description, @placement[:description]) |> record_description!(text) end defp reformat_description(text), do: String.strip(text) @doc false # Record a description in the current scope def record_description!(env, text_block) do text = reformat_description(text_block) Scope.put_attribute(env.module, :description, text) Scope.recorded!(env.module, :attr, :description) :ok end # IMPORTS @placement {:import_types, [toplevel: true]} @doc """ Import types from another module Very frequently your schema module will simply have the `query` and `mutation` blocks, and you'll want to break out your other types into other modules. This macro imports those types for use the current module ## Placement #{Utils.placement_docs(@placement)} ## Examples ``` import_types MyApp.Schema.Types ``` """ defmacro import_types(type_module_ast) do env = __CALLER__ {:ok, _} = type_module_ast |> Macro.expand(env) |> do_import_types(env) :ok end defp do_import_types(type_module, env) when is_atom(type_module) do types = for {ident, name} <- type_module.__absinthe_types__, ident in type_module.__absinthe_exports__ do put_definition( env.module, %Absinthe.Schema.Notation.Definition{ category: :type, source: type_module, identifier: ident, attrs: [name: name], file: env.file, line: env.line } ) ident end directives = for {ident, name} <- type_module.__absinthe_directives__, ident in type_module.__absinthe_exports__ do put_definition( env.module, %Absinthe.Schema.Notation.Definition{ category: :directive, source: type_module, identifier: ident, attrs: [name: name], file: env.file, line: env.line } ) end {:ok, types: types, directives: directives} end defp do_import_types(type_module, _) do raise ArgumentError, """ #{type_module} is not a module This macro must be given a literal module name or a macro which expands to a literal module name. Variables are not supported at this time. """ end @placement {:import_fields, [under: [:input_object, :interface, :object]]} @doc """ Import fields from another object """ defmacro import_fields(type_name, opts \\ []) do __CALLER__ |> recordable!(:import_fields, @placement[:import_fields]) |> record_field_import!(type_name, opts) end defp record_field_import!(env, type_name, opts) do Scope.put_attribute(env.module, :field_imports, {type_name, opts}, accumulate: true) end # TYPE UTILITIES @doc """ Marks a type reference as non null See `field/3` for examples """ defmacro non_null(type) do quote do %Absinthe.Type.NonNull{of_type: unquote(type)} end end @doc """ Marks a type reference as a list of the given type See `field/3` for examples """ defmacro list_of(type) do quote do %Absinthe.Type.List{of_type: unquote(type)} end end # NOTATION UTILITIES # Define a notation scope that will accept attributes @doc false def scope(env, kind, identifier, attrs, block) do open_scope(kind, env, identifier, attrs) # this is probably too simple for now. block |> expand(env) close_scope(kind, env, identifier) Scope.recorded!(env.module, kind, identifier) end defp expand(ast, env) do Macro.prewalk(ast, fn {:@, _, [{:desc, _, [desc]}]} -> Module.put_attribute(env.module, :__absinthe_desc__, desc) {_, _, _} = node -> Macro.expand(node, env) node -> node end) end @doc false # Add a `__reference__` to a generated struct def add_reference(attrs, env, identifier) do attrs |> Keyword.put( :__reference__, Macro.escape(%{ module: env.module, identifier: identifier, location: %{ file: env.file, line: env.line } }) ) end # After verifying it is valid in the current context, open a new notation # scope, setting any provided attributes. defp open_scope(kind, env, identifier, attrs) do attrs = attrs |> add_reference(env, identifier) |> add_description(env) Scope.open(kind, env.module, attrs) end defp add_description(attrs, env) do case Module.get_attribute(env.module, :__absinthe_desc__) do nil -> attrs desc -> Module.put_attribute(env.module, :__absinthe_desc__, nil) Keyword.put(attrs, :description, reformat_description(desc)) end end # CLOSE SCOPE HOOKS @unexported_identifiers ~w(query mutation subscription)a # Close the current scope and return the appropriate # quoted result for the type of operation. defp close_scope(:enum, env, identifier) do close_scope_and_define_type(Type.Enum, env, identifier) end defp close_scope(:object, env, identifier) do close_scope_and_define_type( Type.Object, env, identifier, export: !Enum.member?(@unexported_identifiers, identifier) ) end defp close_scope(:interface, env, identifier) do close_scope_and_define_type(Type.Interface, env, identifier) end defp close_scope(:union, env, identifier) do close_scope_and_define_type(Type.Union, env, identifier) end defp close_scope(:input_object, env, identifier) do close_scope_and_define_type(Type.InputObject, env, identifier) end defp close_scope(:field, env, identifier) do close_scope_and_accumulate_attribute(:fields, env, identifier) end defp close_scope(:arg, env, identifier) do close_scope_and_accumulate_attribute(:args, env, identifier) end defp close_scope(:scalar, env, identifier) do close_scope_and_define_type(Type.Scalar, env, identifier) end defp close_scope(:directive, env, identifier) do close_scope_and_define_directive(env, identifier) end defp close_scope(_, env, _) do Scope.close(env) end defp close_scope_with_name(mod, identifier, opts \\ []) do Scope.close(mod).attrs |> add_name(identifier, opts) end defp close_scope_and_define_directive(env, identifier, def_opts \\ []) do definition = %Absinthe.Schema.Notation.Definition{ category: :directive, builder: Absinthe.Type.Directive, identifier: identifier, attrs: close_scope_with_name(env.module, identifier), opts: def_opts, file: env.file, line: env.line } put_definition(env.module, definition) end defp close_scope_and_define_type(type_module, env, identifier, def_opts \\ []) do attrs = close_scope_with_name(env.module, identifier, title: true) definition = %Absinthe.Schema.Notation.Definition{ category: :type, builder: type_module, identifier: identifier, attrs: attrs, opts: def_opts, file: env.file, line: env.line } put_definition(env.module, definition) end defp put_definition(module, definition) do # Why is accumulate true not working here? # does that only work with the @ form? definitions = Module.get_attribute(module, :absinthe_definitions) || [] Module.put_attribute(module, :absinthe_definitions, [definition | definitions]) end defp close_scope_and_accumulate_attribute(attr_name, env, identifier) do Scope.put_attribute(env.module, attr_name, {identifier, close_scope_with_name(env.module, identifier)}, accumulate: true) end @doc false # Add the default name, if needed, to a struct def add_name(attrs, identifier, opts \\ []) do update_in(attrs, [:name], fn value -> default_name(identifier, value, opts) end) end # Find the name, or default as necessary defp default_name(identifier, nil, opts) do if opts[:title] do identifier |> Atom.to_string |> Utils.camelize else identifier |> Atom.to_string end end defp default_name(_, name, _) do name end @doc false # Get a value at a path @spec get_in_private(atom, [atom]) :: any def get_in_private(mod, path) do Enum.find_value(Scope.on(mod), fn %{attrs: attrs} -> get_in(attrs, [:__private__ | path]) end) end @doc false # Ensure the provided operation can be recorded in the current environment, # in the current scope context def recordable!(env, usage) do recordable!(env, usage, Keyword.get(@placement, usage, [])) end def recordable!(env, usage, kw_rules, opts \\ []) do do_recordable!(env, usage, Enum.into(List.wrap(kw_rules), %{}), opts) end defp do_recordable!(env, usage, %{under: parents} = rules, opts) do case Scope.current(env.module) do %{name: name} -> if Enum.member?(List.wrap(parents), name) do do_recordable!(env, usage, Map.delete(rules, :under), opts) else raise Absinthe.Schema.Notation.Error, only_within(usage, parents, opts) end _ -> raise Absinthe.Schema.Notation.Error, only_within(usage, parents, opts) end end defp do_recordable!(env, usage, %{toplevel: true} = rules, opts) do case Scope.current(env.module) do nil -> do_recordable!(env, usage, Map.delete(rules, :toplevel), opts) _ -> ref = opts[:as] || "`#{usage}`" raise Absinthe.Schema.Notation.Error, "Invalid schema notation: #{ref} must only be used toplevel" end end defp do_recordable!(env, usage, %{toplevel: false} = rules, opts) do case Scope.current(env.module) do nil -> ref = opts[:as] || "`#{usage}`" raise Absinthe.Schema.Notation.Error, "Invalid schema notation: #{ref} must not be used toplevel" _ -> do_recordable!(env, usage, Map.delete(rules, :toplevel), opts) end end defp do_recordable!(env, usage, %{private_lookup: address} = rules, opts) when is_list(address) do case get_in_private(env.module, address) do nil -> ref = opts[:as] || "`#{usage}`" message = "Invalid schema notation: #{ref} failed a private value lookup for `#{address |> List.last}'" raise Absinthe.Schema.Notation.Error, message _ -> do_recordable!(env, usage, Map.delete(rules, :private_lookup), opts) end end defp do_recordable!(env, _, rules, _) when map_size(rules) == 0 do env end @doc false # Get the placement information for a macro @spec placement(atom) :: Keyword.t def placement(usage) do Keyword.get(@placement, usage, []) end # The error message when a macro can only be used within a certain set of # parent scopes. defp only_within(usage, parents, opts) do ref = opts[:as] || "`#{usage}`" parts = List.wrap(parents) |> Enum.map(&"`#{&1}`") |> Enum.join(", ") "Invalid schema notation: #{ref} must only be used within #{parts}" end end