package main import ( "flag" "fmt" "io/ioutil" "os" "path/filepath" "strings" "sync" "text/template" "time" "gopkg.in/yaml.v2" ) /* ---------- Model Konfig ---------- */ type ServiceConfig struct { Services map[string]Service `yaml:"services"` Global GlobalConfig `yaml:"global,omitempty"` } type GlobalConfig struct { ModuleName string `yaml:"module_name"` OutputDir string `yaml:"output_dir"` PackagePrefix string `yaml:"package_prefix"` EnableSwagger bool `yaml:"enable_swagger"` EnableLogging bool `yaml:"enable_logging"` EnableMetrics bool `yaml:"enable_metrics"` } 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]Endpoint `yaml:"endpoints"` Middleware []string `yaml:"middleware,omitempty"` Dependencies []string `yaml:"dependencies,omitempty"` } type Endpoint struct { Methods []string `yaml:"methods"` GetPath string `yaml:"get_path,omitempty"` PostPath string `yaml:"post_path,omitempty"` PutPath string `yaml:"put_path,omitempty"` DeletePath string `yaml:"delete_path,omitempty"` PatchPath string `yaml:"patch_path,omitempty"` Model string `yaml:"model"` ResponseModel string `yaml:"response_model"` Description string `yaml:"description"` Summary string `yaml:"summary"` Tags []string `yaml:"tags"` RequireAuth bool `yaml:"require_auth"` RateLimit int `yaml:"rate_limit,omitempty"` CacheEnabled bool `yaml:"cache_enabled"` CacheTTL int `yaml:"cache_ttl,omitempty"` CustomHeaders map[string]string `yaml:"custom_headers,omitempty"` } /* ---------- Data Template ---------- */ 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 HasMetrics bool HasSwagger bool HasAuth bool HasCache bool Dependencies []string Middleware []string GlobalConfig GlobalConfig } type EndpointData struct { Name string NameLower string NameUpper string NameCamel string Methods []string GetPath string PostPath string PutPath string DeletePath string PatchPath string Model string ResponseModel string Description string Summary string Tags []string HasGet bool HasPost bool HasPut bool HasDelete bool HasPatch bool RequireAuth bool RateLimit int CacheEnabled bool CacheTTL int PathParams []string CustomHeaders map[string]string } /* ---------- Template Handler ---------- */ const handlerTemplate = ` // Code generated by generate-handler.go; DO NOT EDIT. // Generated at: {{.Timestamp}} // Service: {{.ServiceName}} ({{.Category}}) package handlers import ( "context" "net/http" "time" "{{.ModuleName}}/internal/models/reference" "github.com/gin-gonic/gin" "github.com/google/uuid" ) type {{.ServiceName}}Handler struct {} func New{{.ServiceName}}Handler() *{{.ServiceName}}Handler { return &{{.ServiceName}}Handler{} } {{range .Endpoints}} {{if .HasGet}} // // @Summary Get {{.Name}} data // @Description {{.Description}} // @Tags {{join .Tags ","}} // @Accept json // @Produce json {{range .PathParams}}// @Param {{.}} path string true "{{.}}" {{end}} {{if .RequireAuth}}// @Security ApiKeyAuth {{end}} // @Success 200 {object} reference.{{.ResponseModel}} // @Failure 400 {object} reference.ErrorResponse // @Failure 500 {object} reference.ErrorResponse // @Router {{.GetPath}} [get] func (h *{{$.ServiceName}}Handler) Get{{.Name}}(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), time.Duration({{$.Timeout}})*time.Second) defer cancel() reqID := c.GetHeader("X-Request-ID") if reqID == "" { reqID = uuid.New().String() c.Header("X-Request-ID", reqID) } {{range .PathParams}} {{.}} := c.Param("{{.}}") {{end}} // TODO: Panggil service layer, mapping response, error handling dsb. c.JSON(http.StatusOK, gin.H{"status": "success", "request_id": reqID}) } {{end}} {{if .HasPost}} // // @Summary Create {{.Name}} data // @Description {{.Description}} // @Tags {{join .Tags ","}} // @Accept json // @Produce json {{if .RequireAuth}}// @Security ApiKeyAuth {{end}} // @Param request body reference.{{.Model}} true "{{.Name}} data" // @Success 201 {object} reference.{{.ResponseModel}} // @Failure 400 {object} reference.ErrorResponse // @Failure 500 {object} reference.ErrorResponse // @Router {{.PostPath}} [post] func (h *{{$.ServiceName}}Handler) Create{{.Name}}(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), time.Duration({{$.Timeout}})*time.Second) defer cancel() reqID := c.GetHeader("X-Request-ID") if reqID == "" { reqID = uuid.New().String() c.Header("X-Request-ID", reqID) } var req reference.{{.Model}} if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"status": "error", "message": err.Error(), "request_id": reqID}) return } // TODO: Panggil service layer, validasi, error handling dsb. c.JSON(http.StatusCreated, gin.H{"status": "success", "request_id": reqID}) } {{end}} {{if .HasPut}} // // @Summary Update {{.Name}} data // @Description Update {{.Description}} // @Tags {{join .Tags ","}} // @Accept json // @Produce json {{range .PathParams}}// @Param {{.}} path string true "{{.}}" {{end}} {{if .RequireAuth}}// @Security ApiKeyAuth {{end}} // @Param request body reference.{{.Model}} true "{{.Name}} data" // @Success 200 {object} reference.{{.ResponseModel}} // @Failure 400 {object} reference.ErrorResponse // @Failure 500 {object} reference.ErrorResponse // @Router {{.PutPath}} [put] func (h *{{$.ServiceName}}Handler) Update{{.Name}}(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second) defer cancel() requestID := c.GetHeader("X-Request-ID") if requestID == "" { requestID = uuid.New().String() c.Header("X-Request-ID", requestID) } {{range .PathParams}} {{.}} := c.Param("{{.}}") if {{.}} == "" { c.JSON(http.StatusBadRequest, reference.ErrorResponse{ Status: "error", Message: "Missing required parameter {{.}}", RequestID: requestID, }) return } {{end}} var req reference.{{.Model}} if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, reference.ErrorResponse{ Status: "error", Message: "Invalid request body: " + err.Error(), RequestID: requestID, }) return } // TODO: Validasi, panggil service update // result, err := h.service.Update{{.Name}}(ctx, {{range .PathParams}}{{.}}, {{end}}&req) // if err != nil { // c.JSON(http.StatusInternalServerError, ...) // return // } c.JSON(http.StatusOK, gin.H{"status": "success", "request_id": requestID}) } {{end}} {{if .HasPatch}} // // @Summary Patch {{.Name}} data // @Description Patch {{.Description}} // @Tags {{join .Tags ","}} // @Accept json // @Produce json {{range .PathParams}}// @Param {{.}} path string true "{{.}}" {{end}} {{if .RequireAuth}}// @Security ApiKeyAuth {{end}} // @Param request body reference.{{.Model}} true "Partial {{.Name}} data" // @Success 200 {object} reference.{{.ResponseModel}} // @Failure 400 {object} reference.ErrorResponse // @Failure 500 {object} reference.ErrorResponse // @Router {{.PatchPath}} [patch] func (h *{{$.ServiceName}}Handler) Patch{{.Name}}(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second) defer cancel() requestID := c.GetHeader("X-Request-ID") if requestID == "" { requestID = uuid.New().String() c.Header("X-Request-ID", requestID) } {{range .PathParams}} {{.}} := c.Param("{{.}}") if {{.}} == "" { c.JSON(http.StatusBadRequest, reference.ErrorResponse{ Status: "error", Message: "Missing required parameter {{.}}", RequestID: requestID, }) return } {{end}} var req reference.{{.Model}} if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, reference.ErrorResponse{ Status: "error", Message: "Invalid request body: " + err.Error(), RequestID: requestID, }) return } // TODO: Validasi, panggil service patch // result, err := h.service.Patch{{.Name}}(ctx, {{range .PathParams}}{{.}}, {{end}}&req) // if err != nil { // c.JSON(http.StatusInternalServerError, ...) // return // } c.JSON(http.StatusOK, gin.H{"status": "success", "request_id": requestID}) } {{end}} {{if .HasDelete}} // // @Summary Delete {{.Name}} data // @Description Delete {{.Description}} // @Tags {{join .Tags ","}} // @Accept json // @Produce json {{range .PathParams}}// @Param {{.}} path string true "{{.}}" {{end}} {{if .RequireAuth}}// @Security ApiKeyAuth {{end}} // @Success 204 {object} nil // @Failure 400 {object} reference.ErrorResponse // @Failure 500 {object} reference.ErrorResponse // @Router {{.DeletePath}} [delete] func (h *{{$.ServiceName}}Handler) Delete{{.Name}}(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), {{$.Timeout}}*time.Second) defer cancel() requestID := c.GetHeader("X-Request-ID") if requestID == "" { requestID = uuid.New().String() c.Header("X-Request-ID", requestID) } {{range .PathParams}} {{.}} := c.Param("{{.}}") if {{.}} == "" { c.JSON(http.StatusBadRequest, reference.ErrorResponse{ Status: "error", Message: "Missing required parameter {{.}}", RequestID: requestID, }) return } {{end}} // TODO: Panggil service delete // err := h.service.Delete{{.Name}}(ctx, {{range .PathParams}}{{.}}, {{end}}) // if err != nil { // c.JSON(http.StatusInternalServerError, ...) // return // } c.Status(http.StatusNoContent) } {{end}} {{end}} ` /* ---------- Main ---------- */ func main() { flag.Usage = usage outFlag := flag.String("o", "", "override output directory") serviceFlag := flag.String("s", "", "generate only selected service name") tmplFlag := flag.String("tmpl", "", "custom handler template path") flag.Parse() if flag.NArg() < 1 { usage() os.Exit(1) } configFile := flag.Arg(0) config, err := loadConfig(configFile) if err != nil { fmt.Printf("❌ Error loading config: %v\n", err) os.Exit(1) } outDir := firstNonEmpty(*outFlag, config.Global.OutputDir, "internal/handlers") templatePath := firstNonEmpty(*tmplFlag, "") // Template setup var tmpl *template.Template if templatePath != "" { tmpl, err = template.New("handler").Funcs(templateFuncMap()).ParseFiles(templatePath) } else { tmpl, err = template.New("handler").Funcs(templateFuncMap()).Parse(handlerTemplate) } if err != nil { fmt.Printf("❌ Error parsing template: %v\n", err) os.Exit(1) } var wg sync.WaitGroup for svcName, svc := range config.Services { if *serviceFlag != "" && svcName != *serviceFlag { continue } wg.Add(1) go func(serviceName string, service Service) { defer wg.Done() err := generateHandler(serviceName, service, config.Global, outDir, tmpl) if err != nil { fmt.Printf("❌ Error generating handler for %s: %v\n", serviceName, err) } else { fmt.Printf("✅ Generated handler: %s_handler.go\n", strings.ToLower(serviceName)) } }(svcName, svc) } wg.Wait() } func loadConfig(filename string) (*ServiceConfig, error) { data, err := ioutil.ReadFile(filename) if err != nil { return nil, fmt.Errorf("failed to read config file: %w", err) } var config ServiceConfig err = yaml.Unmarshal(data, &config) if err != nil { return nil, fmt.Errorf("failed to parse YAML config: %w", err) } if config.Global.ModuleName == "" { config.Global.ModuleName = "api-service" } if config.Global.OutputDir == "" { config.Global.OutputDir = "internal/handlers" } return &config, nil } func generateHandler(serviceName string, service Service, globalConfig GlobalConfig, outDir string, tmpl *template.Template) error { td := prepareTemplateData(serviceName, service, globalConfig) if err := os.MkdirAll(outDir, 0o755); err != nil { return fmt.Errorf("failed to create output directory: %w", err) } filename := filepath.Join(outDir, fmt.Sprintf("%s_handler.go", td.ServiceLower)) file, err := os.Create(filename) if err != nil { return fmt.Errorf("failed to create handler file: %w", err) } defer file.Close() return tmpl.Execute(file, td) } func prepareTemplateData(serviceName string, svc Service, globalConfig GlobalConfig) TemplateData { td := TemplateData{ ServiceName: svc.Name, ServiceLower: strings.ToLower(svc.Name), ServiceUpper: strings.ToUpper(svc.Name), Category: svc.Category, Package: svc.Package, Description: svc.Description, BaseURL: svc.BaseURL, Timeout: getOrDefault(svc.Timeout, 30), RetryCount: getOrDefault(svc.RetryCount, 3), Timestamp: time.Now().Format("2006-01-02 15:04:05"), ModuleName: globalConfig.ModuleName, HasValidator: true, HasLogger: globalConfig.EnableLogging, HasMetrics: globalConfig.EnableMetrics, HasSwagger: globalConfig.EnableSwagger, Dependencies: svc.Dependencies, Middleware: svc.Middleware, GlobalConfig: globalConfig, } for _, ep := range svc.Endpoints { ed := processEndpoint(ep) if ed.RequireAuth { td.HasAuth = true } if ed.CacheEnabled { td.HasCache = true } td.Endpoints = append(td.Endpoints, ed) } return td } func processEndpoint(ep Endpoint) EndpointData { data := EndpointData{ Name: strings.Title(ep.Summary), NameLower: strings.ToLower(ep.Summary), NameUpper: strings.ToUpper(ep.Summary), NameCamel: toCamelCase(ep.Summary), Methods: ep.Methods, GetPath: ep.GetPath, PostPath: ep.PostPath, PutPath: ep.PutPath, DeletePath: ep.DeletePath, PatchPath: ep.PatchPath, Model: ep.Model, ResponseModel: ep.ResponseModel, Description: ep.Description, Summary: ep.Summary, Tags: ep.Tags, RequireAuth: ep.RequireAuth, RateLimit: ep.RateLimit, CacheEnabled: ep.CacheEnabled, CacheTTL: getOrDefault(ep.CacheTTL, 300), CustomHeaders: ep.CustomHeaders, } for _, method := range ep.Methods { switch strings.ToUpper(method) { case "GET": data.HasGet = true data.PathParams = extractPathParams(ep.GetPath) case "POST": data.HasPost = true case "PUT": data.HasPut = true data.PathParams = extractPathParams(ep.PutPath) case "DELETE": data.HasDelete = true data.PathParams = extractPathParams(ep.DeletePath) case "PATCH": data.HasPatch = true data.PathParams = extractPathParams(ep.PatchPath) } } return data } func extractPathParams(path string) []string { if path == "" { return nil } var params []string parts := strings.Split(path, "/") for _, part := range parts { if strings.HasPrefix(part, ":") { params = append(params, strings.TrimPrefix(part, ":")) } } return params } func toCamelCase(str string) string { words := strings.FieldsFunc(str, func(c rune) bool { return c == '_' || c == '-' || c == ' ' }) if len(words) == 0 { return str } result := strings.Title(strings.ToLower(words[0])) for _, word := range words[1:] { result += strings.Title(strings.ToLower(word)) } return result } func getOrDefault(value, defaultValue int) int { if value == 0 { return defaultValue } return value } func firstNonEmpty(vals ...string) string { for _, v := range vals { if v != "" { return v } } return "" } func templateFuncMap() template.FuncMap { return template.FuncMap{ "contains": strings.Contains, "join": strings.Join, "title": strings.Title, "trimPrefix": strings.TrimPrefix, } } func usage() { fmt.Println("BPJS Dynamic Handler Generator") fmt.Println(`Usage: go run generate-handler.go [flags] Flags: -o Output directory -s Single service name -tmpl Custom handler template path `) }