package main import ( "fmt" "go/ast" "go/parser" "go/token" "io/ioutil" "os" "path/filepath" "regexp" "strings" "text/template" "time" "gopkg.in/yaml.v2" ) // Enhanced structures untuk validasi type HandlerValidation struct { ExistingFunctions map[string]bool NewFunctions []string UpdatedFiles []string CreatedFiles []string } type DirectoryInfo struct { Path string IsFile bool Functions []FunctionInfo Children map[string]*DirectoryInfo } type FunctionInfo struct { Name string Methods []string Endpoint string Config EndpointConfig } type EndpointConfig struct { Methods []string `yaml:"methods"` GetRoutes string `yaml:"get_routes"` PostRoutes string `yaml:"post_routes"` PutRoutes string `yaml:"put_routes"` DeleteRoutes string `yaml:"delete_routes"` GetPath string `yaml:"get_path"` PostPath string `yaml:"post_path"` PutPath string `yaml:"put_path"` DeletePath string `yaml:"delete_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"` } type GlobalConfig struct { ModuleName string `yaml:"module_name"` OutputDir string `yaml:"output_dir"` EnableSwagger bool `yaml:"enable_swagger"` EnableLogging bool `yaml:"enable_logging"` } type ServiceConfig struct { Global GlobalConfig `yaml:"global,omitempty"` Services map[string]Service `yaml:"services"` } 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"` Endpoints map[string]EndpointGroup `yaml:"endpoints"` Dependencies string `yaml:"dependencies,omitempty"` Middleware string `yaml:"middleware,omitempty"` } type EndpointGroup struct { Description string `yaml:"description"` HandlerFolder string `yaml:"handler_folder"` HandlerFile string `yaml:"handler_file"` HandlerName string `yaml:"handler_name"` Functions map[string]FunctionConfig `yaml:"functions"` } type FunctionConfig struct { Methods []string `yaml:"methods"` Path string `yaml:"path"` Model string `yaml:"model"` RoutesLink string `yaml:"routes_link"` // Routes untuk endpoint generation GetRoutes string `yaml:"get_routes"` PostRoutes string `yaml:"post_routes"` PutRoutes string `yaml:"put_routes"` DeleteRoutes string `yaml:"delete_routes"` // ✅ Path untuk swagger documentation GetPath string `yaml:"get_path"` PostPath string `yaml:"post_path"` PutPath string `yaml:"put_path"` DeletePath string `yaml:"delete_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"` } type TemplateData struct { ServiceName string ServiceLower string ServiceUpper string Category string Package string Description string BaseURL string Timeout int RetryCount int Endpoints []EndpointData Timestamp string ModuleName string HasValidator bool HasLogger bool HasSwagger bool GlobalConfig GlobalConfig ShouldGenerateStruct bool ShouldGenerateConstructor bool FunctionalArea string // e.g. "rujukan", "search" HandlerName string } type EndpointData struct { Name string NameLower string NameUpper string NameCamel string Methods []string GetRoutes string PostRoutes string PutRoutes string DeleteRoutes string GetPath string PostPath string PutPath string DeletePath string Model string ResponseModel string RequestModel string DataModel string Description string Summary string Tags []string HasGet bool HasPost bool HasPut bool HasDelete bool RequireAuth bool CacheEnabled bool CacheTTL int PathParams []string ModelPackage string } // Fungsi utama yang diupdate untuk menggunakan separation of concerns func generateHandlerWithValidation(serviceName string, svc Service, gc GlobalConfig) error { // Step 1: Generate base handler file (struct + constructor) - SEKALI SAJA // err := generateBaseHandlerFile(serviceName, svc, gc) // if err != nil { // return fmt.Errorf("generate base handler: %w", err) // } // Step 2: Generate methods files per endpoint group baseDir := gc.OutputDir for groupName, grp := range svc.Endpoints { folder := filepath.Join(baseDir, grp.HandlerFolder) if err := os.MkdirAll(folder, 0755); err != nil { return fmt.Errorf("mkdir %s: %w", folder, err) } // Generate methods file dengan naming yang jelas methodsFileName := fmt.Sprintf("%s.go", strings.ToLower(groupName)) methodsFilePath := filepath.Join(folder, 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 { return fmt.Errorf("create methods file %s: %w", methodsFilePath, err) } fmt.Printf("✅ Created methods file: %s\n", methodsFilePath) } else { // Update existing methods file with new functions only err := updateExistingMethodsFile(methodsFilePath, svc, grp, gc, groupName) if err != nil { return fmt.Errorf("update methods file %s: %w", methodsFilePath, err) } fmt.Printf("✅ Updated methods file: %s\n", methodsFilePath) } } // ✅ Step 2: Generate routes err := generateRoutes(serviceName, svc, gc) if err != nil { return fmt.Errorf("generate routes: %w", err) } return nil } // Create new methods file func createMethodsFileFromConfig(filePath string, svc Service, grp EndpointGroup, gc GlobalConfig, functionalArea string) error { // 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, Package: grp.HandlerFolder, Description: svc.Description, BaseURL: svc.BaseURL, Timeout: svc.Timeout, RetryCount: svc.RetryCount, 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, // PERBAIKAN: Pastikan HandlerName diset } return createMethodsFile(filePath, templateData) } // Create methods file using methods-only template func createMethodsFile(filePath string, templateData TemplateData) error { if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { return err } tmpl := template.New("methods").Funcs(template.FuncMap{ "title": strings.Title, "index": func(slice []string, i int) string { if i >= 0 && i < len(slice) { return slice[i] } return "" }, }) tmpl, err := tmpl.Parse(handlerTemplate) if err != nil { return err } file, err := os.Create(filePath) if err != nil { return err } defer file.Close() return tmpl.Execute(file, templateData) } // Update existing methods file (sama seperti sebelumnya tapi tanpa struct) func updateExistingMethodsFile(filePath string, svc Service, grp EndpointGroup, gc GlobalConfig, functionalArea string) error { existingContent, err := ioutil.ReadFile(filePath) if err != nil { return err } content := string(existingContent) // 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) // PERBAIKAN: Gunakan grp.HandlerName bukan svc.Name 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...) 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, Package: grp.HandlerFolder, Description: svc.Description, BaseURL: svc.BaseURL, Timeout: svc.Timeout, RetryCount: svc.RetryCount, 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, // PERBAIKAN: Pastikan HandlerName diset } newFunctions, err := generateNewMethodsOnly(templateData) if err != nil { return err } // Merge content mergedContent := mergeGoFileContent(content, newFunctions) return ioutil.WriteFile(filePath, []byte(mergedContent), 0644) } // Generate new methods using methods-only template func generateNewMethodsOnly(templateData TemplateData) (string, error) { tmpl := template.New("newMethods") tmpl, err := tmpl.Parse(handlerTemplate) if err != nil { return "", err } var result strings.Builder err = tmpl.Execute(&result, templateData) if err != nil { return "", err } return result.String(), nil } // Helper toCamelCase 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, "") } // Parse struktur direktori dari endpoints YAML func parseDirectoryStructure(endpoints map[string]interface{}, serviceName string) *DirectoryInfo { root := &DirectoryInfo{ Path: "", Children: make(map[string]*DirectoryInfo), } for key, value := range endpoints { parseNestedEndpoints(root, key, value) } return root } func parseNestedEndpoints(parent *DirectoryInfo, name string, value interface{}) { switch v := value.(type) { case map[string]interface{}: // Check if this contains direct endpoint config if isDirectEndpoint(v) { // This is a direct endpoint - create as file parent.Children[name] = &DirectoryInfo{ Path: name, IsFile: true, Functions: []FunctionInfo{parseEndpointToFunction(name, v)}, Children: make(map[string]*DirectoryInfo), } } else { // This is nested structure - create as directory or file child := &DirectoryInfo{ Path: name, IsFile: false, Functions: make([]FunctionInfo, 0), Children: make(map[string]*DirectoryInfo), } // Check if any direct children are endpoints hasDirectEndpoints := false for childName, childValue := range v { if childMap, ok := childValue.(map[string]interface{}); ok && isDirectEndpoint(childMap) { hasDirectEndpoints = true child.Functions = append(child.Functions, parseEndpointToFunction(childName, childMap)) } } if hasDirectEndpoints { child.IsFile = true } parent.Children[name] = child // Recursively parse nested children for childName, childValue := range v { if childMap, ok := childValue.(map[string]interface{}); ok && !isDirectEndpoint(childMap) { parseNestedEndpoints(child, childName, childValue) } } } } } func isDirectEndpoint(m map[string]interface{}) bool { _, hasMethods := m["methods"] _, hasGetPath := m["get_path"] _, hasPostPath := m["post_path"] return hasMethods || hasGetPath || hasPostPath } func parseEndpointToFunction(name string, config map[string]interface{}) FunctionInfo { function := FunctionInfo{ Name: name, Endpoint: name, Methods: make([]string, 0), } if methods, ok := config["methods"].([]interface{}); ok { for _, method := range methods { if methodStr, ok := method.(string); ok { function.Methods = append(function.Methods, strings.ToUpper(strings.TrimSpace(methodStr))) } } } else if methodStr, ok := config["methods"].(string); ok { // Handle case where methods is a string like "GET,POST" methods := strings.Split(methodStr, ",") for _, method := range methods { function.Methods = append(function.Methods, strings.ToUpper(strings.TrimSpace(method))) } } return function } // Process directory structure dan generate files func processDirectoryStructure(baseDir string, dirInfo *DirectoryInfo, service Service, globalConfig GlobalConfig, validation *HandlerValidation) error { for name, child := range dirInfo.Children { currentPath := filepath.Join(baseDir, child.Path) if child.IsFile { // Process as file err := processHandlerFile(currentPath, name, child, service, globalConfig, validation) if err != nil { return fmt.Errorf("failed to process file %s: %w", name, err) } } else { // Create directory dan process children if err := os.MkdirAll(currentPath, 0755); err != nil { return fmt.Errorf("failed to create directory %s: %w", currentPath, err) } err := processDirectoryStructure(currentPath, child, service, globalConfig, validation) if err != nil { return err } } } return nil } func processFunctionData(svc Service, grp EndpointGroup, fname string, fcfg FunctionConfig, gc GlobalConfig) TemplateData { ed := EndpointData{ Name: toCamelCase(fname), NameLower: strings.ToLower(fname), NameUpper: strings.ToUpper(fname), NameCamel: toCamelCase(fname), Methods: fcfg.Methods, GetRoutes: fcfg.GetRoutes, PostRoutes: fcfg.PostRoutes, PutRoutes: fcfg.PutRoutes, DeleteRoutes: fcfg.DeleteRoutes, GetPath: fcfg.Path, PostPath: fcfg.Path, PutPath: fcfg.Path, DeletePath: fcfg.Path, Model: fcfg.Model, ResponseModel: fcfg.ResponseModel, RequestModel: fcfg.RequestModel, 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.Path), ModelPackage: grp.HandlerFolder, } // set 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 "DELETE": ed.HasDelete = true } } return TemplateData{ ServiceName: svc.Name, ServiceLower: strings.ToLower(svc.Name), ServiceUpper: strings.ToUpper(svc.Name), Category: svc.Category, Package: grp.HandlerFolder, Description: svc.Description, BaseURL: svc.BaseURL, Timeout: svc.Timeout, RetryCount: svc.RetryCount, 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, } } // extractPathParams (jika belum ada) func extractPathParams(path string) []string { var ps []string for _, part := range strings.Split(path, "/") { if strings.HasPrefix(part, ":") { ps = append(ps, strings.TrimPrefix(part, ":")) } } return ps } // Process individual handler file - PERBAIKAN: Hapus referensi grp.HandlerName func processHandlerFile(basePath, fileName string, dirInfo *DirectoryInfo, service Service, globalConfig GlobalConfig, validation *HandlerValidation) error { filePath := filepath.Join(filepath.Dir(basePath), fmt.Sprintf("%s.go", fileName)) fmt.Printf("📄 Processing file: %s\n", filePath) // Check if file exists fileExists := false if _, err := os.Stat(filePath); err == nil { fileExists = true fmt.Printf(" 📋 File exists, checking functions...\n") } var existingFunctions map[string]bool if fileExists { // Parse existing functions - PERBAIKAN: Hanya gunakan 1 parameter functions, err := extractExistingFunctions(filePath) if err != nil { fmt.Printf(" ⚠️ Warning: Could not parse existing file: %v\n", err) existingFunctions = make(map[string]bool) } else { existingFunctions = functions fmt.Printf(" ✓ Found %d existing functions\n", len(functions)) } } else { existingFunctions = make(map[string]bool) } // Determine which functions to generate functionsToGenerate := make([]FunctionInfo, 0) for _, fnInfo := range dirInfo.Functions { for _, method := range fnInfo.Methods { functionName := generateFunctionName(fnInfo.Name, method) if !existingFunctions[functionName] { functionsToGenerate = append(functionsToGenerate, FunctionInfo{ Name: fnInfo.Name, Methods: []string{method}, Endpoint: fnInfo.Endpoint, }) validation.NewFunctions = append(validation.NewFunctions, functionName) fmt.Printf(" ✅ Will generate: %s\n", functionName) } else { fmt.Printf(" ⏭️ Already exists: %s\n", functionName) } } } // Generate file if needed if len(functionsToGenerate) > 0 { templateData := prepareTemplateData(fileName, service, globalConfig, functionsToGenerate) if fileExists { // Merge with existing file err := mergeWithExistingFile(filePath, templateData) if err != nil { return fmt.Errorf("failed to merge file %s: %w", filePath, err) } validation.UpdatedFiles = append(validation.UpdatedFiles, filePath) fmt.Printf(" 📝 Updated existing file\n") } else { // Create new file err := createNewHandlerFile(filePath, templateData) if err != nil { return fmt.Errorf("failed to create file %s: %w", filePath, err) } validation.CreatedFiles = append(validation.CreatedFiles, filePath) fmt.Printf(" 📁 Created new file\n") } } else if !fileExists { fmt.Printf(" ⏭️ No functions to generate and file doesn't exist\n") } else { fmt.Printf(" ⏭️ All functions already exist\n") } return nil } // Extract existing functions from Go file func extractExistingFunctions(filePath string) (map[string]bool, error) { fileSet := token.NewFileSet() node, err := parser.ParseFile(fileSet, filePath, nil, parser.ParseComments) if err != nil { return nil, err } functions := make(map[string]bool) ast.Inspect(node, func(n ast.Node) bool { if fn, ok := n.(*ast.FuncDecl); ok { if fn.Name != nil { functions[fn.Name.Name] = true } } return true }) return functions, nil } // Generate function name berdasarkan endpoint dan 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)) default: return fmt.Sprintf("%s%s", strings.Title(method), strings.Title(endpointName)) } } // Prepare template data func prepareTemplateData(packageName string, service Service, globalConfig GlobalConfig, functions []FunctionInfo) TemplateData { endpoints := make([]EndpointData, 0) for _, fnInfo := range functions { endpoint := EndpointData{ Name: strings.Title(fnInfo.Name), NameLower: strings.ToLower(fnInfo.Name), NameUpper: strings.ToUpper(fnInfo.Name), NameCamel: toCamelCase(fnInfo.Name), ModelPackage: packageName, Model: fmt.Sprintf("%sRequest", strings.Title(fnInfo.Name)), ResponseModel: fmt.Sprintf("%sResponse", strings.Title(fnInfo.Name)), RequestModel: fmt.Sprintf("%sRequest", strings.Title(fnInfo.Name)), DataModel: fmt.Sprintf("%sData", strings.Title(fnInfo.Name)), Description: fmt.Sprintf("Handle %s operations", fnInfo.Name), Tags: []string{strings.Title(packageName)}, } // Set paths dan methods for _, method := range fnInfo.Methods { switch strings.ToUpper(method) { case "GET": endpoint.HasGet = true endpoint.GetPath = fmt.Sprintf("/%s/%s", packageName, fnInfo.Name) case "POST": endpoint.HasPost = true endpoint.PostPath = fmt.Sprintf("/%s", packageName) case "PUT": endpoint.HasPut = true endpoint.PutPath = fmt.Sprintf("/%s/%s", packageName, fnInfo.Name) case "DELETE": endpoint.HasDelete = true endpoint.DeletePath = fmt.Sprintf("/%s/%s", packageName, fnInfo.Name) } } endpoints = append(endpoints, endpoint) } return TemplateData{ ServiceName: service.Name, ServiceLower: strings.ToLower(service.Name), ServiceUpper: strings.ToUpper(service.Name), Category: service.Category, Package: packageName, Description: service.Description, BaseURL: service.BaseURL, Timeout: getOrDefault(service.Timeout, 30), RetryCount: getOrDefault(service.RetryCount, 3), Endpoints: endpoints, Timestamp: time.Now().Format("2006-01-02 15:04:05"), ModuleName: globalConfig.ModuleName, HasValidator: true, HasLogger: globalConfig.EnableLogging, HasSwagger: globalConfig.EnableSwagger, GlobalConfig: globalConfig, HandlerName: strings.Title(packageName), } } // Check if handler struct already exists in directory func shouldGenerateHandlerStruct(baseDir, handlerName string) bool { structSignature := fmt.Sprintf("type %sHandler struct", handlerName) files, err := ioutil.ReadDir(baseDir) if err != nil { return true // If directory doesn't exist, generate struct } for _, file := range files { if file.IsDir() || !strings.HasSuffix(file.Name(), ".go") { continue } filePath := filepath.Join(baseDir, file.Name()) content, err := ioutil.ReadFile(filePath) if err != nil { continue } if strings.Contains(string(content), structSignature) { return false // Struct already exists } } return true // Struct not found, should generate } // Check if constructor already exists func shouldGenerateConstructor(baseDir, handlerName string) bool { constructorSignature := fmt.Sprintf("func New%sHandler", handlerName) files, err := ioutil.ReadDir(baseDir) if err != nil { return true } for _, file := range files { if file.IsDir() || !strings.HasSuffix(file.Name(), ".go") { continue } filePath := filepath.Join(baseDir, file.Name()) content, err := ioutil.ReadFile(filePath) if err != nil { continue } if strings.Contains(string(content), constructorSignature) { return false } } return true } // Generate base handler file (struct + constructor only) func generateBaseHandlerFile(serviceName string, svc Service, gc GlobalConfig) error { baseDir := gc.OutputDir if err := os.MkdirAll(baseDir, 0755); err != nil { return fmt.Errorf("mkdir %s: %w", baseDir, err) } baseFileName := fmt.Sprintf("%s_base.go", strings.ToLower(serviceName)) baseFilePath := filepath.Join(baseDir, baseFileName) // Skip if base file already exists if _, err := os.Stat(baseFilePath); err == nil { fmt.Printf("⏭️ Base handler already exists: %s\n", baseFilePath) return nil } templateData := TemplateData{ ServiceName: svc.Name, ServiceLower: strings.ToLower(svc.Name), ServiceUpper: strings.ToUpper(svc.Name), Category: svc.Category, Package: "handlers", Description: svc.Description, BaseURL: svc.BaseURL, Timeout: svc.Timeout, RetryCount: svc.RetryCount, Endpoints: []EndpointData{}, // Empty - base only Timestamp: time.Now().Format("2006-01-02 15:04:05"), ModuleName: gc.ModuleName, HasValidator: true, HasLogger: gc.EnableLogging, HasSwagger: gc.EnableSwagger, GlobalConfig: gc, } return createBaseHandlerFile(baseFilePath, templateData) } // Template untuk base handler (struct + constructor only) const baseHandlerTemplate = `// Package handlers handles {{.HandlerName}} BPJS services - Base Handler // Generated on: {{.Timestamp}} package handlers import ( "{{.ModuleName}}/internal/config" "{{.ModuleName}}/internal/services/bpjs" "{{.ModuleName}}/pkg/logger" "github.com/go-playground/validator/v10" ) // {{.HandlerName}}Handler handles {{.HandlerName}} BPJS services type {{.HandlerName}}Handler struct { service services.VClaimService validator *validator.Validate logger logger.Logger config config.BpjsConfig } // {{.HandlerName}}HandlerConfig contains configuration for {{.HandlerName}}Handler type {{.HandlerName}}HandlerConfig struct { BpjsConfig config.BpjsConfig Logger logger.Logger Validator *validator.Validate } // New{{.HandlerName}}Handler creates a new {{.HandlerName}}Handler func New{{.HandlerName}}Handler(cfg {{.HandlerName}}HandlerConfig) *{{.HandlerName}}Handler { return &{{.HandlerName}}Handler{ service: services.NewService(cfg.BpjsConfig), validator: cfg.Validator, logger: cfg.Logger, config: cfg.BpjsConfig, } } ` // Function to create base handler file func createBaseHandlerFile(filePath string, templateData TemplateData) error { if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { return err } tmpl := template.New("baseHandler").Funcs(template.FuncMap{ "title": strings.Title, }) tmpl, err := tmpl.Parse(baseHandlerTemplate) if err != nil { return err } file, err := os.Create(filePath) if err != nil { return err } defer file.Close() return tmpl.Execute(file, templateData) } // Template untuk handler file lengkap (untuk file baru) const handlerTemplate = `// Package {{.Package}} handles {{.HandlerName}} BPJS services // Generated on: {{.Timestamp}} package handlers import ( "context" "encoding/json" "net/http" "strings" "time" "{{.ModuleName}}/internal/config" "{{.ModuleName}}/internal/models" "{{.ModuleName}}/internal/models/vclaim/{{.Package}}" "{{.ModuleName}}/internal/services/bpjs" "{{.ModuleName}}/pkg/logger" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "github.com/google/uuid" ) // {{.HandlerName}}Handler handles {{.HandlerName}} BPJS services type {{.HandlerName}}Handler struct { service services.VClaimService validator *validator.Validate logger logger.Logger config config.BpjsConfig } // {{.HandlerName}}HandlerConfig contains configuration for {{.HandlerName}}Handler type {{.HandlerName}}HandlerConfig struct { BpjsConfig config.BpjsConfig Logger logger.Logger Validator *validator.Validate } // New{{.HandlerName}}Handler creates a new {{.HandlerName}}Handler func New{{.HandlerName}}Handler(cfg {{.HandlerName}}HandlerConfig) *{{.HandlerName}}Handler { return &{{.HandlerName}}Handler{ service: services.NewService(cfg.BpjsConfig), validator: cfg.Validator, logger: cfg.Logger, config: cfg.BpjsConfig, } } {{range .Endpoints}} {{if .HasGet}} // Get{{.Name}} godoc // @Summary Get {{.Name}} data // @Description {{.Description}} // @Tags {{index .Tags 0}} // @Accept json // @Produce json {{if .RequireAuth}} // @Security ApiKeyAuth {{end}} // @Param X-Request-ID header string false "Request ID for tracking" {{range .PathParams}} // @Param {{.}} path string true "{{.}}" example("example_value") {{end}} // @Success 200 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully retrieved {{.Name}} data" // @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" // @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" // @Failure 404 {object} models.ErrorResponseBpjs "Not found - {{.Name}} not found" // @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" // @Router {{.GetRoutes}} [get] func (h *{{$.HandlerName}}Handler) 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) } {{if $.HasLogger}} h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{ "request_id": requestID, "endpoint": "{{.GetPath}}", {{range .PathParams}} "{{.}}": c.Param("{{.}}"), {{end}} }) {{end}} // Extract path parameters {{range .PathParams}} {{.}} := c.Param("{{.}}") if {{.}} == "" { {{if $.HasLogger}} h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ Status: "error", Message: "Missing required parameter {{.}}", RequestID: requestID, }) return } {{end}} // Call service method var response {{.ModelPackage}}.{{.ResponseModel}} {{if .PathParams}} endpoint := "{{.GetPath}}" {{range .PathParams}} endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) {{end}} resp, err := h.service.GetRawResponse(ctx, endpoint) {{else}} resp, err := h.service.GetRawResponse(ctx, "{{.GetPath}}") {{end}} if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to get {{.Name}}", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ Status: "error", Message: "Internal server error", RequestID: requestID, }) return } // Map the raw response response.MetaData = resp.MetaData if resp.Response != nil { response.Data = &{{.ModelPackage}}.{{.DataModel}}{} if respStr, ok := resp.Response.(string); ok { // Decrypt the response string consID, secretKey, _, tstamp, _ := h.config.SetHeader() decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to decrypt response", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} } else { json.Unmarshal([]byte(decryptedResp), response.Data) } } else if respMap, ok := resp.Response.(map[string]interface{}); ok { // Response is already unmarshaled JSON if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { dataBytes, _ := json.Marshal(dataMap) json.Unmarshal(dataBytes, response.Data) } else { // Try to unmarshal the whole response respBytes, _ := json.Marshal(resp.Response) json.Unmarshal(respBytes, response.Data) } } } // Ensure response has proper fields response.Status = "success" response.RequestID = requestID c.JSON(http.StatusOK, response) } {{end}} {{if .HasPost}} // Create{{.Name}} godoc // @Summary Create new {{.Name}} // @Description Create new {{.Name}} in BPJS system // @Tags {{index .Tags 0}} // @Accept json // @Produce json {{if .RequireAuth}} // @Security ApiKeyAuth {{end}} // @Param X-Request-ID header string false "Request ID for tracking" // @Param request body {{.ModelPackage}}.{{.RequestModel}} true "{{.Name}} data" // @Success 201 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully created {{.Name}}" // @Failure 400 {object} models.ErrorResponseBpjs "Bad request" // @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized" // @Failure 409 {object} models.ErrorResponseBpjs "Conflict" // @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" // @Router {{.PostRoutes}} [post] func (h *{{$.HandlerName}}Handler) 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 {{.ModelPackage}}.{{.RequestModel}} 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.ErrorResponseBpjs{ 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.ErrorResponseBpjs{ Status: "error", Message: "Validation failed: " + err.Error(), RequestID: requestID, }) return } {{if .PathParams}} // Extract path parameters {{range .PathParams}} {{.}} := c.Param("{{.}}") if {{.}} == "" { {{if $.HasLogger}} h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ Status: "error", Message: "Missing required parameter {{.}}", RequestID: requestID, }) return } {{end}} endpoint := "{{.PostPath}}" {{range .PathParams}} endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) {{end}} {{end}} // Call service method var response {{.ModelPackage}}.{{.ResponseModel}} {{if .PathParams}} resp, err := h.service.PostRawResponse(ctx, endpoint, req) {{else}} resp, err := h.service.PostRawResponse(ctx, "{{.PostPath}}", req) {{end}} if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to create {{.Name}}", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} // Handle specific BPJS errors if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") { c.JSON(http.StatusConflict, models.ErrorResponseBpjs{ Status: "error", Message: "{{.Name}} already exists or conflict occurred", RequestID: requestID, }) return } c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ Status: "error", Message: "Internal server error", RequestID: requestID, }) return } // Map the raw response response.MetaData = resp.MetaData if resp.Response != nil { response.Data = &{{.ModelPackage}}.{{.DataModel}}{} if respStr, ok := resp.Response.(string); ok { // Decrypt the response string consID, secretKey, _, tstamp, _ := h.config.SetHeader() decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to decrypt response", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} } else { json.Unmarshal([]byte(decryptedResp), response.Data) } } else if respMap, ok := resp.Response.(map[string]interface{}); ok { // Response is already unmarshaled JSON if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { dataBytes, _ := json.Marshal(dataMap) json.Unmarshal(dataBytes, response.Data) } else { // Try to unmarshal the whole response respBytes, _ := json.Marshal(resp.Response) json.Unmarshal(respBytes, response.Data) } } } // Ensure response has proper fields response.Status = "success" response.RequestID = requestID {{if $.HasLogger}} h.logger.Info("Successfully created {{.Name}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusCreated, response) } {{end}} {{if .HasPut}} // Update{{.Name}} godoc // @Summary Update existing {{.Name}} // @Description Update existing {{.Name}} in BPJS system // @Tags {{index .Tags 0}} // @Accept json // @Produce json {{if .RequireAuth}} // @Security ApiKeyAuth {{end}} // @Param X-Request-ID header string false "Request ID for tracking" {{range .PathParams}} // @Param {{.}} path string true "{{.}}" example("example_value") {{end}} // @Param request body {{.ModelPackage}}.{{.RequestModel}} true "{{.Name}} update data" // @Success 200 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully updated {{.Name}}" // @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" // @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" // @Failure 404 {object} models.ErrorResponseBpjs "Not found - {{.Name}} not found" // @Failure 409 {object} models.ErrorResponseBpjs "Conflict - update conflict occurred" // @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" // @Router {{.PutRoutes}} [put] func (h *{{$.HandlerName}}Handler) 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) } {{if $.HasLogger}} h.logger.Info("Processing Update{{.Name}} request", map[string]interface{}{ "request_id": requestID, "endpoint": "{{.PutPath}}", {{range .PathParams}} "{{.}}": c.Param("{{.}}"), {{end}} }) {{end}} // Extract path parameters {{range .PathParams}} {{.}} := c.Param("{{.}}") if {{.}} == "" { {{if $.HasLogger}} h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ Status: "error", Message: "Missing required parameter {{.}}", RequestID: requestID, }) return } {{end}} // Bind and validate request body var req {{.ModelPackage}}.{{.RequestModel}} 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.ErrorResponseBpjs{ 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.ErrorResponseBpjs{ Status: "error", Message: "Validation failed: " + err.Error(), RequestID: requestID, }) return } // Call service method var response {{.ModelPackage}}.{{.ResponseModel}} {{if .PathParams}} endpoint := "{{.PutPath}}" {{range .PathParams}} endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) {{end}} resp, err := h.service.PutRawResponse(ctx, endpoint, req) {{else}} resp, err := h.service.PutRawResponse(ctx, "{{.PutPath}}", req) {{end}} if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to update {{.Name}}", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} // Handle specific BPJS errors if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") { c.JSON(http.StatusNotFound, models.ErrorResponseBpjs{ Status: "error", Message: "{{.Name}} not found", RequestID: requestID, }) return } if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") { c.JSON(http.StatusConflict, models.ErrorResponseBpjs{ Status: "error", Message: "Update conflict occurred", RequestID: requestID, }) return } c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ Status: "error", Message: "Internal server error", RequestID: requestID, }) return } // Map the raw response response.MetaData = resp.MetaData if resp.Response != nil { response.Data = &{{.ModelPackage}}.{{.DataModel}}{} if respStr, ok := resp.Response.(string); ok { // Decrypt the response string consID, secretKey, _, tstamp, _ := h.config.SetHeader() decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to decrypt response", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} } else { json.Unmarshal([]byte(decryptedResp), response.Data) } } else if respMap, ok := resp.Response.(map[string]interface{}); ok { // Response is already unmarshaled JSON if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { dataBytes, _ := json.Marshal(dataMap) json.Unmarshal(dataBytes, response.Data) } else { // Try to unmarshal the whole response respBytes, _ := json.Marshal(resp.Response) json.Unmarshal(respBytes, response.Data) } } } // Ensure response has proper fields response.Status = "success" response.RequestID = requestID {{if $.HasLogger}} h.logger.Info("Successfully updated {{.Name}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusOK, response) } {{end}} {{if .HasDelete}} // Delete{{.Name}} godoc // @Summary Delete existing {{.Name}} // @Description Delete existing {{.Name}} from BPJS system // @Tags {{index .Tags 0}} // @Accept json // @Produce json {{if .RequireAuth}} // @Security ApiKeyAuth {{end}} // @Param X-Request-ID header string false "Request ID for tracking" {{range .PathParams}} // @Param {{.}} path string true "{{.}}" example("example_value") {{end}} // @Success 200 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully deleted {{.Name}}" // @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" // @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" // @Failure 404 {object} models.ErrorResponseBpjs "Not found - {{.Name}} not found" // @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" // @Router {{.DeleteRoutes}} [delete] func (h *{{$.HandlerName}}Handler) 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) } {{if $.HasLogger}} h.logger.Info("Processing Delete{{.Name}} request", map[string]interface{}{ "request_id": requestID, "endpoint": "{{.DeletePath}}", {{range .PathParams}} "{{.}}": c.Param("{{.}}"), {{end}} }) {{end}} // Extract path parameters {{range .PathParams}} {{.}} := c.Param("{{.}}") if {{.}} == "" { {{if $.HasLogger}} h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ Status: "error", Message: "Missing required parameter {{.}}", RequestID: requestID, }) return } {{end}} // Call service method var response {{.ModelPackage}}.{{.ResponseModel}} {{if .PathParams}} endpoint := "{{.DeletePath}}" {{range .PathParams}} endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) {{end}} resp, err := h.service.DeleteRawResponse(ctx, endpoint) {{else}} resp, err := h.service.DeleteRawResponse(ctx, "{{.DeletePath}}") {{end}} if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to delete {{.Name}}", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} // Handle specific BPJS errors if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") { c.JSON(http.StatusNotFound, models.ErrorResponseBpjs{ Status: "error", Message: "{{.Name}} not found", RequestID: requestID, }) return } c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ Status: "error", Message: "Internal server error", RequestID: requestID, }) return } // Map the raw response response.MetaData = resp.MetaData if resp.Response != nil { response.Data = &{{.ModelPackage}}.{{.DataModel}}{} if respStr, ok := resp.Response.(string); ok { // Decrypt the response string consID, secretKey, _, tstamp, _ := h.config.SetHeader() decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to decrypt response", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} } else { json.Unmarshal([]byte(decryptedResp), response.Data) } } else if respMap, ok := resp.Response.(map[string]interface{}); ok { // Response is already unmarshaled JSON if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { dataBytes, _ := json.Marshal(dataMap) json.Unmarshal(dataBytes, response.Data) } else { // Try to unmarshal the whole response respBytes, _ := json.Marshal(resp.Response) json.Unmarshal(respBytes, response.Data) } } } else { // For delete operations, sometimes there's no data in response response.Data = nil } // Ensure response has proper fields response.Status = "success" response.RequestID = requestID {{if $.HasLogger}} h.logger.Info("Successfully deleted {{.Name}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusOK, response) } {{end}} {{end}}` // Template untuk menambah function saja (untuk file yang sudah ada) const functionsOnlyTemplate = `{{range .Endpoints}} {{if .HasGet}} // Get{{.Name}} godoc // @Summary Get {{.Name}} data // @Description {{.Description}} // @Tags {{index .Tags 0}} // @Accept json // @Produce json {{if .RequireAuth}} // @Security ApiKeyAuth {{end}} // @Param X-Request-ID header string false "Request ID for tracking" {{range .PathParams}} // @Param {{.}} path string true "{{.}}" example("example_value") {{end}} // @Success 200 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully retrieved {{.Name}} data" // @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" // @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" // @Failure 404 {object} models.ErrorResponseBpjs "Not found - {{.Name}} not found" // @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" // @Router {{.GetRoutes}} [get] func (h *{{$.HandlerName}}Handler) 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) } {{if $.HasLogger}} h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{ "request_id": requestID, "endpoint": "{{.GetPath}}", {{range .PathParams}} "{{.}}": c.Param("{{.}}"), {{end}} }) {{end}} // Extract path parameters {{range .PathParams}} {{.}} := c.Param("{{.}}") if {{.}} == "" { {{if $.HasLogger}} h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ Status: "error", Message: "Missing required parameter {{.}}", RequestID: requestID, }) return } {{end}} // Call service method var response {{.ModelPackage}}.{{.ResponseModel}} {{if .PathParams}} endpoint := "{{.GetPath}}" {{range .PathParams}} endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) {{end}} resp, err := h.service.GetRawResponse(ctx, endpoint) {{else}} resp, err := h.service.GetRawResponse(ctx, "{{.GetPath}}") {{end}} if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to get {{.Name}}", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ Status: "error", Message: "Internal server error", RequestID: requestID, }) return } // Map the raw response response.MetaData = resp.MetaData if resp.Response != nil { response.Data = &{{.ModelPackage}}.{{.DataModel}}{} if respStr, ok := resp.Response.(string); ok { // Decrypt the response string consID, secretKey, _, tstamp, _ := h.config.SetHeader() decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to decrypt response", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} } else { json.Unmarshal([]byte(decryptedResp), response.Data) } } else if respMap, ok := resp.Response.(map[string]interface{}); ok { // Response is already unmarshaled JSON if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { dataBytes, _ := json.Marshal(dataMap) json.Unmarshal(dataBytes, response.Data) } else { // Try to unmarshal the whole response respBytes, _ := json.Marshal(resp.Response) json.Unmarshal(respBytes, response.Data) } } } // Ensure response has proper fields response.Status = "success" response.RequestID = requestID c.JSON(http.StatusOK, response) } {{end}} {{if .HasPost}} // Create{{.Name}} godoc // @Summary Create new {{.Name}} // @Description Create new {{.Name}} in BPJS system // @Tags {{index .Tags 0}} // @Accept json // @Produce json {{if .RequireAuth}} // @Security ApiKeyAuth {{end}} // @Param X-Request-ID header string false "Request ID for tracking" // @Param request body {{.ModelPackage}}.{{.RequestModel}} true "{{.Name}} data" // @Success 201 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully created {{.Name}}" // @Failure 400 {object} models.ErrorResponseBpjs "Bad request" // @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized" // @Failure 409 {object} models.ErrorResponseBpjs "Conflict" // @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" // @Router {{.PostRoutes}} [post] func (h *{{$.HandlerName}}Handler) 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 {{.ModelPackage}}.{{.RequestModel}} 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.ErrorResponseBpjs{ 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.ErrorResponseBpjs{ Status: "error", Message: "Validation failed: " + err.Error(), RequestID: requestID, }) return } {{if .PathParams}} // Extract path parameters {{range .PathParams}} {{.}} := c.Param("{{.}}") if {{.}} == "" { {{if $.HasLogger}} h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ Status: "error", Message: "Missing required parameter {{.}}", RequestID: requestID, }) return } {{end}} endpoint := "{{.PostPath}}" {{range .PathParams}} endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) {{end}} {{end}} // Call service method var response {{.ModelPackage}}.{{.ResponseModel}} {{if .PathParams}} resp, err := h.service.PostRawResponse(ctx, endpoint, req) {{else}} resp, err := h.service.PostRawResponse(ctx, "{{.PostPath}}", req) {{end}} if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to create {{.Name}}", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} // Handle specific BPJS errors if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") { c.JSON(http.StatusConflict, models.ErrorResponseBpjs{ Status: "error", Message: "{{.Name}} already exists or conflict occurred", RequestID: requestID, }) return } c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ Status: "error", Message: "Internal server error", RequestID: requestID, }) return } // Map the raw response response.MetaData = resp.MetaData if resp.Response != nil { response.Data = &{{.ModelPackage}}.{{.DataModel}}{} if respStr, ok := resp.Response.(string); ok { // Decrypt the response string consID, secretKey, _, tstamp, _ := h.config.SetHeader() decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to decrypt response", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} } else { json.Unmarshal([]byte(decryptedResp), response.Data) } } else if respMap, ok := resp.Response.(map[string]interface{}); ok { // Response is already unmarshaled JSON if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { dataBytes, _ := json.Marshal(dataMap) json.Unmarshal(dataBytes, response.Data) } else { // Try to unmarshal the whole response respBytes, _ := json.Marshal(resp.Response) json.Unmarshal(respBytes, response.Data) } } } // Ensure response has proper fields response.Status = "success" response.RequestID = requestID {{if $.HasLogger}} h.logger.Info("Successfully created {{.Name}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusCreated, response) } {{end}} {{if .HasPut}} // Update{{.Name}} godoc // @Summary Update existing {{.Name}} // @Description Update existing {{.Name}} in BPJS system // @Tags {{index .Tags 0}} // @Accept json // @Produce json {{if .RequireAuth}} // @Security ApiKeyAuth {{end}} // @Param X-Request-ID header string false "Request ID for tracking" {{range .PathParams}} // @Param {{.}} path string true "{{.}}" example("example_value") {{end}} // @Param request body {{.ModelPackage}}.{{.RequestModel}} true "{{.Name}} update data" // @Success 200 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully updated {{.Name}}" // @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" // @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" // @Failure 404 {object} models.ErrorResponseBpjs "Not found - {{.Name}} not found" // @Failure 409 {object} models.ErrorResponseBpjs "Conflict - update conflict occurred" // @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" // @Router {{.PutRoutes}} [put] func (h *{{$.HandlerName}}Handler) 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) } {{if $.HasLogger}} h.logger.Info("Processing Update{{.Name}} request", map[string]interface{}{ "request_id": requestID, "endpoint": "{{.PutPath}}", {{range .PathParams}} "{{.}}": c.Param("{{.}}"), {{end}} }) {{end}} // Extract path parameters {{range .PathParams}} {{.}} := c.Param("{{.}}") if {{.}} == "" { {{if $.HasLogger}} h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ Status: "error", Message: "Missing required parameter {{.}}", RequestID: requestID, }) return } {{end}} // Bind and validate request body var req {{.ModelPackage}}.{{.RequestModel}} 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.ErrorResponseBpjs{ 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.ErrorResponseBpjs{ Status: "error", Message: "Validation failed: " + err.Error(), RequestID: requestID, }) return } // Call service method var response {{.ModelPackage}}.{{.ResponseModel}} {{if .PathParams}} endpoint := "{{.PutPath}}" {{range .PathParams}} endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) {{end}} resp, err := h.service.PutRawResponse(ctx, endpoint, req) {{else}} resp, err := h.service.PutRawResponse(ctx, "{{.PutPath}}", req) {{end}} if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to update {{.Name}}", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} // Handle specific BPJS errors if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") { c.JSON(http.StatusNotFound, models.ErrorResponseBpjs{ Status: "error", Message: "{{.Name}} not found", RequestID: requestID, }) return } if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") { c.JSON(http.StatusConflict, models.ErrorResponseBpjs{ Status: "error", Message: "Update conflict occurred", RequestID: requestID, }) return } c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ Status: "error", Message: "Internal server error", RequestID: requestID, }) return } // Map the raw response response.MetaData = resp.MetaData if resp.Response != nil { response.Data = &{{.ModelPackage}}.{{.DataModel}}{} if respStr, ok := resp.Response.(string); ok { // Decrypt the response string consID, secretKey, _, tstamp, _ := h.config.SetHeader() decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to decrypt response", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} } else { json.Unmarshal([]byte(decryptedResp), response.Data) } } else if respMap, ok := resp.Response.(map[string]interface{}); ok { // Response is already unmarshaled JSON if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { dataBytes, _ := json.Marshal(dataMap) json.Unmarshal(dataBytes, response.Data) } else { // Try to unmarshal the whole response respBytes, _ := json.Marshal(resp.Response) json.Unmarshal(respBytes, response.Data) } } } // Ensure response has proper fields response.Status = "success" response.RequestID = requestID {{if $.HasLogger}} h.logger.Info("Successfully updated {{.Name}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusOK, response) } {{end}} {{if .HasDelete}} // Delete{{.Name}} godoc // @Summary Delete existing {{.Name}} // @Description Delete existing {{.Name}} from BPJS system // @Tags {{index .Tags 0}} // @Accept json // @Produce json {{if .RequireAuth}} // @Security ApiKeyAuth {{end}} // @Param X-Request-ID header string false "Request ID for tracking" {{range .PathParams}} // @Param {{.}} path string true "{{.}}" example("example_value") {{end}} // @Success 200 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully deleted {{.Name}}" // @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" // @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" // @Failure 404 {object} models.ErrorResponseBpjs "Not found - {{.Name}} not found" // @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" // @Router {{.DeleteRoutes}} [delete] func (h *{{$.HandlerName}}Handler) 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) } {{if $.HasLogger}} h.logger.Info("Processing Delete{{.Name}} request", map[string]interface{}{ "request_id": requestID, "endpoint": "{{.DeletePath}}", {{range .PathParams}} "{{.}}": c.Param("{{.}}"), {{end}} }) {{end}} // Extract path parameters {{range .PathParams}} {{.}} := c.Param("{{.}}") if {{.}} == "" { {{if $.HasLogger}} h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ Status: "error", Message: "Missing required parameter {{.}}", RequestID: requestID, }) return } {{end}} // Call service method var response {{.ModelPackage}}.{{.ResponseModel}} {{if .PathParams}} endpoint := "{{.DeletePath}}" {{range .PathParams}} endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) {{end}} resp, err := h.service.DeleteRawResponse(ctx, endpoint) {{else}} resp, err := h.service.DeleteRawResponse(ctx, "{{.DeletePath}}") {{end}} if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to delete {{.Name}}", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} // Handle specific BPJS errors if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") { c.JSON(http.StatusNotFound, models.ErrorResponseBpjs{ Status: "error", Message: "{{.Name}} not found", RequestID: requestID, }) return } c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ Status: "error", Message: "Internal server error", RequestID: requestID, }) return } // Map the raw response response.MetaData = resp.MetaData if resp.Response != nil { response.Data = &{{.ModelPackage}}.{{.DataModel}}{} if respStr, ok := resp.Response.(string); ok { // Decrypt the response string consID, secretKey, _, tstamp, _ := h.config.SetHeader() decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to decrypt response", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} } else { json.Unmarshal([]byte(decryptedResp), response.Data) } } else if respMap, ok := resp.Response.(map[string]interface{}); ok { // Response is already unmarshaled JSON if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { dataBytes, _ := json.Marshal(dataMap) json.Unmarshal(dataBytes, response.Data) } else { // Try to unmarshal the whole response respBytes, _ := json.Marshal(resp.Response) json.Unmarshal(respBytes, response.Data) } } } else { // For delete operations, sometimes there's no data in response response.Data = nil } // Ensure response has proper fields response.Status = "success" response.RequestID = requestID {{if $.HasLogger}} h.logger.Info("Successfully deleted {{.Name}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusOK, response) } {{end}} {{end}}` // Create new handler file func createNewHandlerFile(filePath string, templateData TemplateData) error { // Create directory if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { return err } // Parse template tmpl := template.New("handler").Funcs(template.FuncMap{ "title": strings.Title, "index": func(slice []string, i int) string { if i >= 0 && i < len(slice) { return slice[i] } return "" }, }) tmpl, err := tmpl.Parse(handlerTemplate) if err != nil { return err } // Create file file, err := os.Create(filePath) if err != nil { return err } defer file.Close() // Execute template return tmpl.Execute(file, templateData) } // Merge dengan existing file func mergeWithExistingFile(filePath string, templateData TemplateData) error { // Read existing content existingContent, err := ioutil.ReadFile(filePath) if err != nil { return err } // Generate new functions newFunctions, err := generateNewFunctionsOnly(templateData) if err != nil { return err } // Merge content mergedContent := mergeGoFileContent(string(existingContent), newFunctions) // Write back return ioutil.WriteFile(filePath, []byte(mergedContent), 0644) } func generateNewFunctionsOnly(templateData TemplateData) (string, error) { funcTemplate := ` {{range .Endpoints}} {{if .HasGet}} // Get{{.Name}} godoc // @Summary Get {{.Name}} data // @Description {{.Description}} // @Tags {{index .Tags 0}} // @Accept json // @Produce json {{if .RequireAuth}} // @Security ApiKeyAuth {{end}} // @Param X-Request-ID header string false "Request ID for tracking" {{range .PathParams}} // @Param {{.}} path string true "{{.}}" example("example_value") {{end}} // @Success 200 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully retrieved {{.Name}} data" // @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" // @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" // @Failure 404 {object} models.ErrorResponseBpjs "Not found - {{.Name}} not found" // @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" // @Router {{.GetRoutes}} [get] func (h *{{$.HandlerName}}Handler) 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) } {{if $.HasLogger}} h.logger.Info("Processing Get{{.Name}} request", map[string]interface{}{ "request_id": requestID, "endpoint": "{{.GetPath}}", {{range .PathParams}} "{{.}}": c.Param("{{.}}"), {{end}} }) {{end}} // Extract path parameters {{range .PathParams}} {{.}} := c.Param("{{.}}") if {{.}} == "" { {{if $.HasLogger}} h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ Status: "error", Message: "Missing required parameter {{.}}", RequestID: requestID, }) return } {{end}} // Call service method var response {{.ModelPackage}}.{{.ResponseModel}} {{if .PathParams}} endpoint := "{{.GetPath}}" {{range .PathParams}} endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) {{end}} resp, err := h.service.GetRawResponse(ctx, endpoint) {{else}} resp, err := h.service.GetRawResponse(ctx, "{{.GetPath}}") {{end}} if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to get {{.Name}}", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ Status: "error", Message: "Internal server error", RequestID: requestID, }) return } // Map the raw response response.MetaData = resp.MetaData if resp.Response != nil { response.Data = &{{.ModelPackage}}.{{.DataModel}}{} if respStr, ok := resp.Response.(string); ok { // Decrypt the response string consID, secretKey, _, tstamp, _ := h.config.SetHeader() decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to decrypt response", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} } else { json.Unmarshal([]byte(decryptedResp), response.Data) } } else if respMap, ok := resp.Response.(map[string]interface{}); ok { // Response is already unmarshaled JSON if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { dataBytes, _ := json.Marshal(dataMap) json.Unmarshal(dataBytes, response.Data) } else { // Try to unmarshal the whole response respBytes, _ := json.Marshal(resp.Response) json.Unmarshal(respBytes, response.Data) } } } // Ensure response has proper fields response.Status = "success" response.RequestID = requestID c.JSON(http.StatusOK, response) } {{end}} {{if .HasPost}} // Create{{.Name}} godoc // @Summary Create new {{.Name}} // @Description Create new {{.Name}} in BPJS system // @Tags {{index .Tags 0}} // @Accept json // @Produce json {{if .RequireAuth}} // @Security ApiKeyAuth {{end}} // @Param X-Request-ID header string false "Request ID for tracking" // @Param request body {{.ModelPackage}}.{{.RequestModel}} true "{{.Name}} data" // @Success 201 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully created {{.Name}}" // @Failure 400 {object} models.ErrorResponseBpjs "Bad request" // @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized" // @Failure 409 {object} models.ErrorResponseBpjs "Conflict" // @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" // @Router {{.PostRoutes}} [post] func (h *{{$.HandlerName}}Handler) 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 {{.ModelPackage}}.{{.RequestModel}} 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.ErrorResponseBpjs{ 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.ErrorResponseBpjs{ Status: "error", Message: "Validation failed: " + err.Error(), RequestID: requestID, }) return } {{if .PathParams}} // Extract path parameters {{range .PathParams}} {{.}} := c.Param("{{.}}") if {{.}} == "" { {{if $.HasLogger}} h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ Status: "error", Message: "Missing required parameter {{.}}", RequestID: requestID, }) return } {{end}} endpoint := "{{.PostPath}}" {{range .PathParams}} endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) {{end}} {{end}} // Call service method var response {{.ModelPackage}}.{{.ResponseModel}} {{if .PathParams}} resp, err := h.service.PostRawResponse(ctx, endpoint, req) {{else}} resp, err := h.service.PostRawResponse(ctx, "{{.PostPath}}", req) {{end}} if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to create {{.Name}}", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} // Handle specific BPJS errors if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") { c.JSON(http.StatusConflict, models.ErrorResponseBpjs{ Status: "error", Message: "{{.Name}} already exists or conflict occurred", RequestID: requestID, }) return } c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ Status: "error", Message: "Internal server error", RequestID: requestID, }) return } // Map the raw response response.MetaData = resp.MetaData if resp.Response != nil { response.Data = &{{.ModelPackage}}.{{.DataModel}}{} if respStr, ok := resp.Response.(string); ok { // Decrypt the response string consID, secretKey, _, tstamp, _ := h.config.SetHeader() decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to decrypt response", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} } else { json.Unmarshal([]byte(decryptedResp), response.Data) } } else if respMap, ok := resp.Response.(map[string]interface{}); ok { // Response is already unmarshaled JSON if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { dataBytes, _ := json.Marshal(dataMap) json.Unmarshal(dataBytes, response.Data) } else { // Try to unmarshal the whole response respBytes, _ := json.Marshal(resp.Response) json.Unmarshal(respBytes, response.Data) } } } // Ensure response has proper fields response.Status = "success" response.RequestID = requestID {{if $.HasLogger}} h.logger.Info("Successfully created {{.Name}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusCreated, response) } {{end}} {{if .HasPut}} // Update{{.Name}} godoc // @Summary Update existing {{.Name}} // @Description Update existing {{.Name}} in BPJS system // @Tags {{index .Tags 0}} // @Accept json // @Produce json {{if .RequireAuth}} // @Security ApiKeyAuth {{end}} // @Param X-Request-ID header string false "Request ID for tracking" {{range .PathParams}} // @Param {{.}} path string true "{{.}}" example("example_value") {{end}} // @Param request body {{.ModelPackage}}.{{.RequestModel}} true "{{.Name}} update data" // @Success 200 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully updated {{.Name}}" // @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" // @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" // @Failure 404 {object} models.ErrorResponseBpjs "Not found - {{.Name}} not found" // @Failure 409 {object} models.ErrorResponseBpjs "Conflict - update conflict occurred" // @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" // @Router {{.PutRoutes}} [put] func (h *{{$.HandlerName}}Handler) 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) } {{if $.HasLogger}} h.logger.Info("Processing Update{{.Name}} request", map[string]interface{}{ "request_id": requestID, "endpoint": "{{.PutPath}}", {{range .PathParams}} "{{.}}": c.Param("{{.}}"), {{end}} }) {{end}} // Extract path parameters {{range .PathParams}} {{.}} := c.Param("{{.}}") if {{.}} == "" { {{if $.HasLogger}} h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ Status: "error", Message: "Missing required parameter {{.}}", RequestID: requestID, }) return } {{end}} // Bind and validate request body var req {{.ModelPackage}}.{{.RequestModel}} 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.ErrorResponseBpjs{ 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.ErrorResponseBpjs{ Status: "error", Message: "Validation failed: " + err.Error(), RequestID: requestID, }) return } // Call service method var response {{.ModelPackage}}.{{.ResponseModel}} {{if .PathParams}} endpoint := "{{.PutPath}}" {{range .PathParams}} endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) {{end}} resp, err := h.service.PutRawResponse(ctx, endpoint, req) {{else}} resp, err := h.service.PutRawResponse(ctx, "{{.PutPath}}", req) {{end}} if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to update {{.Name}}", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} // Handle specific BPJS errors if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") { c.JSON(http.StatusNotFound, models.ErrorResponseBpjs{ Status: "error", Message: "{{.Name}} not found", RequestID: requestID, }) return } if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") { c.JSON(http.StatusConflict, models.ErrorResponseBpjs{ Status: "error", Message: "Update conflict occurred", RequestID: requestID, }) return } c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ Status: "error", Message: "Internal server error", RequestID: requestID, }) return } // Map the raw response response.MetaData = resp.MetaData if resp.Response != nil { response.Data = &{{.ModelPackage}}.{{.DataModel}}{} if respStr, ok := resp.Response.(string); ok { // Decrypt the response string consID, secretKey, _, tstamp, _ := h.config.SetHeader() decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to decrypt response", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} } else { json.Unmarshal([]byte(decryptedResp), response.Data) } } else if respMap, ok := resp.Response.(map[string]interface{}); ok { // Response is already unmarshaled JSON if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { dataBytes, _ := json.Marshal(dataMap) json.Unmarshal(dataBytes, response.Data) } else { // Try to unmarshal the whole response respBytes, _ := json.Marshal(resp.Response) json.Unmarshal(respBytes, response.Data) } } } // Ensure response has proper fields response.Status = "success" response.RequestID = requestID {{if $.HasLogger}} h.logger.Info("Successfully updated {{.Name}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusOK, response) } {{end}} {{if .HasDelete}} // Delete{{.Name}} godoc // @Summary Delete existing {{.Name}} // @Description Delete existing {{.Name}} from BPJS system // @Tags {{index .Tags 0}} // @Accept json // @Produce json {{if .RequireAuth}} // @Security ApiKeyAuth {{end}} // @Param X-Request-ID header string false "Request ID for tracking" {{range .PathParams}} // @Param {{.}} path string true "{{.}}" example("example_value") {{end}} // @Success 200 {object} {{.ModelPackage}}.{{.ResponseModel}} "Successfully deleted {{.Name}}" // @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters" // @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials" // @Failure 404 {object} models.ErrorResponseBpjs "Not found - {{.Name}} not found" // @Failure 500 {object} models.ErrorResponseBpjs "Internal server error" // @Router {{.DeleteRoutes}} [delete] func (h *{{$.HandlerName}}Handler) 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) } {{if $.HasLogger}} h.logger.Info("Processing Delete{{.Name}} request", map[string]interface{}{ "request_id": requestID, "endpoint": "{{.DeletePath}}", {{range .PathParams}} "{{.}}": c.Param("{{.}}"), {{end}} }) {{end}} // Extract path parameters {{range .PathParams}} {{.}} := c.Param("{{.}}") if {{.}} == "" { {{if $.HasLogger}} h.logger.Error("Missing required parameter {{.}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ Status: "error", Message: "Missing required parameter {{.}}", RequestID: requestID, }) return } {{end}} // Call service method var response {{.ModelPackage}}.{{.ResponseModel}} {{if .PathParams}} endpoint := "{{.DeletePath}}" {{range .PathParams}} endpoint = strings.Replace(endpoint, ":{{.}}", {{.}}, 1) {{end}} resp, err := h.service.DeleteRawResponse(ctx, endpoint) {{else}} resp, err := h.service.DeleteRawResponse(ctx, "{{.DeletePath}}") {{end}} if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to delete {{.Name}}", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} // Handle specific BPJS errors if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") { c.JSON(http.StatusNotFound, models.ErrorResponseBpjs{ Status: "error", Message: "{{.Name}} not found", RequestID: requestID, }) return } c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{ Status: "error", Message: "Internal server error", RequestID: requestID, }) return } // Map the raw response response.MetaData = resp.MetaData if resp.Response != nil { response.Data = &{{.ModelPackage}}.{{.DataModel}}{} if respStr, ok := resp.Response.(string); ok { // Decrypt the response string consID, secretKey, _, tstamp, _ := h.config.SetHeader() decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp) if err != nil { {{if $.HasLogger}} h.logger.Error("Failed to decrypt response", map[string]interface{}{ "error": err.Error(), "request_id": requestID, }) {{end}} } else { json.Unmarshal([]byte(decryptedResp), response.Data) } } else if respMap, ok := resp.Response.(map[string]interface{}); ok { // Response is already unmarshaled JSON if dataMap, exists := respMap["{{.ModelPackage}}"]; exists { dataBytes, _ := json.Marshal(dataMap) json.Unmarshal(dataBytes, response.Data) } else { // Try to unmarshal the whole response respBytes, _ := json.Marshal(resp.Response) json.Unmarshal(respBytes, response.Data) } } } else { // For delete operations, sometimes there's no data in response response.Data = nil } // Ensure response has proper fields response.Status = "success" response.RequestID = requestID {{if $.HasLogger}} h.logger.Info("Successfully deleted {{.Name}}", map[string]interface{}{ "request_id": requestID, }) {{end}} c.JSON(http.StatusOK, response) } {{end}} {{end}}` tmpl := template.New("functions") tmpl, err := tmpl.Parse(funcTemplate) if err != nil { return "", err } var result strings.Builder err = tmpl.Execute(&result, templateData) if err != nil { return "", err } // Remove duplicate function definitions by simple regex grouping // This is a simple approach to avoid duplicate functions in merged content funcRegex := regexp.MustCompile(`(?s)(func \(h \*\w+Handler\) Get\w+\(c \*gin.Context\) \{.*?\})`) matches := funcRegex.FindAllString(result.String(), -1) uniqueFuncs := make(map[string]bool) var uniqueResult strings.Builder for _, m := range matches { if !uniqueFuncs[m] { uniqueFuncs[m] = true uniqueResult.WriteString(m) uniqueResult.WriteString("\n\n") } } // If no matches found, return original result if uniqueResult.Len() == 0 { return result.String(), nil } return uniqueResult.String(), nil } 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 BPJS Dynamic Handler Generation with Validation...") fmt.Printf("📁 Config file: %s\n", configFile) if targetService != "" { fmt.Printf("🎯 Target service: %s\n", targetService) } generated := 0 errors := 0 for serviceName, service := range config.Services { if targetService != "" && serviceName != targetService { continue } err := generateHandlerWithValidation(serviceName, service, config.Global) if err != nil { fmt.Printf("❌ Error generating handler for %s: %v\n", serviceName, 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!") } } // Helper functions 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" } return &config, nil } func getOrDefault(value, defaultValue int) int { if value == 0 { return defaultValue } return value } func printUsage() { fmt.Println("BPJS Dynamic Handler Generator with Function Validation") fmt.Println() fmt.Println("Usage:") fmt.Println(" go run generate-handler.go [service-name]") fmt.Println() fmt.Println("Examples:") fmt.Println(" go run generate-handler.go services-config-bpjs.yaml") fmt.Println(" go run generate-handler.go services-config-bpjs.yaml vclaim") } // Generate routes file untuk service func generateRoutes(serviceName string, svc Service, gc GlobalConfig) error { routesFilePath := "internal/routes/v1/routes.go" routesContent, err := ioutil.ReadFile(routesFilePath) if err != nil { return fmt.Errorf("failed to 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 in main routes file\n", svc.Name) return nil } var imports []string var allRoutes []string // ✅ PERBAIKAN: Gunakan groupName dengan benar // ✅ VALIDASI: Track handler folder yang sudah diimport processedFolders := make(map[string]bool) for groupName, grp := range svc.Endpoints { // Import berdasarkan handler folder // imports = append(imports, fmt.Sprintf("\t%sHandlers \"%s/internal/handlers/%s\"", // grp.HandlerFolder, gc.ModuleName, grp.HandlerFolder)) 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) } else { fmt.Printf("⚠️ Skipped duplicate import for folder: %s\n", grp.HandlerFolder) } var routesCode strings.Builder // Gunakan groupName untuk comment dan identifier routesCode.WriteString(fmt.Sprintf("\n\t// %s (%s) routes\n", grp.Description, groupName)) // Handler instantiation menggunakan HandlerName dari 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\tBpjsConfig: cfg.Bpjs,\n") routesCode.WriteString("\t\tLogger: *logger.Default(),\n") routesCode.WriteString("\t\tValidator: validator.New(),\n") routesCode.WriteString("\t})\n") // ✅ GUNAKAN groupName untuk route group path routesCode.WriteString(fmt.Sprintf("\t%sGroup := v1.Group(\"/%s\")\n", strings.ToLower(grp.HandlerName), groupName)) // ← Gunakan groupName di sini // 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" // ✅ MODIFIKASI: Loop through methods dan gunakan specific routes for _, method := range fcfg.Methods { var cleanPath string // ✅ Pilih path berdasarkan method switch strings.ToUpper(method) { case "GET": cleanPath = fcfg.GetRoutes if cleanPath == "" { cleanPath = fcfg.GetPath // fallback ke get_path } case "POST": cleanPath = fcfg.PostRoutes if cleanPath == "" { cleanPath = fcfg.PostPath // fallback ke post_path } case "PUT": cleanPath = fcfg.PutRoutes if cleanPath == "" { cleanPath = fcfg.PutPath // fallback ke put_path } case "DELETE": cleanPath = fcfg.DeleteRoutes if cleanPath == "" { cleanPath = fcfg.DeletePath // fallback ke delete_path } default: fmt.Printf("⚠️ Unsupported HTTP method: %s for function %s\n", method, fname) continue } // ✅ Final fallback ke path jika specific route kosong if cleanPath == "" { cleanPath = fcfg.Path } // ✅ Bersihkan path - hapus prefix groupName jika ada if strings.HasPrefix(cleanPath, "/"+groupName) { cleanPath = strings.TrimPrefix(cleanPath, "/"+groupName) } if cleanPath == "" { cleanPath = "/" } // ✅ Generate route berdasarkan 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 "DELETE": routesCode.WriteString(fmt.Sprintf("\t%s.DELETE(\"%s\", %s.Delete%s)\n", groupVar, cleanPath, handlerVar, endpoint.Name)) } } } } allRoutes = append(allRoutes, routesCode.String()) } // ✅ PERBAIKAN: Insert imports setelah "api-service/internal/database" if len(imports) > 0 { // ✅ PERBAIKAN: Hilangkan newline di awal, langsung import lines saja importSection := strings.Join(imports, "\n") + "\n" // ✅ PERBAIKAN: Cari posisi setelah "api-service/internal/database" databaseImportMarker := fmt.Sprintf("\"%s/internal/database\"", gc.ModuleName) if strings.Contains(routesContentStr, databaseImportMarker) { // Temukan posisi marker markerPos := strings.Index(routesContentStr, databaseImportMarker) // Temukan akhir baris dari marker endOfLinePos := strings.Index(routesContentStr[markerPos:], "\n") + markerPos // Insert import section setelah baris marker routesContentStr = routesContentStr[:endOfLinePos+1] + importSection + routesContentStr[endOfLinePos+1:] fmt.Printf("✅ Inserted imports after database import\n") } else { // Fallback: Insert setelah "import (" jika marker tidak ditemukan importMarker := "import (" if strings.Contains(routesContentStr, importMarker) { importIndex := strings.Index(routesContentStr, importMarker) + len(importMarker) routesContentStr = routesContentStr[:importIndex] + "\n" + importSection + routesContentStr[importIndex:] fmt.Printf("⚠️ Database import not found, inserted after import (\n") } else { fmt.Printf("⚠️ Could not find import section to insert imports\n") } } } // Find and insert routes publishedRoutesMarker := "// ============= PUBLISHED ROUTES ===============================================" if !strings.Contains(routesContentStr, publishedRoutesMarker) { return fmt.Errorf("PUBLISHED ROUTES marker not found in routes.go") } insertionPoint := strings.Index(routesContentStr, publishedRoutesMarker) + len(publishedRoutesMarker) newRoutesContent := routesContentStr[:insertionPoint] + "\n" + strings.Join(allRoutes, "\n") + "\n" + routesContentStr[insertionPoint:] err = ioutil.WriteFile(routesFilePath, []byte(newRoutesContent), 0644) if err != nil { return fmt.Errorf("failed to write updated routes file: %w", err) } fmt.Printf("✅ Updated main routes file with %s routes\n", svc.Name) return nil }