biff.core defines the interfaces and code that connect all the other Biff libs.
Mostly biff.core is a lightweight "system composition" tool that overlaps with Component/Integrant/Mount. It defines some patterns for separating your codebase into small independent chunks, and it has code to turn those chunks into a running system.
biff.core also contains:
-
Malli-based validation helper functions.
-
A few custom data types/function specifications: anything that multiple Biff libs need to know about without either of those libs "owning" the type (e.g. so that you can swap out the libraries).
com.biffweb/core {:mvn/version "2.0.0-rc26"}This library will be a release candidate until all the other Biff 2 libraries have been released. Until then there could be breaking changes, but I don't anticipate any.
Try out the demo app:
$ git clone https://github.com/jacobobryant/biff
$ cd biff
$ git checkout v2.x
$ cd libs/core/
$ clj -A:demo
user=> (require 'com.example)
nil
user=> (in-ns 'com.example)
#object[clojure.lang.Namespace 0x318a81a0 "com.example"]
com.example=> (start)
[main] INFO com.biffweb.core.impl - starting: com.example.lib.config$use_config
[main] INFO com.biffweb.core.impl - starting: com.example.lib.ring$use_webserver
[main] INFO com.example.lib.ring - Web server started on http://localhost:8080
biff.core's system composition code is designed to be:
-
easy to understand (the implementation is ~100 lines of code and is built with plain functions and maps)
-
convenient for pulling functionality out into external libraries (so that other Biff libraries can provide things like web server initialization, authentication flows, etc)
-
repl-friendly: restarting stateful resources shouldn't need to be a regular part of your workflow.
Biff models your application state and configuration via a single "system map" which typically has flat, namespaced keys. On startup, the system map starts empty and then is built up by your modules.
Each module is a map that contains a chunk of application functionality. For example, each page in your web application can have a module containing the HTTP routes for that page.
(def module
{:com.example/routes [""
["/" {:get landing-page}]
...]})Typically you have one module per namespace, then a modules.clj file
aggregates all the modules into a vector.
(def modules
[landing-page/module
settings/module
...])Modules can include a :biff.core/init lifecycle function. Init functions run
at system startup in an unspecified order, before any start functions run. Each
init function takes the entire modules vector and returns a map. The maps from
all the init functions are merged together into an initial system map.
You can also set :biff.core/init to a map instead of a function.
So modules can both define a chunk of application functionality, and they can also aggregate those chunks from other modules into the system map.
(def module
{:biff.core/init
(fn [modules-var]
(let [all-routes (keep :com.example/routes @modules-var)
handler (make-handler all-routes)]
{:com.example/handler handler}))})The init functions actually receive the modules var (#'modules) rather
than receiving the modules value directly. This allows init functions to do
late-binding. For example, the :com.example/handler function could be a
wrapper that rebuilds an underlying handler function whenever it detects that
the modules have changed. See the demo app for
an example.
Modules can define a :biff.core/start function (and a :biff.core/stop
function if needed). Start functions take a system map, start stateful resources
or do other initialization, then return an updated system map. Stop functions
receive the map returned by their associated start function, and they shut down
any stateful resources as needed.
(def module
{:biff.core/id :com.example/webserver
:biff.core/start
(fn [{:com.example/keys [handler port]
:or {port 8080}
:as ctx}]
(assoc ctx ::server
(jetty/run-jetty handler
{:host "localhost" :port port :join? false})))
:biff.core/stop
(fn [{::keys [server]}]
(.stop server))})You specify the module start order by providing a vector of module IDs
(the :biff.core/id value).
(def start-order
[:com.example/config
:com.example/database
:com.example/webserver])The com.biffweb.core/start function takes your modules and their start order
and starts your application, returning the final system map. To stop the
application, pass the system map to com.biffweb.core/stop.
(defonce system (atom {}))
(reset! system (biff.core/start #'modules start-order))
(biff.core/stop @system)You'll typically wrap these calls in your own start, stop, and refresh
functions for repl-driven development.
If you want to override any values set by an init function or add additional
keys to the system map, you can pass an additional initial-system argument:
(def initial-system {...}
(biff.core/start initial-system #'modules start-order)biff.core maintains a global Malli schema registry, to which you can add schema
via com.biffweb.core/register. The com.biffweb.core/validate macro takes a
map and checks any keys with registered schemas to ensure their values match the
schemas.
=> (biff.core/register {:person/age :int})
=> (biff.core/validate {:person/age "three"})
Execution error (AssertionError) at com.biffweb.core.impl/assertion-error (impl.clj:20).
`:person/age "three"` is invalid: should be an integerstart calls validate on modules and on the system map. You can also call
validate wherever else it makes sense to do schema validation. validate
calls compile to no-ops when *assert* is false.
Biff libs call register for all the keys they own (e.g. biff.core registers
a handful of :biff.core/* keys), and it's recommended to register any keys
that your application defines (e.g. config keys).
The system map is meant to contain all your app's configuration, including
secrets. To prevent secrets from accidentally being exposed (e.g. in logs), Biff
libs expect secrets to be wrapped with the com.biffweb.core/secret-delay
function:
(def my-api-key (secret-delay "my-api-key"))
(str my-api-key)
=> "#<SecretDelay: redacted>"
(force my-api-key)
=> "my-api-key"-
Application namespaces that contain modules shouldn't depend on each other; they should instead expose all their functionality via a single
modulemap. Shared functions should be kept elsewhere, e.g. inside alib/folder. -
Application config should be read in by the first module in the start order and inserted into the system map. Other parts of the codebase should always get their config from the system map instead of e.g. reading env vars directly. Config schema should be registered. Config secrets should be wrapped with
biff.core/secret-delayso that they aren't accidentally serialized. Prefer flat, namespaced keys over nested config. -
Although the system map is typically stored in a global atom, that's only meant to be used via the repl. Application code should always receive the system map as a parameter instead of accessing the global atom. Try to keep the system map at the edges of your code rather than passing it deeply down the call stack (functional core, imperative shell). biff.fx and biff.graph help with that.