Files
stack/dev/vendor/bcda-app/bcda/responseutils/v3/writer.go
kert 7e53d9360a chore(bcda): vendor CMSgov/bcda-app + bcda.cms.gov v3 artefacts sparsely, flatten the v3 data dictionary into crosswalk seeds, weekly drift workflow
dev/vendor/MANIFEST.toml pins bcda-app@7decdcc (v3 handler + tests, request
validation middleware, OperationOutcome writer, v3 Postman collection, gzip
fixtures, sandbox config, LICENSE) and the bcda.cms.gov v3 data dictionary,
v2 dictionary and official v3 sample ndjson. vendor_bcda_app.py --verify |
--check | --sync (stdlib only); the bcda-vendor workflow runs --check weekly
and files an issue on drift — nothing auto-commits, a payload change upstream
is something a human reads before the flattener is re-pointed.

gen_bcda_v3_crosswalk.py writes dev/seeds/bcda_v3_data_dictionary.csv
(fhirPath, IDR sourceColumn, ccwMapping, cclfMapping per v3 field),
bcda_v3_cclf_crosswalk.csv (293 CCLF elements -> v3 field) and
bcda_v3_cclf_absent.csv (52 CCLF fields v3 does not carry); the CCLF sheet
has label/name swapped on data rows and is normalised. Tests assert the
committed seeds match the vendored workbook.

refs #1139
2026-10-25 13:08:05 -04:00

178 lines
5.4 KiB
Go

package responseutils
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/CMSgov/bcda-app/bcda/constants"
"github.com/CMSgov/bcda-app/bcda/fhir/r4"
"github.com/CMSgov/bcda-app/bcda/models"
"github.com/CMSgov/bcda-app/log"
"github.com/ccoveille/go-safecast"
)
type FhirResponseWriter struct{}
func NewFhirResponseWriter() FhirResponseWriter {
return FhirResponseWriter{}
}
func (r FhirResponseWriter) Success(ctx context.Context, w http.ResponseWriter, statusCode int, errType, msg string) {
oo := r.CreateOpOutcome(r4.IssueSeverityInformation, r4.IssueTypeCodeInformational, errType, msg)
r.WriteError(ctx, oo, w, statusCode)
}
func (r FhirResponseWriter) Exception(ctx context.Context, w http.ResponseWriter, statusCode int, errType, errMsg string) {
oo := r.CreateOpOutcome(r4.IssueSeverityError, r4.IssueTypeCodeException, errType, errMsg)
r.WriteError(ctx, oo, w, statusCode)
}
func (r FhirResponseWriter) NotFound(ctx context.Context, w http.ResponseWriter, statusCode int, errType, errMsg string) {
oo := r.CreateOpOutcome(r4.IssueSeverityError, r4.IssueTypeCodeNotFound, errType, errMsg)
r.WriteError(ctx, oo, w, statusCode)
}
func (r FhirResponseWriter) OpOutcome(ctx context.Context, w http.ResponseWriter, statusCode int, errType, errMsg string) {
respStatusToFHIRStatusMap := map[int]r4.IssueTypeCode{
400: r4.IssueTypeCodeStructure,
401: r4.IssueTypeCodeForbidden,
403: r4.IssueTypeCodeForbidden,
410: r4.IssueTypeCodeNotFound,
}
oo := r.CreateOpOutcome(r4.IssueSeverityError, respStatusToFHIRStatusMap[statusCode], errType, errMsg)
r.WriteError(ctx, oo, w, statusCode)
}
func (r FhirResponseWriter) JobsBundle(ctx context.Context, w http.ResponseWriter, jobs []*models.Job, host string) {
jb := r.CreateJobsBundle(jobs, host)
r.WriteBundleResponse(jb, w)
}
func (r FhirResponseWriter) CreateJobsBundle(jobs []*models.Job, host string) *r4.Bundle {
var entries []r4.BundleEntry
// generate bundle task entries
for _, job := range jobs {
entry := r.CreateJobsBundleEntry(job, host)
entries = append(entries, *entry)
}
jobLength, err := safecast.ToUint32(len(jobs))
if err != nil {
log.API.Errorln(err)
}
return &r4.Bundle{
ResourceType: "Bundle",
Type: "searchset",
Total: jobLength,
Entry: entries,
}
}
func (r FhirResponseWriter) CreateJobsBundleEntry(job *models.Job, host string) *r4.BundleEntry {
fhirStatusCode := r.GetFhirStatusCode(job.Status)
return &r4.BundleEntry{
Resource: &r4.Task{
ResourceType: "Task",
Identifier: []r4.Identifier{
{
Use: "official",
System: host + "/api/v3/jobs",
Value: fmt.Sprint(job.ID),
},
},
Status: r4.TaskStatus(fhirStatusCode),
Intent: r4.TaskIntentOrder,
Input: []r4.Parameter{
{
Type: r4.CodeableConcept{
Text: "BULK FHIR Export",
},
Value: "GET " + job.RequestURL,
},
},
ExecutionPeriod: r4.Period{
Start: job.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"),
End: job.UpdatedAt.UTC().Format("2006-01-02T15:04:05Z"),
},
},
}
}
func (r FhirResponseWriter) GetFhirStatusCode(status models.JobStatus) r4.TaskStatus {
switch status {
case models.JobStatusFailed, models.JobStatusFailedExpired:
return r4.TaskStatusFailed
case models.JobStatusPending:
return r4.TaskStatusAccepted
case models.JobStatusInProgress:
return r4.TaskStatusInProgress
case models.JobStatusCompleted:
return r4.TaskStatusCompleted
case models.JobStatusArchived, models.JobStatusExpired:
return r4.TaskStatusCompleted // fhir task status does not have an equivalent to `expired` or `archived`
case models.JobStatusCancelled, models.JobStatusCancelledExpired:
return r4.TaskStatusCancelled
}
return ""
}
func (r FhirResponseWriter) CreateOpOutcome(severity r4.IssueSeverityCode, code r4.IssueTypeCode, errType, diagnostics string) *r4.OperationOutcome {
return &r4.OperationOutcome{
ResourceType: "OperationOutcome",
Issue: []r4.Issue{
{
Severity: severity,
Code: code,
Diagnostics: diagnostics,
Details: &r4.CodeableConcept{
// Coding is intentionally omitted for v3 to conform with FHIR base profile
Text: diagnostics,
},
},
},
}
}
func (r FhirResponseWriter) WriteError(ctx context.Context, outcome *r4.OperationOutcome, w http.ResponseWriter, code int) {
logger := log.GetCtxLogger(ctx)
w.Header().Set(constants.ContentType, constants.FHIRJsonContentType)
w.WriteHeader(code)
_, err := r.WriteOperationOutcome(w, outcome)
if err != nil {
logger.WithField("resp_status", http.StatusInternalServerError).Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (r FhirResponseWriter) WriteOperationOutcome(w io.Writer, outcome *r4.OperationOutcome) (int, error) {
outcomeJSON, err := json.Marshal(outcome)
if err != nil {
return -1, err
}
return w.Write(outcomeJSON)
}
func (r FhirResponseWriter) WriteBundleResponse(bundle *r4.Bundle, w http.ResponseWriter) {
bundleJSON, err := json.Marshal(bundle)
if err != nil {
log.API.WithField("resp_status", http.StatusInternalServerError).Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set(constants.ContentType, constants.JsonContentType)
w.WriteHeader(http.StatusOK)
_, err = w.Write(bundleJSON)
if err != nil {
log.API.WithField("resp_status", http.StatusInternalServerError).Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}