hook v0.4.0 Hook behaviour

An interface to define and leverage runtime resolution.

In various module and function docs it is common to document options like so:

Options

  • :option1_name - type. # since no default is specified, this option is required
  • :option2_name - type. Descriptive text. # since no default is specified, this option is required
  • :option3_name - type. default. # since a default is specified, this option is not required
  • :option4_name - type. default. Descriptive text. # since a default is specified, this option is not required

Compile time Configuration

These options can be set compile time to configure how m:Hook.hook/1 behaves. m:Hook.hook/1 will be referred to as "the macro".

  • mappings - keyword(). Mappings that the macro will resolve against.
  • resolve_at - :compile_time | :run_time | :never. When :compile_time or :run_time, the macro will resolve its term at the respective time. When :never, the macro will compile directly into its term with no attempt to resolve against mappings.
  • top_level_module_allowlist - [module()]. When the macro's calling module's top-level-module is in this list, the :resolve_at option will be respected. Otherwise, it will behave as if resolve_at: :never for that instance of the macro. This allows developers to control whether or not individual instances of the macro should ever attempt resolution or not with a top-level-module granularity. The common value for this option is the top level module of your application so that any instance of the macro throughout your application's code will respect the :resolve_at option, and any instance of the macro outside your application's code will behave as if resolve_at: :never. An example of "code outside your application" is if any of your application's dependencies happen to also use the macro.

Groups

Mappings are defined under group/0s. Hook defaults the group to be the calling process making process isolation of mappings frictionless. Any term can also be supplied as the group. Putting :foo under :bar in the :red group, than fetching it from the :blue group will fail.

Resolution

Starting with the calling process:

  1. does the process define a mapping for the term

    1.1. the term has been resolved

  2. does the process define fallbacks

    2.1. for each fallback in order: goto step 1

  3. does the process have :"$ancestors"

    3.1. for each ancestor in order: goto step 1

  4. does the process have :"$callers"

    3.1. for each caller in order: goto step 1

  5. the term has not been resolved

Performance

In general, the thing to focus on with respect to performance is whether the function call is getting serialized through a GenServer or not. You will find that only calls that result in a write get serialized and those that only read do not.

Functions that get serialized through a GenServer:

Functions that do not get serialized through a GenServer:

This means that a "hot path" that is resolving a term via these functions should remain performant outside of extreme cases.

Link to this section Summary

Functions

Asserts that all callbacks defined via Hook.callback/4 have been satisfied.

Defines a callback that can be consumed and asserted on.

Return group's callbacks.

Prepend group to the calling process' fallbacks.

Fetches the value for key for group.

Gets the value for key for group returning default if it is not defined.

Gets all values for group.

A macro that compiles into term itself or a Hook.fetch(term) call.

Puts value under key for group.

Puts all key value pairs under group.

Resolves a callback for the calling process and executes it with args.

Link to this section Types

Link to this type

callback_opt()

callback_opt() :: {:group, group()} | {:count, count()}
Link to this type

callback_opts()

callback_opts() :: [callback_opt()]
Link to this type

config()

config() :: %{hook?: boolean() | (any() -> boolean())}
Link to this type

count()

count() :: pos_integer() | :infinity
Link to this type

fun_key()

fun_key() :: {function_name :: atom(), arity :: non_neg_integer()}
Link to this type

group()

group() :: pid() | atom()
Link to this type

key()

key() :: any()
Link to this type

mappings()

mappings() :: [{key(), value()} | {key(), value(), group()}]
Link to this type

value()

value() :: any()

Link to this section Functions

Link to this function

assert(group \\ self())

Asserts that all callbacks defined via Hook.callback/4 have been satisfied.

Returns :ok if the assertion holds, raises otherwise.

Infinity-callbacks and 0-callbacks are inherently satisfied.

Link to this function

callback(module, function_name, function, opts \\ [])

Defines a callback that can be consumed and asserted on.

Note: module will be defined under the calling process.

This function will raise when the specified callback is not a public function on module.

Options

:count - How many times the callback can be consumed.

  • :infinity - The callback can be consumed an infinite number of times. For a module, function, and arity, only a single infinity-callback will be defined at once, last write wins. Infinity-callbacks are always consumed after non-infinity-callbacks.
  • 0 - The callback should never be consumed. Raises an error if it is. The callback is removed upon raising.
Link to this function

callbacks(group \\ self())

Return group's callbacks.

Link to this function

fallback(src_group \\ self(), dest_group)

Prepend group to the calling process' fallbacks.

NOTE: :global cannot be used as a src_group.

Link to this function

fetch(key, group \\ self())

Fetches the value for key for group.

Link to this function

fetch!(key, group \\ self())

See Hook.fetch/2.

Link to this function

get(key, default \\ nil, group \\ self())

Gets the value for key for group returning default if it is not defined.

Link to this function

get_all(group \\ self())

Gets all values for group.

Link to this macro

hook(term)

(macro)

A macro that compiles into term itself or a Hook.fetch(term) call.

Check the "Compile time Configuration" section for information about configuring this functionality.

Link to this function

put(key, value, group \\ self())

Puts value under key for group.

Link to this function

put_all(kvps, group \\ self())

Puts all key value pairs under group.

Link to this function

resolve_callback(module, fun_key, args)

Resolves a callback for the calling process and executes it with args.

Link to this section Callbacks

Link to this callback

assert(group)

assert(group()) :: :ok
Link to this callback

callback(module, function_name, function, callback_opts)

callback(module(), function_name :: atom(), (... -> any()), callback_opts()) ::
  :ok
Link to this callback

callbacks(group)

callbacks(group()) :: %{
  resolved: [],
  unresolved: [{count(), pid(), function()}]
}
Link to this callback

fallback(dest, src)

fallback(dest :: group(), src :: group()) ::
  :ok | {:error, {:invalid_group, :destination | :source}}
Link to this callback

fetch(key, group)

fetch(key(), group()) :: {:ok, any()} | :error
Link to this callback

fetch!(any, group)

fetch!(any(), group()) :: any()
Link to this callback

get(key, default, group)

get(key(), default :: any(), group()) :: {:ok, any()} | :error
Link to this callback

get_all(group)

get_all(group()) :: {:ok, %{}} | :error
Link to this callback

put(key, value, group)

put(key(), value(), group()) :: :ok
Link to this callback

put_all(mappings)

put_all(mappings()) :: :ok
Link to this callback

put_all(mappings, group)

put_all(mappings(), group()) :: :ok
Link to this callback

resolve_callback(module, fun_key, args)

resolve_callback(module(), fun_key(), args :: [any()]) :: any()