diff --git a/docker/Dockerfile b/docker/Dockerfile index 8668fe2..df4280d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -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 @@ -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 diff --git a/http.go b/http.go index 241eb77..13e448c 100644 --- a/http.go +++ b/http.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "io" "log/slog" "net/http" "sync/atomic" @@ -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", @@ -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))) } @@ -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) diff --git a/http_response.go b/http_response.go index 6bd7cf5..b6d3e35 100644 --- a/http_response.go +++ b/http_response.go @@ -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("", " ") @@ -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) diff --git a/main.go b/main.go index 971b46e..0c129d0 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,7 @@ import ( var ( //go:embed resources.tar.gz - resourcesTar []byte + seedResourcesTarball []byte bindAddr = "127.0.0.1:8080" diff --git a/resources.go b/resources.go index 1c46c42..3504860 100644 --- a/resources.go +++ b/resources.go @@ -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) } } @@ -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 @@ -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) }