1443 lines
56 KiB
Plaintext
1443 lines
56 KiB
Plaintext
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// SatuSehatHandlerData contains template data for Satu Sehat handler generation
|
|
type SatuSehatHandlerData struct {
|
|
Name string
|
|
NameLower string
|
|
NameUpper string
|
|
Category string
|
|
CategoryPath string
|
|
CategoryParts []string
|
|
ModuleName string
|
|
HasGet bool
|
|
HasPost bool
|
|
HasPut bool
|
|
HasPatch bool
|
|
HasDelete bool
|
|
GetEndpoint string
|
|
PostEndpoint string
|
|
PutEndpoint string
|
|
PatchEndpoint string
|
|
DeleteEndpoint string
|
|
Timestamp string
|
|
DirectoryDepth int
|
|
FhirResource string
|
|
}
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
fmt.Println("Usage: go run generate-satusehat-handler.go [level1[/level2[/level3[/level4]]]]/entity [methods]")
|
|
fmt.Println("Examples:")
|
|
fmt.Println(" go run generate-satusehat-handler.go fhir/patient get post put patch")
|
|
fmt.Println(" go run generate-satusehat-handler.go master/organization get post")
|
|
fmt.Println(" go run generate-satusehat-handler.go integration/encounter/observation get post")
|
|
fmt.Println(" go run generate-satusehat-handler.go api/v1/fhir/r4/patient get post put patch delete")
|
|
fmt.Println(" go run generate-satusehat-handler.go location get")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Parse entity path (up to 4 levels + entity)
|
|
entityPath := os.Args[1]
|
|
methods := []string{}
|
|
if len(os.Args) > 2 {
|
|
methods = os.Args[2:]
|
|
} else {
|
|
// Default methods for FHIR resources
|
|
methods = []string{"get", "post", "put", "patch"}
|
|
}
|
|
|
|
// Parse multi-level category and entity
|
|
var categoryParts []string
|
|
var entityName string
|
|
var category string
|
|
|
|
parts := strings.Split(entityPath, "/")
|
|
|
|
if len(parts) > 1 && len(parts) <= 5 { // Up to 4 levels + entity
|
|
categoryParts = parts[:len(parts)-1]
|
|
entityName = parts[len(parts)-1]
|
|
category = strings.Join(categoryParts, "/")
|
|
} else if len(parts) == 1 {
|
|
category = ""
|
|
entityName = parts[0]
|
|
categoryParts = []string{}
|
|
} else {
|
|
fmt.Println("❌ Error: Invalid path format. Use up to 4 levels like 'level1/level2/level3/level4/entity' or just 'entity'")
|
|
fmt.Printf("❌ You provided %d levels, maximum is 4 levels + entity\n", len(parts)-1)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Format names
|
|
entityName = strings.Title(entityName) // PascalCase entity name
|
|
entityLower := strings.ToLower(entityName)
|
|
entityUpper := strings.ToUpper(entityName)
|
|
|
|
// FHIR Resource name (capitalize first letter only)
|
|
fhirResource := strings.Title(strings.ToLower(entityName))
|
|
|
|
data := SatuSehatHandlerData{
|
|
Name: entityName,
|
|
NameLower: entityLower,
|
|
NameUpper: entityUpper,
|
|
Category: category,
|
|
CategoryPath: category,
|
|
CategoryParts: categoryParts,
|
|
ModuleName: "api-service",
|
|
DirectoryDepth: len(categoryParts),
|
|
FhirResource: fhirResource,
|
|
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
|
|
}
|
|
|
|
// Set methods and endpoints for FHIR resources
|
|
for _, m := range methods {
|
|
switch strings.ToLower(m) {
|
|
case "get":
|
|
data.HasGet = true
|
|
data.GetEndpoint = fmt.Sprintf("%s/{id}", fhirResource)
|
|
case "post":
|
|
data.HasPost = true
|
|
data.PostEndpoint = fhirResource
|
|
case "put":
|
|
data.HasPut = true
|
|
data.PutEndpoint = fmt.Sprintf("%s/{id}", fhirResource)
|
|
case "patch":
|
|
data.HasPatch = true
|
|
data.PatchEndpoint = fmt.Sprintf("%s/{id}", fhirResource)
|
|
case "delete":
|
|
data.HasDelete = true
|
|
data.DeleteEndpoint = fmt.Sprintf("%s/{id}", fhirResource)
|
|
}
|
|
}
|
|
|
|
// Create directories with multi-level support
|
|
var handlerDir, modelDir string
|
|
if category != "" {
|
|
// Multi-level directory support
|
|
handlerDirParts := append([]string{"internal", "handlers", "satusehat"}, categoryParts...)
|
|
modelDirParts := append([]string{"internal", "models", "satusehat"}, categoryParts...)
|
|
|
|
handlerDir = filepath.Join(handlerDirParts...)
|
|
modelDir = filepath.Join(modelDirParts...)
|
|
} else {
|
|
// No category: direct internal/handlers/satusehat/
|
|
handlerDir = filepath.Join("internal", "handlers", "satusehat")
|
|
modelDir = filepath.Join("internal", "models", "satusehat")
|
|
}
|
|
|
|
// Create directories
|
|
for _, d := range []string{handlerDir, modelDir} {
|
|
if err := os.MkdirAll(d, 0755); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// Generate files
|
|
generateOptimizedSatuSehatHandlerFile(data, handlerDir)
|
|
generateOptimizedSatuSehatModelFile(data, modelDir)
|
|
// updateOptimizedSatuSehatRoutesFile(data)
|
|
|
|
fmt.Printf("✅ Successfully generated optimized Satu Sehat handler: %s\n", entityName)
|
|
if category != "" {
|
|
fmt.Printf("📁 Category Path: %s (%d levels deep)\n", category, data.DirectoryDepth)
|
|
}
|
|
fmt.Printf("📁 FHIR Resource: %s\n", fhirResource)
|
|
fmt.Printf("📁 Handler: %s\n", filepath.Join(handlerDir, entityLower+".go"))
|
|
fmt.Printf("📁 Model: %s\n", filepath.Join(modelDir, entityLower+".go"))
|
|
}
|
|
|
|
// ================= OPTIMIZED HANDLER GENERATION =====================
|
|
|
|
func generateOptimizedSatuSehatHandlerFile(data SatuSehatHandlerData, handlerDir string) {
|
|
var modelsImportPath string
|
|
if data.Category != "" {
|
|
modelsImportPath = data.ModuleName + "/internal/models/satusehat/" + data.Category
|
|
} else {
|
|
modelsImportPath = data.ModuleName + "/internal/models/satusehat"
|
|
}
|
|
|
|
handlerContent := `package handlers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"` + data.ModuleName + `/internal/config"
|
|
"` + modelsImportPath + `"
|
|
services "` + data.ModuleName + `/internal/services/satusehat"
|
|
"` + data.ModuleName + `/pkg/logger"
|
|
"` + data.ModuleName + `/pkg/validator"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/go-playground/validator/v10"
|
|
)
|
|
|
|
// ` + data.Name + `Handler handles ` + data.NameLower + ` Satu Sehat FHIR services with multi-level organization
|
|
// Generated for FHIR Resource: ` + data.FhirResource + `
|
|
// Path: ` + data.Category + `
|
|
// Directory depth: ` + fmt.Sprintf("%d", data.DirectoryDepth) + ` levels
|
|
type ` + data.Name + `Handler struct {
|
|
service services.SatuSehatService
|
|
validator *validator.Validate
|
|
logger logger.Logger
|
|
config *config.SatuSehatConfig
|
|
}
|
|
|
|
// HandlerConfig contains configuration for ` + data.Name + `Handler
|
|
type ` + data.Name + `HandlerConfig struct {
|
|
SatuSehatConfig *config.SatuSehatConfig
|
|
Logger logger.Logger
|
|
Validator *validator.Validate
|
|
}
|
|
|
|
// New` + data.Name + `Handler creates a new optimized ` + data.Name + `Handler for Satu Sehat FHIR
|
|
func New` + data.Name + `Handler(cfg *` + data.Name + `HandlerConfig) *` + data.Name + `Handler {
|
|
return &` + data.Name + `Handler{
|
|
service: services.NewSatuSehatService(cfg.SatuSehatConfig),
|
|
validator: cfg.Validator,
|
|
logger: cfg.Logger,
|
|
config: cfg.SatuSehatConfig,
|
|
}
|
|
}`
|
|
|
|
// Add optimized methods based on flags
|
|
if data.HasPost {
|
|
handlerContent += generateOptimizedSatuSehatCreateMethod(data)
|
|
}
|
|
|
|
if data.HasPut {
|
|
handlerContent += generateOptimizedSatuSehatUpdateMethod(data)
|
|
}
|
|
|
|
if data.HasPatch {
|
|
handlerContent += generateOptimizedSatuSehatPatchMethod(data)
|
|
}
|
|
|
|
if data.HasDelete {
|
|
handlerContent += generateOptimizedSatuSehatDeleteMethod(data)
|
|
}
|
|
|
|
if data.HasGet {
|
|
handlerContent += generateOptimizedSatuSehatGetMethod(data)
|
|
handlerContent += generateOptimizedSatuSehatSearchMethod(data)
|
|
}
|
|
|
|
// Add helper methods
|
|
handlerContent += generateSatuSehatHelperMethods(data)
|
|
|
|
writeFile(filepath.Join(handlerDir, data.NameLower+".go"), handlerContent)
|
|
}
|
|
|
|
func generateOptimizedSatuSehatCreateMethod(data SatuSehatHandlerData) string {
|
|
var routePath, tagName string
|
|
if data.Category != "" {
|
|
routePath = "satusehat/" + data.Category + "/" + data.NameLower
|
|
tagParts := append([]string{"SatuSehat"}, data.CategoryParts...)
|
|
tagParts = append(tagParts, strings.Title(data.NameLower))
|
|
tagName = strings.Join(tagParts, "-")
|
|
} else {
|
|
routePath = "satusehat/" + data.NameLower
|
|
tagName = "SatuSehat-" + strings.Title(data.NameLower)
|
|
}
|
|
|
|
return `
|
|
|
|
// Create` + data.Name + ` creates a new FHIR ` + data.FhirResource + ` resource
|
|
// @Summary Create a new FHIR ` + data.FhirResource + ` resource
|
|
// @Description Create a new ` + data.FhirResource + ` resource in Satu Sehat ecosystem with FHIR R4 compliance
|
|
// @Description FHIR Resource: ` + data.FhirResource + ` | Path: ` + data.Category + `
|
|
// @Tags ` + tagName + `
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param Authorization header string true "Bearer token from Satu Sehat OAuth2"
|
|
// @Param request body models.` + data.Name + `CreateRequest true "` + data.FhirResource + ` FHIR resource creation request"
|
|
// @Success 201 {object} models.` + data.Name + `Response "` + data.FhirResource + ` resource created successfully"
|
|
// @Failure 400 {object} models.FhirOperationOutcome "Bad request - validation error"
|
|
// @Failure 401 {object} models.FhirOperationOutcome "Unauthorized - invalid or expired token"
|
|
// @Failure 422 {object} models.FhirOperationOutcome "Unprocessable entity - FHIR validation error"
|
|
// @Failure 500 {object} models.FhirOperationOutcome "Internal server error"
|
|
// @Router /api/v1/` + routePath + ` [post]
|
|
func (h *` + data.Name + `Handler) Create` + data.Name + `(c *gin.Context) {
|
|
requestID := uuid.New().String()
|
|
startTime := time.Now()
|
|
|
|
h.logger.Info("Creating FHIR ` + data.FhirResource + ` resource", map[string]interface{}{
|
|
"request_id": requestID,
|
|
"timestamp": startTime,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"category_path": "` + data.Category + `",
|
|
"directory_depth": ` + fmt.Sprintf("%d", data.DirectoryDepth) + `,
|
|
})
|
|
|
|
var req models.` + data.Name + `CreateRequest
|
|
req.RequestID = requestID
|
|
req.Timestamp = startTime
|
|
|
|
// Enhanced JSON binding with FHIR validation
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
h.logger.Error("Failed to bind FHIR JSON", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"request_id": requestID,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"path": "` + routePath + `",
|
|
})
|
|
h.sendFhirErrorResponse(c, http.StatusBadRequest, "structure",
|
|
"Invalid FHIR resource structure", err.Error(), requestID)
|
|
return
|
|
}
|
|
|
|
// FHIR resource validation
|
|
if err := req.ValidateFhir(); err != nil {
|
|
h.logger.Error("FHIR validation failed", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"request_id": requestID,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"path": "` + routePath + `",
|
|
})
|
|
h.sendFhirErrorResponse(c, http.StatusUnprocessableEntity, "business-rule",
|
|
"FHIR resource validation failed", err.Error(), requestID)
|
|
return
|
|
}
|
|
|
|
// Struct validation
|
|
if err := h.validator.Struct(&req); err != nil {
|
|
h.logger.Error("Struct validation failed", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"request_id": requestID,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"path": "` + routePath + `",
|
|
})
|
|
h.sendFhirErrorResponse(c, http.StatusBadRequest, "structure",
|
|
"Resource structure validation failed", h.formatValidationError(err), requestID)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
var fhirResponse models.FhirResponse
|
|
if err := h.service.CreateResource(ctx, "` + data.PostEndpoint + `", req, &fhirResponse); err != nil {
|
|
h.logger.Error("Failed to create FHIR resource", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"request_id": requestID,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"endpoint": "` + data.PostEndpoint + `",
|
|
"path": "` + routePath + `",
|
|
})
|
|
|
|
statusCode, errorCode := h.categorizeFhirError(err)
|
|
h.sendFhirErrorResponse(c, statusCode, errorCode,
|
|
"Failed to create ` + data.FhirResource + ` resource", err.Error(), requestID)
|
|
return
|
|
}
|
|
|
|
// Check for FHIR OperationOutcome
|
|
if fhirResponse.ResourceType == "OperationOutcome" {
|
|
h.logger.Warn("Satu Sehat returned OperationOutcome", map[string]interface{}{
|
|
"request_id": requestID,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"path": "` + routePath + `",
|
|
"outcome": fhirResponse,
|
|
})
|
|
|
|
h.sendFhirOperationOutcome(c, http.StatusUnprocessableEntity, fhirResponse, requestID)
|
|
return
|
|
}
|
|
|
|
duration := time.Since(startTime)
|
|
h.logger.Info("FHIR ` + data.FhirResource + ` resource created successfully", map[string]interface{}{
|
|
"request_id": requestID,
|
|
"duration": duration.String(),
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": fhirResponse.ID,
|
|
"path": "` + routePath + `",
|
|
})
|
|
|
|
h.sendFhirSuccessResponse(c, http.StatusCreated, "` + data.FhirResource + ` resource created successfully",
|
|
fhirResponse, requestID)
|
|
}`
|
|
}
|
|
|
|
func generateOptimizedSatuSehatUpdateMethod(data SatuSehatHandlerData) string {
|
|
var routePath, tagName string
|
|
if data.Category != "" {
|
|
routePath = "satusehat/" + data.Category + "/" + data.NameLower
|
|
tagParts := append([]string{"SatuSehat"}, data.CategoryParts...)
|
|
tagParts = append(tagParts, strings.Title(data.NameLower))
|
|
tagName = strings.Join(tagParts, "-")
|
|
} else {
|
|
routePath = "satusehat/" + data.NameLower
|
|
tagName = "SatuSehat-" + strings.Title(data.NameLower)
|
|
}
|
|
|
|
return `
|
|
|
|
// Update` + data.Name + ` updates an existing FHIR ` + data.FhirResource + ` resource
|
|
// @Summary Update (replace) an existing FHIR ` + data.FhirResource + ` resource
|
|
// @Description Update an existing ` + data.FhirResource + ` resource in Satu Sehat ecosystem with FHIR R4 compliance
|
|
// @Description FHIR Resource: ` + data.FhirResource + ` | Path: ` + data.Category + `
|
|
// @Tags ` + tagName + `
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param Authorization header string true "Bearer token from Satu Sehat OAuth2"
|
|
// @Param id path string true "` + data.FhirResource + ` resource ID"
|
|
// @Param request body models.` + data.Name + `UpdateRequest true "` + data.FhirResource + ` FHIR resource update request"
|
|
// @Success 200 {object} models.` + data.Name + `Response "` + data.FhirResource + ` resource updated successfully"
|
|
// @Failure 400 {object} models.FhirOperationOutcome "Bad request - validation error"
|
|
// @Failure 401 {object} models.FhirOperationOutcome "Unauthorized - invalid or expired token"
|
|
// @Failure 404 {object} models.FhirOperationOutcome "Resource not found"
|
|
// @Failure 422 {object} models.FhirOperationOutcome "Unprocessable entity - FHIR validation error"
|
|
// @Failure 500 {object} models.FhirOperationOutcome "Internal server error"
|
|
// @Router /api/v1/` + routePath + `/{id} [put]
|
|
func (h *` + data.Name + `Handler) Update` + data.Name + `(c *gin.Context) {
|
|
requestID := uuid.New().String()
|
|
startTime := time.Now()
|
|
id := c.Param("id")
|
|
|
|
h.logger.Info("Updating FHIR ` + data.FhirResource + ` resource", map[string]interface{}{
|
|
"request_id": requestID,
|
|
"timestamp": startTime,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": id,
|
|
"category_path": "` + data.Category + `",
|
|
"directory_depth": ` + fmt.Sprintf("%d", data.DirectoryDepth) + `,
|
|
})
|
|
|
|
if id == "" {
|
|
h.sendFhirErrorResponse(c, http.StatusBadRequest, "required",
|
|
"Resource ID is required", "", requestID)
|
|
return
|
|
}
|
|
|
|
var req models.` + data.Name + `UpdateRequest
|
|
req.RequestID = requestID
|
|
req.Timestamp = startTime
|
|
req.ID = id
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
h.logger.Error("Failed to bind FHIR JSON for update", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"request_id": requestID,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": id,
|
|
"path": "` + routePath + `",
|
|
})
|
|
h.sendFhirErrorResponse(c, http.StatusBadRequest, "structure",
|
|
"Invalid FHIR resource structure", err.Error(), requestID)
|
|
return
|
|
}
|
|
|
|
if err := req.ValidateFhir(); err != nil {
|
|
h.logger.Error("FHIR update validation failed", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"request_id": requestID,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": id,
|
|
"path": "` + routePath + `",
|
|
})
|
|
h.sendFhirErrorResponse(c, http.StatusUnprocessableEntity, "business-rule",
|
|
"FHIR resource validation failed", err.Error(), requestID)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
endpoint := fmt.Sprintf("` + strings.Replace(data.PutEndpoint, "{id}", "%s", 1) + `", id)
|
|
var fhirResponse models.FhirResponse
|
|
|
|
if err := h.service.UpdateResource(ctx, endpoint, req, &fhirResponse); err != nil {
|
|
h.logger.Error("Failed to update FHIR resource", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"request_id": requestID,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": id,
|
|
"path": "` + routePath + `",
|
|
})
|
|
|
|
statusCode, errorCode := h.categorizeFhirError(err)
|
|
h.sendFhirErrorResponse(c, statusCode, errorCode,
|
|
"Failed to update ` + data.FhirResource + ` resource", err.Error(), requestID)
|
|
return
|
|
}
|
|
|
|
if fhirResponse.ResourceType == "OperationOutcome" {
|
|
h.logger.Warn("Satu Sehat returned OperationOutcome for update", map[string]interface{}{
|
|
"request_id": requestID,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": id,
|
|
"path": "` + routePath + `",
|
|
"outcome": fhirResponse,
|
|
})
|
|
|
|
h.sendFhirOperationOutcome(c, http.StatusUnprocessableEntity, fhirResponse, requestID)
|
|
return
|
|
}
|
|
|
|
duration := time.Since(startTime)
|
|
h.logger.Info("FHIR ` + data.FhirResource + ` resource updated successfully", map[string]interface{}{
|
|
"request_id": requestID,
|
|
"duration": duration.String(),
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": id,
|
|
"path": "` + routePath + `",
|
|
})
|
|
|
|
h.sendFhirSuccessResponse(c, http.StatusOK, "` + data.FhirResource + ` resource updated successfully",
|
|
fhirResponse, requestID)
|
|
}`
|
|
}
|
|
|
|
func generateOptimizedSatuSehatPatchMethod(data SatuSehatHandlerData) string {
|
|
var routePath, tagName string
|
|
if data.Category != "" {
|
|
routePath = "satusehat/" + data.Category + "/" + data.NameLower
|
|
tagParts := append([]string{"SatuSehat"}, data.CategoryParts...)
|
|
tagParts = append(tagParts, strings.Title(data.NameLower))
|
|
tagName = strings.Join(tagParts, "-")
|
|
} else {
|
|
routePath = "satusehat/" + data.NameLower
|
|
tagName = "SatuSehat-" + strings.Title(data.NameLower)
|
|
}
|
|
|
|
return `
|
|
|
|
// Patch` + data.Name + ` partially updates an existing FHIR ` + data.FhirResource + ` resource
|
|
// @Summary Patch (partial update) an existing FHIR ` + data.FhirResource + ` resource
|
|
// @Description Partially update an existing ` + data.FhirResource + ` resource in Satu Sehat ecosystem with FHIR R4 compliance
|
|
// @Description FHIR Resource: ` + data.FhirResource + ` | Path: ` + data.Category + `
|
|
// @Tags ` + tagName + `
|
|
// @Accept json-patch+json
|
|
// @Produce json
|
|
// @Param Authorization header string true "Bearer token from Satu Sehat OAuth2"
|
|
// @Param id path string true "` + data.FhirResource + ` resource ID"
|
|
// @Param request body models.` + data.Name + `PatchRequest true "` + data.FhirResource + ` FHIR resource patch request"
|
|
// @Success 200 {object} models.` + data.Name + `Response "` + data.FhirResource + ` resource patched successfully"
|
|
// @Failure 400 {object} models.FhirOperationOutcome "Bad request - validation error"
|
|
// @Failure 401 {object} models.FhirOperationOutcome "Unauthorized - invalid or expired token"
|
|
// @Failure 404 {object} models.FhirOperationOutcome "Resource not found"
|
|
// @Failure 422 {object} models.FhirOperationOutcome "Unprocessable entity - FHIR validation error"
|
|
// @Failure 500 {object} models.FhirOperationOutcome "Internal server error"
|
|
// @Router /api/v1/` + routePath + `/{id} [patch]
|
|
func (h *` + data.Name + `Handler) Patch` + data.Name + `(c *gin.Context) {
|
|
requestID := uuid.New().String()
|
|
startTime := time.Now()
|
|
id := c.Param("id")
|
|
|
|
h.logger.Info("Patching FHIR ` + data.FhirResource + ` resource", map[string]interface{}{
|
|
"request_id": requestID,
|
|
"timestamp": startTime,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": id,
|
|
"category_path": "` + data.Category + `",
|
|
"directory_depth": ` + fmt.Sprintf("%d", data.DirectoryDepth) + `,
|
|
})
|
|
|
|
if id == "" {
|
|
h.sendFhirErrorResponse(c, http.StatusBadRequest, "required",
|
|
"Resource ID is required", "", requestID)
|
|
return
|
|
}
|
|
|
|
var req models.` + data.Name + `PatchRequest
|
|
req.RequestID = requestID
|
|
req.Timestamp = startTime
|
|
req.ID = id
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
h.logger.Error("Failed to bind FHIR patch JSON", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"request_id": requestID,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": id,
|
|
"path": "` + routePath + `",
|
|
})
|
|
h.sendFhirErrorResponse(c, http.StatusBadRequest, "structure",
|
|
"Invalid FHIR patch structure", err.Error(), requestID)
|
|
return
|
|
}
|
|
|
|
if err := req.ValidateFhirPatch(); err != nil {
|
|
h.logger.Error("FHIR patch validation failed", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"request_id": requestID,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": id,
|
|
"path": "` + routePath + `",
|
|
})
|
|
h.sendFhirErrorResponse(c, http.StatusUnprocessableEntity, "business-rule",
|
|
"FHIR patch validation failed", err.Error(), requestID)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
endpoint := fmt.Sprintf("` + strings.Replace(data.PatchEndpoint, "{id}", "%s", 1) + `", id)
|
|
var fhirResponse models.FhirResponse
|
|
|
|
if err := h.service.PatchResource(ctx, endpoint, req, &fhirResponse); err != nil {
|
|
h.logger.Error("Failed to patch FHIR resource", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"request_id": requestID,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": id,
|
|
"path": "` + routePath + `",
|
|
})
|
|
|
|
statusCode, errorCode := h.categorizeFhirError(err)
|
|
h.sendFhirErrorResponse(c, statusCode, errorCode,
|
|
"Failed to patch ` + data.FhirResource + ` resource", err.Error(), requestID)
|
|
return
|
|
}
|
|
|
|
if fhirResponse.ResourceType == "OperationOutcome" {
|
|
h.logger.Warn("Satu Sehat returned OperationOutcome for patch", map[string]interface{}{
|
|
"request_id": requestID,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": id,
|
|
"path": "` + routePath + `",
|
|
"outcome": fhirResponse,
|
|
})
|
|
|
|
h.sendFhirOperationOutcome(c, http.StatusUnprocessableEntity, fhirResponse, requestID)
|
|
return
|
|
}
|
|
|
|
duration := time.Since(startTime)
|
|
h.logger.Info("FHIR ` + data.FhirResource + ` resource patched successfully", map[string]interface{}{
|
|
"request_id": requestID,
|
|
"duration": duration.String(),
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": id,
|
|
"path": "` + routePath + `",
|
|
})
|
|
|
|
h.sendFhirSuccessResponse(c, http.StatusOK, "` + data.FhirResource + ` resource patched successfully",
|
|
fhirResponse, requestID)
|
|
}`
|
|
}
|
|
|
|
func generateOptimizedSatuSehatDeleteMethod(data SatuSehatHandlerData) string {
|
|
var routePath, tagName string
|
|
if data.Category != "" {
|
|
routePath = "satusehat/" + data.Category + "/" + data.NameLower
|
|
tagParts := append([]string{"SatuSehat"}, data.CategoryParts...)
|
|
tagParts = append(tagParts, strings.Title(data.NameLower))
|
|
tagName = strings.Join(tagParts, "-")
|
|
} else {
|
|
routePath = "satusehat/" + data.NameLower
|
|
tagName = "SatuSehat-" + strings.Title(data.NameLower)
|
|
}
|
|
|
|
return `
|
|
|
|
// Delete` + data.Name + ` deletes an existing FHIR ` + data.FhirResource + ` resource
|
|
// @Summary Delete an existing FHIR ` + data.FhirResource + ` resource
|
|
// @Description Delete an existing ` + data.FhirResource + ` resource in Satu Sehat ecosystem with FHIR R4 compliance
|
|
// @Description FHIR Resource: ` + data.FhirResource + ` | Path: ` + data.Category + `
|
|
// @Tags ` + tagName + `
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param Authorization header string true "Bearer token from Satu Sehat OAuth2"
|
|
// @Param id path string true "` + data.FhirResource + ` resource ID"
|
|
// @Success 204 "` + data.FhirResource + ` resource deleted successfully"
|
|
// @Failure 400 {object} models.FhirOperationOutcome "Bad request - invalid ID"
|
|
// @Failure 401 {object} models.FhirOperationOutcome "Unauthorized - invalid or expired token"
|
|
// @Failure 404 {object} models.FhirOperationOutcome "Resource not found"
|
|
// @Failure 500 {object} models.FhirOperationOutcome "Internal server error"
|
|
// @Router /api/v1/` + routePath + `/{id} [delete]
|
|
func (h *` + data.Name + `Handler) Delete` + data.Name + `(c *gin.Context) {
|
|
requestID := uuid.New().String()
|
|
startTime := time.Now()
|
|
id := c.Param("id")
|
|
|
|
h.logger.Info("Deleting FHIR ` + data.FhirResource + ` resource", map[string]interface{}{
|
|
"request_id": requestID,
|
|
"timestamp": startTime,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": id,
|
|
"category_path": "` + data.Category + `",
|
|
"directory_depth": ` + fmt.Sprintf("%d", data.DirectoryDepth) + `,
|
|
})
|
|
|
|
if id == "" {
|
|
h.sendFhirErrorResponse(c, http.StatusBadRequest, "required",
|
|
"Resource ID is required", "", requestID)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
endpoint := fmt.Sprintf("` + strings.Replace(data.DeleteEndpoint, "{id}", "%s", 1) + `", id)
|
|
|
|
if err := h.service.DeleteResource(ctx, endpoint); err != nil {
|
|
h.logger.Error("Failed to delete FHIR resource", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"request_id": requestID,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": id,
|
|
"path": "` + routePath + `",
|
|
})
|
|
|
|
statusCode, errorCode := h.categorizeFhirError(err)
|
|
h.sendFhirErrorResponse(c, statusCode, errorCode,
|
|
"Failed to delete ` + data.FhirResource + ` resource", err.Error(), requestID)
|
|
return
|
|
}
|
|
|
|
duration := time.Since(startTime)
|
|
h.logger.Info("FHIR ` + data.FhirResource + ` resource deleted successfully", map[string]interface{}{
|
|
"request_id": requestID,
|
|
"duration": duration.String(),
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": id,
|
|
"path": "` + routePath + `",
|
|
})
|
|
|
|
c.Status(http.StatusNoContent)
|
|
}`
|
|
}
|
|
|
|
func generateOptimizedSatuSehatGetMethod(data SatuSehatHandlerData) string {
|
|
var routePath, tagName string
|
|
if data.Category != "" {
|
|
routePath = "satusehat/" + data.Category + "/" + data.NameLower
|
|
tagParts := append([]string{"SatuSehat"}, data.CategoryParts...)
|
|
tagParts = append(tagParts, strings.Title(data.NameLower))
|
|
tagName = strings.Join(tagParts, "-")
|
|
} else {
|
|
routePath = "satusehat/" + data.NameLower
|
|
tagName = "SatuSehat-" + strings.Title(data.NameLower)
|
|
}
|
|
|
|
return `
|
|
|
|
// Get` + data.Name + ` retrieves a specific FHIR ` + data.FhirResource + ` resource by ID
|
|
// @Summary Get a specific FHIR ` + data.FhirResource + ` resource by ID
|
|
// @Description Retrieve a specific ` + data.FhirResource + ` resource from Satu Sehat ecosystem with FHIR R4 compliance
|
|
// @Description FHIR Resource: ` + data.FhirResource + ` | Path: ` + data.Category + `
|
|
// @Tags ` + tagName + `
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param Authorization header string true "Bearer token from Satu Sehat OAuth2"
|
|
// @Param id path string true "` + data.FhirResource + ` resource ID"
|
|
// @Success 200 {object} models.` + data.Name + `Response "` + data.FhirResource + ` resource retrieved successfully"
|
|
// @Failure 400 {object} models.FhirOperationOutcome "Bad request - invalid ID"
|
|
// @Failure 401 {object} models.FhirOperationOutcome "Unauthorized - invalid or expired token"
|
|
// @Failure 404 {object} models.FhirOperationOutcome "Resource not found"
|
|
// @Failure 500 {object} models.FhirOperationOutcome "Internal server error"
|
|
// @Router /api/v1/` + routePath + `/{id} [get]
|
|
func (h *` + data.Name + `Handler) Get` + data.Name + `(c *gin.Context) {
|
|
requestID := uuid.New().String()
|
|
startTime := time.Now()
|
|
id := c.Param("id")
|
|
|
|
h.logger.Info("Getting FHIR ` + data.FhirResource + ` resource", map[string]interface{}{
|
|
"request_id": requestID,
|
|
"timestamp": startTime,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": id,
|
|
"category_path": "` + data.Category + `",
|
|
"directory_depth": ` + fmt.Sprintf("%d", data.DirectoryDepth) + `,
|
|
})
|
|
|
|
if id == "" {
|
|
h.sendFhirErrorResponse(c, http.StatusBadRequest, "required",
|
|
"Resource ID is required", "", requestID)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
endpoint := fmt.Sprintf("` + strings.Replace(data.GetEndpoint, "{id}", "%s", 1) + `", id)
|
|
var fhirResponse models.FhirResponse
|
|
|
|
if err := h.service.GetResource(ctx, endpoint, &fhirResponse); err != nil {
|
|
h.logger.Error("Failed to get FHIR resource", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"request_id": requestID,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": id,
|
|
"path": "` + routePath + `",
|
|
})
|
|
|
|
statusCode, errorCode := h.categorizeFhirError(err)
|
|
h.sendFhirErrorResponse(c, statusCode, errorCode,
|
|
"Failed to get ` + data.FhirResource + ` resource", err.Error(), requestID)
|
|
return
|
|
}
|
|
|
|
if fhirResponse.ResourceType == "OperationOutcome" {
|
|
h.logger.Info("FHIR ` + data.FhirResource + ` resource not found", map[string]interface{}{
|
|
"request_id": requestID,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": id,
|
|
"path": "` + routePath + `",
|
|
"outcome": fhirResponse,
|
|
})
|
|
|
|
h.sendFhirOperationOutcome(c, http.StatusNotFound, fhirResponse, requestID)
|
|
return
|
|
}
|
|
|
|
duration := time.Since(startTime)
|
|
h.logger.Info("FHIR ` + data.FhirResource + ` resource retrieved successfully", map[string]interface{}{
|
|
"request_id": requestID,
|
|
"duration": duration.String(),
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"resource_id": id,
|
|
"path": "` + routePath + `",
|
|
})
|
|
|
|
h.sendFhirSuccessResponse(c, http.StatusOK, "` + data.FhirResource + ` resource retrieved successfully",
|
|
fhirResponse, requestID)
|
|
}`
|
|
}
|
|
|
|
func generateOptimizedSatuSehatSearchMethod(data SatuSehatHandlerData) string {
|
|
var routePath, tagName string
|
|
if data.Category != "" {
|
|
routePath = "satusehat/" + data.Category + "/" + data.NameLower
|
|
tagParts := append([]string{"SatuSehat"}, data.CategoryParts...)
|
|
tagParts = append(tagParts, strings.Title(data.NameLower))
|
|
tagName = strings.Join(tagParts, "-")
|
|
} else {
|
|
routePath = "satusehat/" + data.NameLower
|
|
tagName = "SatuSehat-" + strings.Title(data.NameLower)
|
|
}
|
|
|
|
return `
|
|
|
|
// Search` + data.Name + ` searches for FHIR ` + data.FhirResource + ` resources with parameters
|
|
// @Summary Search for FHIR ` + data.FhirResource + ` resources
|
|
// @Description Search for ` + data.FhirResource + ` resources in Satu Sehat ecosystem with FHIR R4 search parameters
|
|
// @Description FHIR Resource: ` + data.FhirResource + ` | Path: ` + data.Category + `
|
|
// @Tags ` + tagName + `
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param Authorization header string true "Bearer token from Satu Sehat OAuth2"
|
|
// @Param _count query integer false "Number of results to return (default: 10)"
|
|
// @Param _page query integer false "Page number for pagination (default: 1)"
|
|
// @Param _include query string false "Include referenced resources"
|
|
// @Param _revinclude query string false "Reverse include referenced resources"
|
|
// @Success 200 {object} models.FhirBundleResponse "` + data.FhirResource + ` resources search results"
|
|
// @Failure 400 {object} models.FhirOperationOutcome "Bad request - invalid search parameters"
|
|
// @Failure 401 {object} models.FhirOperationOutcome "Unauthorized - invalid or expired token"
|
|
// @Failure 500 {object} models.FhirOperationOutcome "Internal server error"
|
|
// @Router /api/v1/` + routePath + ` [get]
|
|
func (h *` + data.Name + `Handler) Search` + data.Name + `(c *gin.Context) {
|
|
requestID := uuid.New().String()
|
|
startTime := time.Now()
|
|
|
|
h.logger.Info("Searching FHIR ` + data.FhirResource + ` resources", map[string]interface{}{
|
|
"request_id": requestID,
|
|
"timestamp": startTime,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"query_params": c.Request.URL.Query(),
|
|
"category_path": "` + data.Category + `",
|
|
"directory_depth": ` + fmt.Sprintf("%d", data.DirectoryDepth) + `,
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
// Build search parameters
|
|
searchParams := make(map[string]string)
|
|
for key, values := range c.Request.URL.Query() {
|
|
if len(values) > 0 {
|
|
searchParams[key] = values[0]
|
|
}
|
|
}
|
|
|
|
var fhirBundle models.FhirBundleResponse
|
|
if err := h.service.SearchResources(ctx, "` + data.FhirResource + `", searchParams, &fhirBundle); err != nil {
|
|
h.logger.Error("Failed to search FHIR resources", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"request_id": requestID,
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"search_params": searchParams,
|
|
"path": "` + routePath + `",
|
|
})
|
|
|
|
statusCode, errorCode := h.categorizeFhirError(err)
|
|
h.sendFhirErrorResponse(c, statusCode, errorCode,
|
|
"Failed to search ` + data.FhirResource + ` resources", err.Error(), requestID)
|
|
return
|
|
}
|
|
|
|
duration := time.Since(startTime)
|
|
h.logger.Info("FHIR ` + data.FhirResource + ` resources search completed", map[string]interface{}{
|
|
"request_id": requestID,
|
|
"duration": duration.String(),
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"total_results": fhirBundle.Total,
|
|
"search_params": searchParams,
|
|
"path": "` + routePath + `",
|
|
})
|
|
|
|
h.sendFhirBundleResponse(c, http.StatusOK, "` + data.FhirResource + ` resources search completed",
|
|
fhirBundle, requestID)
|
|
}`
|
|
}
|
|
|
|
func generateSatuSehatHelperMethods(data SatuSehatHandlerData) string {
|
|
return `
|
|
|
|
// Helper methods for ` + data.Name + `Handler with FHIR support
|
|
func (h *` + data.Name + `Handler) sendFhirSuccessResponse(c *gin.Context, statusCode int, message string, data interface{}, requestID string) {
|
|
response := models.` + data.Name + `Response{
|
|
FhirResponse: models.FhirResponse{
|
|
ResourceType: "` + data.FhirResource + `",
|
|
Meta: models.FhirMeta{
|
|
LastUpdated: time.Now().Format(time.RFC3339),
|
|
VersionId: "1",
|
|
},
|
|
},
|
|
BaseResponse: models.BaseResponse{
|
|
Status: "success",
|
|
Message: message,
|
|
Data: data,
|
|
Metadata: &models.ResponseMetadata{
|
|
Timestamp: time.Now(),
|
|
Version: "FHIR R4",
|
|
RequestID: requestID,
|
|
Path: "` + data.Category + `",
|
|
Depth: ` + fmt.Sprintf("%d", data.DirectoryDepth) + `,
|
|
FhirResource: "` + data.FhirResource + `",
|
|
},
|
|
},
|
|
}
|
|
c.JSON(statusCode, response)
|
|
}
|
|
|
|
func (h *` + data.Name + `Handler) sendFhirErrorResponse(c *gin.Context, statusCode int, errorCode, message, details, requestID string) {
|
|
operationOutcome := models.FhirOperationOutcome{
|
|
ResourceType: "OperationOutcome",
|
|
ID: requestID,
|
|
Meta: models.FhirMeta{
|
|
LastUpdated: time.Now().Format(time.RFC3339),
|
|
VersionId: "1",
|
|
},
|
|
Issue: []models.FhirOperationOutcomeIssue{{
|
|
Severity: h.mapHttpStatusToSeverity(statusCode),
|
|
Code: errorCode,
|
|
Details: models.FhirCodeableConcept{
|
|
Text: message,
|
|
},
|
|
Diagnostics: details,
|
|
}},
|
|
}
|
|
c.JSON(statusCode, operationOutcome)
|
|
}a
|
|
|
|
func (h *` + data.Name + `Handler) sendFhirOperationOutcome(c *gin.Context, statusCode int, outcome models.FhirResponse, requestID string) {
|
|
c.JSON(statusCode, outcome)
|
|
}
|
|
|
|
func (h *` + data.Name + `Handler) sendFhirBundleResponse(c *gin.Context, statusCode int, message string, bundle models.FhirBundleResponse, requestID string) {
|
|
bundle.Meta = models.FhirMeta{
|
|
LastUpdated: time.Now().Format(time.RFC3339),
|
|
VersionId: "1",
|
|
}
|
|
c.JSON(statusCode, bundle)
|
|
}
|
|
|
|
func (h *` + data.Name + `Handler) formatValidationError(err error) string {
|
|
if validationErrors, ok := err.(validator.ValidationErrors); ok {
|
|
var messages []string
|
|
for _, e := range validationErrors {
|
|
switch e.Tag() {
|
|
case "required":
|
|
messages = append(messages, fmt.Sprintf("Field '%s' is required", e.Field()))
|
|
case "min":
|
|
messages = append(messages, fmt.Sprintf("Field '%s' must be at least %s characters", e.Field(), e.Param()))
|
|
case "max":
|
|
messages = append(messages, fmt.Sprintf("Field '%s' must be at most %s characters", e.Field(), e.Param()))
|
|
case "oneof":
|
|
messages = append(messages, fmt.Sprintf("Field '%s' must be one of: %s", e.Field(), e.Param()))
|
|
case "url":
|
|
messages = append(messages, fmt.Sprintf("Field '%s' must be a valid URL", e.Field()))
|
|
case "uuid":
|
|
messages = append(messages, fmt.Sprintf("Field '%s' must be a valid UUID", e.Field()))
|
|
default:
|
|
messages = append(messages, fmt.Sprintf("Field '%s' is invalid", e.Field()))
|
|
}
|
|
}
|
|
return fmt.Sprintf("FHIR validation failed: %v", messages)
|
|
}
|
|
return err.Error()
|
|
}
|
|
|
|
func (h *` + data.Name + `Handler) categorizeFhirError(err error) (int, string) {
|
|
if err == nil {
|
|
return http.StatusOK, "informational"
|
|
}
|
|
|
|
errStr := err.Error()
|
|
|
|
// FHIR-specific error categorization
|
|
if strings.Contains(errStr, "unauthorized") || strings.Contains(errStr, "invalid token") {
|
|
return http.StatusUnauthorized, "security"
|
|
}
|
|
|
|
if strings.Contains(errStr, "not found") || strings.Contains(errStr, "404") {
|
|
return http.StatusNotFound, "not-found"
|
|
}
|
|
|
|
if strings.Contains(errStr, "validation") || strings.Contains(errStr, "invalid") {
|
|
return http.StatusUnprocessableEntity, "business-rule"
|
|
}
|
|
|
|
if h.isTimeoutError(err) {
|
|
return http.StatusRequestTimeout, "timeout"
|
|
}
|
|
|
|
if h.isNetworkError(err) {
|
|
return http.StatusBadGateway, "transient"
|
|
}
|
|
|
|
return http.StatusInternalServerError, "exception"
|
|
}
|
|
|
|
func (h *` + data.Name + `Handler) mapHttpStatusToSeverity(statusCode int) string {
|
|
switch {
|
|
case statusCode >= 500:
|
|
return "fatal"
|
|
case statusCode >= 400:
|
|
return "error"
|
|
case statusCode >= 300:
|
|
return "warning"
|
|
default:
|
|
return "information"
|
|
}
|
|
}
|
|
|
|
func (h *` + data.Name + `Handler) isTimeoutError(err error) bool {
|
|
return err != nil && (strings.Contains(err.Error(), "timeout") ||
|
|
strings.Contains(err.Error(), "deadline exceeded"))
|
|
}
|
|
|
|
func (h *` + data.Name + `Handler) isNetworkError(err error) bool {
|
|
return err != nil && (strings.Contains(err.Error(), "connection refused") ||
|
|
strings.Contains(err.Error(), "no such host") ||
|
|
strings.Contains(err.Error(), "network unreachable"))
|
|
}`
|
|
}
|
|
func generateOptimizedSatuSehatModelFile(data SatuSehatHandlerData, modelDir string) {
|
|
modelContent := `package models
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
"regexp"
|
|
)
|
|
|
|
// ` + data.Name + ` Satu Sehat FHIR R4 Models with Enhanced Multi-Level Support
|
|
// Generated at: ` + data.Timestamp + `
|
|
// FHIR Resource: ` + data.FhirResource + `
|
|
// Category Path: ` + data.Category + `
|
|
// Directory Depth: ` + fmt.Sprintf("%d", data.DirectoryDepth) + ` levels
|
|
|
|
// Base FHIR structures
|
|
type FhirResource struct {
|
|
ResourceType string ` + "`json:\"resourceType\"`" + `
|
|
ID string ` + "`json:\"id,omitempty\"`" + `
|
|
Meta FhirMeta ` + "`json:\"meta,omitempty\"`" + `
|
|
}
|
|
|
|
type FhirMeta struct {
|
|
VersionId string ` + "`json:\"versionId,omitempty\"`" + `
|
|
LastUpdated string ` + "`json:\"lastUpdated,omitempty\"`" + `
|
|
Profile []string ` + "`json:\"profile,omitempty\"`" + `
|
|
Security []FhirCoding ` + "`json:\"security,omitempty\"`" + `
|
|
Tag []FhirCoding ` + "`json:\"tag,omitempty\"`" + `
|
|
}
|
|
|
|
type FhirCoding struct {
|
|
System string ` + "`json:\"system,omitempty\"`" + `
|
|
Version string ` + "`json:\"version,omitempty\"`" + `
|
|
Code string ` + "`json:\"code,omitempty\"`" + `
|
|
Display string ` + "`json:\"display,omitempty\"`" + `
|
|
}
|
|
|
|
type FhirCodeableConcept struct {
|
|
Coding []FhirCoding ` + "`json:\"coding,omitempty\"`" + `
|
|
Text string ` + "`json:\"text,omitempty\"`" + `
|
|
}
|
|
|
|
type FhirReference struct {
|
|
Reference string ` + "`json:\"reference,omitempty\"`" + `
|
|
Type string ` + "`json:\"type,omitempty\"`" + `
|
|
Identifier FhirIdentifier ` + "`json:\"identifier,omitempty\"`" + `
|
|
Display string ` + "`json:\"display,omitempty\"`" + `
|
|
}
|
|
|
|
type FhirIdentifier struct {
|
|
Use string ` + "`json:\"use,omitempty\"`" + `
|
|
Type FhirCodeableConcept ` + "`json:\"type,omitempty\"`" + `
|
|
System string ` + "`json:\"system,omitempty\"`" + `
|
|
Value string ` + "`json:\"value,omitempty\"`" + `
|
|
Period FhirPeriod ` + "`json:\"period,omitempty\"`" + `
|
|
Assigner FhirReference ` + "`json:\"assigner,omitempty\"`" + `
|
|
}
|
|
|
|
type FhirPeriod struct {
|
|
Start string ` + "`json:\"start,omitempty\"`" + `
|
|
End string ` + "`json:\"end,omitempty\"`" + `
|
|
}
|
|
|
|
// FHIR OperationOutcome for error handling
|
|
type FhirOperationOutcome struct {
|
|
ResourceType string ` + "`json:\"resourceType\"`" + `
|
|
ID string ` + "`json:\"id,omitempty\"`" + `
|
|
Meta FhirMeta ` + "`json:\"meta,omitempty\"`" + `
|
|
Issue []FhirOperationOutcomeIssue ` + "`json:\"issue\"`" + `
|
|
}
|
|
|
|
type FhirOperationOutcomeIssue struct {
|
|
Severity string ` + "`json:\"severity\"`" + `
|
|
Code string ` + "`json:\"code\"`" + `
|
|
Details FhirCodeableConcept ` + "`json:\"details,omitempty\"`" + `
|
|
Diagnostics string ` + "`json:\"diagnostics,omitempty\"`" + `
|
|
Location []string ` + "`json:\"location,omitempty\"`" + `
|
|
Expression []string ` + "`json:\"expression,omitempty\"`" + `
|
|
}
|
|
|
|
// FHIR Bundle for search results
|
|
type FhirBundleResponse struct {
|
|
ResourceType string ` + "`json:\"resourceType\"`" + `
|
|
ID string ` + "`json:\"id,omitempty\"`" + `
|
|
Meta FhirMeta ` + "`json:\"meta,omitempty\"`" + `
|
|
Type string ` + "`json:\"type\"`" + `
|
|
Total int ` + "`json:\"total,omitempty\"`" + `
|
|
Link []FhirBundleLink ` + "`json:\"link,omitempty\"`" + `
|
|
Entry []FhirBundleEntry ` + "`json:\"entry,omitempty\"`" + `
|
|
}
|
|
|
|
type FhirBundleLink struct {
|
|
Relation string ` + "`json:\"relation\"`" + `
|
|
URL string ` + "`json:\"url\"`" + `
|
|
}
|
|
|
|
type FhirBundleEntry struct {
|
|
FullURL string ` + "`json:\"fullUrl,omitempty\"`" + `
|
|
Resource interface{} ` + "`json:\"resource,omitempty\"`" + `
|
|
Search FhirBundleEntrySearch ` + "`json:\"search,omitempty\"`" + `
|
|
}
|
|
|
|
type FhirBundleEntrySearch struct {
|
|
Mode string ` + "`json:\"mode,omitempty\"`" + `
|
|
Score string ` + "`json:\"score,omitempty\"`" + `
|
|
}
|
|
|
|
// Base request/response structures with FHIR integration
|
|
type BaseRequest struct {
|
|
RequestID string ` + "`json:\"request_id,omitempty\"`" + `
|
|
Timestamp time.Time ` + "`json:\"timestamp,omitempty\"`" + `
|
|
}
|
|
|
|
type BaseResponse struct {
|
|
Status string ` + "`json:\"status\"`" + `
|
|
Message string ` + "`json:\"message\"`" + `
|
|
Data interface{} ` + "`json:\"data,omitempty\"`" + `
|
|
Error *ErrorResponse ` + "`json:\"error,omitempty\"`" + `
|
|
Metadata *ResponseMetadata ` + "`json:\"metadata,omitempty\"`" + `
|
|
}
|
|
|
|
type ErrorResponse struct {
|
|
Code string ` + "`json:\"code\"`" + `
|
|
Message string ` + "`json:\"message\"`" + `
|
|
Details string ` + "`json:\"details,omitempty\"`" + `
|
|
}
|
|
|
|
type ResponseMetadata struct {
|
|
Timestamp time.Time ` + "`json:\"timestamp\"`" + `
|
|
Version string ` + "`json:\"version\"`" + `
|
|
RequestID string ` + "`json:\"request_id,omitempty\"`" + `
|
|
Path string ` + "`json:\"path,omitempty\"`" + `
|
|
Depth int ` + "`json:\"depth,omitempty\"`" + `
|
|
FhirResource string ` + "`json:\"fhir_resource,omitempty\"`" + `
|
|
}
|
|
|
|
// ` + data.Name + ` Response Structure with FHIR integration
|
|
type ` + data.Name + `Response struct {
|
|
FhirResource
|
|
BaseResponse
|
|
}
|
|
|
|
// Generic FHIR Response
|
|
type FhirResponse struct {
|
|
ResourceType string ` + "`json:\"resourceType\"`" + `
|
|
ID string ` + "`json:\"id,omitempty\"`" + `
|
|
Meta FhirMeta ` + "`json:\"meta,omitempty\"`" + `
|
|
Content map[string]interface{} ` + "`json:\"-\"`" + ` // For dynamic content
|
|
}`
|
|
|
|
// Add CRUD request structures based on methods
|
|
if data.HasPost {
|
|
modelContent += `
|
|
|
|
// ` + data.Name + ` CREATE Request Structure with FHIR R4 Validation
|
|
type ` + data.Name + `CreateRequest struct {
|
|
BaseRequest
|
|
ResourceType string ` + "`json:\"resourceType\" binding:\"required\" validate:\"required,eq=` + data.FhirResource + `\"`" + `
|
|
|
|
// Core FHIR ` + data.FhirResource + ` fields - customize based on specific resource
|
|
// Path: ` + data.Category + `
|
|
Meta FhirMeta ` + "`json:\"meta,omitempty\"`" + `
|
|
Identifier []FhirIdentifier ` + "`json:\"identifier,omitempty\" validate:\"dive\"`" + `
|
|
Active *bool ` + "`json:\"active,omitempty\"`" + `
|
|
Name []FhirHumanName ` + "`json:\"name,omitempty\" validate:\"dive\"`" + `
|
|
Telecom []FhirContactPoint ` + "`json:\"telecom,omitempty\" validate:\"dive\"`" + `
|
|
Gender string ` + "`json:\"gender,omitempty\" validate:\"omitempty,oneof=male female other unknown\"`" + `
|
|
BirthDate string ` + "`json:\"birthDate,omitempty\" validate:\"omitempty,datetime=2006-01-02\"`" + `
|
|
Address []FhirAddress ` + "`json:\"address,omitempty\" validate:\"dive\"`" + `
|
|
}
|
|
|
|
// Additional FHIR data types for ` + data.FhirResource + `
|
|
type FhirHumanName struct {
|
|
Use string ` + "`json:\"use,omitempty\" validate:\"omitempty,oneof=usual official temp nickname anonymous old maiden\"`" + `
|
|
Text string ` + "`json:\"text,omitempty\"`" + `
|
|
Family string ` + "`json:\"family,omitempty\"`" + `
|
|
Given []string ` + "`json:\"given,omitempty\"`" + `
|
|
Prefix []string ` + "`json:\"prefix,omitempty\"`" + `
|
|
Suffix []string ` + "`json:\"suffix,omitempty\"`" + `
|
|
Period FhirPeriod ` + "`json:\"period,omitempty\"`" + `
|
|
}
|
|
|
|
type FhirContactPoint struct {
|
|
System string ` + "`json:\"system,omitempty\" validate:\"omitempty,oneof=phone fax email pager url sms other\"`" + `
|
|
Value string ` + "`json:\"value,omitempty\"`" + `
|
|
Use string ` + "`json:\"use,omitempty\" validate:\"omitempty,oneof=home work temp old mobile\"`" + `
|
|
Rank int ` + "`json:\"rank,omitempty\" validate:\"omitempty,min=1\"`" + `
|
|
Period FhirPeriod ` + "`json:\"period,omitempty\"`" + `
|
|
}
|
|
|
|
type FhirAddress struct {
|
|
Use string ` + "`json:\"use,omitempty\" validate:\"omitempty,oneof=home work temp old billing\"`" + `
|
|
Type string ` + "`json:\"type,omitempty\" validate:\"omitempty,oneof=postal physical both\"`" + `
|
|
Text string ` + "`json:\"text,omitempty\"`" + `
|
|
Line []string ` + "`json:\"line,omitempty\"`" + `
|
|
City string ` + "`json:\"city,omitempty\"`" + `
|
|
District string ` + "`json:\"district,omitempty\"`" + `
|
|
State string ` + "`json:\"state,omitempty\"`" + `
|
|
PostalCode string ` + "`json:\"postalCode,omitempty\"`" + `
|
|
Country string ` + "`json:\"country,omitempty\"`" + `
|
|
Period FhirPeriod ` + "`json:\"period,omitempty\"`" + `
|
|
}
|
|
|
|
// ValidateFhir validates the ` + data.Name + `CreateRequest with FHIR R4 business rules
|
|
func (r *` + data.Name + `CreateRequest) ValidateFhir() error {
|
|
if r.ResourceType != "` + data.FhirResource + `" {
|
|
return fmt.Errorf("invalid resourceType: expected ` + data.FhirResource + `, got %s", r.ResourceType)
|
|
}
|
|
|
|
// Validate required identifiers for Indonesian context
|
|
if len(r.Identifier) == 0 {
|
|
return fmt.Errorf("at least one identifier is required for ` + data.FhirResource + ` resource")
|
|
}
|
|
|
|
// Validate NIK if present
|
|
for _, identifier := range r.Identifier {
|
|
if identifier.System == "https://fhir.kemkes.go.id/id/nik" {
|
|
if !isValidNIK(identifier.Value) {
|
|
return fmt.Errorf("invalid NIK format: %s", identifier.Value)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Path: ` + data.Category + `
|
|
return nil
|
|
}
|
|
|
|
// ToJSON converts struct to JSON string
|
|
func (r *` + data.Name + `CreateRequest) ToJSON() (string, error) {
|
|
data, err := json.Marshal(r)
|
|
return string(data), err
|
|
}`
|
|
}
|
|
|
|
if data.HasPut {
|
|
modelContent += `
|
|
|
|
// ` + data.Name + ` UPDATE Request Structure with FHIR R4 Validation
|
|
type ` + data.Name + `UpdateRequest struct {
|
|
BaseRequest
|
|
ID string ` + "`json:\"id\" binding:\"required\" validate:\"required,uuid\"`" + `
|
|
ResourceType string ` + "`json:\"resourceType\" binding:\"required\" validate:\"required,eq=` + data.FhirResource + `\"`" + `
|
|
Meta FhirMeta ` + "`json:\"meta,omitempty\"`" + `
|
|
Identifier []FhirIdentifier ` + "`json:\"identifier,omitempty\" validate:\"dive\"`" + `
|
|
Active *bool ` + "`json:\"active,omitempty\"`" + `
|
|
Name []FhirHumanName ` + "`json:\"name,omitempty\" validate:\"dive\"`" + `
|
|
Telecom []FhirContactPoint ` + "`json:\"telecom,omitempty\" validate:\"dive\"`" + `
|
|
Gender string ` + "`json:\"gender,omitempty\" validate:\"omitempty,oneof=male female other unknown\"`" + `
|
|
BirthDate string ` + "`json:\"birthDate,omitempty\" validate:\"omitempty,datetime=2006-01-02\"`" + `
|
|
Address []FhirAddress ` + "`json:\"address,omitempty\" validate:\"dive\"`" + `
|
|
}
|
|
|
|
// ValidateFhir validates the ` + data.Name + `UpdateRequest with FHIR R4 business rules
|
|
func (r *` + data.Name + `UpdateRequest) ValidateFhir() error {
|
|
if r.ResourceType != "` + data.FhirResource + `" {
|
|
return fmt.Errorf("invalid resourceType: expected ` + data.FhirResource + `, got %s", r.ResourceType)
|
|
}
|
|
|
|
if r.ID == "" {
|
|
return fmt.Errorf("resource ID is required for update operation")
|
|
}
|
|
|
|
// Path: ` + data.Category + `
|
|
return nil
|
|
}
|
|
|
|
// ToJSON converts struct to JSON string
|
|
func (r *` + data.Name + `UpdateRequest) ToJSON() (string, error) {
|
|
data, err := json.Marshal(r)
|
|
return string(data), err
|
|
}`
|
|
}
|
|
|
|
if data.HasPatch {
|
|
modelContent += `
|
|
|
|
// ` + data.Name + ` PATCH Request Structure with FHIR R4 JSON Patch
|
|
type ` + data.Name + `PatchRequest struct {
|
|
BaseRequest
|
|
ID string ` + "`json:\"id\" binding:\"required\" validate:\"required,uuid\"`" + `
|
|
Patches []FhirJsonPatch ` + "`json:\"patches\" binding:\"required\" validate:\"required,dive\"`" + `
|
|
}
|
|
|
|
type FhirJsonPatch struct {
|
|
Op string ` + "`json:\"op\" binding:\"required\" validate:\"required,oneof=add remove replace move copy test\"`" + `
|
|
Path string ` + "`json:\"path\" binding:\"required\" validate:\"required\"`" + `
|
|
Value interface{} ` + "`json:\"value,omitempty\"`" + `
|
|
From string ` + "`json:\"from,omitempty\"`" + `
|
|
}
|
|
|
|
// ValidateFhirPatch validates the ` + data.Name + `PatchRequest with FHIR R4 patch rules
|
|
func (r *` + data.Name + `PatchRequest) ValidateFhirPatch() error {
|
|
if r.ID == "" {
|
|
return fmt.Errorf("resource ID is required for patch operation")
|
|
}
|
|
|
|
if len(r.Patches) == 0 {
|
|
return fmt.Errorf("at least one patch operation is required")
|
|
}
|
|
|
|
// Validate each patch operation
|
|
for i, patch := range r.Patches {
|
|
if patch.Path == "" {
|
|
return fmt.Errorf("patch[%d]: path is required", i)
|
|
}
|
|
|
|
if patch.Op == "move" || patch.Op == "copy" {
|
|
if patch.From == "" {
|
|
return fmt.Errorf("patch[%d]: from is required for %s operation", i, patch.Op)
|
|
}
|
|
}
|
|
|
|
if patch.Op != "remove" && patch.Op != "test" && patch.Value == nil {
|
|
return fmt.Errorf("patch[%d]: value is required for %s operation", i, patch.Op)
|
|
}
|
|
}
|
|
|
|
// Path: ` + data.Category + `
|
|
return nil
|
|
}
|
|
|
|
// ToJSON converts struct to JSON string
|
|
func (r *` + data.Name + `PatchRequest) ToJSON() (string, error) {
|
|
data, err := json.Marshal(r)
|
|
return string(data), err
|
|
}`
|
|
}
|
|
|
|
// Add validation helper functions
|
|
modelContent += `
|
|
|
|
// FHIR validation helper functions for Indonesian context
|
|
func isValidNIK(nik string) bool {
|
|
if len(nik) != 16 {
|
|
return false
|
|
}
|
|
|
|
// Check if all characters are digits
|
|
matched, _ := regexp.MatchString("^[0-9]{16}$", nik)
|
|
return matched
|
|
}
|
|
|
|
func isValidFhirDate(date string) bool {
|
|
// FHIR date format: YYYY, YYYY-MM, or YYYY-MM-DD
|
|
patterns := []string{
|
|
"^[0-9]{4}$", // Year only
|
|
"^[0-9]{4}-[0-9]{2}$", // Year-Month
|
|
"^[0-9]{4}-[0-9]{2}-[0-9]{2}$", // Full date
|
|
}
|
|
|
|
for _, pattern := range patterns {
|
|
matched, _ := regexp.MatchString(pattern, date)
|
|
if matched {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isValidFhirDateTime(datetime string) bool {
|
|
// FHIR datetime format with timezone
|
|
pattern := "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$"
|
|
matched, _ := regexp.MatchString(pattern, datetime)
|
|
return matched
|
|
}
|
|
|
|
func isValidFhirID(id string) bool {
|
|
// FHIR ID: 1-64 characters, alphanumeric, dash, dot
|
|
if len(id) < 1 || len(id) > 64 {
|
|
return false
|
|
}
|
|
|
|
pattern := "^[A-Za-z0-9\\-\\.]{1,64}$"
|
|
matched, _ := regexp.MatchString(pattern, id)
|
|
return matched
|
|
}
|
|
|
|
// GetPathInfo returns information about the multi-level FHIR path
|
|
func GetFhirPathInfo() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"fhir_resource": "` + data.FhirResource + `",
|
|
"path": "` + data.Category + `",
|
|
"depth": ` + fmt.Sprintf("%d", data.DirectoryDepth) + `,
|
|
"parts": []string{` + `"` + strings.Join(data.CategoryParts, `", "`) + `"` + `},
|
|
"base_url": "https://api-satusehat-stg.dto.kemkes.go.id/fhir-r4/v1",
|
|
"profile_url": "https://fhir.kemkes.go.id/r4/StructureDefinition/` + data.FhirResource + `",
|
|
}
|
|
}`
|
|
|
|
writeFile(filepath.Join(modelDir, data.NameLower+".go"), modelContent)
|
|
}
|
|
|
|
// Continue with model generation, routes generation, and utility functions...
|
|
// [Model generation and other functions would continue here following the same pattern]
|
|
|
|
func writeFile(filename, content string) {
|
|
if err := os.WriteFile(filename, []byte(content), 0644); err != nil {
|
|
fmt.Printf("❌ Error creating file %s: %v\n", filename, err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("✅ Generated Satu Sehat FHIR file: %s\n", filename)
|
|
}
|