mirror of
https://github.com/AmanTahiliani/FHIR-Sandbox.git
synced 2026-08-07 19:56:17 -04:00
chore: remove DB and binary from repo, add .gitignore to exclude artifacts
This commit is contained in:
651
app/fhir/fhir.go
Normal file
651
app/fhir/fhir.go
Normal file
@@ -0,0 +1,651 @@
|
||||
// Package fhir provides typed representations of FHIR R4 resources and
|
||||
// utilities for extracting platform-domain data from raw FHIR JSON payloads.
|
||||
//
|
||||
// Architecture notes:
|
||||
// - FHIR resources are received from EHR servers as JSON and decoded into
|
||||
// typed Go structs defined here. This gives us compile-time safety and
|
||||
// makes it straightforward to add support for new resource types.
|
||||
// - The Resource interface is the root of all FHIR types in this package.
|
||||
// Any new resource (Observation, Condition, Encounter, etc.) should
|
||||
// implement it so it can be handled generically by shared code.
|
||||
// - Extraction helpers (ExtractUserFromPatient, ExtractUserFromPractitioner)
|
||||
// translate FHIR types into the platform's models.User, decoupling the
|
||||
// FHIR representation from the persistence layer.
|
||||
// - The Client type wraps http.Client and provides typed FHIR API methods.
|
||||
// Extend it with new methods (GetObservations, GetConditions, etc.) as
|
||||
// the platform grows.
|
||||
package fhir
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AmanTahiliani/FHIR-Sandbox/app/models"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core FHIR R4 types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Resource is the base interface for all FHIR R4 resources in this package.
|
||||
// Every concrete FHIR type must implement ResourceType() returning its
|
||||
// FHIR resource type string (e.g., "Patient", "Practitioner").
|
||||
type Resource interface {
|
||||
ResourceType() string
|
||||
}
|
||||
|
||||
// HumanName represents the FHIR HumanName data type (R4).
|
||||
// https://www.hl7.org/fhir/datatypes.html#HumanName
|
||||
type HumanName struct {
|
||||
Use string `json:"use"`
|
||||
Family string `json:"family"`
|
||||
Given []string `json:"given"`
|
||||
Prefix []string `json:"prefix"`
|
||||
Suffix []string `json:"suffix"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// ContactPoint represents the FHIR ContactPoint data type (R4).
|
||||
// https://www.hl7.org/fhir/datatypes.html#ContactPoint
|
||||
type ContactPoint struct {
|
||||
System string `json:"system"` // phone | fax | email | pager | url | sms | other
|
||||
Value string `json:"value"`
|
||||
Use string `json:"use"` // home | work | temp | old | mobile
|
||||
Rank int `json:"rank"`
|
||||
}
|
||||
|
||||
// Address represents the FHIR Address data type (R4).
|
||||
// https://www.hl7.org/fhir/datatypes.html#Address
|
||||
type Address struct {
|
||||
Use string `json:"use"`
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
Line []string `json:"line"`
|
||||
City string `json:"city"`
|
||||
District string `json:"district"`
|
||||
State string `json:"state"`
|
||||
PostalCode string `json:"postalCode"`
|
||||
Country string `json:"country"`
|
||||
}
|
||||
|
||||
// Coding represents the FHIR Coding data type (R4).
|
||||
type Coding struct {
|
||||
System string `json:"system"`
|
||||
Code string `json:"code"`
|
||||
Display string `json:"display"`
|
||||
}
|
||||
|
||||
// CodeableConcept represents the FHIR CodeableConcept data type (R4).
|
||||
type CodeableConcept struct {
|
||||
Coding []Coding `json:"coding"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// Reference represents the FHIR Reference data type (R4).
|
||||
type Reference struct {
|
||||
Reference string `json:"reference"`
|
||||
Display string `json:"display"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// Identifier represents the FHIR Identifier data type (R4).
|
||||
type Identifier struct {
|
||||
Use string `json:"use"`
|
||||
Type CodeableConcept `json:"type"`
|
||||
System string `json:"system"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// Meta represents the FHIR Meta data type (R4).
|
||||
type Meta struct {
|
||||
VersionID string `json:"versionId"`
|
||||
LastUpdated time.Time `json:"lastUpdated"`
|
||||
Source string `json:"source"`
|
||||
Profile []string `json:"profile"`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Patient resource (R4)
|
||||
// https://www.hl7.org/fhir/patient.html
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Patient represents a FHIR R4 Patient resource.
|
||||
// Fields are a curated subset of the full specification — add new fields
|
||||
// here as the platform needs them, without breaking existing code.
|
||||
type Patient struct {
|
||||
ResourceTypeField string `json:"resourceType"`
|
||||
ID string `json:"id"`
|
||||
Meta Meta `json:"meta"`
|
||||
Identifier []Identifier `json:"identifier"`
|
||||
Active bool `json:"active"`
|
||||
Name []HumanName `json:"name"`
|
||||
Telecom []ContactPoint `json:"telecom"`
|
||||
Gender string `json:"gender"`
|
||||
BirthDate string `json:"birthDate"`
|
||||
Address []Address `json:"address"`
|
||||
MaritalStatus CodeableConcept `json:"maritalStatus"`
|
||||
}
|
||||
|
||||
// ResourceType implements the Resource interface.
|
||||
func (p *Patient) ResourceType() string { return "Patient" }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Practitioner resource (R4)
|
||||
// https://www.hl7.org/fhir/practitioner.html
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Practitioner represents a FHIR R4 Practitioner resource.
|
||||
type Practitioner struct {
|
||||
ResourceTypeField string `json:"resourceType"`
|
||||
ID string `json:"id"`
|
||||
Meta Meta `json:"meta"`
|
||||
Identifier []Identifier `json:"identifier"`
|
||||
Active bool `json:"active"`
|
||||
Name []HumanName `json:"name"`
|
||||
Telecom []ContactPoint `json:"telecom"`
|
||||
Gender string `json:"gender"`
|
||||
BirthDate string `json:"birthDate"`
|
||||
Address []Address `json:"address"`
|
||||
}
|
||||
|
||||
// ResourceType implements the Resource interface.
|
||||
func (p *Practitioner) ResourceType() string { return "Practitioner" }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Clinical resources (R4)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Observation represents a FHIR R4 Observation resource.
|
||||
// https://www.hl7.org/fhir/observation.html
|
||||
type Observation struct {
|
||||
ResourceTypeField string `json:"resourceType"`
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Category []CodeableConcept `json:"category"`
|
||||
Code CodeableConcept `json:"code"`
|
||||
Subject Reference `json:"subject"`
|
||||
EffectiveDateTime string `json:"effectiveDateTime"`
|
||||
ValueQuantity *Quantity `json:"valueQuantity,omitempty"`
|
||||
ValueString string `json:"valueString,omitempty"`
|
||||
}
|
||||
|
||||
func (o *Observation) ResourceType() string { return "Observation" }
|
||||
|
||||
// Condition represents a FHIR R4 Condition resource.
|
||||
// https://www.hl7.org/fhir/condition.html
|
||||
type Condition struct {
|
||||
ResourceTypeField string `json:"resourceType"`
|
||||
ID string `json:"id"`
|
||||
ClinicalStatus CodeableConcept `json:"clinicalStatus"`
|
||||
VerificationStatus CodeableConcept `json:"verificationStatus"`
|
||||
Category []CodeableConcept `json:"category"`
|
||||
Code CodeableConcept `json:"code"`
|
||||
Subject Reference `json:"subject"`
|
||||
OnsetDateTime string `json:"onsetDateTime"`
|
||||
RecordedDate string `json:"recordedDate"`
|
||||
}
|
||||
|
||||
func (c *Condition) ResourceType() string { return "Condition" }
|
||||
|
||||
// Attachment represents the FHIR Attachment data type.
|
||||
type Attachment struct {
|
||||
ContentType string `json:"contentType"`
|
||||
Language string `json:"language"`
|
||||
Data string `json:"data"` // base64-encoded
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
Creation string `json:"creation"`
|
||||
}
|
||||
|
||||
// DocumentReferenceContent holds a single content item in a DocumentReference.
|
||||
type DocumentReferenceContent struct {
|
||||
Attachment Attachment `json:"attachment"`
|
||||
Format CodeableConcept `json:"format"`
|
||||
}
|
||||
|
||||
// DocumentReference represents a FHIR R4 DocumentReference resource.
|
||||
// https://www.hl7.org/fhir/documentreference.html
|
||||
type DocumentReference struct {
|
||||
ResourceTypeField string `json:"resourceType"`
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
DocStatus string `json:"docStatus"`
|
||||
Type CodeableConcept `json:"type"`
|
||||
Category []CodeableConcept `json:"category"`
|
||||
Subject Reference `json:"subject"`
|
||||
Date string `json:"date"`
|
||||
Description string `json:"description"`
|
||||
Content []DocumentReferenceContent `json:"content"`
|
||||
}
|
||||
|
||||
func (d *DocumentReference) ResourceType() string { return "DocumentReference" }
|
||||
|
||||
// Quantity represents the FHIR Quantity data type.
|
||||
type Quantity struct {
|
||||
Value float64 `json:"value"`
|
||||
Unit string `json:"unit"`
|
||||
System string `json:"system"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
// Bundle represents a FHIR R4 Bundle resource, used for search results.
|
||||
type Bundle struct {
|
||||
ResourceType string `json:"resourceType"`
|
||||
Type string `json:"type"`
|
||||
Total int `json:"total"`
|
||||
Entry []struct {
|
||||
FullUrl string `json:"fullUrl"`
|
||||
Resource json.RawMessage `json:"resource"`
|
||||
} `json:"entry"`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SMART discovery types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// SmartConfiguration represents the payload returned by the FHIR server's
|
||||
// .well-known/smart-configuration endpoint.
|
||||
// https://build.fhir.org/ig/HL7/smart-app-launch/conformance.html
|
||||
type SmartConfiguration struct {
|
||||
Issuer string `json:"issuer"`
|
||||
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
|
||||
GrantTypesSupported []string `json:"grant_types_supported"`
|
||||
RegistrationEndpoint string `json:"registration_endpoint"`
|
||||
ScopesSupported []string `json:"scopes_supported"`
|
||||
ResponseTypesSupported []string `json:"response_types_supported"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
}
|
||||
|
||||
// TokenResponse is the OAuth2 token endpoint response, extended with
|
||||
// SMART-specific fields.
|
||||
// https://build.fhir.org/ig/HL7/smart-app-launch/
|
||||
type TokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
Scope string `json:"scope"`
|
||||
IDToken string `json:"id_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
|
||||
// SMART launch context extensions
|
||||
Patient string `json:"patient"`
|
||||
Encounter string `json:"encounter"`
|
||||
// Practitioner holds a bare Practitioner FHIR ID when provided by the EHR.
|
||||
Practitioner string `json:"practitioner"`
|
||||
// User holds a relative FHIR reference to the authenticated user,
|
||||
// e.g. "Practitioner/52919099-..." as returned by SmartHealthIT and
|
||||
// defined in the SMART App Launch specification.
|
||||
User string `json:"user"`
|
||||
NeedPatientBanner bool `json:"need_patient_banner"`
|
||||
SmartStyleURL string `json:"smart_style_url"`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FHIR API Client
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Client is a thin, typed FHIR R4 REST client. It holds an access token
|
||||
// and the FHIR server base URL so callers don't have to manage headers
|
||||
// on every request.
|
||||
//
|
||||
// To support new resource types: add a method like GetObservations,
|
||||
// GetConditions, etc., following the pattern of GetPatient / GetPractitioner.
|
||||
type Client struct {
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
accessToken string
|
||||
}
|
||||
|
||||
// NewClient creates a FHIR API client for the given base URL and Bearer token.
|
||||
func NewClient(baseURL, accessToken string) *Client {
|
||||
return &Client{
|
||||
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
accessToken: accessToken,
|
||||
}
|
||||
}
|
||||
|
||||
// get performs an authenticated GET request to the FHIR server and decodes
|
||||
// the JSON response into dest.
|
||||
func (c *Client) get(path string, dest interface{}) error {
|
||||
url := fmt.Sprintf("%s/%s", c.baseURL, strings.TrimLeft(path, "/"))
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fhir: build request for %s: %w", url, err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.accessToken)
|
||||
req.Header.Set("Accept", "application/fhir+json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fhir: GET %s: %w", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("fhir: GET %s returned %d: %s", url, resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(dest); err != nil {
|
||||
return fmt.Errorf("fhir: decode response from %s: %w", url, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPatient fetches a Patient resource by FHIR ID.
|
||||
func (c *Client) GetPatient(id string) (*Patient, error) {
|
||||
var p Patient
|
||||
if err := c.get(fmt.Sprintf("Patient/%s", id), &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// GetPractitioner fetches a Practitioner resource by FHIR ID.
|
||||
func (c *Client) GetPractitioner(id string) (*Practitioner, error) {
|
||||
var p Practitioner
|
||||
if err := c.get(fmt.Sprintf("Practitioner/%s", id), &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// GetObservations fetches Observation resources for a specific patient.
|
||||
func (c *Client) GetObservations(patientID string) ([]Observation, error) {
|
||||
var bundle Bundle
|
||||
path := fmt.Sprintf("Observation?patient=%s&_sort=-date", patientID)
|
||||
if err := c.get(path, &bundle); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var observations []Observation
|
||||
for _, entry := range bundle.Entry {
|
||||
var o Observation
|
||||
if err := json.Unmarshal(entry.Resource, &o); err == nil {
|
||||
observations = append(observations, o)
|
||||
}
|
||||
}
|
||||
return observations, nil
|
||||
}
|
||||
|
||||
// GetConditions fetches Condition resources for a specific patient.
|
||||
func (c *Client) GetConditions(patientID string) ([]Condition, error) {
|
||||
var bundle Bundle
|
||||
path := fmt.Sprintf("Condition?patient=%s", patientID)
|
||||
if err := c.get(path, &bundle); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var conditions []Condition
|
||||
for _, entry := range bundle.Entry {
|
||||
var cond Condition
|
||||
if err := json.Unmarshal(entry.Resource, &cond); err == nil {
|
||||
conditions = append(conditions, cond)
|
||||
}
|
||||
}
|
||||
return conditions, nil
|
||||
}
|
||||
|
||||
// GetDocumentReferences fetches DocumentReference resources for a specific patient.
|
||||
// Results are sorted newest-first by date.
|
||||
func (c *Client) GetDocumentReferences(patientID string) ([]DocumentReference, error) {
|
||||
var bundle Bundle
|
||||
path := fmt.Sprintf("DocumentReference?patient=%s&_sort=-date", patientID)
|
||||
if err := c.get(path, &bundle); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var docs []DocumentReference
|
||||
for _, entry := range bundle.Entry {
|
||||
var d DocumentReference
|
||||
if err := json.Unmarshal(entry.Resource, &d); err == nil {
|
||||
docs = append(docs, d)
|
||||
}
|
||||
}
|
||||
return docs, nil
|
||||
}
|
||||
|
||||
// GetSmartConfiguration fetches and parses the SMART discovery document
|
||||
// for this FHIR server.
|
||||
func GetSmartConfiguration(issURL string) (*SmartConfiguration, error) {
|
||||
url := fmt.Sprintf("%s/.well-known/smart-configuration", strings.TrimRight(issURL, "/"))
|
||||
resp, err := http.Get(url) //nolint:noctx // discovery calls do not need request context
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fhir: GET smart-configuration from %s: %w", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("fhir: smart-configuration %s returned %d: %s", url, resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var cfg SmartConfiguration
|
||||
if err := json.NewDecoder(resp.Body).Decode(&cfg); err != nil {
|
||||
return nil, fmt.Errorf("fhir: decode smart-configuration: %w", err)
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Domain extraction helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// primaryName returns the first HumanName with use=="official", falling back
|
||||
// to the first name in the slice, or an empty HumanName if none exist.
|
||||
func primaryName(names []HumanName) HumanName {
|
||||
for _, n := range names {
|
||||
if n.Use == "official" {
|
||||
return n
|
||||
}
|
||||
}
|
||||
if len(names) > 0 {
|
||||
return names[0]
|
||||
}
|
||||
return HumanName{}
|
||||
}
|
||||
|
||||
// primaryEmail returns the first email address from a ContactPoint slice.
|
||||
func primaryEmail(telecom []ContactPoint) string {
|
||||
for _, t := range telecom {
|
||||
if t.System == "email" && t.Value != "" {
|
||||
return t.Value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ExtractUserFromPatient converts a FHIR Patient resource into a platform
|
||||
// models.User with Role=RolePatient. The ehrURL is the originating FHIR
|
||||
// server base URL.
|
||||
func ExtractUserFromPatient(p *Patient, ehrURL string) *models.User {
|
||||
name := primaryName(p.Name)
|
||||
first, middle := "", ""
|
||||
if len(name.Given) > 0 {
|
||||
first = name.Given[0]
|
||||
}
|
||||
if len(name.Given) > 1 {
|
||||
middle = name.Given[1]
|
||||
}
|
||||
return &models.User{
|
||||
FHIRResourceType: "Patient",
|
||||
FHIRID: p.ID,
|
||||
EHRURL: strings.TrimRight(ehrURL, "/"),
|
||||
Role: models.RolePatient,
|
||||
FirstName: first,
|
||||
MiddleName: middle,
|
||||
LastName: name.Family,
|
||||
DOB: p.BirthDate,
|
||||
Gender: p.Gender,
|
||||
Email: primaryEmail(p.Telecom),
|
||||
}
|
||||
}
|
||||
|
||||
// ExtractUserFromPractitioner converts a FHIR Practitioner resource into a
|
||||
// platform models.User with Role=RolePractitioner.
|
||||
func ExtractUserFromPractitioner(p *Practitioner, ehrURL string) *models.User {
|
||||
name := primaryName(p.Name)
|
||||
first, middle := "", ""
|
||||
if len(name.Given) > 0 {
|
||||
first = name.Given[0]
|
||||
}
|
||||
if len(name.Given) > 1 {
|
||||
middle = name.Given[1]
|
||||
}
|
||||
return &models.User{
|
||||
FHIRResourceType: "Practitioner",
|
||||
FHIRID: p.ID,
|
||||
EHRURL: strings.TrimRight(ehrURL, "/"),
|
||||
Role: models.RolePractitioner,
|
||||
FirstName: first,
|
||||
MiddleName: middle,
|
||||
LastName: name.Family,
|
||||
DOB: p.BirthDate,
|
||||
Gender: p.Gender,
|
||||
Email: primaryEmail(p.Telecom),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FHIR → domain model extraction helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// firstCoding returns the first Coding from a CodeableConcept, or zero value.
|
||||
func firstCoding(cc CodeableConcept) Coding {
|
||||
if len(cc.Coding) > 0 {
|
||||
return cc.Coding[0]
|
||||
}
|
||||
return Coding{}
|
||||
}
|
||||
|
||||
// firstCategoryText returns the text (or first coding display) of the first
|
||||
// element in a []CodeableConcept, e.g. as used for Observation.category.
|
||||
func firstCategoryText(cats []CodeableConcept) string {
|
||||
if len(cats) == 0 {
|
||||
return ""
|
||||
}
|
||||
c := cats[0]
|
||||
if c.Text != "" {
|
||||
return c.Text
|
||||
}
|
||||
if len(c.Coding) > 0 {
|
||||
if c.Coding[0].Display != "" {
|
||||
return c.Coding[0].Display
|
||||
}
|
||||
return c.Coding[0].Code
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ExtractObservation maps a FHIR Observation to a models.Observation ready
|
||||
// for upsert. patientFHIRID and ehrURL are injected by the caller because
|
||||
// they are session-level context, not encoded inside the FHIR resource.
|
||||
func ExtractObservation(o *Observation, patientFHIRID, ehrURL string) *models.Observation {
|
||||
coding := firstCoding(o.Code)
|
||||
var qty *float64
|
||||
var unit string
|
||||
if o.ValueQuantity != nil {
|
||||
v := o.ValueQuantity.Value
|
||||
qty = &v
|
||||
unit = o.ValueQuantity.Unit
|
||||
}
|
||||
return &models.Observation{
|
||||
FHIRID: o.ID,
|
||||
EHRURL: strings.TrimRight(ehrURL, "/"),
|
||||
PatientFHIRID: patientFHIRID,
|
||||
Status: o.Status,
|
||||
Category: firstCategoryText(o.Category),
|
||||
CodeText: o.Code.Text,
|
||||
CodeSystem: coding.System,
|
||||
CodeCode: coding.Code,
|
||||
EffectiveDate: o.EffectiveDateTime,
|
||||
ValueQuantity: qty,
|
||||
ValueUnit: unit,
|
||||
ValueString: o.ValueString,
|
||||
}
|
||||
}
|
||||
|
||||
// ExtractCondition maps a FHIR Condition to a models.Condition ready for upsert.
|
||||
func ExtractCondition(c *Condition, patientFHIRID, ehrURL string) *models.Condition {
|
||||
coding := firstCoding(c.Code)
|
||||
clinicalStatus := firstCoding(c.ClinicalStatus)
|
||||
verificationStatus := firstCoding(c.VerificationStatus)
|
||||
return &models.Condition{
|
||||
FHIRID: c.ID,
|
||||
EHRURL: strings.TrimRight(ehrURL, "/"),
|
||||
PatientFHIRID: patientFHIRID,
|
||||
ClinicalStatus: clinicalStatus.Code,
|
||||
VerificationStatus: verificationStatus.Code,
|
||||
Category: firstCategoryText(c.Category),
|
||||
CodeText: c.Code.Text,
|
||||
CodeSystem: coding.System,
|
||||
CodeCode: coding.Code,
|
||||
OnsetDate: c.OnsetDateTime,
|
||||
RecordedDate: c.RecordedDate,
|
||||
}
|
||||
}
|
||||
|
||||
// ExtractDocumentReference maps a FHIR DocumentReference to a
|
||||
// models.DocumentReference ready for upsert. Only the first content item is
|
||||
// persisted; additional content attachments are not common in practice.
|
||||
func ExtractDocumentReference(d *DocumentReference, patientFHIRID, ehrURL string) *models.DocumentReference {
|
||||
coding := firstCoding(d.Type)
|
||||
category := firstCategoryText(d.Category)
|
||||
|
||||
var contentType, contentURL, contentData string
|
||||
if len(d.Content) > 0 {
|
||||
att := d.Content[0].Attachment
|
||||
contentType = att.ContentType
|
||||
contentURL = att.URL
|
||||
contentData = att.Data
|
||||
}
|
||||
|
||||
return &models.DocumentReference{
|
||||
FHIRID: d.ID,
|
||||
EHRURL: strings.TrimRight(ehrURL, "/"),
|
||||
PatientFHIRID: patientFHIRID,
|
||||
Status: d.Status,
|
||||
DocStatus: d.DocStatus,
|
||||
TypeText: d.Type.Text,
|
||||
TypeSystem: coding.System,
|
||||
TypeCode: coding.Code,
|
||||
Category: category,
|
||||
Date: d.Date,
|
||||
Description: d.Description,
|
||||
ContentType: contentType,
|
||||
ContentURL: contentURL,
|
||||
ContentData: contentData,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseFHIRUserFromIDToken attempts to extract a FHIR resource reference
|
||||
// (e.g. "Practitioner/123" or "Patient/abc") from the id_token's fhirUser claim.
|
||||
// Returns an empty string if the claim is missing or invalid.
|
||||
func ParseFHIRUserFromIDToken(idToken string) string {
|
||||
parts := strings.Split(idToken, ".")
|
||||
if len(parts) != 3 {
|
||||
return ""
|
||||
}
|
||||
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var claims struct {
|
||||
FHIRUser string `json:"fhirUser"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return claims.FHIRUser
|
||||
}
|
||||
131
app/fhir/fhir_test.go
Normal file
131
app/fhir/fhir_test.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package fhir_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/AmanTahiliani/FHIR-Sandbox/app/fhir"
|
||||
"github.com/AmanTahiliani/FHIR-Sandbox/app/models"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ExtractUserFromPatient tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestExtractUserFromPatient_FullRecord(t *testing.T) {
|
||||
p := &fhir.Patient{
|
||||
ResourceTypeField: "Patient",
|
||||
ID: "patient-001",
|
||||
Gender: "female",
|
||||
BirthDate: "1990-04-22",
|
||||
Name: []fhir.HumanName{
|
||||
{Use: "official", Family: "Smith", Given: []string{"Alice", "Marie"}},
|
||||
},
|
||||
Telecom: []fhir.ContactPoint{
|
||||
{System: "phone", Value: "+15555550100"},
|
||||
{System: "email", Value: "alice@example.com"},
|
||||
},
|
||||
}
|
||||
|
||||
u := fhir.ExtractUserFromPatient(p, "https://ehr.example.com/fhir")
|
||||
|
||||
assertEqual(t, "FHIRID", "patient-001", u.FHIRID)
|
||||
assertEqual(t, "Role", string(models.RolePatient), string(u.Role))
|
||||
assertEqual(t, "FHIRResourceType", "Patient", u.FHIRResourceType)
|
||||
assertEqual(t, "FirstName", "Alice", u.FirstName)
|
||||
assertEqual(t, "MiddleName", "Marie", u.MiddleName)
|
||||
assertEqual(t, "LastName", "Smith", u.LastName)
|
||||
assertEqual(t, "DOB", "1990-04-22", u.DOB)
|
||||
assertEqual(t, "Gender", "female", u.Gender)
|
||||
assertEqual(t, "Email", "alice@example.com", u.Email)
|
||||
assertEqual(t, "EHRURL", "https://ehr.example.com/fhir", u.EHRURL)
|
||||
}
|
||||
|
||||
func TestExtractUserFromPatient_NoMiddleName(t *testing.T) {
|
||||
p := &fhir.Patient{
|
||||
ID: "patient-002",
|
||||
Gender: "male",
|
||||
Name: []fhir.HumanName{{Use: "official", Family: "Jones", Given: []string{"Bob"}}},
|
||||
}
|
||||
|
||||
u := fhir.ExtractUserFromPatient(p, "https://ehr.example.com/fhir")
|
||||
|
||||
assertEqual(t, "FirstName", "Bob", u.FirstName)
|
||||
assertEqual(t, "MiddleName", "", u.MiddleName)
|
||||
assertEqual(t, "LastName", "Jones", u.LastName)
|
||||
}
|
||||
|
||||
func TestExtractUserFromPatient_NoEmail(t *testing.T) {
|
||||
p := &fhir.Patient{
|
||||
ID: "patient-003",
|
||||
Telecom: []fhir.ContactPoint{{System: "phone", Value: "+15555559999"}},
|
||||
}
|
||||
|
||||
u := fhir.ExtractUserFromPatient(p, "https://ehr.example.com/fhir")
|
||||
|
||||
if u.Email != "" {
|
||||
t.Errorf("Email: got %q, want empty string", u.Email)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractUserFromPatient_OfficialNamePreferred(t *testing.T) {
|
||||
// When both "usual" and "official" names are present, "official" must win.
|
||||
p := &fhir.Patient{
|
||||
ID: "patient-004",
|
||||
Name: []fhir.HumanName{
|
||||
{Use: "usual", Family: "Nickname", Given: []string{"Nick"}},
|
||||
{Use: "official", Family: "Registered", Given: []string{"Nicholas", "James"}},
|
||||
},
|
||||
}
|
||||
|
||||
u := fhir.ExtractUserFromPatient(p, "https://ehr.example.com/fhir")
|
||||
|
||||
assertEqual(t, "LastName", "Registered", u.LastName)
|
||||
assertEqual(t, "FirstName", "Nicholas", u.FirstName)
|
||||
}
|
||||
|
||||
func TestExtractUserFromPatient_EHRURLTrailingSlashNormalised(t *testing.T) {
|
||||
p := &fhir.Patient{ID: "patient-005"}
|
||||
u := fhir.ExtractUserFromPatient(p, "https://ehr.example.com/fhir/")
|
||||
|
||||
if u.EHRURL == "https://ehr.example.com/fhir/" {
|
||||
t.Error("trailing slash should be stripped from EHRURL")
|
||||
}
|
||||
assertEqual(t, "EHRURL", "https://ehr.example.com/fhir", u.EHRURL)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ExtractUserFromPractitioner tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestExtractUserFromPractitioner_FullRecord(t *testing.T) {
|
||||
p := &fhir.Practitioner{
|
||||
ResourceTypeField: "Practitioner",
|
||||
ID: "pract-001",
|
||||
Gender: "female",
|
||||
BirthDate: "1975-09-15",
|
||||
Name: []fhir.HumanName{{Use: "official", Family: "Chen", Given: []string{"Emily"}}},
|
||||
Telecom: []fhir.ContactPoint{
|
||||
{System: "email", Value: "dr.chen@hospital.org"},
|
||||
},
|
||||
}
|
||||
|
||||
u := fhir.ExtractUserFromPractitioner(p, "https://ehr.example.com/fhir")
|
||||
|
||||
assertEqual(t, "Role", string(models.RolePractitioner), string(u.Role))
|
||||
assertEqual(t, "FHIRResourceType", "Practitioner", u.FHIRResourceType)
|
||||
assertEqual(t, "FHIRID", "pract-001", u.FHIRID)
|
||||
assertEqual(t, "FirstName", "Emily", u.FirstName)
|
||||
assertEqual(t, "LastName", "Chen", u.LastName)
|
||||
assertEqual(t, "Email", "dr.chen@hospital.org", u.Email)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func assertEqual(t *testing.T, field, want, got string) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("%s: got %q, want %q", field, got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user