Packages
Driver8: WebDriver control interface skeleton. Sessions, plugs and unicorns
Current section
Files
Jump to
Current section
Files
lib/driver8.ex
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.5-dev.1"
@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