Files
antrean-anjungan/tools/satusehat/generate-satusehat-handler.go

2119 lines
65 KiB
Go

package main
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"text/template"
"time"
"gopkg.in/yaml.v2"
)
// ================== STRUCTURES FOR SEPARATION OF CONCERNS ==================
// HandlerValidation tracks validation and generation status with thread safety
type HandlerValidation struct {
ExistingFunctions map[string]bool
NewFunctions []string
UpdatedFiles []string
CreatedFiles []string
mu sync.Mutex // For thread-safe access to slices
}
// DirectoryInfo represents directory structure for nested endpoints
type DirectoryInfo struct {
Path string
IsFile bool
Functions []FunctionInfo
Children map[string]*DirectoryInfo
}
// FunctionInfo contains information about a function
type FunctionInfo struct {
Name string
Methods []string
Endpoint string
Config EndpointConfig
}
// EndpointConfig contains configuration for an endpoint
type EndpointConfig struct {
Methods []string `yaml:"methods"`
GetPath string `yaml:"get_path"`
PostPath string `yaml:"post_path"`
PutPath string `yaml:"put_path"`
PatchPath string `yaml:"patch_path"`
DeletePath string `yaml:"delete_path"`
SearchPath string `yaml:"search_path"`
Model string `yaml:"model"`
ResponseModel string `yaml:"response_model"`
RequestModel string `yaml:"request_model"`
Description string `yaml:"description"`
Summary string `yaml:"summary"`
Tags []string `yaml:"tags"`
RequireAuth bool `yaml:"require_auth"`
CacheEnabled bool `yaml:"cache_enabled"`
CacheTTL int `yaml:"cache_ttl"`
FhirProfiles []string `yaml:"fhir_profiles"`
SearchParams []string `yaml:"search_params"`
}
// GlobalConfig contains global configuration
type GlobalConfig struct {
ModuleName string `yaml:"module_name"`
OutputDir string `yaml:"output_dir"`
EnableSwagger bool `yaml:"enable_swagger"`
EnableLogging bool `yaml:"enable_logging"`
EnableMetrics bool `yaml:"enable_metrics"`
EnableAuth bool `yaml:"enable_auth"`
BaseURL string `yaml:"base_url"`
Version string `yaml:"version"`
Environment string `yaml:"environment"`
FhirVersion string `yaml:"fhir_version"`
ProfileURL string `yaml:"profile_url"`
}
// ServiceConfig contains complete service configuration
type ServiceConfig struct {
Global GlobalConfig `yaml:"global,omitempty"`
Services map[string]Service `yaml:"services"`
}
// Service represents a single service
type Service struct {
Name string `yaml:"name"`
Category string `yaml:"category"`
Package string `yaml:"package"`
Description string `yaml:"description"`
BaseURL string `yaml:"base_url"`
Timeout int `yaml:"timeout"`
RetryCount int `yaml:"retry_count"`
FhirResource string `yaml:"fhir_resource,omitempty"`
Validation ValidationConfig `yaml:"validation,omitempty"`
Auth AuthConfig `yaml:"authentication,omitempty"`
Endpoints map[string]EndpointGroup `yaml:"endpoints"`
Dependencies string `yaml:"dependencies,omitempty"`
Middleware string `yaml:"middleware,omitempty"`
}
// ValidationConfig contains validation rules
type ValidationConfig struct {
EnableFhirValidation bool `yaml:"enable_fhir_validation"`
RequiredFields []string `yaml:"required_fields"`
CustomValidators []string `yaml:"custom_validators"`
}
// AuthConfig contains authentication configuration
type AuthConfig struct {
Type string `yaml:"type"`
TokenURL string `yaml:"token_url"`
Scopes []string `yaml:"scopes"`
}
// EndpointGroup represents a group of related endpoints - MODIFIED to match your YAML
type EndpointGroup struct {
Description string `yaml:"description"`
Basic map[string]interface{} `yaml:"basic"`
// Generated fields
HandlerFolder string
HandlerFile string
HandlerName string
Functions map[string]FunctionConfig
}
// FunctionConfig contains individual function configuration
type FunctionConfig struct {
Methods []string `yaml:"methods"`
Path string `yaml:"path"`
Model string `yaml:"model"`
RoutesLink string `yaml:"routes_link"`
// Path for swagger documentation
GetPath string `yaml:"get_path"`
PostPath string `yaml:"post_path"`
PutPath string `yaml:"put_path"`
PatchPath string `yaml:"patch_path"`
DeletePath string `yaml:"delete_path"`
SearchPath string `yaml:"search_path"`
ResponseModel string `yaml:"response_model"`
RequestModel string `yaml:"request_model"`
Description string `yaml:"description"`
Summary string `yaml:"summary"`
Tags []string `yaml:"tags"`
RequireAuth bool `yaml:"require_auth"`
CacheEnabled bool `yaml:"cache_enabled"`
CacheTTL int `yaml:"cache_ttl"`
FhirProfiles []string `yaml:"fhir_profiles"`
SearchParams []string `yaml:"search_params"`
}
// TemplateData contains data for template generation
type TemplateData struct {
ServiceName string
ServiceLower string
ServiceUpper string
Category string
CategoryParts []string
Package string
Description string
BaseURL string
Timeout int
RetryCount int
FhirResource string
Endpoints []EndpointData
Timestamp string
ModuleName string
HasValidator bool
HasLogger bool
HasSwagger bool
GlobalConfig GlobalConfig
ShouldGenerateStruct bool
ShouldGenerateConstructor bool
FunctionalArea string
HandlerName string
DirectoryDepth int
}
// EndpointData contains endpoint-specific data
type EndpointData struct {
Name string
NameLower string
NameUpper string
NameCamel string
Methods []string
GetPath string
PostPath string
PutPath string
PatchPath string
DeletePath string
SearchPath string
Model string
ResponseModel string
RequestModel string
DataModel string
Description string
Summary string
Tags []string
HasGet bool
HasPost bool
HasPut bool
HasPatch bool
HasDelete bool
HasSearch bool
RequireAuth bool
CacheEnabled bool
CacheTTL int
PathParams []string
ModelPackage string
FhirResource string
CategoryPath string
}
// ================== MAIN GENERATION FUNCTIONS ==================
// generateHandlerWithValidation generates handler with separation of concerns
func generateHandlerWithValidation(
ctx context.Context,
serviceName string,
svc Service,
gc GlobalConfig,
) error {
validation := &HandlerValidation{
ExistingFunctions: make(map[string]bool),
NewFunctions: []string{},
UpdatedFiles: []string{},
CreatedFiles: []string{},
}
baseDir := gc.OutputDir
var wg sync.WaitGroup
errChan := make(chan error, len(svc.Endpoints))
// Debug: Print service information
fmt.Printf("🔍 Debug: Processing service %s with %d endpoints\n", serviceName, len(svc.Endpoints))
// Process each endpoint group concurrently
for groupName, grp := range svc.Endpoints {
wg.Add(1)
go func(groupName string, grp EndpointGroup) {
defer wg.Done()
select {
case <-ctx.Done():
errChan <- ctx.Err()
return
default:
// Debug: Print endpoint group information
fmt.Printf("🔍 Debug: Processing endpoint group %s with %d functions\n", groupName, len(grp.Functions))
// Create handler folder structure
categoryParts := strings.Split(svc.Category, "/")
handlerDirParts := append([]string{baseDir}, categoryParts...)
handlerDir := filepath.Join(handlerDirParts...)
if err := os.MkdirAll(handlerDir, 0755); err != nil {
errChan <- fmt.Errorf("mkdir %s: %w", handlerDir, err)
return
}
// Generate methods file with clear naming
methodsFileName := fmt.Sprintf("%s.go", strings.ToLower(svc.Package))
methodsFilePath := filepath.Join(handlerDir, methodsFileName)
// Check if methods file exists
fileExists := false
if _, err := os.Stat(methodsFilePath); err == nil {
fileExists = true
}
if !fileExists {
// Create new methods file
err := createMethodsFileFromConfig(
methodsFilePath, svc, grp, gc, groupName,
)
if err != nil {
errChan <- fmt.Errorf("create methods file %s: %w", methodsFilePath, err)
return
}
validation.mu.Lock()
validation.CreatedFiles = append(validation.CreatedFiles, methodsFilePath)
validation.mu.Unlock()
fmt.Printf("✅ Created methods file: %s\n", methodsFilePath)
} else {
// Update existing methods file with new functions only
err := updateExistingMethodsFile(
methodsFilePath, svc, grp, gc, groupName, validation,
)
if err != nil {
errChan <- fmt.Errorf("update methods file %s: %w", methodsFilePath, err)
return
}
validation.mu.Lock()
if len(validation.NewFunctions) > 0 {
validation.UpdatedFiles = append(validation.UpdatedFiles, methodsFilePath)
}
validation.mu.Unlock()
fmt.Printf("✅ Updated methods file: %s\n", methodsFilePath)
}
}
}(groupName, grp)
}
// Wait for all goroutines to complete
wg.Wait()
close(errChan)
// Check for errors
for err := range errChan {
if err != nil {
return err
}
}
// Generate models (sequential for simplicity)
if err := generateModels(ctx, svc, gc); err != nil {
return fmt.Errorf("generate models: %w", err)
}
// Generate routes
if err := generateRoutes(serviceName, svc, gc); err != nil {
return fmt.Errorf("generate routes: %w", err)
}
return nil
}
// createMethodsFileFromConfig creates new methods file from configuration
func createMethodsFileFromConfig(
filePath string,
svc Service,
grp EndpointGroup,
gc GlobalConfig,
functionalArea string,
) error {
// Debug: Print function information
fmt.Printf("🔍 Debug: Creating methods file with %d functions\n", len(grp.Functions))
for funcName, funcConfig := range grp.Functions {
fmt.Printf("🔍 Debug: Function %s has methods: %v\n", funcName, funcConfig.Methods)
}
// Collect all functions for this group
var allEndpoints []EndpointData
for fname, fcfg := range grp.Functions {
td := processFunctionData(svc, grp, fname, fcfg, gc)
allEndpoints = append(allEndpoints, td.Endpoints...)
}
templateData := TemplateData{
ServiceName: svc.Name,
ServiceLower: strings.ToLower(svc.Name),
ServiceUpper: strings.ToUpper(svc.Name),
Category: svc.Category,
CategoryParts: strings.Split(svc.Category, "/"),
Package: svc.Package,
Description: svc.Description,
BaseURL: svc.BaseURL,
Timeout: svc.Timeout,
RetryCount: svc.RetryCount,
FhirResource: svc.FhirResource,
Endpoints: allEndpoints,
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
ModuleName: gc.ModuleName,
HasValidator: true,
HasLogger: gc.EnableLogging,
HasSwagger: gc.EnableSwagger,
GlobalConfig: gc,
ShouldGenerateStruct: false, // NEVER generate struct in methods file
ShouldGenerateConstructor: false, // NEVER generate constructor in methods file
FunctionalArea: functionalArea,
HandlerName: grp.HandlerName,
DirectoryDepth: len(strings.Split(svc.Category, "/")),
}
return createMethodsFile(filePath, templateData)
}
// updateExistingMethodsFile updates existing methods file with new functions
func updateExistingMethodsFile(
filePath string,
svc Service,
grp EndpointGroup,
gc GlobalConfig,
functionalArea string,
validation *HandlerValidation,
) error {
existingContent, err := ioutil.ReadFile(filePath)
if err != nil {
return err
}
content := string(existingContent)
// Debug: Print function information
fmt.Printf("🔍 Debug: Updating methods file with %d functions\n", len(grp.Functions))
for funcName, funcConfig := range grp.Functions {
fmt.Printf("🔍 Debug: Function %s has methods: %v\n", funcName, funcConfig.Methods)
}
// Check for existing functions and collect new ones
var newEndpoints []EndpointData
for fname, fcfg := range grp.Functions {
functionExists := false
for _, method := range fcfg.Methods {
funcName := generateFunctionName(fname, method)
sig := fmt.Sprintf("func (h *%sHandler) %s", grp.HandlerName, funcName)
if strings.Contains(content, sig) {
fmt.Printf("⚠️ Skip existing: %s (%s)\n", fname, funcName)
functionExists = true
break
}
}
if functionExists {
continue
}
td := processFunctionData(svc, grp, fname, fcfg, gc)
newEndpoints = append(newEndpoints, td.Endpoints...)
validation.mu.Lock()
validation.NewFunctions = append(validation.NewFunctions, fname)
validation.mu.Unlock()
fmt.Printf("✅ Will add: %s\n", fname)
}
if len(newEndpoints) == 0 {
fmt.Printf("⏭️ No new functions to add\n")
return nil
}
// Generate new functions using methods-only template
templateData := TemplateData{
ServiceName: svc.Name,
ServiceLower: strings.ToLower(svc.Name),
ServiceUpper: strings.ToUpper(svc.Name),
Category: svc.Category,
CategoryParts: strings.Split(svc.Category, "/"),
Package: svc.Package,
Description: svc.Description,
BaseURL: svc.BaseURL,
Timeout: svc.Timeout,
RetryCount: svc.RetryCount,
FhirResource: svc.FhirResource,
Endpoints: newEndpoints,
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
ModuleName: gc.ModuleName,
HasValidator: true,
HasLogger: gc.EnableLogging,
HasSwagger: gc.EnableSwagger,
GlobalConfig: gc,
ShouldGenerateStruct: false, // NEVER
ShouldGenerateConstructor: false, // NEVER
FunctionalArea: functionalArea,
HandlerName: grp.HandlerName,
DirectoryDepth: len(strings.Split(svc.Category, "/")),
}
newFunctions, err := generateNewMethodsOnly(templateData)
if err != nil {
return err
}
// Merge content
mergedContent := mergeGoFileContent(content, newFunctions)
return ioutil.WriteFile(filePath, []byte(mergedContent), 0644)
}
// processFunctionData processes individual function configuration
func processFunctionData(
svc Service,
grp EndpointGroup,
fname string,
fcfg FunctionConfig,
gc GlobalConfig,
) TemplateData {
fmt.Printf("🔍 Debug: Processing function %s with methods: %v\n", fname, fcfg.Methods)
ed := EndpointData{
Name: toCamelCase(fname),
NameLower: strings.ToLower(fname),
NameUpper: strings.ToUpper(fname),
NameCamel: toCamelCase(fname),
Methods: fcfg.Methods,
GetPath: fcfg.GetPath,
PostPath: fcfg.PostPath,
PutPath: fcfg.PutPath,
PatchPath: fcfg.PatchPath,
DeletePath: fcfg.DeletePath,
SearchPath: fcfg.SearchPath,
Model: fcfg.Model,
ResponseModel: fcfg.ResponseModel,
RequestModel: fcfg.Model,
DataModel: strings.Replace(fcfg.ResponseModel, "Response", "Data", 1),
Description: fcfg.Description,
Summary: fcfg.Summary,
Tags: fcfg.Tags,
RequireAuth: fcfg.RequireAuth,
CacheEnabled: fcfg.CacheEnabled,
CacheTTL: fcfg.CacheTTL,
PathParams: extractPathParams(fcfg.GetPath),
ModelPackage: svc.Package,
FhirResource: svc.FhirResource,
CategoryPath: svc.Category,
}
// Set method flags
for _, m := range fcfg.Methods {
switch strings.ToUpper(m) {
case "GET":
ed.HasGet = true
case "POST":
ed.HasPost = true
case "PUT":
ed.HasPut = true
case "PATCH":
ed.HasPatch = true
case "DELETE":
ed.HasDelete = true
case "SEARCH":
ed.HasSearch = true
}
}
fmt.Printf("🔍 Debug: Function %s flags - Get:%t, Post:%t, Put:%t, Patch:%t, Delete:%t, Search:%t\n",
fname, ed.HasGet, ed.HasPost, ed.HasPut, ed.HasPatch, ed.HasDelete, ed.HasSearch)
return TemplateData{
ServiceName: svc.Name,
ServiceLower: strings.ToLower(svc.Name),
ServiceUpper: strings.ToUpper(svc.Name),
Category: svc.Category,
CategoryParts: strings.Split(svc.Category, "/"),
Package: svc.Package,
Description: svc.Description,
BaseURL: svc.BaseURL,
Timeout: svc.Timeout,
RetryCount: svc.RetryCount,
FhirResource: svc.FhirResource,
Endpoints: []EndpointData{ed},
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
ModuleName: gc.ModuleName,
HasValidator: true,
HasLogger: gc.EnableLogging,
HasSwagger: gc.EnableSwagger,
GlobalConfig: gc,
HandlerName: grp.HandlerName,
DirectoryDepth: len(strings.Split(svc.Category, "/")),
}
}
// generateModels generates model files for the service
func generateModels(ctx context.Context, svc Service, gc GlobalConfig) error {
// Create model directory structure
categoryParts := strings.Split(svc.Category, "/")
modelDirParts := append([]string{"internal", "models"}, categoryParts...)
modelDir := filepath.Join(modelDirParts...)
if err := os.MkdirAll(modelDir, 0755); err != nil {
return fmt.Errorf("mkdir %s: %w", modelDir, err)
}
modelFilePath := filepath.Join(modelDir, strings.ToLower(svc.Package)+".go")
// Check if model file exists
if _, err := os.Stat(modelFilePath); err == nil {
fmt.Printf("⏭️ Model file already exists: %s\n", modelFilePath)
return nil
}
// Check if context was cancelled
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// Generate model content
templateData := TemplateData{
ServiceName: svc.Name,
ServiceLower: strings.ToLower(svc.Name),
ServiceUpper: strings.ToUpper(svc.Name),
Category: svc.Category,
CategoryParts: categoryParts,
Package: svc.Package,
FhirResource: svc.FhirResource,
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
ModuleName: gc.ModuleName,
DirectoryDepth: len(categoryParts),
}
tmpl := template.New("model")
tmpl, err := tmpl.Parse(modelTemplate)
if err != nil {
return err
}
file, err := os.Create(modelFilePath)
if err != nil {
return err
}
defer file.Close()
err = tmpl.Execute(file, templateData)
if err != nil {
return err
}
fmt.Printf("✅ Created model file: %s\n", modelFilePath)
return nil
}
// generateRoutes generates routes for the service
func generateRoutes(serviceName string, svc Service, gc GlobalConfig) error {
routesFilePath := filepath.Join(gc.OutputDir, "routes.go")
if _, err := os.Stat(routesFilePath); os.IsNotExist(err) {
// Create routes file if it doesn't exist
if err := os.MkdirAll(filepath.Dir(routesFilePath), 0755); err != nil {
return fmt.Errorf("mkdir %s: %w", filepath.Dir(routesFilePath), err)
}
// Create a basic routes file
routesContent := fmt.Sprintf(`package handlers
import (
"%s/internal/config"
"%s/pkg/logger"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
// SetupRoutes sets up all routes for the application
func SetupRoutes(r *gin.Engine, cfg *config.Config) error {
// Setup routes will be added here
return nil
}
`, gc.ModuleName, gc.ModuleName)
if err := ioutil.WriteFile(routesFilePath, []byte(routesContent), 0644); err != nil {
return fmt.Errorf("create routes file: %w", err)
}
fmt.Printf("✅ Created routes file: %s\n", routesFilePath)
}
routesContent, err := ioutil.ReadFile(routesFilePath)
if err != nil {
return fmt.Errorf("read routes file: %w", err)
}
routesContentStr := string(routesContent)
// Check if routes are already registered
if strings.Contains(routesContentStr, fmt.Sprintf("Register%sRoutes", svc.Name)) {
fmt.Printf("⚠️ Routes for %s already registered\n", svc.Name)
return nil
}
var imports []string
var allRoutes []string
// Track handler folders that have been imported
processedFolders := make(map[string]bool)
for groupName, grp := range svc.Endpoints {
// Import based on handler folder
if !processedFolders[grp.HandlerFolder] {
importLine := fmt.Sprintf("\t%sHandlers \"%s/internal/handlers/%s\"",
grp.HandlerFolder, gc.ModuleName, grp.HandlerFolder)
imports = append(imports, importLine)
processedFolders[grp.HandlerFolder] = true
fmt.Printf("✅ Added import: %sHandlers\n", grp.HandlerFolder)
}
var routesCode strings.Builder
// Use groupName for comment and identifier
routesCode.WriteString(fmt.Sprintf("\n\t// %s (%s) routes\n", grp.Description, groupName))
// Handler instantiation using HandlerName from config
routesCode.WriteString(fmt.Sprintf("\t%sHandler := %sHandlers.New%sHandler(%sHandlers.%sHandlerConfig{\n",
strings.ToLower(grp.HandlerName),
grp.HandlerFolder,
grp.HandlerName,
grp.HandlerFolder,
grp.HandlerName))
routesCode.WriteString("\t\tConfig: cfg,\n")
routesCode.WriteString("\t\tLogger: logger.Default(),\n")
routesCode.WriteString("\t\tValidator: validator.New(),\n")
routesCode.WriteString("\t})\n")
// Use groupName for route group path
routesCode.WriteString(fmt.Sprintf("\t%sGroup := r.Group(\"/%s\")\n",
strings.ToLower(grp.HandlerName), groupName))
// Process functions
for fname, fcfg := range grp.Functions {
td := processFunctionData(svc, grp, fname, fcfg, gc)
for _, endpoint := range td.Endpoints {
handlerVar := strings.ToLower(grp.HandlerName) + "Handler"
groupVar := strings.ToLower(grp.HandlerName) + "Group"
// Loop through methods and use specific routes
for _, method := range fcfg.Methods {
var cleanPath string
// Choose path based on method
switch strings.ToUpper(method) {
case "GET":
cleanPath = fcfg.GetPath
case "POST":
cleanPath = fcfg.PostPath
case "PUT":
cleanPath = fcfg.PutPath
case "PATCH":
cleanPath = fcfg.PatchPath
case "DELETE":
cleanPath = fcfg.DeletePath
case "SEARCH":
cleanPath = fcfg.SearchPath
default:
fmt.Printf("⚠️ Unsupported HTTP method: %s for function %s\n", method, fname)
continue
}
// Fallback to path if specific route is empty
if cleanPath == "" {
cleanPath = fcfg.Path
}
// Clean path - remove groupName prefix if exists
if strings.HasPrefix(cleanPath, "/"+groupName) {
cleanPath = strings.TrimPrefix(cleanPath, "/"+groupName)
}
if cleanPath == "" {
cleanPath = "/"
}
// Generate route based on method
switch strings.ToUpper(method) {
case "GET":
routesCode.WriteString(fmt.Sprintf("\t%s.GET(\"%s\", %s.Get%s)\n",
groupVar, cleanPath, handlerVar, endpoint.Name))
case "POST":
routesCode.WriteString(fmt.Sprintf("\t%s.POST(\"%s\", %s.Create%s)\n",
groupVar, cleanPath, handlerVar, endpoint.Name))
case "PUT":
routesCode.WriteString(fmt.Sprintf("\t%s.PUT(\"%s\", %s.Update%s)\n",
groupVar, cleanPath, handlerVar, endpoint.Name))
case "PATCH":
routesCode.WriteString(fmt.Sprintf("\t%s.PATCH(\"%s\", %s.Patch%s)\n",
groupVar, cleanPath, handlerVar, endpoint.Name))
case "DELETE":
routesCode.WriteString(fmt.Sprintf("\t%s.DELETE(\"%s\", %s.Delete%s)\n",
groupVar, cleanPath, handlerVar, endpoint.Name))
case "SEARCH":
routesCode.WriteString(fmt.Sprintf("\t%s.GET(\"%s\", %s.Search%s)\n",
groupVar, cleanPath, handlerVar, endpoint.Name))
}
}
}
}
allRoutes = append(allRoutes, routesCode.String())
}
// Insert imports after existing imports
if len(imports) > 0 {
importSection := strings.Join(imports, "\n") + "\n"
// Find position after import section
importMarker := "import ("
if strings.Contains(routesContentStr, importMarker) {
importIndex := strings.Index(routesContentStr, importMarker)
closingParenIndex := strings.Index(routesContentStr[importIndex:], "\n)") + importIndex
if closingParenIndex > importIndex {
routesContentStr = routesContentStr[:closingParenIndex+2] + "\n" + importSection + routesContentStr[closingParenIndex+2:]
}
}
}
// Find and insert routes
setupRoutesMarker := "func SetupRoutes"
if !strings.Contains(routesContentStr, setupRoutesMarker) {
return fmt.Errorf("SetupRoutes function not found in routes.go")
}
funcStart := strings.Index(routesContentStr, setupRoutesMarker)
funcBodyStart := strings.Index(routesContentStr[funcStart:], "{") + funcStart + 1
newRoutesContent := routesContentStr[:funcBodyStart+1] + "\n" + strings.Join(allRoutes, "\n") + "\n" + routesContentStr[funcBodyStart+1:]
err = ioutil.WriteFile(routesFilePath, []byte(newRoutesContent), 0644)
if err != nil {
return fmt.Errorf("write updated routes file: %w", err)
}
fmt.Printf("✅ Updated routes file with %s routes\n", svc.Name)
return nil
}
// ================== HELPER FUNCTIONS ==================
// generateFunctionName generates function name based on endpoint and method
func generateFunctionName(endpointName, method string) string {
switch strings.ToUpper(method) {
case "GET":
return fmt.Sprintf("Get%s", strings.Title(endpointName))
case "POST":
return fmt.Sprintf("Create%s", strings.Title(endpointName))
case "PUT":
return fmt.Sprintf("Update%s", strings.Title(endpointName))
case "DELETE":
return fmt.Sprintf("Delete%s", strings.Title(endpointName))
case "PATCH":
return fmt.Sprintf("Patch%s", strings.Title(endpointName))
case "SEARCH":
return fmt.Sprintf("Search%s", strings.Title(endpointName))
default:
return fmt.Sprintf("%s%s", strings.Title(method), strings.Title(endpointName))
}
}
// extractPathParams extracts path parameters from URL path
func extractPathParams(path string) []string {
var params []string
for _, part := range strings.Split(path, "/") {
if strings.HasPrefix(part, ":") {
params = append(params, strings.TrimPrefix(part, ":"))
}
}
return params
}
// toCamelCase converts string to CamelCase
func toCamelCase(s string) string {
parts := strings.FieldsFunc(s, func(r rune) bool {
return r == '_' || r == '-' || r == ' '
})
for i, p := range parts {
parts[i] = strings.Title(strings.ToLower(p))
}
return strings.Join(parts, "")
}
// createMethodsFile creates methods file using template
func createMethodsFile(filePath string, templateData TemplateData) error {
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
return err
}
// Debug: Print template data
fmt.Printf("🔍 Debug: Creating methods file with %d endpoints\n", len(templateData.Endpoints))
for _, endpoint := range templateData.Endpoints {
fmt.Printf("🔍 Debug: Endpoint %s has methods: %v\n", endpoint.Name, endpoint.Methods)
}
// Create template with basic functions
tmpl := template.New("methods").Funcs(template.FuncMap{
"title": strings.Title,
"upper": strings.ToUpper,
"lower": strings.ToLower,
"index": func(slice []string, i int) string {
if i >= 0 && i < len(slice) {
return slice[i]
}
return ""
},
"add": func(a, b int) int {
return a + b
},
"sub": func(a, b int) int {
return a - b
},
})
tmpl, err := tmpl.Parse(handlerTemplate)
if err != nil {
return fmt.Errorf("parse template: %w", err)
}
file, err := os.Create(filePath)
if err != nil {
return err
}
defer file.Close()
return tmpl.Execute(file, templateData)
}
// generateNewMethodsOnly generates only new methods for existing files
func generateNewMethodsOnly(templateData TemplateData) (string, error) {
// Debug: Print template data
fmt.Printf("🔍 Debug: Generating new methods with %d endpoints\n", len(templateData.Endpoints))
for _, endpoint := range templateData.Endpoints {
fmt.Printf("🔍 Debug: Endpoint %s has methods: %v\n", endpoint.Name, endpoint.Methods)
}
// Create template with basic functions only
tmpl := template.New("newMethods").Funcs(template.FuncMap{
"title": strings.Title,
"upper": strings.ToUpper,
"lower": strings.ToLower,
"index": func(slice []string, i int) string {
if i >= 0 && i < len(slice) {
return slice[i]
}
return ""
},
})
tmpl, err := tmpl.Parse(methodsOnlyTemplate)
if err != nil {
return "", err
}
var result strings.Builder
err = tmpl.Execute(&result, templateData)
if err != nil {
return "", err
}
return result.String(), nil
}
// mergeGoFileContent merges new functions into existing file
func mergeGoFileContent(existingContent, newFunctions string) string {
// Find last closing brace
re := regexp.MustCompile(`}\s*$`)
lastBraceIndex := re.FindStringIndex(existingContent)
if lastBraceIndex == nil {
return existingContent + "\n" + newFunctions
}
before := existingContent[:lastBraceIndex[0]]
after := existingContent[lastBraceIndex[0]:]
return before + "\n" + newFunctions + "\n" + after
}
// ================== MAIN FUNCTION ==================
func main() {
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
}
configFile := os.Args[1]
var targetService string
if len(os.Args) > 2 {
targetService = os.Args[2]
}
config, err := loadConfig(configFile)
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
os.Exit(1)
}
fmt.Println("🚀 Starting Handler Generation with Validation...")
fmt.Printf("📁 Config file: %s\n", configFile)
if targetService != "" {
fmt.Printf("🎯 Target service: %s\n", targetService)
}
// Create context with cancellation for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Worker pool for concurrent processing
type result struct {
serviceName string
err error
}
results := make(chan result)
jobs := make(chan string, len(config.Services))
// Start worker pool
var wg sync.WaitGroup
numWorkers := 4 // Adjust based on your system
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for serviceName := range jobs {
select {
case <-ctx.Done():
return
case results <- result{
serviceName: serviceName,
err: generateHandlerWithValidation(ctx, serviceName, config.Services[serviceName], config.Global),
}:
}
}
}()
}
// Send jobs to workers
go func() {
for serviceName := range config.Services {
if targetService != "" && serviceName != targetService {
continue
}
select {
case <-ctx.Done():
break
case jobs <- serviceName:
}
}
close(jobs)
}()
// Collect results
go func() {
wg.Wait()
close(results)
}()
// Process results
generated := 0
errors := 0
for res := range results {
if res.err != nil {
fmt.Printf("❌ Error generating handler for %s: %v\n", res.serviceName, res.err)
errors++
continue
}
generated++
}
// Summary
fmt.Println("\n📊 Generation Summary:")
fmt.Printf("✅ Successfully processed: %d services\n", generated)
if errors > 0 {
fmt.Printf("❌ Failed: %d services\n", errors)
}
if generated > 0 {
fmt.Println("🎉 Generation completed successfully!")
}
}
// loadConfig loads configuration from YAML file
func loadConfig(filename string) (*ServiceConfig, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
var config ServiceConfig
err = yaml.Unmarshal(data, &config)
if err != nil {
return nil, err
}
// Set defaults
if config.Global.ModuleName == "" {
config.Global.ModuleName = "api-service"
}
if config.Global.OutputDir == "" {
config.Global.OutputDir = "internal/handlers"
}
// Debug: Print loaded configuration
fmt.Printf("🔍 Debug: Loaded %d services\n", len(config.Services))
// Process endpoint groups to add missing handler information
for serviceName, service := range config.Services {
fmt.Printf("🔍 Debug: Processing service %s with %d endpoints\n", serviceName, len(service.Endpoints))
for endpointName, endpointGroup := range service.Endpoints {
fmt.Printf("🔍 Debug: Processing endpoint %s with %d basic configs\n", endpointName, len(endpointGroup.Basic))
// Generate handler information based on service and endpoint names
endpointGroup.HandlerFolder = strings.ToLower(service.Package)
endpointGroup.HandlerFile = fmt.Sprintf("%s.go", strings.ToLower(service.Package))
endpointGroup.HandlerName = toCamelCase(serviceName)
// Convert basic map to functions map
endpointGroup.Functions = make(map[string]FunctionConfig)
// Debug: Print the structure of basic configs
fmt.Printf("🔍 Debug: Processing basic config for endpoint %s\n", endpointName)
// Treat the entire Basic map as a single function configuration
funcConfig := FunctionConfig{}
// Process each field in the basic config
for key, value := range endpointGroup.Basic {
keyStr := fmt.Sprintf("%v", key)
processYAMLField(keyStr, value, &funcConfig)
}
fmt.Printf("🔍 Debug: Final function config for %s: methods=%v, get_path=%s\n",
endpointName, funcConfig.Methods, funcConfig.GetPath)
endpointGroup.Functions["basic"] = funcConfig
// Update the service endpoints
service.Endpoints[endpointName] = endpointGroup
}
config.Services[serviceName] = service
}
return &config, nil
}
// processYAMLField processes a single YAML field and updates the function config
func processYAMLField(key string, value interface{}, funcConfig *FunctionConfig) {
fmt.Printf("🔍 Debug: Processing key %s with value %v (type: %T)\n", key, value, value)
switch key {
case "methods":
if methods, ok := value.([]interface{}); ok {
for _, method := range methods {
if methodStr, ok := method.(string); ok {
funcConfig.Methods = append(funcConfig.Methods, methodStr)
fmt.Printf("🔍 Debug: Added method %s\n", methodStr)
}
}
}
case "get_path":
if path, ok := value.(string); ok {
funcConfig.GetPath = path
fmt.Printf("🔍 Debug: Set get_path to %s\n", path)
}
case "post_path":
if path, ok := value.(string); ok {
funcConfig.PostPath = path
fmt.Printf("🔍 Debug: Set post_path to %s\n", path)
}
case "put_path":
if path, ok := value.(string); ok {
funcConfig.PutPath = path
fmt.Printf("🔍 Debug: Set put_path to %s\n", path)
}
case "patch_path":
if path, ok := value.(string); ok {
funcConfig.PatchPath = path
fmt.Printf("🔍 Debug: Set patch_path to %s\n", path)
}
case "delete_path":
if path, ok := value.(string); ok {
funcConfig.DeletePath = path
fmt.Printf("🔍 Debug: Set delete_path to %s\n", path)
}
case "search_path":
if path, ok := value.(string); ok {
funcConfig.SearchPath = path
fmt.Printf("🔍 Debug: Set search_path to %s\n", path)
}
case "model":
if model, ok := value.(string); ok {
funcConfig.Model = model
fmt.Printf("🔍 Debug: Set model to %s\n", model)
}
case "response_model":
if model, ok := value.(string); ok {
funcConfig.ResponseModel = model
fmt.Printf("🔍 Debug: Set response_model to %s\n", model)
}
case "description":
if desc, ok := value.(string); ok {
funcConfig.Description = desc
fmt.Printf("🔍 Debug: Set description to %s\n", desc)
}
case "summary":
if summary, ok := value.(string); ok {
funcConfig.Summary = summary
fmt.Printf("🔍 Debug: Set summary to %s\n", summary)
}
case "tags":
if tags, ok := value.([]interface{}); ok {
for _, tag := range tags {
if tagStr, ok := tag.(string); ok {
funcConfig.Tags = append(funcConfig.Tags, tagStr)
fmt.Printf("🔍 Debug: Added tag %s\n", tagStr)
}
}
}
case "require_auth":
if auth, ok := value.(bool); ok {
funcConfig.RequireAuth = auth
fmt.Printf("🔍 Debug: Set require_auth to %t\n", auth)
}
case "cache_enabled":
if cache, ok := value.(bool); ok {
funcConfig.CacheEnabled = cache
fmt.Printf("🔍 Debug: Set cache_enabled to %t\n", cache)
}
case "cache_ttl":
if ttl, ok := value.(int); ok {
funcConfig.CacheTTL = ttl
fmt.Printf("🔍 Debug: Set cache_ttl to %d\n", ttl)
}
case "fhir_profiles":
if profiles, ok := value.([]interface{}); ok {
for _, profile := range profiles {
if profileStr, ok := profile.(string); ok {
funcConfig.FhirProfiles = append(funcConfig.FhirProfiles, profileStr)
fmt.Printf("🔍 Debug: Added fhir_profile %s\n", profileStr)
}
}
}
case "search_params":
if params, ok := value.([]interface{}); ok {
for _, param := range params {
if paramStr, ok := param.(string); ok {
funcConfig.SearchParams = append(funcConfig.SearchParams, paramStr)
fmt.Printf("🔍 Debug: Added search_param %s\n", paramStr)
}
}
}
default:
fmt.Printf("⚠️ Debug: Unknown key %s\n", key)
}
}
// printUsage prints usage information
func printUsage() {
fmt.Println("Satu Sehat FHIR Handler Generator")
fmt.Println()
fmt.Println("Usage:")
fmt.Println(" go run generate-satusehat-handler.go <config-file> [service-name]")
fmt.Println()
fmt.Println("Examples:")
fmt.Println(" go run generate-satusehat-handler.go services-config-satusehat.yaml")
fmt.Println(" go run generate-satusehat-handler.go services-config-satusehat.yaml patient")
}
// ================== TEMPLATES ==================
const handlerTemplate = `// Package {{.Package}} handles {{.HandlerName}} services
// Generated on: {{.Timestamp}}
package {{.Package}}
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"{{.ModuleName}}/internal/config"
"{{.ModuleName}}/internal/models/{{.Package}}"
"{{.ModuleName}}/pkg/logger"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
// {{.HandlerName}} handles {{.HandlerName}} services
type {{.HandlerName}} struct {
service interface{}
validator *validator.Validate
logger logger.Logger
config *config.Config
}
// {{.HandlerName}}Config contains configuration for {{.HandlerName}}
type {{.HandlerName}}Config struct {
Config *config.Config
Logger logger.Logger
Validator *validator.Validate
}
// New{{.HandlerName}} creates a new {{.HandlerName}}
func New{{.HandlerName}}(cfg {{.HandlerName}}Config) *{{.HandlerName}} {
return &{{.HandlerName}}{
service: nil, // Initialize with appropriate service
validator: cfg.Validator,
logger: cfg.Logger,
config: cfg.Config,
}
}
{{range .Endpoints}}
{{if .HasPost}}
// Create{{.Name}} creates a new resource
// @Summary Create a new resource
// @Description Create a new resource in the system
// @Tags {{index .Tags 0}}
// @Accept json
// @Produce json
// @Param Authorization header string true "Bearer token"
// @Param request body models.{{.Name}}CreateRequest true "Resource creation request"
// @Success 201 {object} models.{{.Name}}Response "Resource created successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 401 {object} models.ErrorResponse "Unauthorized"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router {{.PostPath}} [post]
func (h *{{$.HandlerName}}) Create{{.Name}}(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second)
defer cancel()
// Generate request ID if not present
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
c.Header("X-Request-ID", requestID)
}
{{if $.HasLogger}}
h.logger.Info("Processing Create{{.Name}} request", map[string]interface{}{
"request_id": requestID,
"endpoint": "{{.PostPath}}",
})
{{end}}
// Bind and validate request body
var req models.{{.Name}}CreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
{{if $.HasLogger}}
h.logger.Error("Failed to bind request body", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
{{end}}
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Status: "error",
Message: "Invalid request body: " + err.Error(),
RequestID: requestID,
})
return
}
// Validate request structure
if err := h.validator.Struct(&req); err != nil {
{{if $.HasLogger}}
h.logger.Error("Request validation failed", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
{{end}}
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Status: "error",
Message: "Validation failed: " + err.Error(),
RequestID: requestID,
})
return
}
// TODO: Implement service call
// Example: resp, err := h.service.CreateResource(ctx, req)
// For now, return a mock response
response := models.{{.Name}}Response{
Status: "success",
Message: "{{.Name}} created successfully",
RequestID: requestID,
Data: nil, // Replace with actual data
}
{{if $.HasLogger}}
h.logger.Info("Successfully created {{.Name}}", map[string]interface{}{
"request_id": requestID,
})
{{end}}
c.JSON(http.StatusCreated, response)
}
{{end}}
{{if .HasGet}}
// Get{{.Name}} retrieves a specific resource by ID
// @Summary Get a specific resource by ID
// @Description Retrieve a specific resource from the system
// @Tags {{index .Tags 0}}
// @Accept json
// @Produce json
// @Param Authorization header string true "Bearer token"
// @Param id path string true "Resource ID"
// @Success 200 {object} models.{{.Name}}Response "Resource retrieved successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 401 {object} models.ErrorResponse "Unauthorized"
// @Failure 404 {object} models.ErrorResponse "Resource not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router {{.GetPath}} [get]
func (h *{{$.HandlerName}}) Get{{.Name}}(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second)
defer cancel()
// Generate request ID if not present
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
c.Header("X-Request-ID", requestID)
}
id := c.Param("id")
if id == "" {
{{if $.HasLogger}}
h.logger.Error("Missing required parameter id", map[string]interface{}{
"request_id": requestID,
})
{{end}}
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Status: "error",
Message: "Missing required parameter id",
RequestID: requestID,
})
return
}
{{if $.HasLogger}}
h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{
"request_id": requestID,
"endpoint": "{{.GetPath}}",
"id": id,
})
{{end}}
// TODO: Implement service call
// Example: resp, err := h.service.GetResource(ctx, id)
// For now, return a mock response
response := models.{{.Name}}Response{
Status: "success",
Message: "{{.Name}} retrieved successfully",
RequestID: requestID,
Data: nil, // Replace with actual data
}
{{if $.HasLogger}}
h.logger.Info("Successfully retrieved {{.Name}}", map[string]interface{}{
"request_id": requestID,
"id": id,
})
{{end}}
c.JSON(http.StatusOK, response)
}
{{end}}
{{if .HasPut}}
// Update{{.Name}} updates an existing resource
// @Summary Update an existing resource
// @Description Update an existing resource in the system
// @Tags {{index .Tags 0}}
// @Accept json
// @Produce json
// @Param Authorization header string true "Bearer token"
// @Param id path string true "Resource ID"
// @Param request body models.{{.Name}}UpdateRequest true "Resource update request"
// @Success 200 {object} models.{{.Name}}Response "Resource updated successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 401 {object} models.ErrorResponse "Unauthorized"
// @Failure 404 {object} models.ErrorResponse "Resource not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router {{.PutPath}} [put]
func (h *{{$.HandlerName}}) Update{{.Name}}(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second)
defer cancel()
// Generate request ID if not present
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
c.Header("X-Request-ID", requestID)
}
id := c.Param("id")
if id == "" {
{{if $.HasLogger}}
h.logger.Error("Missing required parameter id", map[string]interface{}{
"request_id": requestID,
})
{{end}}
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Status: "error",
Message: "Missing required parameter id",
RequestID: requestID,
})
return
}
{{if $.HasLogger}}
h.logger.Info("Processing Update{{.Name}} request", map[string]interface{}{
"request_id": requestID,
"endpoint": "{{.PutPath}}",
"id": id,
})
{{end}}
// Bind and validate request body
var req models.{{.Name}}UpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
{{if $.HasLogger}}
h.logger.Error("Failed to bind request body", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
{{end}}
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Status: "error",
Message: "Invalid request body: " + err.Error(),
RequestID: requestID,
})
return
}
// Validate request structure
if err := h.validator.Struct(&req); err != nil {
{{if $.HasLogger}}
h.logger.Error("Request validation failed", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
{{end}}
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Status: "error",
Message: "Validation failed: " + err.Error(),
RequestID: requestID,
})
return
}
// TODO: Implement service call
// Example: resp, err := h.service.UpdateResource(ctx, id, req)
// For now, return a mock response
response := models.{{.Name}}Response{
Status: "success",
Message: "{{.Name}} updated successfully",
RequestID: requestID,
Data: nil, // Replace with actual data
}
{{if $.HasLogger}}
h.logger.Info("Successfully updated {{.Name}}", map[string]interface{}{
"request_id": requestID,
"id": id,
})
{{end}}
c.JSON(http.StatusOK, response)
}
{{end}}
{{if .HasDelete}}
// Delete{{.Name}} deletes a resource
// @Summary Delete a resource
// @Description Delete a resource from the system
// @Tags {{index .Tags 0}}
// @Accept json
// @Produce json
// @Param Authorization header string true "Bearer token"
// @Param id path string true "Resource ID"
// @Success 200 {object} models.SuccessResponse "Resource deleted successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 401 {object} models.ErrorResponse "Unauthorized"
// @Failure 404 {object} models.ErrorResponse "Resource not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router {{.DeletePath}} [delete]
func (h *{{$.HandlerName}}) Delete{{.Name}}(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second)
defer cancel()
// Generate request ID if not present
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
c.Header("X-Request-ID", requestID)
}
id := c.Param("id")
if id == "" {
{{if $.HasLogger}}
h.logger.Error("Missing required parameter id", map[string]interface{}{
"request_id": requestID,
})
{{end}}
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Status: "error",
Message: "Missing required parameter id",
RequestID: requestID,
})
return
}
{{if $.HasLogger}}
h.logger.Info("Processing Delete{{.Name}} request", map[string]interface{}{
"request_id": requestID,
"endpoint": "{{.DeletePath}}",
"id": id,
})
{{end}}
// TODO: Implement service call
// Example: err := h.service.DeleteResource(ctx, id)
// For now, return a mock response
response := models.SuccessResponse{
Status: "success",
Message: "{{.Name}} deleted successfully",
RequestID: requestID,
}
{{if $.HasLogger}}
h.logger.Info("Successfully deleted {{.Name}}", map[string]interface{}{
"request_id": requestID,
"id": id,
})
{{end}}
c.JSON(http.StatusOK, response)
}
{{end}}
{{if .HasSearch}}
// Search{{.Name}} searches for resources
// @Summary Search for resources
// @Description Search for resources in the system
// @Tags {{index .Tags 0}}
// @Accept json
// @Produce json
// @Param Authorization header string true "Bearer token"
// @Param query query string false "Search query"
// @Param limit query int false "Limit results" default(10)
// @Param offset query int false "Offset results" default(0)
// @Success 200 {object} models.{{.Name}}SearchResponse "Search results"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 401 {object} models.ErrorResponse "Unauthorized"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router {{.SearchPath}} [get]
func (h *{{$.HandlerName}}) Search{{.Name}}(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second)
defer cancel()
// Generate request ID if not present
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
c.Header("X-Request-ID", requestID)
}
// Get query parameters
query := c.Query("query")
limit := 10
if l := c.Query("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
limit = parsed
}
}
offset := 0
if o := c.Query("offset"); o != "" {
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
offset = parsed
}
}
{{if $.HasLogger}}
h.logger.Info("Processing Search{{.Name}} request", map[string]interface{}{
"request_id": requestID,
"endpoint": "{{.SearchPath}}",
"query": query,
"limit": limit,
"offset": offset,
})
{{end}}
// TODO: Implement service call
// Example: resp, err := h.service.SearchResources(ctx, query, limit, offset)
// For now, return a mock response
response := models.{{.Name}}SearchResponse{
Status: "success",
Message: "Search completed successfully",
RequestID: requestID,
Data: models.SearchData{
Results: []interface{}{}, // Replace with actual results
Total: 0,
Limit: limit,
Offset: offset,
},
}
{{if $.HasLogger}}
h.logger.Info("Successfully completed search", map[string]interface{}{
"request_id": requestID,
"query": query,
"results": len(response.Data.Results),
})
{{end}}
c.JSON(http.StatusOK, response)
}
{{end}}
{{end}}`
const methodsOnlyTemplate = `{{range .Endpoints}}
{{if .HasPost}}
// Create{{.Name}} creates a new resource
func (h *{{$.HandlerName}}) Create{{.Name}}(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second)
defer cancel()
// Generate request ID if not present
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
c.Header("X-Request-ID", requestID)
}
{{if $.HasLogger}}
h.logger.Info("Processing Create{{.Name}} request", map[string]interface{}{
"request_id": requestID,
"endpoint": "{{.PostPath}}",
})
{{end}}
// Bind and validate request body
var req models.{{.Name}}CreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
{{if $.HasLogger}}
h.logger.Error("Failed to bind request body", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
{{end}}
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Status: "error",
Message: "Invalid request body: " + err.Error(),
RequestID: requestID,
})
return
}
// Validate request structure
if err := h.validator.Struct(&req); err != nil {
{{if $.HasLogger}}
h.logger.Error("Request validation failed", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
{{end}}
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Status: "error",
Message: "Validation failed: " + err.Error(),
RequestID: requestID,
})
return
}
// TODO: Implement service call
// Example: resp, err := h.service.CreateResource(ctx, req)
// For now, return a mock response
response := models.{{.Name}}Response{
Status: "success",
Message: "{{.Name}} created successfully",
RequestID: requestID,
Data: nil, // Replace with actual data
}
{{if $.HasLogger}}
h.logger.Info("Successfully created {{.Name}}", map[string]interface{}{
"request_id": requestID,
})
{{end}}
c.JSON(http.StatusCreated, response)
}
{{end}}
{{if .HasGet}}
// Get{{.Name}} retrieves a specific resource by ID
func (h *{{$.HandlerName}}) Get{{.Name}}(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second)
defer cancel()
// Generate request ID if not present
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
c.Header("X-Request-ID", requestID)
}
id := c.Param("id")
if id == "" {
{{if $.HasLogger}}
h.logger.Error("Missing required parameter id", map[string]interface{}{
"request_id": requestID,
})
{{end}}
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Status: "error",
Message: "Missing required parameter id",
RequestID: requestID,
})
return
}
{{if $.HasLogger}}
h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{
"request_id": requestID,
"endpoint": "{{.GetPath}}",
"id": id,
})
{{end}}
// TODO: Implement service call
// Example: resp, err := h.service.GetResource(ctx, id)
// For now, return a mock response
response := models.{{.Name}}Response{
Status: "success",
Message: "{{.Name}} retrieved successfully",
RequestID: requestID,
Data: nil, // Replace with actual data
}
{{if $.HasLogger}}
h.logger.Info("Successfully retrieved {{.Name}}", map[string]interface{}{
"request_id": requestID,
"id": id,
})
{{end}}
c.JSON(http.StatusOK, response)
}
{{end}}
{{if .HasPut}}
// Update{{.Name}} updates an existing resource
func (h *{{$.HandlerName}}) Update{{.Name}}(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second)
defer cancel()
// Generate request ID if not present
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
c.Header("X-Request-ID", requestID)
}
id := c.Param("id")
if id == "" {
{{if $.HasLogger}}
h.logger.Error("Missing required parameter id", map[string]interface{}{
"request_id": requestID,
})
{{end}}
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Status: "error",
Message: "Missing required parameter id",
RequestID: requestID,
})
return
}
{{if $.HasLogger}}
h.logger.Info("Processing Update{{.Name}} request", map[string]interface{}{
"request_id": requestID,
"endpoint": "{{.PutPath}}",
"id": id,
})
{{end}}
// Bind and validate request body
var req models.{{.Name}}UpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
{{if $.HasLogger}}
h.logger.Error("Failed to bind request body", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
{{end}}
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Status: "error",
Message: "Invalid request body: " + err.Error(),
RequestID: requestID,
})
return
}
// Validate request structure
if err := h.validator.Struct(&req); err != nil {
{{if $.HasLogger}}
h.logger.Error("Request validation failed", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
{{end}}
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Status: "error",
Message: "Validation failed: " + err.Error(),
RequestID: requestID,
})
return
}
// TODO: Implement service call
// Example: resp, err := h.service.UpdateResource(ctx, id, req)
// For now, return a mock response
response := models.{{.Name}}Response{
Status: "success",
Message: "{{.Name}} updated successfully",
RequestID: requestID,
Data: nil, // Replace with actual data
}
{{if $.HasLogger}}
h.logger.Info("Successfully updated {{.Name}}", map[string]interface{}{
"request_id": requestID,
"id": id,
})
{{end}}
c.JSON(http.StatusOK, response)
}
{{end}}
{{if .HasDelete}}
// Delete{{.Name}} deletes a resource
func (h *{{$.HandlerName}}) Delete{{.Name}}(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second)
defer cancel()
// Generate request ID if not present
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
c.Header("X-Request-ID", requestID)
}
id := c.Param("id")
if id == "" {
{{if $.HasLogger}}
h.logger.Error("Missing required parameter id", map[string]interface{}{
"request_id": requestID,
})
{{end}}
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Status: "error",
Message: "Missing required parameter id",
RequestID: requestID,
})
return
}
{{if $.HasLogger}}
h.logger.Info("Processing Delete{{.Name}} request", map[string]interface{}{
"request_id": requestID,
"endpoint": "{{.DeletePath}}",
"id": id,
})
{{end}}
// TODO: Implement service call
// Example: err := h.service.DeleteResource(ctx, id)
// For now, return a mock response
response := models.SuccessResponse{
Status: "success",
Message: "{{.Name}} deleted successfully",
RequestID: requestID,
}
{{if $.HasLogger}}
h.logger.Info("Successfully deleted {{.Name}}", map[string]interface{}{
"request_id": requestID,
"id": id,
})
{{end}}
c.JSON(http.StatusOK, response)
}
{{end}}
{{if .HasSearch}}
// Search{{.Name}} searches for resources
func (h *{{$.HandlerName}}) Search{{.Name}}(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second)
defer cancel()
// Generate request ID if not present
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
c.Header("X-Request-ID", requestID)
}
// Get query parameters
query := c.Query("query")
limit := 10
if l := c.Query("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
limit = parsed
}
}
offset := 0
if o := c.Query("offset"); o != "" {
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
offset = parsed
}
}
{{if $.HasLogger}}
h.logger.Info("Processing Search{{.Name}} request", map[string]interface{}{
"request_id": requestID,
"endpoint": "{{.SearchPath}}",
"query": query,
"limit": limit,
"offset": offset,
})
{{end}}
// TODO: Implement service call
// Example: resp, err := h.service.SearchResources(ctx, query, limit, offset)
// For now, return a mock response
response := models.{{.Name}}SearchResponse{
Status: "success",
Message: "Search completed successfully",
RequestID: requestID,
Data: models.SearchData{
Results: []interface{}{}, // Replace with actual results
Total: 0,
Limit: limit,
Offset: offset,
},
}
{{if $.HasLogger}}
h.logger.Info("Successfully completed search", map[string]interface{}{
"request_id": requestID,
"query": query,
"results": len(response.Data.Results),
})
{{end}}
c.JSON(http.StatusOK, response)
}
{{end}}
{{end}}`
const modelTemplate = `package models
import (
"time"
"github.com/go-playground/validator/v10"
)
// BaseRequest contains common fields for all requests
type BaseRequest struct {
RequestID string ` + "`json:\"request_id,omitempty\"`" + `
Timestamp time.Time ` + "`json:\"timestamp,omitempty\"`" + `
}
// BaseResponse contains common fields for all responses
type BaseResponse struct {
Status string ` + "`json:\"status\"`" + `
Message string ` + "`json:\"message\"`" + `
Data interface{} ` + "`json:\"data,omitempty\"`" + `
RequestID string ` + "`json:\"request_id,omitempty\"`" + `
}
// ErrorResponse represents an error response
type ErrorResponse struct {
Status string ` + "`json:\"status\"`" + `
Message string ` + "`json:\"message\"`" + `
RequestID string ` + "`json:\"request_id,omitempty\"`" + `
}
// SuccessResponse represents a success response
type SuccessResponse struct {
Status string ` + "`json:\"status\"`" + `
Message string ` + "`json:\"message\"`" + `
RequestID string ` + "`json:\"request_id,omitempty\"`" + `
}
// SearchData contains search results
type SearchData struct {
Results []interface{} ` + "`json:\"results\"`" + `
Total int ` + "`json:\"total\"`" + `
Limit int ` + "`json:\"limit\"`" + `
Offset int ` + "`json:\"offset\"`" + `
}
// {{.ServiceName}} Models
// Generated at: {{.Timestamp}}
// {{.ServiceName}}CreateRequest represents a request to create a {{.ServiceName}}
type {{.ServiceName}}CreateRequest struct {
BaseRequest
// Add fields specific to {{.ServiceName}} creation
Name string ` + "`json:\"name\" validate:\"required\"`" + `
Description string ` + "`json:\"description\" validate:\"omitempty\"`" + `
}
// {{.ServiceName}}UpdateRequest represents a request to update a {{.ServiceName}}
type {{.ServiceName}}UpdateRequest struct {
BaseRequest
ID string ` + "`json:\"id\" validate:\"required,uuid\"`" + `
Name string ` + "`json:\"name\" validate:\"required\"`" + `
Description string ` + "`json:\"description\" validate:\"omitempty\"`" + `
}
// {{.ServiceName}}Response represents a response for {{.ServiceName}} operations
type {{.ServiceName}}Response struct {
BaseResponse
}
// {{.ServiceName}}SearchResponse represents a response for {{.ServiceName}} search
type {{.ServiceName}}SearchResponse struct {
BaseResponse
Data SearchData ` + "`json:\"data\"`" + `
}
// {{.ServiceName}}Data represents the data structure for {{.ServiceName}}
type {{.ServiceName}}Data struct {
ID string ` + "`json:\"id\"`" + `
Name string ` + "`json:\"name\"`" + `
Description string ` + "`json:\"description,omitempty\"`" + `
CreatedAt time.Time ` + "`json:\"created_at\"`" + `
UpdatedAt time.Time ` + "`json:\"updated_at,omitempty\"`" + `
}
// Validate validates the {{.ServiceName}}CreateRequest
func (r *{{.ServiceName}}CreateRequest) Validate() error {
validate := validator.New()
return validate.Struct(r)
}
// Validate validates the {{.ServiceName}}UpdateRequest
func (r *{{.ServiceName}}UpdateRequest) Validate() error {
validate := validator.New()
return validate.Struct(r)
}
// New{{.ServiceName}}Data creates a new {{.ServiceName}}Data with default values
func New{{.ServiceName}}Data(name, description string) *{{.ServiceName}}Data {
now := time.Now()
return &{{.ServiceName}}Data{
ID: "", // Will be set by the database
Name: name,
Description: description,
CreatedAt: now,
UpdatedAt: now,
}
}`