Clojure Case Study: Using datafy and nav protocols to enhance a FHIR Library

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:

  1. The Resource id (5100 here)
  2. The Resource type (Patient from the example)
  3. The base server url (in clj-fhir this is defined as a dynamic var 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 an Organization reference (as we just saw)
  • An Encounter has references to Patient, EpisodeOfCare, Practitioner, Appointment, Condition, etc. resources.
  • An Observation has a reference to a Patient and an Encounter.

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 to get-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 is p/datafy where p is the alias for clojure.core.protocols. The value is a function of r which represents the resource 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. The k and v 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 an if-let function calling follow-reference. That function returns nil when the input attributes is not a reference type so in that case we return the value unchanged. If it is not nil it returns the de-referenced value by returning the result of follow-reference.
  • For non-array attribute values (i.e. if (:reference v) is not nil), we return the result of a call to follow-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. :crazy_face:

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.

Hi @stand, I’m working with FHIR resources and would be interested in trying out your clj-fhir + nav library. Please let me know if this is possible. Thanks!

Would this be for commercial use? I haven’t open sourced the library though I have vague plans of doing so eventually.

No. It would only be used by myself for exploring FHIR data. I’m fine with alpha/beta level code for this purpose and would provide feedback.

Sounds good! Let me clean it up a bit and I’ll put it out there. Might have to wait til the weekend though.

1 Like