feat: base go mkiso
This commit is contained in:
133
internal/repo/patient.go
Normal file
133
internal/repo/patient.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PatientData holds the response from the patient-data API.
|
||||
type PatientData struct {
|
||||
AccessionNumber string `json:"accession_number"`
|
||||
MedrecID string `json:"medrec_id"`
|
||||
RegID string `json:"reg_id"`
|
||||
PatientName string `json:"patient_name"`
|
||||
Modality string `json:"modality"`
|
||||
StudyDescription string `json:"study_description"`
|
||||
}
|
||||
|
||||
// PatientRepo is an HTTP client for the external patient-data API.
|
||||
type PatientRepo struct {
|
||||
baseURL string
|
||||
endpoint string
|
||||
httpClient *http.Client
|
||||
authHeader string
|
||||
authToken string
|
||||
retryCount int
|
||||
retrySleep time.Duration
|
||||
}
|
||||
|
||||
// NewPatientRepo creates a new PatientRepo.
|
||||
func NewPatientRepo(baseURL, endpoint, authHeader, authToken string, timeout time.Duration, retry int, retryBackoff time.Duration) *PatientRepo {
|
||||
return &PatientRepo{
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
endpoint: endpoint,
|
||||
httpClient: &http.Client{
|
||||
Timeout: timeout,
|
||||
},
|
||||
authHeader: authHeader,
|
||||
authToken: authToken,
|
||||
retryCount: retry,
|
||||
retrySleep: retryBackoff,
|
||||
}
|
||||
}
|
||||
|
||||
// ByAccessionNumber fetches patient data for the given accession number.
|
||||
// Returns (nil, nil) if:
|
||||
// - patient API is not configured (empty base URL)
|
||||
// - accession number is not found (HTTP 404)
|
||||
// This allows callers to gracefully degrade when patient info is unavailable.
|
||||
func (r *PatientRepo) ByAccessionNumber(ctx context.Context, acc string) (*PatientData, error) {
|
||||
if r.baseURL == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
u, err := url.Parse(r.baseURL + r.endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid patient API URL: %w", err)
|
||||
}
|
||||
u.RawQuery = url.Values{"accession_number": {acc}}.Encode()
|
||||
|
||||
var lastErr error
|
||||
for attempt := 0; attempt <= r.retryCount; attempt++ {
|
||||
if attempt > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(r.retrySleep):
|
||||
}
|
||||
}
|
||||
|
||||
data, err := r.fetchPatientData(ctx, u.String(), acc)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
lastErr = err
|
||||
|
||||
if !isRetryable(err) {
|
||||
return nil, lastErr
|
||||
}
|
||||
}
|
||||
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
// fetchPatientData performs a single HTTP request to the patient API.
|
||||
func (r *PatientRepo) fetchPatientData(ctx context.Context, requestURL, acc string) (*PatientData, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", requestURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
||||
if r.authHeader != "" && r.authToken != "" {
|
||||
req.Header.Set(r.authHeader, r.authToken)
|
||||
}
|
||||
|
||||
resp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("patient API request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
var data PatientData
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return nil, fmt.Errorf("decode patient API response: %w", err)
|
||||
}
|
||||
return &data, nil
|
||||
|
||||
case http.StatusNotFound:
|
||||
return nil, nil // not found is not an error — gracefully degrade
|
||||
|
||||
default:
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("patient API returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
}
|
||||
|
||||
// isRetryable returns true if the error is a transient network error
|
||||
// that might succeed on retry.
|
||||
func isRetryable(err error) bool {
|
||||
msg := err.Error()
|
||||
return strings.Contains(msg, "timeout") ||
|
||||
strings.Contains(msg, "connection refused") ||
|
||||
strings.Contains(msg, "no such host") ||
|
||||
strings.Contains(msg, "connection reset") ||
|
||||
strings.Contains(msg, "Temporary failure")
|
||||
}
|
||||
Reference in New Issue
Block a user