Blaze is built as a single Docker image, along with a separate frontend image. There is also an uberjar for standalone use.
The most reliable way to build Blaze is through GitHub CI. When you create a pull request (PR), a Docker image with the label pr-<num>
is built. You can use this image after the pipeline completes successfully.
Blaze is written in Clojure, a modern LISP for the JVM.
The latest LTS/stable releases of:
- a Clojure-aware IDE (Emacs, IntelliJ IDEA with Cursive plugin, Vim, VSCodium...)
- Java
- nodejs
- GNU Make
- Clojure, with CLI tools
- clj-kondo
- cljfmt:
clj -Ttools install io.github.weavejester/cljfmt '{:git/tag "
<latest-stable-release>
"}' :as cljfmt
- Create FHIR profiles:
make -C job-ig build
- Create the uberjar in the
target
directory:
make uberjar
- Build the Blaze Docker image:
docker build -t blaze:latest .
- Build the frontend Docker image:
make build-frontend
As for writing code in any LISP in general, the recommended way to hack on Blaze is to use REPL-Driven Development (RDD). This is, to fire up a REPL, connect to it, and evaluate the running system within your IDE as you change it. More information about RDD.
Since Blaze is organized into modules, you can fire up a REPL in either of two ways: from the root directory ("a global REPL") or from the specific module you are currently working on ("a local REPL"). A global REPL is better suited for local end-to-end (E2E) testing, running and exploration - it loads the entire system. In contrast, a local REPL is better suited for focused work on a particular module - it only loads the bare minimum amount of namespaces required to make that module function in isolation. Moreover, local REPLs provide you with a faster feedback loop, since they enable you to eval the module's (unit) tests - something you simply cannot do from a global REPL, since they are not included in its classpath.
You can run a REPL to run Blaze as a system using the following Makefile alias:
make emacs-repl
For more details, see the Makefile
, deps.edn
, and dev/blaze/dev.clj
files.
- add
-Dclojure.server.repl='{:address,\"0.0.0.0\",:port,5555,:accept,clojure.core.server/repl}'
to theJAVA_TOOL_OPTIONS
env var - bind port 5555
- create the remote REPL in your IDE, and connect to it.
Developing a new feature will always include writing the corresponding unit and/or integration tests. Whether you write them upfront or after the fact is up to you. That being said, writing them before/while you actually implement a new feature may make it easier to reason about and assess the feature in the works. Whatever the case, the tests will make it easier to ensure that the new feature is implemented correctly, both at module and system level.
This project uses a CI pipeline, which checks:
- unit tests,
- integration tests, and
- code coverage (which should only increase on each commit).
For more details, see the
.github/
directory.
The configuration of the development system is done with the same environment variables used in the production system. Documentation: Environment Variables.
- Create a release branch named
release-v<version>
, e.g.,release-v0.29.0
. - Update all occurrences of the old version (e.g.,
0.28.0
) to the new version (e.g.,0.29.0
). - Update the
CHANGELOG.md
based on the milestone. - Create a commit with the title
Release v<version>
. - Create a PR from the release branch to
main
. - Merge the PR.
- Create and push a tag named
v<version>
, e.g.,v0.13.1
, onmain
at the merge commit. - Copy the release notes from the
CHANGELOG.md
into the GitHub release.
Follow the Clojure Style Guide, enforced by cljfmt
. For more details, check the cljfmt.edn
file.
Blaze is primarily implemented using pure functions. Pure functions depend only on their arguments and produce an output without side effects. This makes them referentially transparent, meaning their behavior does not change based on when or how often they are called.
Blaze uses anomalies for error handling, instead of exceptions. Anomalies separate the error context from the error itself without interrupting the execution flow. For more information, see the anomaly module.
Components are entities within Blaze and may have state.
An example of a stateful component is the local database node.
Components reside in a namespace with a constructor function called new-<component-name>
.
In production, we use the integrant library to wire all of Blaze components together.
(ns blaze.db.node
(:require
[blaze.module :as m]
[clojure.spec.alpha :as s]
[integrant.core :as ig])
(:import
[java.lang AutoCloseable]))
(defn new-node
"Creates a new local database node."
[dep-a dep-b])
(defmethod m/pre-init-spec :blaze.db/node [_]
(s/keys :req-un [dep-a dep-b]))
(defmethod ig/init-key :blaze.db/node
[_ {:keys [dep-a dep-b]}]
(log/info "Open local database node")
(new-node dep-a dep-b))
(defmethod ig/halt-key! :blaze.db/node
[_ node]
(log/info "Close local database node")
(.close ^AutoCloseable node))
new-node
creates a new local database node with dependenciesdep-a
anddep-b
.halt-key!`` implements
AutoCloseable` to ensure resources are properly released when the node is closed.m/pre-init-spec
provides a spec for the dependency map to ensure correct configuration.ig/init-key
initializes the node and logs a meaningful message at info level.ig/halt-key!
closes the node and releases any held resources.
Every public function should have a spec. Function specs are declared in a namespace with the suffix -spec
appended to the function's namespace. Public module-level function specs reside in the src
folder, while inner-module public function specs reside in the test
folder. This ensures that specs are used in tests but not included in the global classpath, reducing the uberjar and memory footprint.
Avoid using reflection. To enable reflection warnings, add (set! *warn-on-reflection* true)
to each namespace with Java interop.