Saltar al contenido
Patrones del Mundo Real

Patrones del Mundo Real

Esta página muestra cómo la biblioteca FHIRPath de Go se integra en servicios Go de producción. Cada patrón es autocontenido y demuestra un escenario de integración común.

Middleware HTTP para Evaluación de Parámetros de Búsqueda FHIR®

Un servidor FHIR® frecuentemente necesita evaluar parámetros de búsqueda contra recursos almacenados. El siguiente middleware evalúa una expresión FHIRPath proporcionada en un parámetro de consulta y retorna el resultado como JSON.

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"

	"github.com/gofhir/fhirpath"
)

// FHIRPathHandler evaluates a FHIRPath expression against a posted FHIR resource.
//
// Usage:
//   POST /fhirpath?expr=Patient.name.family
//   Body: { "resourceType": "Patient", ... }
//
// Response:
//   { "result": ["Smith"], "count": 1 }
func FHIRPathHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST required", http.StatusMethodNotAllowed)
		return
	}

	expr := r.URL.Query().Get("expr")
	if expr == "" {
		http.Error(w, `missing "expr" query parameter`, http.StatusBadRequest)
		return
	}

	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "failed to read body", http.StatusBadRequest)
		return
	}
	defer r.Body.Close()

	// Validate that the body is valid JSON
	if !json.Valid(body) {
		http.Error(w, "body is not valid JSON", http.StatusBadRequest)
		return
	}

	// Use EvaluateCached for automatic expression caching
	result, err := fhirpath.EvaluateCached(body, expr)
	if err != nil {
		http.Error(w, fmt.Sprintf("evaluation error: %v", err), http.StatusUnprocessableEntity)
		return
	}

	// Convert the result to a JSON-serializable form
	items := make([]interface{}, len(result))
	for i, v := range result {
		items[i] = v.String()
	}

	response := map[string]interface{}{
		"result": items,
		"count":  len(items),
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(response)
}

