defmodule Singyeong.Plugin do @moduledoc """ The base plugin behaviour. Your plugin should only have one module implementing this behaviour. Note that this behaviour is explicitly, **intentionally** NOT implemented as a GenServer or similar! You are, of course, free to implement all the callbacks in this module as just `GenServer.call(me, :whatever)`, but this is not going to be explicitly supported by this behaviour. The reasoning for this is that it's significantly easier for runtime checks as to whether a callback is implemented or not if the behaviour isn't implemented as a GenServer-like module. Of course, plugin capabilities exist to help deal with this concern, but I find that it's easier to simply not take that risk and enforce a certain implementation style rather than risking potential runtime crashes due to capabilities being claimed without implementation. """ @type event() :: any() @type undo_state() :: any() @type payload() :: map() @type frame() :: {:text, map()} @type auth_string() :: binary() @type ip() :: binary() @type direction() :: :client | :server @optional_callbacks handle_event: 2, undo: 2, handle_global_event: 3, global_undo: 3, auth: 2 @doc """ Provides information about the plugin. This is cached when the plugin is loaded. """ @callback manifest() :: Singyeong.Plugin.Manifest.t() @doc """ Called when the plugin is first loaded. Returns a list of children to be added to the initial supervision tree. """ @callback load() :: {:ok, [ :supervisor.child_spec() | {module(), term()} | module() | Supervisor.child_spec() ]} | {:ok, []} | {:ok, nil} | {:error, binary()} @doc """ Called when this plugin is supposed to handle an event. The events passed to this function are those specified in `Manifest.events` only. Built-in events will never be passed to this function. Requires the `:custom_events` capability. Plugin event handling behaves in a middleware-type fashion. A plugin can choose to pass execution to the next plugin in the chain via `:next`, halt execution with `:halt`, or return an error. Returning an error immediately halts execution and unwinds as much as possible. Halting will immediately stop processing of the event, and will not return anything to the client, as it is assumed that the halted execution was not meant to return any side-effects to the client. On success, this function returns a tuple starting with either `:next` or `:halt` In non-halt cases, the third return element is the undo state. This is some state that the plugin uses to undo any stateful changes it may have made during the processing of this event, such that any error can result in a rollback of stateful changes. Pure functions may simply return `nil` for the undo state, or just not include it as an element of the tuple. **Note that a `nil` undo state is assumed to mean that this function has *nothing* to undo, and as such the undo function for this module will *never* be called if the event handled by this function requires undo!** That is, **returning an undo state of `nil` means that your undo function will never be called**. The reason for this is simply that **the order in which `handle_event` vs `undo` is called cannot be guaranteed**, and so you cannot rely on strict ordering of these function calls for reliable undo behaviour. """ @callback handle_event(binary(), any()) :: {:next, [frame()], undo_state()} | {:next, frame(), undo_state()} | {:next, [frame()]} | {:next, frame()} | :halt | {:error, binary(), undo_state()} | {:error, binary()} @doc """ Called when this plugin is supposed to undo some stateful changes. Implementations of this function are *expected* to be impure, and may do more or less whatever they want. The `undo_state` parameter may be **any** value, **with the exception that a `nil` undo state will never be passed to this function, for any reason**. """ @callback undo(binary(), undo_state()) :: :ok | {:error, binary()} @doc """ Called when a client identifies with the server. The auth string the client sends, as well as the client's IP address, are passed to this function. Note that **there is no guarantee of IPv4 vs IPv6 address**, and your code should be able to handle both cases. Requires the `:auth` capability. """ @callback auth(auth_string(), ip()) :: :ok | :restricted | {:error, binary()} @doc """ Behaves like `handle_event/2`, with several caveats: - Undo is handled by `global_undo/3`. - The frame returned **should** be the same frame as was passed in, with any necessary modifications. - A list of frames cannot be returned, only a single frame. - This function is called BEFORE the rest of the event processing pipeline. - This function is aware of the "directionality" of an event, that is, this function receives an argument telling it whether the direction of the event is to the `:client` or to the `:server`. This argument is also passed to `global_undo/3`. - **This function is for events only, not all payloads.** Payloads such as identify, heartbeat, ... will not ever be passed to this function. """ @callback handle_global_event(binary(), direction(), payload()) :: {:next, payload(), undo_state()} | {:next, payload()} | :halt | {:error, binary(), undo_state()} | {:error, binary()} @doc """ Behaves like `undo/2`, but for `handle_global_event/3`. See the docs for `handle_global_event/3` for caveats to be aware of. """ @callback global_undo(binary(), direction(), undo_state()) :: :ok | {:error, binary()} end