Skip to content
Draft
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
2 changes: 2 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ LABEL org.opencontainers.image.url="https://github.com/dcarbone/php-fhir-test"
LABEL org.opencontainers.image.source="https://github.com/dcarbone/php-fhir-test"
LABEL org.opencontainers.image.licenses="Apache-2.0"
LABEL org.opencontainers.image.title="PHP FHIR Test Server Build Image"
LABEL org.opencontainers.image.description="PHP FHIR test API server"

RUN apk add --update make

Expand All @@ -25,6 +26,7 @@ LABEL org.opencontainers.image.url="https://github.com/dcarbone/php-fhir-test"
LABEL org.opencontainers.image.source="https://github.com/dcarbone/php-fhir-test"
LABEL org.opencontainers.image.licenses="Apache-2.0"
LABEL org.opencontainers.image.title="PHP FHIR Test Server Image"
LABEL org.opencontainers.image.description="PHP FHIR test API server"

COPY --from=build /app/bin/php-fhir-test-server /php-fhir-test-server

Expand Down
87 changes: 57 additions & 30 deletions http.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"io"
"log/slog"
"net/http"
"sync/atomic"
Expand Down Expand Up @@ -41,21 +42,32 @@ func handlerGetVersionList() http.HandlerFunc {
}
}

func handlerGetVersionResourceList(fv FHIRVersion) http.HandlerFunc {
func handlerGetVersionResourceTypeList(fv FHIRVersion) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != fmt.Sprintf("/%s", fv) && r.URL.Path != fmt.Sprintf("/%s/", fv) {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
respondInKind(w, r, versionResourceMap[fv])
versionResourceMapMu.RLock()
rm := versionResourceMap[fv]
versionResourceMapMu.RUnlock()
respondInKind(w, r, rm)
}
}

