Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clinic: adds API endpoints and UI to list, search and view patients #3

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions clinic/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions clinic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"shadow-cljs": "^2.25.6"
},
"dependencies": {
"highlight.js": "11.5.1",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
Expand Down
12 changes: 11 additions & 1 deletion clinic/shadow-cljs.edn
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
[binaryage/devtools "1.0.7"]
[cljs-ajax "0.7.5"]
[day8.re-frame/http-fx "0.2.4"]
[day8.re-frame/re-frame-10x "1.8.1"]
[day8.re-frame/tracing "0.6.2"]
[kibu/pushy "0.3.8"]
[nrepl "1.0.0"]
[re-frame "1.3.0"]
Expand All @@ -12,4 +14,12 @@
:output-dir "resources/public/js"
:asset-path "/js"
:modules {:app {:entries [clinic.core]}}
:devtools {:after-load clinic.core/mount-root}}}}
:devtools {:preloads [day8.re-frame-10x.preload]
:after-load clinic.core/mount-root}
:dev {:compiler-options
{:closure-defines
{re-frame.trace.trace-enabled? true
day8.re-frame.tracing.trace-enabled? true}}}
:release {:build-options
{:ns-aliases
{day8.re-frame.tracing day8.re-frame.tracing-stubs}}}}}}
30 changes: 29 additions & 1 deletion clinic/src/clj/clinic/fhir/client.clj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"Performs a HTTP POST request on a FHIR server at the given `base-url` for a
given `resource` with given HTTP `headers`.

Returns HTTP response of the server after JSON parsing its body."
Returns the HTTP response of the server after JSON parsing its body."
[base-url resource headers]
(-> (if (= "Bundle" (resource :resourceType))
base-url ; Bundle resources should POST at the server root
Expand All @@ -15,3 +15,31 @@
:body (json/generate-string resource)
:throw-exceptions false})
(update :body json/parse-string true)))

(defn get-all
"Searches the given `resource-type` on a FHIR server at the given `base-url`
and appends the given `query-params` to the request for filtering the search
results.

Returns the HTTP response of the server (FHIR Bundle with `searchset` type)
after JSON parsing its body."
[base-url resource-type query-params]
(-> base-url
(str "/" resource-type)
(http/get {:headers {"Accept" "application/fhir+json"}
:query-params query-params
:throw-exceptions false})
(update :body json/parse-string true)))

(defn get-by-id
"Looks up a FHIR resource of the given `resource-type` with the given `id` on
a FHIR server at the given `base-url`.

Returns the HTTP response of the server (Patient resource) after JSON parsing
its body."
[base-url resource-type id]
(-> base-url
(str "/" resource-type "/" id)
(http/get {:headers {"Accept" "application/fhir+json"}
:throw-exceptions false})
(update :body json/parse-string true)))
33 changes: 31 additions & 2 deletions clinic/src/clj/clinic/routes/patient.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(ns clinic.routes.patient
(:require [clinic.service.patient :as svc]
[compojure.core :refer [defroutes POST]]
[compojure.core :refer [defroutes GET POST]]
[ring.util.response :as r]))

(defn- create-patient! [{{fhir-server-url :fhir-server-base-url} :config
Expand All @@ -19,5 +19,34 @@
:invalid-params (r/status 400)
(throw e))))))

(defn- list-patients [{{fhir-server-url :fhir-server-base-url} :config
{:keys [phone offset count]} :params}]
(try
;; `params` in request contains form + query params. Therefore, destructure
;; only what is needed.
(-> (svc/get-all fhir-server-url {:phone phone
:offset offset
:count count})
(r/response)
(r/status 200))
(catch Exception e
(case (:type (ex-data e))
:invalid-params (r/status 400)
(throw e)))))

(defn- get-patient [{{fhir-server-url :fhir-server-base-url} :config
{:keys [id]} :params}]
(try
(-> (svc/get-by-id fhir-server-url id)
(r/response)
(r/status 200))
(catch Exception e
(case (:type (ex-data e))
:invalid-params (r/status 400)
:patient-not-found (r/status 404)
(throw e)))))

(defroutes handler
(POST "/" _ create-patient!))
(POST "/" _ create-patient!)
(GET "/" _ list-patients)
(GET "/:id" _ get-patient))
63 changes: 59 additions & 4 deletions clinic/src/clj/clinic/service/patient.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
(:require [clinic.fhir.client :as fc]
[clinic.fhir.utils :as fu]
[clinic.specs.patient :as specs]
[clinic.utils :as u]
[clojure.spec.alpha :as s]
[clojure.string :as string]))