func main() {
	http.HandleFunc("/fhirpath", FHIRPathHandler)
	log.Println("Listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Prueba del Middleware

curl -X POST "http://localhost:8080/fhirpath?expr=Patient.name.family" \
  -H "Content-Type: application/json" \
  -d '{"resourceType":"Patient","name":[{"family":"Smith","given":["John"]}]}'

Respuesta esperada:

{"count":1,"result":["Smith"]}

Pipeline de Procesamiento por Lotes

Cuando necesita evaluar expresiones contra una gran cantidad de recursos (por ejemplo, extrayendo índices de búsqueda), precompile la expresión una vez y reutilícela:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"sync"

	"github.com/gofhir/fhirpath"
)

// ExtractionRule defines a field to extract from a resource.
type ExtractionRule struct {
	Name       string
	Expression *fhirpath.Expression
}

// ExtractionResult holds the extracted values for one resource.
type ExtractionResult struct {
	ResourceID string
	Fields     map[string][]string
	Errors     []string
}

// BatchExtractor precompiles expressions and runs them against many resources.
type BatchExtractor struct {
	rules []ExtractionRule
}

// NewBatchExtractor creates an extractor from a map of name -> FHIRPath expression strings.
func NewBatchExtractor(expressions map[string]string) (*BatchExtractor, error) {
	rules := make([]ExtractionRule, 0, len(expressions))
	for name, expr := range expressions {
		compiled, err := fhirpath.Compile(expr)
		if err != nil {
			return nil, fmt.Errorf("failed to compile %q: %w", name, err)
		}
		rules = append(rules, ExtractionRule{Name: name, Expression: compiled})
	}
	return &BatchExtractor{rules: rules}, nil
}

// Extract evaluates all rules against a single resource.
func (b *BatchExtractor) Extract(resource []byte) ExtractionResult {
	result := ExtractionResult{
		Fields: make(map[string][]string),
	}

	// Try to get the resource ID
	id, _ := fhirpath.EvaluateToString(resource, "id")
	result.ResourceID = id

	for _, rule := range b.rules {
		col, err := rule.Expression.Evaluate(resource)
		if err != nil {
			result.Errors = append(result.Errors,
				fmt.Sprintf("%s: %v", rule.Name, err))
			continue
		}
		values := make([]string, len(col))
		for i, v := range col {
			values[i] = v.String()
		}
		result.Fields[rule.Name] = values
	}

	return result
}

// ExtractBatch processes multiple resources concurrently.
func (b *BatchExtractor) ExtractBatch(resources [][]byte, workers int) []ExtractionResult {
	results := make([]ExtractionResult, len(resources))
	var wg sync.WaitGroup
	sem := make(chan struct{}, workers)

	for i, resource := range resources {
		wg.Add(1)
		sem <- struct{}{} // acquire semaphore slot

		go func(idx int, res []byte) {
			defer wg.Done()
			defer func() { <-sem }() // release semaphore slot

			results[idx] = b.Extract(res)
		}(i, resource)
	}

	wg.Wait()
	return results
}

func main() {
	// Define extraction rules for Patient resources
	extractor, err := NewBatchExtractor(map[string]string{
		"family":    "Patient.name.where(use = 'official').family",
		"given":     "Patient.name.where(use = 'official').given",
		"birthDate": "Patient.birthDate",
		"city":      "Patient.address.where(use = 'home').city",
		"phone":     "Patient.telecom.where(system = 'phone').value",
	})
	if err != nil {
		log.Fatal(err)
	}

	// Simulate a batch of patients
	patients := [][]byte{
		[]byte(`{
			"resourceType": "Patient", "id": "p1",
			"name": [{"use": "official", "family": "Smith", "given": ["Alice"]}],
			"birthDate": "1990-05-15",
			"address": [{"use": "home", "city": "Portland"}],
			"telecom": [{"system": "phone", "value": "555-0001"}]
		}`),
		[]byte(`{
			"resourceType": "Patient", "id": "p2",
			"name": [{"use": "official", "family": "Johnson", "given": ["Bob", "James"]}],
			"birthDate": "1985-11-20",
			"address": [{"use": "home", "city": "Seattle"}],
			"telecom": [{"system": "phone", "value": "555-0002"}, {"system": "email", "value": "bob@example.com"}]
		}`),
		[]byte(`{
			"resourceType": "Patient", "id": "p3",
			"name": [{"use": "official", "family": "Williams", "given": ["Carol"]}],
			"birthDate": "1978-03-08",
			"address": [{"use": "work", "city": "Denver"}],
			"telecom": [{"system": "email", "value": "carol@example.com"}]
		}`),
	}

	// Process with 4 concurrent workers
	results := extractor.ExtractBatch(patients, 4)

	// Output results
	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	for _, r := range results {
		fmt.Printf("--- Patient %s ---\n", r.ResourceID)
		enc.Encode(r.Fields)
		if len(r.Errors) > 0 {
			fmt.Println("  Errors:", r.Errors)
		}
	}
}

Endpoint de Validación de Expresiones

Antes de almacenar una expresión FHIRPath proporcionada por el usuario (por ejemplo, en una base de datos de configuración), debe validar que se compile correctamente. Este endpoint hace exactamente eso:

package main

import (
	"encoding/json"
	"log"
	"net/http"

	"github.com/gofhir/fhirpath"
)

type ValidateRequest struct {
	Expression string `json:"expression"`
}

type ValidateResponse struct {
	Valid   bool   `json:"valid"`
	Error   string `json:"error,omitempty"`
}

func ValidateExpressionHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST required", http.StatusMethodNotAllowed)
		return
	}

	var req ValidateRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid JSON body", http.StatusBadRequest)
		return
	}

	if req.Expression == "" {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(ValidateResponse{
			Valid: false,
			Error: "expression is required",
		})
		return
	}

	// Try to compile the expression
	_, err := fhirpath.Compile(req.Expression)

	resp := ValidateResponse{Valid: err == nil}
	if err != nil {
		resp.Error = err.Error()
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

func main() {
	http.HandleFunc("/validate-expression", ValidateExpressionHandler)
	log.Println("Listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Prueba del Endpoint de Validación

Expresión válida:

curl -X POST http://localhost:8080/validate-expression \
  -H "Content-Type: application/json" \
  -d '{"expression": "Patient.name.where(use = '\''official'\'').family"}'
{"valid": true}

Expresión inválida:

curl -X POST http://localhost:8080/validate-expression \
  -H "Content-Type: application/json" \
  -d '{"expression": "Patient.name.where(!!!"}'
{"valid": false, "error": "...parse error details..."}

Patrones de Manejo de Errores en Producción

El manejo robusto de errores es esencial para código de producción. Estos son los patrones que debe seguir.

Distinción entre Errores de Compilación y Evaluación

package main

import (
	"errors"
	"fmt"
	"log"

	"github.com/gofhir/fhirpath"
)

func safeEvaluate(resource []byte, expr string) {
	// Step 1: Compile the expression (catches syntax errors)
	compiled, err := fhirpath.Compile(expr)
	if err != nil {
		log.Printf("COMPILE ERROR for %q: %v", expr, err)
		return
	}

	// Step 2: Evaluate against the resource (catches runtime errors)
	result, err := compiled.Evaluate(resource)
	if err != nil {
		log.Printf("EVAL ERROR for %q: %v", expr, err)
		return
	}

	fmt.Printf("Expression: %s\n", expr)
	fmt.Printf("Result:     %v (count: %d)\n\n", result, len(result))
}

func main() {
	patient := []byte(`{
		"resourceType": "Patient",
		"id": "pat-err",
		"name": [{"family": "TestPatient"}]
	}`)

	// Valid expression
	safeEvaluate(patient, "Patient.name.family")

	// Syntax error
	safeEvaluate(patient, "Patient.name.!!!")

	// Valid expression but path does not exist -- returns empty, not an error
	safeEvaluate(patient, "Patient.maritalStatus.coding.code")
}

Envolviendo Errores con Contexto

func extractField(resource []byte, resourceID, fieldName, expr string) (string, error) {
	result, err := fhirpath.EvaluateCached(resource, expr)
	if err != nil {
		return "", fmt.Errorf(
			"failed to extract %s from resource %s: %w",
			fieldName, resourceID, err,
		)
	}

	if result.Empty() {
		return "", nil // field is absent -- not an error in FHIR
	}

	return result[0].String(), nil
}

Degradación Elegante

En muchos escenarios de producción, se desea extraer la mayor cantidad de datos posible, registrando las fallas en lugar de abortar:

package main

import (
	"fmt"
	"log"

	"github.com/gofhir/fhirpath"
)

// FieldSpec defines a field to extract.
type FieldSpec struct {
	Name       string
	Expression string
	Required   bool
}

// ExtractFields extracts all specified fields, collecting errors for failed ones.
func ExtractFields(resource []byte, specs []FieldSpec) (map[string]string, []error) {
	fields := make(map[string]string)
	var errs []error

	for _, spec := range specs {
		value, err := fhirpath.EvaluateToString(resource, spec.Expression)
		if err != nil {
			errs = append(errs, fmt.Errorf("field %s: %w", spec.Name, err))
			continue
		}

		if value == "" && spec.Required {
			errs = append(errs,
				fmt.Errorf("field %s: required but empty", spec.Name))
			continue
		}

		fields[spec.Name] = value
	}

	return fields, errs
}

func main() {
	patient := []byte(`{
		"resourceType": "Patient",
		"id": "pat-degrade",
		"name": [{"family": "Torres", "given": ["Sofia"]}],
		"gender": "female"
	}`)

	specs := []FieldSpec{
		{Name: "family", Expression: "Patient.name.family", Required: true},
		{Name: "given", Expression: "Patient.name.given.first()", Required: true},
		{Name: "gender", Expression: "Patient.gender", Required: false},
		{Name: "birthDate", Expression: "Patient.birthDate", Required: true},
		{Name: "phone", Expression: "Patient.telecom.where(system = 'phone').value", Required: false},
	}

	fields, errs := ExtractFields(patient, specs)

	fmt.Println("Extracted fields:")
	for k, v := range fields {
		fmt.Printf("  %s = %s\n", k, v)
	}

	if len(errs) > 0 {
		fmt.Println("\nWarnings/Errors:")
		for _, e := range errs {
			log.Printf("  %v", e)
		}
	}
}

Monitoreo del Rendimiento de la Caché

En servicios de larga ejecución, monitoree la caché de expresiones para asegurar buenas tasas de acierto:

package main

import (
	"fmt"
	"log"
	"net/http"
	"encoding/json"

	"github.com/gofhir/fhirpath"
)

// CacheStatsHandler exposes expression cache metrics.
func CacheStatsHandler(w http.ResponseWriter, r *http.Request) {
	stats := fhirpath.DefaultCache.Stats()

	response := map[string]interface{}{
		"size":     stats.Size,
		"limit":    stats.Limit,
		"hits":     stats.Hits,
		"misses":   stats.Misses,
		"hit_rate": fhirpath.DefaultCache.HitRate(),
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(response)
}

func main() {
	// Simulate some cache usage
	patient := []byte(`{"resourceType":"Patient","name":[{"family":"Doe"}]}`)

	for i := 0; i < 100; i++ {
		fhirpath.EvaluateCached(patient, "Patient.name.family")
	}
	for i := 0; i < 50; i++ {
		fhirpath.EvaluateCached(patient, "Patient.id")
	}

	stats := fhirpath.DefaultCache.Stats()
	fmt.Printf("Cache size: %d / %d\n", stats.Size, stats.Limit)
	fmt.Printf("Hits: %d, Misses: %d\n", stats.Hits, stats.Misses)
	fmt.Printf("Hit rate: %.1f%%\n", fhirpath.DefaultCache.HitRate())

	// Expose as an HTTP endpoint
	http.HandleFunc("/metrics/cache", CacheStatsHandler)
	log.Println("Cache metrics at :8080/metrics/cache")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Resumen de Mejores Prácticas

PrácticaPor Qué
Usar EvaluateCached en manejadores de solicitudesEvita recompilar la misma expresión en cada solicitud
Usar Compile + Expression.Evaluate en trabajos por lotesCompilar una vez, evaluar muchas veces para máximo rendimiento
Validar expresiones proporcionadas por usuarios antes de almacenarlasDetecta errores de sintaxis temprano, antes de que causen fallas en tiempo de ejecución
Registrar errores de evaluación pero no abortarLos datos FHIR® son inherentemente variables; los campos ausentes son normales
Monitorear DefaultCache.Stats()Asegura que la caché tenga el tamaño correcto para su carga de trabajo
Usar workers concurrentes con un semáforoAprovecha la concurrencia de Go sin sobrecargar la CPU
Separar errores de compilación de errores de evaluaciónProporciona diagnósticos más claros a operadores y usuarios
Última actualización