func handlerGetResourceBundle(fv FHIRVersion, rscType string) http.HandlerFunc {
func handlerGetVersionResourceBundle(fv FHIRVersion) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rscType := r.PathValue("rsc_type")
rp := getRequestParams(r)

versionResourceMapMu.RLock()
rscs := versionResourceMap[fv].GetResourcesByType(rscType, rp.Count)
versionResourceMapMu.RUnlock()

if len(rscs) == 0 {
http.Error(w, fmt.Sprintf("No resources of type %q for version %q found.", rscType, fv.String()), http.StatusNotFound)
return
}

out := Bundle{
ResourceType: "Bundle",
Expand All @@ -69,36 +81,52 @@ func handlerGetResourceBundle(fv FHIRVersion, rscType string) http.HandlerFunc {
}
}

func handlerGetVersionResource(fv FHIRVersion, rscType string) http.HandlerFunc {
func handlerGetVersionResource(fv FHIRVersion) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log := getRequestLogger(r)
rp := getRequestParams(r)
if rp.Count != 0 {
http.Error(w, "_count must be zero or undefined with specific resource ID", http.StatusBadRequest)

rscType := r.PathValue("rsc_type")
rscId := r.PathValue("rsc_id")

versionResourceMapMu.RLock()
rsc := versionResourceMap[fv].GetResource(rscType, rscId)
versionResourceMapMu.RUnlock()

if nil == rsc {
log.Error("Resource not found", "rsc_id", rscId)
http.Error(w, fmt.Sprintf("no version %q resource %q found with id %q", fv.String(), rscType, rscId), http.StatusNotFound)
return
}

resourceId := r.PathValue("resource_id")
if resourceId == "" {
log.Error("Unable to parse resource_id param from path")
http.Error(w, "missing resource_id path parameter", http.StatusBadRequest)
respondInKind(w, r, rsc)
}
}

func handlerPutVersionResource(_ FHIRVersion) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log := getRequestLogger(r)

// todo: need to do better with this impl...

if r.Body == nil {
log.Error("Empty body seen")
http.Error(w, "request body must not be empty", http.StatusBadRequest)
return
}

rsc := versionResourceMap[fv].GetResource(rscType, resourceId)

if nil != rsc {
respondInKind(w, r, rsc)
b, err := io.ReadAll(r.Body)
if err != nil {
log.Error("Error reading PUT body", "err", err)
http.Error(w, fmt.Sprintf("error reading request body: %v", err), http.StatusUnprocessableEntity)
return
}

log.Error("Resource not found", "resource_id", resourceId)
http.Error(w, fmt.Sprintf("no version %q resource %q found with id %q", fv.String(), rscType, resourceId), http.StatusNotFound)
respondInKind(w, r, b)
}
}

func addHandler(log *slog.Logger, mux *http.ServeMux, route string, hdl http.HandlerFunc) {
log.Info("Adding route handler", "route", route)
log.Debug("Adding route handler", "route", route)

mux.HandleFunc(route, middlewareEmbedLogger(log.With("route", route), middlewareParseRequestParams(hdl)))
}
Expand All @@ -108,21 +136,20 @@ func runWebserver(log *slog.Logger) error {

mux := http.NewServeMux()

for fv, resourceMap := range versionResourceMap {
// get version resource list
addHandler(log, mux, fmt.Sprintf("GET /%s/", fv.String()), handlerGetVersionResourceList(fv))
// get version list
addHandler(log, mux, "GET /{$}", handlerGetVersionList())

for _, rscType := range resourceMap.ResourceTypes() {
// get version resource bundle
addHandler(log, mux, fmt.Sprintf("GET /%s/%s/", fv.String(), rscType), handlerGetResourceBundle(fv, rscType))
versionResourceMapMu.RLock()
for fv := range versionResourceMap {
// version read handlers
addHandler(log, mux, fmt.Sprintf("GET /%s/", fv.String()), handlerGetVersionResourceTypeList(fv))
addHandler(log, mux, fmt.Sprintf("GET /%s/{rsc_type}", fv.String()), handlerGetVersionResourceBundle(fv))
addHandler(log, mux, fmt.Sprintf("GET /%s/{rsc_type}/{rsc_id}", fv.String()), handlerGetVersionResource(fv))

// get specific version resource by id
addHandler(log, mux, fmt.Sprintf("GET /%s/%s/{resource_id}/", fv.String(), rscType), handlerGetVersionResource(fv, rscType))
}
// version write handlers
addHandler(log, mux, fmt.Sprintf("PUT /%s/{rsc_type}/{rsc_id}", fv.String()), handlerPutVersionResource(fv))
}

// get version list
addHandler(log, mux, "GET /{$}", handlerGetVersionList())
versionResourceMapMu.RUnlock()

log.Info("Webserver running", "addr", bindAddr)

Expand Down
14 changes: 14 additions & 0 deletions http_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ func respondInKind(w http.ResponseWriter, r *http.Request, data any) {

switch true {
case rp.AcceptFormat.IsJson():
if b, ok := data.([]byte); ok {
if _, err = w.Write(b); err != nil {
log.Error("Error sending already encoded JSON", "err", err)
}
return
}

je := json.NewEncoder(w)
if rp.Pretty {
je.SetIndent("", " ")
Expand All @@ -40,6 +47,13 @@ func respondInKind(w http.ResponseWriter, r *http.Request, data any) {
}

case rp.AcceptFormat.IsXml():
if b, ok := data.([]byte); ok {
if _, err = w.Write(b); err != nil {
log.Error("Error sending already encoded XML", "err", err)
}
return
}

// write header
if _, err = w.Write([]byte(xml.Header)); err != nil {
log.Error("Error writing XML lead in", "err", err)
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (

var (
//go:embed resources.tar.gz
resourcesTar []byte
seedResourcesTarball []byte

bindAddr = "127.0.0.1:8080"

Expand Down
25 changes: 13 additions & 12 deletions resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ import (
)

var (
versionResourceMap map[FHIRVersion]*ResourceMap
versionResourceMapMu sync.RWMutex
versionResourceMap map[FHIRVersion]*ResourceMap
)

func init() {
versionResourceMap = make(map[FHIRVersion]*ResourceMap)
for fv := FHIRVersionDSTU1; fv <= FHIRVersionR5; fv++ {
for fv := FHIRVersionDSTU1; fv <= FHIRVersionMock; fv++ {
versionResourceMap[fv] = newResourceMap(fv)
}
}
Expand Down Expand Up @@ -268,9 +269,11 @@ func (rm *ResourceMap) MarshalXML(xe *xml.Encoder, _ xml.StartElement) error {
}

func versionList() FHIRVersions {
out := make(FHIRVersions, 0)
for fv := FHIRVersionDSTU1; fv <= FHIRVersionR5; fv++ {
out = append(out, fv)
out := make(FHIRVersions, len(versionResourceMap))
i := 0
for fv := range versionResourceMap {
out[i] = fv
i++
}
slices.SortFunc(out, fhirVersionSemanticSortFunc(true))
return out
Expand Down Expand Up @@ -299,18 +302,16 @@ func parseSeedResources(ctx context.Context, tr *tar.Reader, th *tar.Header, fv
}

func extractSeedResources(ctx context.Context, log *slog.Logger) error {
versionResourceMapMu.Lock()
defer versionResourceMapMu.Unlock()

var (
fv FHIRVersion
)

log.Info("Extracting FHIR resources...")

defer func() {
// zero out the resources tar, free up some memory
resourcesTar = nil
}()
log.Info("Seeding FHIR resources from embedded tarball...")

gr, err := gzip.NewReader(bytes.NewReader(resourcesTar))
gr, err := gzip.NewReader(bytes.NewReader(seedResourcesTarball))
if err != nil {
return fmt.Errorf("error creating gzip reader: %w", err)
}
Expand Down