From 78187b8aab6fafb214be9ae25d659811bba990b7 Mon Sep 17 00:00:00 2001 From: Daniel Carbone Date: Fri, 21 Feb 2025 15:54:32 -0600 Subject: [PATCH 1/2] feature/put --- http.go | 63 ++++++++++++++++++++++++++++------------------------ main.go | 2 +- resources.go | 25 +++++++++++---------- 3 files changed, 48 insertions(+), 42 deletions(-) diff --git a/http.go b/http.go index 241eb77..362931b 100644 --- a/http.go +++ b/http.go @@ -41,21 +41,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,7 +80,7 @@ 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) @@ -78,27 +89,25 @@ func handlerGetVersionResource(fv FHIRVersion, rscType string) http.HandlerFunc 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) - return - } + rscType := r.PathValue("rsc_type") + rscId := r.PathValue("rsc_id") - rsc := versionResourceMap[fv].GetResource(rscType, resourceId) + versionResourceMapMu.RLock() + rsc := versionResourceMap[fv].GetResource(rscType, rscId) + versionResourceMapMu.RUnlock() - if nil != rsc { - respondInKind(w, r, rsc) + 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 } - 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, rsc) } } 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,22 +117,18 @@ 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)) - - for _, rscType := range resourceMap.ResourceTypes() { - // get version resource bundle - addHandler(log, mux, fmt.Sprintf("GET /%s/%s/", fv.String(), rscType), handlerGetResourceBundle(fv, rscType)) - - // get specific version resource by id - addHandler(log, mux, fmt.Sprintf("GET /%s/%s/{resource_id}/", fv.String(), rscType), handlerGetVersionResource(fv, rscType)) - } - } - // get version list addHandler(log, mux, "GET /{$}", handlerGetVersionList()) + versionResourceMapMu.RLock() + for fv := range versionResourceMap { + // get version resource list + 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)) + } + versionResourceMapMu.RUnlock() + log.Info("Webserver running", "addr", bindAddr) return http.ListenAndServe(bindAddr, mux) 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) } From 1229db231ded995072a2418b0fa4ef148bb10fdf Mon Sep 17 00:00:00 2001 From: Daniel Carbone Date: Sat, 20 Sep 2025 08:15:07 -0500 Subject: [PATCH 2/2] checking in half thoughts --- docker/Dockerfile | 2 ++ http.go | 34 ++++++++++++++++++++++++++++------ http_response.go | 14 ++++++++++++++ 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 7da57ea..6699cb2 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 362931b..13e448c 100644 --- a/http.go +++ b/http.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "io" "log/slog" "net/http" "sync/atomic" @@ -83,11 +84,6 @@ func handlerGetVersionResourceBundle(fv FHIRVersion) 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) - return - } rscType := r.PathValue("rsc_type") rscId := r.PathValue("rsc_id") @@ -106,6 +102,29 @@ func handlerGetVersionResource(fv FHIRVersion) http.HandlerFunc { } } +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 + } + + 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 + } + + respondInKind(w, r, b) + } +} + func addHandler(log *slog.Logger, mux *http.ServeMux, route string, hdl http.HandlerFunc) { log.Debug("Adding route handler", "route", route) @@ -122,10 +141,13 @@ func runWebserver(log *slog.Logger) error { versionResourceMapMu.RLock() for fv := range versionResourceMap { - // get version resource list + // 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)) + + // version write handlers + addHandler(log, mux, fmt.Sprintf("PUT /%s/{rsc_type}/{rsc_id}", fv.String()), handlerPutVersionResource(fv)) } versionResourceMapMu.RUnlock() 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)