Skip to content

Commit

Permalink
Merge pull request #28 from halprin/polymorphism
Browse files Browse the repository at this point in the history
Polymorphism
halprin authored Apr 17, 2024
2 parents e19360e + ce3248d commit 3a30b81
Showing 28 changed files with 468 additions and 47 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

Evaluates [FHIR Path](https://hl7.org/fhirpath/) against [FHIR](http://hl7.org/fhir/) resources.

This is a work in progress. 35 out of the 686 [official tests](https://hl7.org/fhirpath/tests.html) pass.
This is a work in progress. 36 out of the 686 [official tests](https://hl7.org/fhirpath/tests.html) pass.

## Install

1 change: 1 addition & 0 deletions context/DSTU2.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions context/R4.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions context/R4B.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions context/R5.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions context/STU3.json

Large diffs are not rendered by default.

112 changes: 112 additions & 0 deletions context/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package context

import (
_ "embed"
"encoding/json"
)

//go:generate go run generate.go

type Definition struct {
Version string `json:"version"`
Resources map[string]ResourceTypeDefinition `json:"resources"`
}

type ResourceTypeDefinition struct {
Name string `json:"name"`
Fields map[string]FieldTypes
}

type FieldTypes struct {
Name string `json:"name"`
Types []string `json:"types"`
}

//go:embed R5.json
var r5json string

//go:embed R4B.json
var r4bjson string

//go:embed R4.json
var r4json string

//go:embed STU3.json
var stu3json string

//go:embed DSTU2.json
var dstu2json string

var r5 Definition
var r4b Definition
var r4 Definition
var stu3 Definition
var dstu2 Definition

func init() {

if len(r5json) == 0 {
return
}

err := json.Unmarshal([]byte(r5json), &r5)
if err != nil {
panic(err)
}

if len(r4bjson) == 0 {
return
}

err = json.Unmarshal([]byte(r4bjson), &r4b)
if err != nil {
panic(err)
}

if len(r4json) == 0 {
return
}

err = json.Unmarshal([]byte(r4json), &r4)
if err != nil {
panic(err)
}

if len(stu3json) == 0 {
return
}

err = json.Unmarshal([]byte(stu3json), &stu3)
if err != nil {
panic(err)
}

if len(dstu2json) == 0 {
return
}

err = json.Unmarshal([]byte(dstu2json), &dstu2)
if err != nil {
panic(err)
}
}

func R5() Definition {
return r5
}

func R4B() Definition {
return r4b
}

func R4() Definition {
return r4
}

func STU3() Definition {
return stu3
}

func DSTU2() Definition {
return dstu2
}
192 changes: 192 additions & 0 deletions context/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
//go:build ignore

package main

import (
"encoding/json"
"fmt"
"github.com/halprin/fhirpath/context"
"golang.org/x/net/html"
"io"
"log"
"net/http"
"os"
)

type structureDefinition struct {
Name string `json:"name"`
Snapshot struct {
Element []struct {
Path string `json:"path"`
Type []struct {
Code string `json:"code"`
} `json:"type"`
} `json:"element"`
} `json:"snapshot"`
}

func main() {

fhirVersions := []string{
"R5",
"R4B",
"R4",
"STU3",
"DSTU2",
}

for _, version := range fhirVersions {
err := constructTypesForFhirVersion(version)
if err != nil {
log.Fatalf("Error occured during construction of context definitions: %s", err.Error())
}
}
}

func constructTypesForFhirVersion(fhirVersion string) error {
log.Printf("Constructing types for FHIR version %s\n", fhirVersion)

resources, err := getResourcesForVersion(fhirVersion)
if err != nil {
return err
}

resourceDefinitions := make([]structureDefinition, 0, len(resources))

for _, resource := range resources {
resourceDefinition, err := fetchStructureDefinition(fhirVersion, resource)
if err != nil {
return err
}

resourceDefinitions = append(resourceDefinitions, resourceDefinition)
}

log.Printf("Converting structure definitions to our definitions for FHIR version %s\n", fhirVersion)

types := context.Definition{
Version: fhirVersion,
Resources: map[string]context.ResourceTypeDefinition{},
}

for _, resourceDefinition := range resourceDefinitions {
resource := context.ResourceTypeDefinition{
Name: resourceDefinition.Name,
Fields: map[string]context.FieldTypes{},
}

for _, elem := range resourceDefinition.Snapshot.Element {
field := context.FieldTypes{Name: elem.Path}
for _, currentType := range elem.Type {
field.Types = append(field.Types, currentType.Code)
}
resource.Fields[field.Name] = field
}

types.Resources[resource.Name] = resource
}

log.Printf("Marshalling our definitions into JSON for FHIR version %s\n", fhirVersion)

jsonData, err := json.Marshal(types)
if err != nil {
return err
}

path := fmt.Sprintf("%s.json", fhirVersion)

log.Printf("Writing JSON data to file %s for FHIR version %s\n", path, fhirVersion)

err = os.WriteFile(path, jsonData, 0644)
if err != nil {
return err
}

return nil
}

func getResourcesForVersion(fhirVersion string) ([]string, error) {
log.Printf("Downloading FHIR resource list for version %s\n", fhirVersion)

url := fmt.Sprintf("https://hl7.org/fhir/%s/resourcelist.html", fhirVersion)

response, err := http.Get(url)
if err != nil {
return nil, err
}
defer response.Body.Close()

resources := make([]string, 0)

log.Printf("Parsing FHIR resources for FHIR version %s\n", fhirVersion)

tokenizer := html.NewTokenizer(response.Body)

var isInAlphabeticalDivTag bool
var isInResourceATag bool

for {
tokenType := tokenizer.Next()

switch {
case tokenType == html.ErrorToken:
return resources, nil
case tokenType == html.StartTagToken:
token := tokenizer.Token()
if isInAlphabeticalDivTag && token.Data == "a" {
for _, attribute := range token.Attr {
if attribute.Key == "title" && attribute.Val != "Maturity Level" && attribute.Val != "Normative Content" {
isInResourceATag = true
}
}
}
if token.Data == "div" {
for _, attribute := range token.Attr {
if attribute.Key == "id" && attribute.Val == "tabs-2" { // tab-2 is the alphabetical listing of resources
isInAlphabeticalDivTag = true
}
}
}
case tokenType == html.EndTagToken:
token := tokenizer.Token()
if isInAlphabeticalDivTag && token.Data == "div" {
isInAlphabeticalDivTag = false
}
if isInResourceATag && token.Data == "a" {
isInResourceATag = false
}
case tokenType == html.TextToken:
if isInResourceATag {
token := tokenizer.Token()
resources = append(resources, token.Data)
}
}
}
}

func fetchStructureDefinition(fhirVersion string, resource string) (structureDefinition, error) {
log.Printf("Fetching structure definition for FHIR version %s, resource %s\n", fhirVersion, resource)

url := fmt.Sprintf("https://hl7.org/fhir/%s/%s.profile.json", fhirVersion, resource)

response, err := http.Get(url)
if err != nil {
return structureDefinition{}, err
}
defer response.Body.Close()

responseBody, err := io.ReadAll(response.Body)
if err != nil {
return structureDefinition{}, err
}

log.Printf("Parsing structure definition for FHIR version %s, resource %s\n", fhirVersion, resource)

var resourceDefinition structureDefinition
err = json.Unmarshal(responseBody, &resourceDefinition)
if err != nil {
return structureDefinition{}, err
}

return resourceDefinition, nil
}
47 changes: 46 additions & 1 deletion evaluate.go
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ package fhirpath

import (
"encoding/json"
"github.com/halprin/fhirpath/context"
"github.com/halprin/fhirpath/internal/engine"
"github.com/halprin/fhirpath/internal/grammar"
)
@@ -11,17 +12,23 @@ import (
// This function is a generic function, so it takes a type parameter. Upon evaluation, any results that are not the same as the type parameter are filtered out. If you want nothing filtered out, use `any` as the type paramter.
func Evaluate[T any](fhirString string, fhirPath string) ([]T, error) {

return EvaluateWithContext[T](fhirString, fhirPath, context.Definition{})
}

func EvaluateWithContext[T any](fhirString string, fhirPath string, context context.Definition) ([]T, error) {
fhir, err := unmarshalFhir(fhirString)
if err != nil {
return nil, err
}

fhir = convertFhirNumbers(fhir)

tree, err := grammar.CreateTree(fhirPath)
if err != nil {
return nil, err
}

result, err := engine.Execute[T](fhir, tree)
result, err := engine.Execute[T](fhir, tree, context)
if err != nil {
return nil, err
}
@@ -39,3 +46,41 @@ func unmarshalFhir(fhir string) (map[string]interface{}, error) {

return fhirObject, nil
}

// convertFhirNumbers is a recursive function that converts numbers in the FHIR JSON
// from float64 to int if they are actually integers.
// The function takes a map[string]interface{} as input representing the FHIR JSON.
//
// The function iterates over each key-value pair in the map and calls the helper function
// convertFhirNumbersRecursive to convert any numbers to their corresponding int values.
//
// After iterating through all key-value pairs, the function returns the updated FHIR JSON map.
func convertFhirNumbers(fhir map[string]interface{}) map[string]interface{} {
for currentKey, currentValue := range fhir {
fhir[currentKey] = convertFhirNumbersRecursive(currentValue)
}

return fhir
}

func convertFhirNumbersRecursive(value interface{}) interface{} {
switch v := value.(type) {
case float64:
// Convert float64 to int if it's really an int
if float64(int(v)) == v {
return int(v)
}
return v
case map[string]interface{}:
// Process map values
for currentKey, currentValue := range v {
v[currentKey] = convertFhirNumbersRecursive(currentValue)
}
case []interface{}:
// Process slice values
for currentIndex, currentValue := range v {
v[currentIndex] = convertFhirNumbersRecursive(currentValue)
}
}
return value
}
Loading

0 comments on commit 3a30b81

Please sign in to comment.