I a big fan of the datafy
and nav
protocols that were introduced to clojure in 1.10. There are, as far as I can tell, few examples of its usage in the wild. There is Sean Corfield’s excellent blog post (which you should read as an introduction) describing its incorporation into the next.jdbc
library, but not as much more that I can find out there and this is a problem. To help remedy that problem I’d like to submit an example from my work in the healthcare industry.
HL7, an international standards organization, has defined a data standard/protocol for the health care domain that goes by the acronym FHIR (pronounced “fire”). The spec describes something called a FHIR Server which is able to serve health related data via a RESTful API for suitable clients. I have written such a client as a clojure library I call clj-fhir
which I will be demonstrating here.
The library will be aliased to fhir
(require '[clj-fhir.core :as fhir])
FHIR Introduction
The FHIR specification divides the health care domain into a set of about 150 “Resources” (e.g. Patient, Medication, Procedure, Encounter, etc.). In clj-fhir
, a particular Resource is a data structure (read map) and is accessed as follows:
(def pt (fhir/get-resource-by-id :Patient 5100))
Each Resource has three parameters that uniquely identify it:
- The Resource id (
5100
here) - The Resource type (
Patient
from the example) - The base server url (in
clj-fhir
this is defined as a dynamicvar
named*fhir-server*
).
The three parameters can be combined to create a Universal Resource Identifier (URI) which is also a URL. So, say we bind *fhir-server*
to https://fhir.example.org/
, the URI for our example patient would be
https://fhir.example.org/Patient/5100
If you do an HTTP GET of this kind of URL, a Resource Representation is returned in one of a few formats depending on the Accept
header. You can see then that the fhir/get-resource-by-id
function is little more than a straight HTTP call with some ceremony around error handling and some authentication/authorization like an OAuth token exchange.
Let’s take a look at the structure of pt
. There are multiple representations of a Resource (JSON, XML) but clj-fhir
turns it into a nice clojure map.
{:meta
{:versionId "2",
:lastUpdated "2022-10-30T00:35:11.171+00:00",
:source "#tGZn52d3eKYGyWVS"},
:managingOrganization {:reference "Organization/5120"},
:name [{:use "official", :family "Vandervort", :given ["Dennis"]}],
:birthDate "1992-08-01",
:resourceType "Patient",
:active true,
:id "5100",
:telecom [{:system "phone", :value "034-286-1479"}],
:gender "male",
:text
{:status "generated",
:div
"<div xmlns=\"http://www.w3.org/1999/xhtml\"><div class=\"hapiHeaderText\">Dennis <b>VANDERVORT </b></div><table class=\"hapiPropertyTable\"><tbody><tr><td>Date of birth</td><td><span>01 August 1992</span></td></tr></tbody></table></div>"}}
This is a Patient Resource. If you look at the spec doc in that link you’ll see that each Resource has a list of attributes. Each attribute has a type and a cardinality. For example the active
attribute has a type of boolean and a cardinality of 0..1
, meaning it can either be true
or false
, and it is optional with only one value allowed.
There is one attribute I want to highlight here. Take a look at managingOrganization
. The value is a Reference type, which is an example of what REST calls a hypermedia control. Specifically, it is typed as a reference to an Organization Resource. This is how FHIR Resources are associated. In our example the reference value is Organization/5120
. This is a relative URL so it should be trivial to create a function that de-references Reference types. In fact clj-fhir
has such a function:
(defn follow-reference
"Given a resource attribute containing a reference this will return
that referenced resource. A relative reference will be relative to *fhir-server*."
[attribute]
(if (:reference attribute)
(if-let [ref-uri (try (URI. (:reference attribute)) ;java.net.URI
(catch Exception e nil))]
(if (.isAbsolute ref-uri)
(get-resource-by-id (str ref-uri))
(get-resource-by-id (str *fhir-server* "/" (str ref-uri)))))))
Note that in addition to the 2 parameter arity shown in my original call above, get-resource-by-id
has a single parameter arity that takes a stringified URI. This is the form used in follow-reference
. Also, the reference
attribute can be an absolute URI so we do need to account for that possibility.
Navigating the Resource Graph
The collection of Resources in the FHIR data model is chock-full of these Reference types. Just a few examples:
- A
Patient
has anOrganization
reference (as we just saw) - An
Encounter
has references toPatient
,EpisodeOfCare
,Practitioner
,Appointment
,Condition
, etc. resources. - An
Observation
has a reference to aPatient
and anEncounter
.
and so on. In fact the specification for each Resource has a section that lists the references the Resource has and the Resources that reference it. Taken together it defines a huge graph of connected Resources.
It occurred to me after clojure 1.10 came out that with all this linkage, it might be useful to wire this FHIR graph up with the clojure Datafiable
and Navigable
protocols. The follow-reference
function above already does most of the work for me.
So I did it! Here is a function that “datafys” (datafies?) a FHIR Resource.
(defn datafy-resource
"Given a FHIR resource it adds a datafy implementation that
provides reference navigation."
[resource]
(let [existing-metadata (meta resource)
datafied-metadata
(assoc existing-metadata
`p/datafy
(fn [r]
(with-meta r
{`p/nav (fn [_ k v]
(cond
(sequential? v) (map #(if-let [dref (follow-reference %)] dref %) v)
(:reference v) (follow-reference v)
:else v))})))]
(with-meta resource datafied-metadata)))
There’s a fair amount going on here so let’s add a few notes. Here’s what’s happening in this function.
- First we grab whatever metadata is attached to the input
resource
. The result of calls toget-resource-by-id
and other functions that return Resources does contain metadata so we need to preserve that. - Next we
assoc
to that metadata a key/value pair. The key isp/datafy
wherep
is the alias forclojure.core.protocols
. The value is a function ofr
which represents theresource
map input parameter. - The function of
r
we are creating attaches another metadata key/value pair to the Resource (r
in the function). The Resource is a standard clojure map and we plan to just operate on reference links so we don’t need to “datafy” the resource any further. - The key/value pair added to define the navigation has a key of
p/nav
. The value is another function with three parameters. The first is the resource itself which we won’t be using so we specify it as'_'
per clojure conventions. Thek
andv
parameters represent each of the keys and values in the resource. - The function body is a
cond
. We first check to see if the value is an array. Some Resource attributes take an array of references so we need to apply navigation to all the elements. - For attributes with arrays of reference types we
map
anif-let
function callingfollow-reference
. That function returnsnil
when the input attributes is not a reference type so in that case we return the value unchanged. If it is notnil
it returns the de-referenced value by returning the result offollow-reference
. - For non-array attribute values (i.e. if
(:reference v)
is notnil
), we return the result of a call tofollow-reference
. - In all other cases we just return the attribute value.
The upshot of all this is that whenever a Resource is returned by the library, any attributes which contain reference types get enhanced with the ability to de-reference those values via a call to nav
.
I think it’s worth pausing for a bit to consider this. The FHIR specification has this idea of Resources that reference other Resources comprising a Health Domain Graph. This idea is abstracted into a type called Reference
. With a single clojure function, we have given this idea life by actually enabling navigation of the Health Domain Graph. This isn’t some bolted on feature either, it’s actually built into the core functionality of clojure via a couple of protocols. I think that’s pretty cool!
How Does This Look?
To demonstrate navigation, I prepared a little screen share session. It uses Cognitect’s REBL data browser which is able to apply nav
to datafied things.
In the video I am browsing an Observation Resource which has a subject
attribute referencing a Patient and a performer
attribute which can reference a few different Resources. Each of the referenced Resources have references of their own so I go nav
-crazy.
If you watched that video you’ll note I added a bonus navigation I didn’t talk about here. Each Resource maintains its own revision history (See the meta.versionId
attribute in my pt
var above). Getting a Resource with get-resource-by-id
normally returns the most recent version but all the others are available, so I also made that revision history navigable. Perhaps I can show how I did that in another post.
I hope this was useful and will give you the inspiration to use these protocols if you can.