Expand Down Expand Up @@ -54,12 +55,66 @@
(throw (ex-info "invalid create params"
{:type :invalid-params
:details (s/explain-data ::specs/create-params params)})))
(let [{status :status
body :body} (fc/create! fhir-server-url
(domain->fhir params)
nil)]
(let [{:keys [status body]} (fc/create! fhir-server-url
(-> params
;; ignore phone number formatting
;; characters and only keep its
;; digits.
(update :phone u/extract-digits)
(domain->fhir))
nil)]
(cond
(= status 201) (fhir->domain body)
:else (throw (ex-info "upstream service error"
{:type :upstream-error
:response {:status status :body body}})))))

(defn get-all
"Lists patient resources from a FHIR server at the given `fhir-server-url` and
uses the given `params` to apply filters to the search.

The accepted `params` are:
- `:phone` (optional): The phone number of the Patient.
- `:offset` (optional, default 0): The number of Patient resources to skip in
the result set.
- `:count` (optional, default 10): The maximum count of Patient resources to
return with the result.
"
[fhir-server-url params]
(when-not (s/valid? ::specs/get-all-params params)
(throw (ex-info "invalid get-all params"
{:type :invalid-params
:details (s/explain-data ::specs/get-all-params params)})))
(let [{:keys [phone offset count]} params
query-params (cond-> {:_offset "0"
:_count "10"}
phone (assoc :phone (u/extract-digits phone))
offset (assoc :_offset offset)
count (assoc :_count count))
{:keys [status body]} (fc/get-all fhir-server-url "Patient" query-params)]
(cond
(= status 200) (->> body
(:entry)
(map :resource)
(map fhir->domain))
:else (throw (ex-info "upstream service error"
{:type :upstream-error
:response {:status status :body body}})))))

(defn get-by-id
"Gets a Patient resource by its `id` from a FHIR server at the given
`fhir-server-url`."
[fhir-server-url id]
(when-not (s/valid? ::specs/id id)
(throw (ex-info "invalid `id` path param"
{:type :invalid-params
:details (s/explain-data ::specs/id id)})))
(let [{:keys [status body]} (fc/get-by-id fhir-server-url "Patient" id)]
(cond
(= status 200) (fhir->domain body)
(= status 404) (throw (ex-info "patient not found"
{:type :patient-not-found
:patient-id id}))
:else (throw (ex-info "upstream service error"
{:type :upstream-error
:response {:status status :body body}})))))
23 changes: 18 additions & 5 deletions clinic/src/cljc/clinic/specs/patient.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
[clojure.string :as string]))

(def ^:private not-blank? (complement string/blank?))
(def ^:private int-string? (partial re-matches #"\d+"))

(defn phone-number? [v]
;; Not strictly checking the input sequence for digits and allowing room for
;; phone number formatting characters. Taking the number of digits in a phone
;; number from the E.164 standard. https://en.wikipedia.org/wiki/E.164
(and (re-matches #"\+?[\d-()x\[\]\. ]+" v)
(<= 8 (count (re-seq #"\d" v)) 15)))

(defn- date? [v]
#?(:clj (try (java.time.LocalDate/parse v)
Expand All @@ -19,12 +27,17 @@
(s/def ::gender #{"male" "female" "other" "unknown"})
(s/def ::marital-status (s/nilable #{"A" "D" "I" "L" "M" "P" "S" "T" "U" "W" "UNK"}))
(s/def ::email (s/nilable (s/and string? not-blank?)))
(s/def ::phone (s/nilable (s/and string? not-blank?)))
(s/def ::phone (s/nilable (s/and string? phone-number?)))
(s/def ::offset (s/nilable int-string?))
(s/def ::count (s/nilable (s/and int-string? #(<= 1 (parse-long %) 20))))

(s/def ::create-params
(s/keys :req-un [::first-name ::last-name ::birth-date ::gender]
:opt-un [::marital-status ::email ::phone]))
(s/keys :req-un [::first-name ::last-name ::birth-date ::gender ::phone]
:opt-un [::marital-status ::email]))

(s/def ::patient
(s/keys :req-un [::id ::first-name ::last-name ::birth-date ::gender]
:opt-un [::marital-status ::email ::phone]))
(s/keys :req-un [::id ::first-name ::last-name ::birth-date ::gender ::phone]
:opt-un [::marital-status ::email]))

(s/def ::get-all-params
(s/keys :opt-un [::offset ::count ::phone]))
4 changes: 4 additions & 0 deletions clinic/src/cljc/clinic/utils.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(ns clinic.utils)

(defn extract-digits [s]
(apply str (re-seq #"\d" s)))
Loading