Fast adapters with clear syntax and build-in safety.
- Fast (as fast as hardcoded
defdelegate
s) - Easy (define a behaviour and it takes care of everything else)
- Safe (will error if the implementation does not match the behaviour)
- Clean (clearly separated marked behaviour/delegate versus functions)
- Flexible (change implementation/adapter at runtime)
In addition to these basic qualities it is:
- Compatible and tested with releases (
distillery
,mix release
) - Documentation compatible (each stub copies the documentation of the
@callback
) - Spec / Dialyzer (each stub has a spec matching the
@callback
) - IDE (intelligent code completion / intellisense) compatible [stubs]
def deps do
[
{:adapter, "~> 1.0"}
]
end
defmodule SessionRepo do
use Adapter
# Define the adapter behavior
behaviour do
@doc ~S"""
Lookup a sessions based on token.
"""
@callback get(token :: binary) :: {:ok, Session.t | nil} | {:error, atom}
end
# Add module functions outside the behaviour definition
# These can use the behaviour's callbacks like they exist as functions.
@spec get!(binary) :: Session.t | nil | no_return
def get!(token) do
case get(token) do
{:ok, result} -> result
{:error, reason} -> raise "SessionRepo: #{reason}"
end
end
end
# PostgreSQL implementation
defmodule SessionRepo.PostgreSQL do
@behaviour SessionRepo
@impl SessionRepo
def get(token), do: ...
end
# Redis implementation
defmodule SessionRepo.Redis do
@behaviour SessionRepo
@impl SessionRepo
def get(token), do: ...
end
# Now configure
SessionRepo.configure(SessionRepo.PostgreSQL)
# Runtime switching possible
SessionRepo.configure(SessionRepo.Redis)
Adapters come with the following configuration options:
-
:default
(none), a default implementation to link to at first compile. -
:error
(:module_name_not_configured
), an atom that is returned in an error tuple when the adapter has not been configured. Can be set to:raise
to raise instead of returning an error tuple. -
:log
(:info
), the log level of the configuration message. The following levels are allowed::debug
,:info
, and:notice
. To disable logging of configuration setlog: false
. -
:mode
(:compile
), determines the implementation type of the adapter pattern. The following modes are supported::compile
, the stubs are replaced each time by recompiling the module. This gives hardcoded performance while still allowing changes of adapter at runtime.:compile_env
, the macro hardcodes the adapter at compile time. This works by usingApplication.compile_env
. (orApplication.get_env
below Elixir 1.11.) It mirrors the standard adapter pattern using module attributes and defdelegates. It is fast, but requires the adapter to be set at compile and can no longer be changed at runtime like startup.:get_env
, the macro generates aApplication.get_env
pattern. Looking up the set adapter for each call, allowing for easy runtime switching. This is slower in use than a:compile
and:compile_env
, but the fastest to re-configure and simpler than:compile
when it comes to the underlying mechanic.
-
:random
(true
), wraps the default implementation in anEnum.random([...])
to trickdialyzer
. Dialyzer might error out, because it detects the hardcoded [error] values before the adapter is configured. To avoid this causing issues the hardcoded value is wrapped in random. This forces dialyzer to respect the spec instead of the implementation. -
:validate
(true
), whether to perform configuration validation. This will verify a given implementation actually implements the complete behaviour. It will error out and refuse to configure if there are functions missing or have a wrong arity. Setting this tofalse
will skip validation, making configuration slightly faster and allow setting incomplete implementations.
When using :compile_env
or :get_env
the implementation will default to using
:adapter
as app and the module name as key when doing configuration lookups.
To define a custom config location pass app: :my_app, key: :my_repo
.
First define the module that can use different adapters.
The behavior of the adapter is defined with @callback
like normal,
but this time wrapped in a behavio[u]r
macro.
defmodule SessionRepo do
use Adapter
behaviour do
@doc ~S"""
Lookup a sessions based on token.
"""
@callback get(token :: binary) :: {:ok, Session.t | nil} | {:error, atom}
end
@spec get!(binary) :: Session.t | nil | no_return
def get!(token) do
case get(token) do
{:ok, result} -> result
{:error, reason} -> raise "SessionRepo: #{reason}"
end
end
end
The functionality is quite simple.
Inside the behavio[u]r
block each @callback
is tracked
and documentation and spec recorded.
After the behavio[u]r
block each recorded callback will generate a stub.
Each stub will be given the recorded documentation and spec.
This allows functions in the module to call the functions
from the defined behavio[u]r.
On configuration either the application config updated to reflect the change
or in :compile
mode the module is purged and recompiled with the new adapter.
- Improve documentation
- Travis builds
Initial release.
MIT License
Copyright (c) 2020 Ian Luites
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.