This repository has been archived on 2023-07-11. You can view files and clone it, but cannot push or open issues or pull requests.
Go to file
2014-08-05 13:42:19 -04:00
src/clj_hl7_fhir update fhir request response return handling to detect if the body has a json (fhir) response (probably an operation outcome resource) and parse+return it when it makes sense to (in conjunction with the Location header following option) 2014-08-01 15:29:53 -04:00
.gitignore initial commit 2014-07-04 09:04:45 -04:00
LICENSE initial commit 2014-07-04 09:04:45 -04:00
project.clj this is the wrong url 2014-07-08 08:30:47 -04:00
README.md some formatting fixes 2014-08-05 13:42:19 -04:00

clj-hl7-fhir

HL7 FHIR JSON client for use in Clojure applications. This is a fairly low-level wrapper over the HL7 FHIR RESTful API and does not include any model classes (or anything of the sort) for the various HL7 resources. FHIR API calls wrapped by this library work with JSON data represented as Clojure EDN data.

The primary goal of clj-hl7-fhir is to make getting data into and out of an HL7 FHIR server as simple as possible, without needing to know much about the RESTful API (the URL conventions working with HTTP status codes, reading back data from paged bundles, encoding search parameters, etc). How you create and/or read the HL7 data and what you do with it is beyond the scope of this library.

Leiningen

[clj-hl7-fhir "0.1"]

TODO

This library is still early along in development, and some important features are missing at the moment:

Usage

Most of the basic RESTful API operations are supported currently.

All of the functions that wrap FHIR API calls are located in clj-hl7-fhir.core:

(use 'clj-hl7-fhir.core)

base-url

All core API functions take a base-url parameter. This is the Service Root URL for which all API calls are made to.

For example, to use UHN's HAPI FHIR test server:

(def server-url "http://fhirtest.uhn.ca/base")

read / vread

There are a couple options for reading single resources by ID.

get-resource takes the resource type and ID and returns a FHIR resource. Alternatively, you can specify a relative resource URL instead of separate type and ID arguments. You can also optionally include a specific version number to retrieve.

get-resource-bundle works similarly to get-resource, except that it returns a FHIR bundle instead of a resource.

Examples
; reading a single resource by ID
(get-resource server-url :patient 37)
=> {:address
    [{:use "home"
      :line ["10 Duxon Street"]
      :city "VICTORIA"
      :state "BC"
      :zip "V8N 1Y4"
      :country "Can"}]
    :managingOrganization {:resource "Organization/1.3.6.1.4.1.12201"}
    :name [{:family ["Duck"] 
            :given ["Donald"]}]
    :birthDate "1980-06-01T00:00:00"
    :resourceType "Patient"
    :identifier
    [{:use "official"
      :label "UHN MRN 7000135"
      :system "urn:oid:2.16.840.1.113883.3.239.18.148"
      :value "7000135"
      :assigner {:resource "Organization/1.3.6.1.4.1.12201"}}]
    :telecom
    [{:system "phone" :use "home"}
     {:system "phone" :use "work"}
     {:system "phone" :use "mobile"}
     {:system "email" :use "home"}]
    :gender
    {:coding
     [{:system "http://hl7.org/fhir/v3/AdministrativeGender"
       :code "M"}]}
    :text
    {:status "generated"
     :div
     "<div><div class=\"hapiHeaderText\"> Donald <b>DUCK </b></div><table class=\"hapiPropertyTable\"><tbody><tr><td>Identifier</td><td>UHN MRN 7000135</td></tr><tr><td>Address</td><td><span>10 Duxon Street </span><br/><span>VICTORIA </span><span>BC </span><span>Can </span></td></tr><tr><td>Date of birth</td><td><span>01 June 1980</span></td></tr></tbody></table></div>"}}
     
; trying to read a non-existant resource
(get-resource server-url :patient 9001)
=> nil

; reading a specific version of a resource
(get-resource server-url :patient 1654 :version 3)
=> { 
    ; ... similar to the above example resource return value ... 
    }

; trying to read an invalid resource
(get-resource server-url :foobar 42)
ExceptionInfo FHIR request failed: HTTP 400

Searching for resources is performed via search. It returns a FHIR bundle containing all the resources that matched the search parameters given. If you provide no search parameters then all resources of the type given will be returned (though they will be paged likely, as per the FHIR specs).

Search parameters are specified as a vector, where each parameter should be defined using the helper functions:

Helper function Description and usage example
eq Equals
(eq :name "smith")
lt Less then
(lt :value 10)
lte Less then or equal to
(lte :date "2013-08-15")
gt Greater then
(gt :value 10)
gte Greater then or equal to
(gte :date "2013-08-15")
between Between
(between :date "2013-01-01" "2013-12-31")

Note that you can also use a plain old string for parameter names instead of keywords if you wish

If a parameter value needs to include a namespace, you can use the namespace helper to help properly encode this information in the search parameters:

(eq :gender (namespaced "http://hl7.org/fhir/v3/AdministrativeGender" "M"))

There are also a few helper functions in clj-hl7-fhir.util for converting java.util.Date objects into properly formatted ISO date/time strings that match FHIR specifications:

Function Example output
->timestamp 2014-08-05T10:49:37-04:00
->local-timestamp 2014-08-05T10:49:37-04:00
->date 2014-08-05

As mentioned above, search results will be returned in a FHIR bundle, which contains a vector of all the matching resources. For convenience, you can use collect-resources to return a sequence of just the resources by passing/threading the results from search into this function.

(collect-resources
  (search server-url ...)

Larger search results will be paged. Some helper functions are available to make working with paged search results easier:

  • fetch-next-page takes a search result bundle and uses it to get and return the next page of search results. If there are no more pages of results, returns nil.
  • fetch-all takes a search result bundle, and fetches all pages of search results, and then returns a bundle which contains the full list of match resources.
  • search-and-fetch convenience function that is the same as doing: (fetch-all (search ...)). Takes the same arguments as search.
Examples
; list all patients
; (http://server-url/Patient)
(search server-url :patient [])

; find all patients with name "dogie"
; (http://server-url/Patient?name=dogie)
(search server-url :patient [(eq :name "dogie")])

; find all female patients
; (http://server-url/Patient?gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fv3%2FAdministrativeGender%7CF)
(search server-url :patient [(eq :gender (namespaced "http://hl7.org/fhir/v3/AdministrativeGender" "F"))])

; also works (depending on the exact data and server spec compliance)
; (http://server-url/Patient?gender=F)
(search server-url :patient [(eq :gender "F")])

; find all male patients with a birthdate before Jan 1, 1980
; (http://server-url/Patient?birthdate=%3C1980-01-01&gender=M)
(search server-url :patient [(eq :gender "M")
                             (lt :birthdate "1980-01-01")])
                             
; find all encounter (visit) resources for a patient specified by 
; identifier (MRN in this case)
; (http://server-url/Encounter?subject.identifier=7007482)
(search server-url :encounter [(eq :subject.identifier "7007482")])

; search using an invalid parameter (unrecognized by the server)
; (http://server-url/Patient?foobar=baz)
(search server-url :patient [(eq :foobar "baz")])
ExceptionInfo FHIR request failed: HTTP 400

create

Adding new resources is a simple matter once you have a FHIR resource represented as a Clojure map. Simply pass the resource to create. By default, if creation is successful, the new resource is returned.

Optionally, you can specify an additional :return-resource? false to return a full URL to the newly created resource instead (this can be useful if you need the new resource's ID for example, as the returned FHIR resource would not include this information).

create will throw an exception if the resource you pass in is not a Clojure map that contains a :resourceType key with a value that is anything other then "Bundle").

Examples
(def new-patient
  {:managingOrganization {:resource "Organization/1.3.6.1.4.1.12201"}
   :name [{:given ["Nurse"]
           :family ["Test"]}]
   :birthDate "1965-11-19T00:00:00-05:00"
   :resourceType "Patient"
   :identifier
   [{:assigner {:resource "Organization/1.3.6.1.4.1.12201"}
     :system "urn:oid:2.16.840.1.113883.3.239.18.148"
     :use "official"
     :value "7010168"
     :label "University Health Network MRN 7010168"}]
   :telecom
   [{:system "phone" :use "home" :value "(416)000-0000"}
    {:system "phone" :use "work"}
    {:system "phone" :use "mobile"}
    {:system "email" :use "home"}]
   :gender
   {:coding
    [{:system "http://hl7.org/fhir/v3/AdministrativeGender"
      :code "F"}]}
   :text {:div "<div/>"}}

; create a new resource. will return a map that should look almost identical to the 
; above (some servers may autogenerate the :text :div value, if so that value will 
; be included in the returned map of course)
(create server-url :patient new-patient)
=> {
    ; resource
    } 

; create a new resource, but only return the URL to the created resource
(create server-url :patient new-patient :return-resource? false)
=> "http://server-url/Patient/1234/_history/1"

; trying to create a resource with an invalid resource map
(create server-url :patient {:foo "bar"})
Exception Not a valid FHIR resource 

; trying to create a resource that the server rejects
; (exact HTTP status returned may vary from server to server unfortunately! some 
; servers do validation better then others and may return an HTTP 400 instead. 
; HTTP 422 is another result defined in the spec for an invalid/unusable resource)
(create server-url :patient {:resourceType "foobar" 
                             :foo "bar"})
ExceptionInfo FHIR request failed: HTTP 500

update

Updating existing resources is accomplished via update which takes an ID along with a FHIR resource map, similar to what you would provide with create. The ID of course specifies the existing resource to be update. By default, if the update is successful, the newly updated resource is returned.

Optionally, you can specify an additional :return-resource? false to return a full URL to the updated resource instead (this can be useful if you need the resource's ID/version for example, as the returned FHIR resource would not include this information).

Additionally, you can limit updates to only proceed if the latest version of the resource on the server matches a version number you specify by passing an extra :version [version-number] argument. If the latest version of the resource on the server does not match, the resource will not be updated and an exception is thrown.

update will throw an exception if the resource you pass in is not a Clojure map that contains a :resourceType key with a value that is anything other then "Bundle").

Examples
(def updated-patient
  {:managingOrganization {:resource "Organization/1.3.6.1.4.1.12201"}
   :name [{:given ["Nurse"]
           :family ["Test"]}]
   :birthDate "1965-11-19T00:00:00-05:00"
   :resourceType "Patient"
   :identifier
   [{:assigner {:resource "Organization/1.3.6.1.4.1.12201"}
     :system "urn:oid:2.16.840.1.113883.3.239.18.148"
     :use "official"
     :value "7010168"
     :label "University Health Network MRN 7010168"}]
   :telecom
   [{:system "phone" :use "home" :value "(416)000-0000"}
    {:system "phone" :use "work" :value "555-555-5555}
    {:system "phone" :use "mobile"}
    {:system "email" :use "home"}]
   :gender
   {:coding
    [{:system "http://hl7.org/fhir/v3/AdministrativeGender"
      :code "F"}]}
   :text {:div "<div/>"}}

; updates an existing resource. will return a map that should look almost identical to the 
; above (some servers may autogenerate the :text :div value, if so that value will be 
; included in the returned map of course)
(update server-url :patient 1234 updated-patient)
=> {
    ; resource
    } 

; updates an existing resource, but only return the URL to the updated resource
(update server-url :patient 1234 updated-patient)
=> "http://server-url/Patient/1234/_history/2"

; update an existing resource only if the version matches
(update server-url :patient 1234 updated-patient :version 1)
=> {
    ; resource
    } 

; NOTE: error responses are identical to clj-hl7-fhir.core/create. see examples for that
;       function for more information

Error Handling

All API functions throw exceptions via ex-info when an unexpected error response is returned from the HL7 FHIR server. An "unexpected error response" is anything that is not defined to be part of the particular operation's successful result(s). e.g. a "read" operation that returns an HTTP 400 or HTTP 500 status instead of HTTP 200.

When this type of response is encountered, an exception is thrown which will contain the response, which can be obtained in your exception handler via ex-data. If the response is detected to be a FHIR OperationOutcome resource, it will be parsed and set as the response, otherwise the raw response body is set in the exception.

; trying to read an invalid resource
(get-resource server-url :foobar 42)
ExceptionInfo FHIR request failed: HTTP 400  clojure.core/ex-info (core.clj:4403)

; more detailed error information can be obtained via ex-data
(try
  (get-resource server-url :foobar 42)
  (catch Exception e
    (let [operation-outcome (ex-data e)]
      ; TODO: proper error handling goes here
      operation-outcome)))
=> {:status 400
    :fhir-resource? true
    :response
    {:resourceType "OperationOutcome"
     :text
     {:status "empty"
      :div
      "<div>No narrative template available for resource profile: http://hl7.org/fhir/profiles/OperationOutcome</div>"}
     :issue
     [{:severity "error"
       :details
       "Unknown resource type 'Foobar' - Server knows how to handle: [User, Condition, Supply, GVFVariant, Organization, Group, ValueSet, Coverage, ImmunizationRecommendation, Appointment, MedicationDispense, MedicationPrescription, Slot, AppointmentResponse, MedicationStatement, SequencingLab, Questionnaire, Composition, OperationOutcome, Conformance, Media, Other, Profile, DocumentReference, Immunization, Microarray, OrderResponse, ConceptMap, Practitioner, ImagingStudy, GVFMeta, CarePlan, Provenance, Device, Query, Order, Procedure, Substance, DiagnosticReport, Medication, MessageHeader, DocumentManifest, Availability, MedicationAdministration, Encounter, SecurityEvent, GeneExpression, SequencingAnalysis, List, DeviceObservationReport, Claim, FamilyHistory, Location, AllergyIntolerance, GeneticAnalysis, Observation, RelatedPerson, Specimen, Alert, Patient, Remittance, AdverseReaction, DiagnosticOrder]"}]}}

License

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.