defmodule Driver8 do @moduledoc """ This module minimizes the amount of work one needs to do to create a WebDriver extension. Typical usecase would look something like this: defmodule Driver8.Http do @moduledoc false use GenServer require Logger require Driver8.Session import Driver8.WireProtocol def start_link(_opts) do GenServer.start_link(__MODULE__, [], name: MyAwesomeExtension) end @impl true def init(_opts) do :ok = Driver8.Session.extend("my-awesome-extension", :get) {:ok, []} end @impl true def handle_call({:post, ["my-awesome-extension", "get"], session_id, parameters}, _from, state) do response = ... # prepare you response here {:reply, {:ok, response}, state} end @impl true def handle_call(request, _from, state) do {:reply, GenServer.call(Driver8, request), state} end end Important steps to create your extension are: # Create a GenServer # Register it as driver level extension (`Driver8.extend`) or session level extension (`Driver8.Session.extend`) # Define implementation of `handle_call` for your endpoints # Define catch all method and pass the message to `Driver8` Last step is optional (presumably you are in control of the client code that calls your extension), but it will give you a nice error message on the client end instead of killing your GenServer. """ use GenServer @version "0.1.4" @doc """ Starts a `Driver8` process linked to the current process (to be used as a part of supervision tree). Once process is started it will register itself with default name `Driver8`. This name is used by `Driver8.Plug` to call for request handling. ## Options * :name - value used for name registration, if not passed defaults to `Driver8` """ def start_link(opts) do name = Keyword.get(opts, :name, Driver8) GenServer.start_link(__MODULE__, [prefix: name], name: name) end @doc """ Returns the status of the driver to be used in the `/status` endpoint. """ def is_ready(driver_name \\ Driver8) do GenServer.call(driver_name, :is_ready) end @doc """ Registers the calling process as an **driver level** extension, responsible for handling requests on a given path. Methods can be an atom like `:get` or a list of atoms. Defaults to `[:get, :post, :put, :delete, :head, :patch]`. You can pass a different name to be used to find Driver8, defaults to `Driver8`. ## Examples :ok = Driver8.extend("my-extension") :ok = Driver8.extend("my-extension", [:get, :post]) :ok = Driver8.extend("my-extension", [:get, :post], Driver8.IKnowWhatIamDoing) After extension is registered with any of the examples call [http://localhost:8085/my-extension]() will be translated to the message to the GenServer you have registered as extension. You are expected to have a method similar to @impl true def handle_call({:get, ["my-extension"], _parameters}, _from, state) do {:reply, {:ok, {200, %{"result" => "Everything went fine."}}}, state} end or @impl true def handle_call({:get, ["my-extension"], _parameters}, _from, state) do {:reply, {:ok, {401, "Go away!"}}, state} end First case will result in json response, second in text response. """ def extend(path, methods \\ [:get, :post, :put, :delete, :head, :patch], driver_name \\ Driver8) do methods = cond do is_atom(methods) -> [methods] is_list(methods) -> methods true -> raise "methods can be either an atom (:get) or a list ([:get, :post])" end {:ok, extensions_registry} = GenServer.call(driver_name, :extensions) {:ok, _} = Registry.register( extensions_registry, path, Enum.map(methods, fn x -> Atom.to_string(x) |> String.upcase() end) ) :ok end @doc """ In case you decide to handle routes yourself this method allows you to dispatch a call to a driver level extension. Method is expected be a Plug.Conn method (so "GET", "POST", etc.) """ def call_extension(method, path, params, driver_name \\ Driver8) do GenServer.call(driver_name, {method, path, params}) end @doc """ A Utility method that will return you a supervisor that is used to register sessions and registry where all the sessions are registered """ def supervisor_and_registry(driver_name \\ Driver8) do GenServer.call(driver_name, :supervisor_and_registry) end @doc """ Returns current version of Driver8 """ def version() do @version end @impl true def init(opts) do {:ok, supervisor_pid} = DynamicSupervisor.start_link(strategy: :one_for_one) prefix = Keyword.get(opts, :prefix, Driver8) registry = String.to_atom("#{prefix}.Registry") {:ok, _} = Registry.start_link(keys: :unique, name: registry) extensions_registry = String.to_atom("#{prefix}.Extension.Registry") {:ok, _} = Registry.start_link(keys: :unique, name: extensions_registry) {:ok, [supervisor: supervisor_pid, registry: registry, extensions: extensions_registry]} end @impl true def handle_call(:supervisor_and_registry, _from, state) do {:reply, {:ok, state[:supervisor], state[:registry]}, state} end @impl true def handle_call(:extensions, _from, state) do {:reply, {:ok, state[:extensions]}, state} end @impl true def handle_call(:is_ready, _from, state) do {:reply, :ready, state} end @impl true def handle_call({:remove_session, session_id}, _from, state) do {:reply, Driver8.Session.remove(state[:registry], session_id), state} end @impl true def handle_call({:find_session, session_id}, _from, state) do {:reply, Driver8.Session.find(state[:registry], session_id), state} end @impl true def handle_call(request, _from, state) do case request do {method, path, _opts} -> case Registry.lookup(state[:extensions], Enum.at(path, 0)) do [{pid, methods}] -> if method in methods do {:reply, GenServer.call( pid, method_to_atom(request) ), state} else {:reply, {:error, "Not found", request}, state} end [] -> {:reply, {:error, "Not found", request}, state} end {method, path, _session_id, _parameters} -> {:reply, {:error, "Driver8: There is no extension registered to handle method: '#{method}' on path: #{ inspect(path) }"}, state} _ -> {:reply, {:error, "Driver8: Cannot process session request #{inspect(request)}"}, state} end end defp method_to_atom(request) do request |> Tuple.insert_at( 0, elem(request, 0) |> String.downcase() |> String.to_existing_atom() ) |> Tuple.delete_at(1) end end