From 4d72e31352ea2faff0d7699e72dd5ea46f2665fb Mon Sep 17 00:00:00 2001 From: Meninjar Date: Mon, 18 Aug 2025 20:02:34 +0700 Subject: [PATCH] Creat Service BPJS --- README.md | 2 + .../handlers/bpjs/{ => reference}/peserta.go | 0 .../models/bpjs/{ => reference}/peserta.go | 0 internal/routes/v1/routes.go | 2 +- tools/generate-bpjs-handler.go | 689 ++++++++++++++++++ tools/generate-handler.go | 465 +++++++----- 6 files changed, 988 insertions(+), 170 deletions(-) rename internal/handlers/bpjs/{ => reference}/peserta.go (100%) rename internal/models/bpjs/{ => reference}/peserta.go (100%) create mode 100644 tools/generate-bpjs-handler.go diff --git a/README.md b/README.md index daca4519..04855e0b 100644 --- a/README.md +++ b/README.md @@ -209,3 +209,5 @@ const products = await axios.get('/api/v1/products'); --- **Total waktu setup: 5 menit** | **Generate CRUD: 30 detik** | **Testing: Langsung di Swagger** + + diff --git a/internal/handlers/bpjs/peserta.go b/internal/handlers/bpjs/reference/peserta.go similarity index 100% rename from internal/handlers/bpjs/peserta.go rename to internal/handlers/bpjs/reference/peserta.go diff --git a/internal/models/bpjs/peserta.go b/internal/models/bpjs/reference/peserta.go similarity index 100% rename from internal/models/bpjs/peserta.go rename to internal/models/bpjs/reference/peserta.go diff --git a/internal/routes/v1/routes.go b/internal/routes/v1/routes.go index ba076e3a..cc4f5ad3 100644 --- a/internal/routes/v1/routes.go +++ b/internal/routes/v1/routes.go @@ -1,7 +1,7 @@ package v1 import ( - bpjsPesertaHandlers "api-service/internal/handlers/bpjs" + bpjsPesertaHandlers "api-service/internal/handlers/bpjs/reference" retribusiHandlers "api-service/internal/handlers/retribusi" "api-service/internal/config" diff --git a/tools/generate-bpjs-handler.go b/tools/generate-bpjs-handler.go new file mode 100644 index 00000000..1134de0b --- /dev/null +++ b/tools/generate-bpjs-handler.go @@ -0,0 +1,689 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" +) + +// BpjsHandlerData contains template data for BPJS handler generation +type BpjsHandlerData struct { + Name string + NameLower string + NamePlural string + ModuleName string + Category string + HasGet bool + HasGetByID bool + HasPost bool + HasPut bool + HasDelete bool + HasStats bool + HasFilter bool + Timestamp string + Endpoints []BpjsEndpoint +} + +// BpjsEndpoint represents a BPJS API endpoint configuration +type BpjsEndpoint struct { + Name string + Method string + Path string + Description string + Parameters []EndpointParam +} + +// EndpointParam represents an endpoint parameter +type EndpointParam struct { + Name string + Type string + Required bool + Description string + Location string // "path", "query", "body" +} + +func main() { + if len(os.Args) < 2 { + fmt.Println("Usage: go run generate-bpjs-handler.go [category/]entity [methods]") + fmt.Println("Example: go run generate-bpjs-handler.go reference/peserta get getbyid") + fmt.Println("Example: go run generate-bpjs-handler.go peserta get getbyid") + os.Exit(1) + } + + // Parse entity input (could be "entity" or "category/entity") + entityInput := os.Args[1] + var category, entityName string + + if strings.Contains(entityInput, "/") { + parts := strings.Split(entityInput, "/") + if len(parts) != 2 { + fmt.Println("Invalid format. Use: category/entity or entity") + os.Exit(1) + } + category = parts[0] + entityName = strings.Title(parts[1]) // PascalCase entity name + } else { + category = "" + entityName = strings.Title(entityInput) // PascalCase entity name + } + + methods := []string{} + if len(os.Args) > 2 { + methods = os.Args[2:] + } else { + // Default methods if none specified + methods = []string{"get", "getbyid"} + } + + entityLower := strings.ToLower(entityName) + entityPlural := entityLower + "s" + data := BpjsHandlerData{ + Name: entityName, + NameLower: entityLower, + NamePlural: entityPlural, + ModuleName: "api-service", + Category: category, + Timestamp: time.Now().Format("2006-01-02 15:04:05"), + } + + // Set methods based on arguments + for _, m := range methods { + switch strings.ToLower(m) { + case "get": + data.HasGet = true + case "getbyid": + data.HasGetByID = true + case "post": + data.HasPost = true + case "put": + data.HasPut = true + case "delete": + data.HasDelete = true + case "stats": + data.HasStats = true + } + } + + // Define default endpoints based on entity + data.Endpoints = generateDefaultEndpoints(data) + + // Create directories based on category + var handlerDir, modelDir string + if category != "" { + handlerDir = filepath.Join("internal", "handlers", "bpjs", category) + modelDir = filepath.Join("internal", "models", "bpjs", category) + } else { + handlerDir = filepath.Join("internal", "handlers", "bpjs", entityLower) + modelDir = filepath.Join("internal", "models", "bpjs", entityLower) + } + + for _, d := range []string{handlerDir, modelDir} { + if err := os.MkdirAll(d, 0755); err != nil { + panic(err) + } + } + + // Generate files + generateBpjsHandlerFile(data, handlerDir) + generateBpjsModelFile(data, modelDir) + updateBpjsRoutesFile(data) + + fmt.Printf("✅ Successfully generated BPJS handler: %s\n", entityName) + if category != "" { + fmt.Printf("📁 Category: %s\n", category) + } + fmt.Printf("📁 Handler: %s\n", filepath.Join(handlerDir, entityLower+".go")) + fmt.Printf("📁 Model: %s\n", filepath.Join(modelDir, entityLower+".go")) +} + +// generateDefaultEndpoints creates default endpoints based on entity type +func generateDefaultEndpoints(data BpjsHandlerData) []BpjsEndpoint { + endpoints := []BpjsEndpoint{} + + switch data.NameLower { + case "peserta": + endpoints = append(endpoints, BpjsEndpoint{ + Name: "GetByNIK", + Method: "GET", + Path: "/Peserta/nik/%s/tglSEP/%s", + Description: "Get participant data by NIK and service date", + Parameters: []EndpointParam{ + {Name: "nik", Type: "string", Required: true, Description: "NIK KTP", Location: "path"}, + {Name: "tglSEP", Type: "string", Required: true, Description: "Service date (yyyy-MM-dd)", Location: "path"}, + }, + }) + + case "poli": + endpoints = append(endpoints, BpjsEndpoint{ + Name: "GetAll", + Method: "GET", + Path: "/referensi/poli", + Description: "Get all poli reference data", + Parameters: []EndpointParam{}, + }) + + case "diagnosa": + endpoints = append(endpoints, BpjsEndpoint{ + Name: "GetAll", + Method: "GET", + Path: "/referensi/diagnosa", + Description: "Get all diagnosa reference data", + Parameters: []EndpointParam{}, + }) + + case "provider": + endpoints = append(endpoints, BpjsEndpoint{ + Name: "GetByJenis", + Method: "GET", + Path: "/referensi/faskes/%s/%s", + Description: "Get provider data by type and level", + Parameters: []EndpointParam{ + {Name: "jnsFaskes", Type: "string", Required: true, Description: "Jenis Faskes (1=Faskes 1, 2=Faskes 2)", Location: "path"}, + {Name: "kdFaskes", Type: "string", Required: true, Description: "Kode Faskes", Location: "path"}, + }, + }) + + default: + // Generic endpoint + endpoints = append(endpoints, BpjsEndpoint{ + Name: "GetAll", + Method: "GET", + Path: fmt.Sprintf("/referensi/%s", data.NameLower), + Description: fmt.Sprintf("Get all %s reference data", data.NameLower), + Parameters: []EndpointParam{}, + }) + } + + return endpoints +} + +// ================= BPJS HANDLER GENERATION ===================== + +func generateBpjsHandlerFile(data BpjsHandlerData, handlerDir string) { + // Determine import path based on category + var modelsImportPath string + if data.Category != "" { + modelsImportPath = `models "` + data.ModuleName + `/internal/models/bpjs/` + data.Category + `"` + } else { + modelsImportPath = `models "` + data.ModuleName + `/internal/models/bpjs/` + data.NameLower + `"` + } + + handlerContent := `package handlers + +import ( + "context" + "fmt" + "net/http" + "time" + "` + data.ModuleName + `/internal/config" + services "` + data.ModuleName + `/internal/services/bpjs" + "github.com/gin-gonic/gin" +) + +// ` + data.Name + `Handler handles BPJS ` + data.NameLower + ` operations +type ` + data.Name + `Handler struct { + bpjsService services.VClaimService +} + +// New` + data.Name + `Handler creates a new ` + data.Name + `Handler instance +func New` + data.Name + `Handler(cfg config.BpjsConfig) *` + data.Name + `Handler { + return &` + data.Name + `Handler{ + bpjsService: services.NewService(cfg), + } +}` + + // Add methods based on endpoints + for _, endpoint := range data.Endpoints { + handlerContent += generateBpjsEndpointMethod(data, endpoint) + } + + // Add helper methods + handlerContent += generateBpjsHelperMethods(data) + + writeFile(filepath.Join(handlerDir, data.NameLower+".go"), handlerContent) +} + +func generateBpjsEndpointMethod(data BpjsHandlerData, endpoint BpjsEndpoint) string { + methodName := endpoint.Name + + // Generate parameter extraction + paramExtraction := "" + paramValidation := "" + pathParams := []string{} + + for _, param := range endpoint.Parameters { + if param.Location == "path" { + paramExtraction += "\t" + param.Name + ` := c.Param("` + param.Name + `")` + "\n" + pathParams = append(pathParams, param.Name) + + if param.Required { + paramValidation += ` + if ` + param.Name + ` == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "` + strings.Title(param.Name) + ` parameter is required", + "message": "` + param.Description + ` tidak boleh kosong", + }) + return + } +` + } + + // Add date validation if parameter contains date + if strings.Contains(param.Name, "tgl") || strings.Contains(param.Name, "date") { + paramValidation += ` + // Validate date format + if _, err := time.Parse("2006-01-02", ` + param.Name + `); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid date format", + "message": "Format tanggal harus yyyy-MM-dd", + }) + return + } +` + } + } + } + + // Generate endpoint URL building + endpointURL := endpoint.Path + if len(pathParams) > 0 { + endpointURL = `fmt.Sprintf("` + endpoint.Path + `", ` + strings.Join(pathParams, ", ") + `)` + } else { + endpointURL = `"` + endpoint.Path + `"` + } + + // Generate route path for documentation + routePath := endpoint.Path + for _, param := range endpoint.Parameters { + if param.Location == "path" { + routePath = strings.Replace(routePath, "%s", "{"+param.Name+"}", 1) + } + } + + // Generate category prefix for router documentation + categoryPrefix := "" + if data.Category != "" { + categoryPrefix = "/" + data.Category + } + + swaggerParams := generateSwaggerParams(endpoint.Parameters) + + return ` + +// ` + methodName + ` godoc +// @Summary ` + endpoint.Description + ` +// @Description ` + endpoint.Description + ` +// @Tags bpjs` + categoryPrefix + ` +// @Accept json +// @Produce json` + swaggerParams + ` +// @Success 200 {object} models.` + data.Name + `Response "Success response" +// @Failure 400 {object} map[string]interface{} "Bad request" +// @Failure 404 {object} map[string]interface{} "Data not found" +// @Failure 500 {object} map[string]interface{} "Internal server error" +// @Router /api/v1/bpjs` + categoryPrefix + routePath + ` [` + strings.ToLower(endpoint.Method) + `] +func (h *` + data.Name + `Handler) ` + methodName + `(c *gin.Context) { +` + paramExtraction + paramValidation + ` + // Create context with timeout + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + // Build endpoint URL + endpoint := ` + endpointURL + ` + + // Call BPJS service + var result map[string]interface{} + if err := h.bpjsService.Get(ctx, endpoint, &result); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to fetch ` + data.NameLower + ` data", + "message": err.Error(), + }) + return + } + + // Return successful response + c.JSON(http.StatusOK, gin.H{ + "message": "Data ` + data.NameLower + ` berhasil diambil", + "data": result, + }) +} +` +} + +func generateSwaggerParams(params []EndpointParam) string { + result := "" + for _, param := range params { + required := "false" + if param.Required { + required = "true" + } + result += ` +// @Param ` + param.Name + ` ` + param.Location + ` ` + param.Type + ` ` + required + ` "` + param.Description + `"` + } + return result +} + +func generateBpjsHelperMethods(data BpjsHandlerData) string { + handlerName := data.Name + + return ` + +// Helper methods for error handling and response formatting + +// handleBPJSError handles BPJS service errors and returns appropriate HTTP responses +func (h *` + handlerName + `Handler) handleBPJSError(c *gin.Context, err error, operation string) { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": fmt.Sprintf("Failed to %s", operation), + "message": err.Error(), + }) +} + +// validateDateFormat validates if the date string is in yyyy-MM-dd format +func (h *` + handlerName + `Handler) validateDateFormat(dateStr string) error { + _, err := time.Parse("2006-01-02", dateStr) + return err +} + +// buildSuccessResponse builds a standardized success response +func (h *` + handlerName + `Handler) buildSuccessResponse(message string, data interface{}) gin.H { + return gin.H{ + "message": message, + "data": data, + } +} +` +} + +// ================= BPJS MODEL GENERATION ===================== + +func generateBpjsModelFile(data BpjsHandlerData, modelDir string) { + modelContent := `package models + +// ` + data.Name + `Response represents the response structure for BPJS ` + data.NameLower + ` data +type ` + data.Name + `Response struct { + Message string ` + "`json:\"message\"`" + ` + Data map[string]interface{} ` + "`json:\"data\"`" + ` +} + +// ` + data.Name + `RawResponse represents the raw response structure from BPJS API +type ` + data.Name + `RawResponse struct { + MetaData struct { + Code string ` + "`json:\"code\"`" + ` + Message string ` + "`json:\"message\"`" + ` + } ` + "`json:\"metaData\"`" + ` + Response interface{} ` + "`json:\"response\"`" + ` +} +` + + // Add entity-specific models + switch data.NameLower { + case "peserta": + modelContent += generatePesertaModels() + case "poli": + modelContent += generatePoliModels() + case "diagnosa": + modelContent += generateDiagnosaModels() + case "provider": + modelContent += generateProviderModels() + } + + // Add common request/response models + modelContent += generateCommonBpjsModels(data) + + writeFile(filepath.Join(modelDir, data.NameLower+".go"), modelContent) +} + +func generatePesertaModels() string { + return ` + +// PesertaRequest represents the request structure for BPJS participant search +type PesertaRequest struct { + NIK string ` + "`json:\"nik\" binding:\"required\"`" + ` + TglSEP string ` + "`json:\"tglSEP\" binding:\"required\"`" + ` +} + +// PesertaData represents the participant data structure +type PesertaData struct { + NoKartu string ` + "`json:\"noKartu\"`" + ` + NIK string ` + "`json:\"nik\"`" + ` + Nama string ` + "`json:\"nama\"`" + ` + Pisa string ` + "`json:\"pisa\"`" + ` + Sex string ` + "`json:\"sex\"`" + ` + TglLahir string ` + "`json:\"tglLahir\"`" + ` + Pob string ` + "`json:\"pob\"`" + ` + KdProvider string ` + "`json:\"kdProvider\"`" + ` + NmProvider string ` + "`json:\"nmProvider\"`" + ` + KelasRawat string ` + "`json:\"kelasRawat\"`" + ` + Keterangan string ` + "`json:\"keterangan\"`" + ` + NoTelepon string ` + "`json:\"noTelepon\"`" + ` + Alamat string ` + "`json:\"alamat\"`" + ` + KdPos string ` + "`json:\"kdPos\"`" + ` + Pekerjaan string ` + "`json:\"pekerjaan\"`" + ` + StatusKawin string ` + "`json:\"statusKawin\"`" + ` + TglCetakKartu string ` + "`json:\"tglCetakKartu\"`" + ` + TglTAT string ` + "`json:\"tglTAT\"`" + ` + TglTMT string ` + "`json:\"tglTMT\"`" + ` + ProvUmum struct { + KdProvider string ` + "`json:\"kdProvider\"`" + ` + NmProvider string ` + "`json:\"nmProvider\"`" + ` + } ` + "`json:\"provUmum\"`" + ` + JenisPeserta struct { + KdJenisPeserta string ` + "`json:\"kdJenisPeserta\"`" + ` + NmJenisPeserta string ` + "`json:\"nmJenisPeserta\"`" + ` + } ` + "`json:\"jenisPeserta\"`" + ` + KelasTanggungan struct { + KdKelas string ` + "`json:\"kdKelas\"`" + ` + NmKelas string ` + "`json:\"nmKelas\"`" + ` + } ` + "`json:\"kelasTanggungan\"`" + ` + Informasi struct { + Dinsos string ` + "`json:\"dinsos\"`" + ` + NoSKTM string ` + "`json:\"noSKTM\"`" + ` + ProlanisPRB string ` + "`json:\"prolanisPRB\"`" + ` + } ` + "`json:\"informasi\"`" + ` + Cob struct { + NoAsuransi string ` + "`json:\"noAsuransi\"`" + ` + NmAsuransi string ` + "`json:\"nmAsuransi\"`" + ` + TglTAT string ` + "`json:\"tglTAT\"`" + ` + TglTMT string ` + "`json:\"tglTMT\"`" + ` + } ` + "`json:\"cob\"`" + ` + HakKelas struct { + Kode string ` + "`json:\"kode\"`" + ` + Nama string ` + "`json:\"nama\"`" + ` + } ` + "`json:\"hakKelas\"`" + ` + Mr struct { + NoMR string ` + "`json:\"noMR\"`" + ` + NoTelepon string ` + "`json:\"noTelepon\"`" + ` + } ` + "`json:\"mr\"`" + ` + ProvRujuk struct { + KdProvider string ` + "`json:\"kdProvider\"`" + ` + NmProvider string ` + "`json:\"nmProvider\"`" + ` + } ` + "`json:\"provRujuk\"`" + ` + StatusPeserta struct { + Kode string ` + "`json:\"kode\"`" + ` + Nama string ` + "`json:\"nama\"`" + ` + } ` + "`json:\"statusPeserta\"`" + ` +}` +} + +func generatePoliModels() string { + return ` + +// PoliData represents the poli reference data structure +type PoliData struct { + KdPoli string ` + "`json:\"kdPoli\"`" + ` + NmPoli string ` + "`json:\"nmPoli\"`" + ` +} + +// PoliListResponse represents the response structure for poli list +type PoliListResponse struct { + List []PoliData ` + "`json:\"list\"`" + ` +}` +} + +func generateDiagnosaModels() string { + return ` + +// DiagnosaData represents the diagnosa reference data structure +type DiagnosaData struct { + KdDiag string ` + "`json:\"kdDiag\"`" + ` + NmDiag string ` + "`json:\"nmDiag\"`" + ` +} + +// DiagnosaListResponse represents the response structure for diagnosa list +type DiagnosaListResponse struct { + Diagnosa []DiagnosaData ` + "`json:\"diagnosa\"`" + ` +}` +} + +func generateProviderModels() string { + return ` + +// ProviderData represents the provider reference data structure +type ProviderData struct { + KdProvider string ` + "`json:\"kdProvider\"`" + ` + NmProvider string ` + "`json:\"nmProvider\"`" + ` + JnsFaskes string ` + "`json:\"jnsFaskes\"`" + ` + Alamat string ` + "`json:\"alamat\"`" + ` + NoTelp string ` + "`json:\"noTelp\"`" + ` +} + +// ProviderListResponse represents the response structure for provider list +type ProviderListResponse struct { + Faskes []ProviderData ` + "`json:\"faskes\"`" + ` +}` +} + +func generateCommonBpjsModels(data BpjsHandlerData) string { + return ` + +// ErrorResponse represents error response structure +type ErrorResponse struct { + Error string ` + "`json:\"error\"`" + ` + Message string ` + "`json:\"message\"`" + ` + Code int ` + "`json:\"code,omitempty\"`" + ` +} + +// BPJSMetaData represents BPJS API metadata structure +type BPJSMetaData struct { + Code string ` + "`json:\"code\"`" + ` + Message string ` + "`json:\"message\"`" + ` +} + +// ` + data.Name + `Filter represents filter parameters for ` + data.NameLower + ` queries +type ` + data.Name + `Filter struct { + NIK *string ` + "`form:\"nik\" json:\"nik,omitempty\"`" + ` + TglSEP *string ` + "`form:\"tglSEP\" json:\"tglSEP,omitempty\"`" + ` +} +` +} + +// ================= ROUTES GENERATION ===================== + +func updateBpjsRoutesFile(data BpjsHandlerData) { + routesFile := "internal/routes/v1/routes.go" + content, err := os.ReadFile(routesFile) + if err != nil { + fmt.Printf("⚠️ Could not read routes.go: %v\n", err) + fmt.Printf("📝 Please manually add these routes to your routes.go file:\n") + printBpjsRoutesSample(data) + return + } + + routesContent := string(content) + + // Determine import path based on category + var importPath string + if data.Category != "" { + importPath = data.ModuleName + "/internal/handlers/bpjs/" + data.Category + } else { + importPath = data.ModuleName + "/internal/handlers/bpjs/" + data.NameLower + } + + // Add import + importPattern := `bpjs` + strings.Title(data.NameLower) + `Handlers "` + importPath + `"` + + if !strings.Contains(routesContent, importPattern) { + importToAdd := "\t" + `bpjs` + strings.Title(data.NameLower) + `Handlers "` + importPath + `"` + if strings.Contains(routesContent, "import (") { + routesContent = strings.Replace(routesContent, "import (", + "import (\n"+importToAdd, 1) + } + } + + // Generate routes + newRoutes := "\t\t// BPJS " + data.Name + " endpoints\n" + newRoutes += "\t\tbpjs" + strings.Title(data.NameLower) + "Handler := bpjs" + strings.Title(data.NameLower) + "Handlers.New" + data.Name + "Handler(cfg.Bpjs)\n" + + // Add routes based on endpoints + for _, endpoint := range data.Endpoints { + routePath := endpoint.Path + for _, param := range endpoint.Parameters { + if param.Location == "path" { + routePath = strings.Replace(routePath, "%s", ":"+param.Name, 1) + } + } + + methodName := endpoint.Name + routePrefix := "/bpjs" + if data.Category != "" { + routePrefix = "/bpjs/" + data.Category + } + + newRoutes += "\t\tv1." + strings.ToUpper(endpoint.Method) + `("` + routePrefix + routePath + `", bpjs` + strings.Title(data.NameLower) + "Handler." + methodName + ")\n" + } + + newRoutes += "\n" + insertMarker := "\t\tprotected := v1.Group(\"/\")" + if strings.Contains(routesContent, insertMarker) { + if !strings.Contains(routesContent, "New"+data.Name+"Handler") { + routesContent = strings.Replace(routesContent, insertMarker, + newRoutes+insertMarker, 1) + } else { + fmt.Printf("✅ Routes for BPJS %s already exist, skipping...\n", data.Name) + return + } + } + + if err := os.WriteFile(routesFile, []byte(routesContent), 0644); err != nil { + fmt.Printf("Error writing routes.go: %v\n", err) + return + } + + fmt.Printf("✅ Updated routes.go with BPJS %s endpoints\n", data.Name) +} + +func printBpjsRoutesSample(data BpjsHandlerData) { + fmt.Printf(` +// BPJS %s endpoints +bpjs%sHandler := bpjs%sHandlers.New%sHandler(cfg.Bpjs) +`, data.Name, strings.Title(data.NameLower), strings.Title(data.NameLower), data.Name) + + for _, endpoint := range data.Endpoints { + routePath := endpoint.Path + for _, param := range endpoint.Parameters { + if param.Location == "path" { + routePath = strings.Replace(routePath, "%s", ":"+param.Name, 1) + } + } + + routePrefix := "/bpjs" + if data.Category != "" { + routePrefix = "/bpjs/" + data.Category + } + + fmt.Printf("\tv1.%s(\"%s%s\", bpjs%sHandler.%s)\n", + strings.ToUpper(endpoint.Method), routePrefix, routePath, strings.Title(data.NameLower), endpoint.Name) + } + fmt.Println() +} + +// ================= UTILITY FUNCTIONS ===================== + +func writeFile(filename, content string) { + if err := os.WriteFile(filename, []byte(content), 0644); err != nil { + fmt.Printf("❌ Error creating file %s: %v\n", filename, err) + return + } + fmt.Printf("✅ Generated: %s\n", filename) +} diff --git a/tools/generate-handler.go b/tools/generate-handler.go index 242373f1..b3ac31f5 100644 --- a/tools/generate-handler.go +++ b/tools/generate-handler.go @@ -13,6 +13,8 @@ type HandlerData struct { Name string NameLower string NamePlural string + Category string + CategoryPath string ModuleName string TableName string HasGet bool @@ -27,12 +29,16 @@ type HandlerData struct { func main() { if len(os.Args) < 2 { - fmt.Println("Usage: go run generate-handler.go entity [methods]") - fmt.Println("Example: go run generate-handler.go order get post put delete stats") + fmt.Println("Usage: go run generate-handler.go [category/]entity [methods]") + fmt.Println("Examples:") + fmt.Println(" go run generate-handler.go product get post") + fmt.Println(" go run generate-handler.go reference/peserta get post put delete") + fmt.Println(" go run generate-handler.go master/wilayah get") os.Exit(1) } - entityName := strings.Title(os.Args[1]) // PascalCase entity name + // Parse entity path (could be "entity" or "category/entity") + entityPath := os.Args[1] methods := []string{} if len(os.Args) > 2 { methods = os.Args[2:] @@ -41,14 +47,40 @@ func main() { methods = []string{"get", "post", "put", "delete"} } + // Parse category and entity + var category, entityName string + if strings.Contains(entityPath, "/") { + parts := strings.Split(entityPath, "/") + if len(parts) != 2 { + fmt.Println("❌ Error: Invalid path format. Use 'category/entity' or just 'entity'") + os.Exit(1) + } + category = parts[0] + entityName = parts[1] + } else { + category = "" + entityName = entityPath + } + + // Format names + entityName = strings.Title(entityName) // PascalCase entity name entityLower := strings.ToLower(entityName) entityPlural := entityLower + "s" - tableName := "data_" + entityLower + + // Table name: include category if exists + var tableName string + if category != "" { + tableName = "data_" + category + "_" + entityLower + } else { + tableName = "data_" + entityLower + } data := HandlerData{ Name: entityName, NameLower: entityLower, NamePlural: entityPlural, + Category: category, + CategoryPath: category, ModuleName: "api-service", TableName: tableName, HasPagination: true, @@ -77,10 +109,20 @@ func main() { data.HasStats = true } - // Create directories - handlerDir := filepath.Join("internal", "handlers", entityLower) - modelDir := filepath.Join("internal", "models", entityLower) - + // Create directories with improved logic + var handlerDir, modelDir string + + if category != "" { + // Dengan kategori: internal/handlers/category/ + handlerDir = filepath.Join("internal", "handlers", category) + modelDir = filepath.Join("internal", "models", category) + } else { + // Tanpa kategori: langsung internal/handlers/ + handlerDir = filepath.Join("internal", "handlers") + modelDir = filepath.Join("internal", "models") + } + + // Buat direktori for _, d := range []string{handlerDir, modelDir} { if err := os.MkdirAll(d, 0755); err != nil { panic(err) @@ -93,20 +135,29 @@ func main() { updateRoutesFile(data) fmt.Printf("✅ Successfully generated handler: %s\n", entityName) + if category != "" { + fmt.Printf("📁 Category: %s\n", category) + } fmt.Printf("📁 Handler: %s\n", filepath.Join(handlerDir, entityLower+".go")) fmt.Printf("📁 Model: %s\n", filepath.Join(modelDir, entityLower+".go")) } // ================= HANDLER GENERATION ===================== - func generateHandlerFile(data HandlerData, handlerDir string) { - // FIXED: Proper string formatting + // Build import path based on category + var modelsImportPath string + if data.Category != "" { + modelsImportPath = data.ModuleName + "/internal/models/" + data.Category + } else { + modelsImportPath = data.ModuleName + "/internal/models" + } + handlerContent := `package handlers import ( "` + data.ModuleName + `/internal/config" "` + data.ModuleName + `/internal/database" - models "` + data.ModuleName + `/internal/models/` + data.NameLower + `" + models "` + modelsImportPath + `" "context" "database/sql" "fmt" @@ -123,8 +174,8 @@ import ( ) var ( - db database.Service - once sync.Once + db database.Service + once sync.Once validate *validator.Validate ) @@ -156,35 +207,47 @@ func New` + data.Name + `Handler() *` + data.Name + `Handler { return &` + data.Name + `Handler{ db: db, } -} -` +}` // Add methods if data.HasGet { handlerContent += generateGetMethods(data) } + if data.HasPost { handlerContent += generateCreateMethod(data) } + if data.HasPut { handlerContent += generateUpdateMethod(data) } + if data.HasDelete { handlerContent += generateDeleteMethod(data) } + if data.HasStats { handlerContent += generateStatsMethod(data) } // Add helper methods handlerContent += generateHelperMethods(data) - writeFile(filepath.Join(handlerDir, data.NameLower+".go"), handlerContent) } func generateGetMethods(data HandlerData) string { - // FIXED: Proper formatting without printf placeholders + // Build route path based on category + var routePath, singleRoutePath string + if data.Category != "" { + routePath = data.Category + "/" + data.NamePlural + singleRoutePath = data.Category + "/" + data.NameLower + } else { + routePath = data.NamePlural + singleRoutePath = data.NameLower + } + return ` + // Get` + data.Name + ` godoc // @Summary Get ` + data.NameLower + ` with pagination and optional aggregation // @Description Returns a paginated list of ` + data.NamePlural + ` with optional summary statistics @@ -199,7 +262,7 @@ func generateGetMethods(data HandlerData) string { // @Success 200 {object} models.` + data.Name + `GetResponse "Success response" // @Failure 400 {object} models.ErrorResponse "Bad request" // @Failure 500 {object} models.ErrorResponse "Internal server error" -// @Router /api/v1/` + data.NamePlural + ` [get] +// @Router /api/v1/` + routePath + ` [get] func (h *` + data.Name + `Handler) Get` + data.Name + `(c *gin.Context) { // Parse pagination parameters limit, offset, err := h.parsePaginationParams(c) @@ -225,12 +288,12 @@ func (h *` + data.Name + `Handler) Get` + data.Name + `(c *gin.Context) { // Execute concurrent operations var ( - items []models.` + data.Name + ` - total int + items []models.` + data.Name + ` + total int aggregateData *models.AggregateData - wg sync.WaitGroup - errChan = make(chan error, 3) - mu sync.Mutex + wg sync.WaitGroup + errChan = make(chan error, 3) + mu sync.Mutex ) // Fetch total count @@ -244,7 +307,7 @@ func (h *` + data.Name + `Handler) Get` + data.Name + `(c *gin.Context) { } }() - // Fetch main data - FIXED: Proper method name + // Fetch main data wg.Add(1) go func() { defer wg.Done() @@ -290,8 +353,8 @@ func (h *` + data.Name + `Handler) Get` + data.Name + `(c *gin.Context) { meta := h.calculateMeta(limit, offset, total) response := models.` + data.Name + `GetResponse{ Message: "Data ` + data.NameLower + ` berhasil diambil", - Data: items, - Meta: meta, + Data: items, + Meta: meta, } if includeAggregation && aggregateData != nil { @@ -312,7 +375,7 @@ func (h *` + data.Name + `Handler) Get` + data.Name + `(c *gin.Context) { // @Failure 400 {object} models.ErrorResponse "Invalid ID format" // @Failure 404 {object} models.ErrorResponse "` + data.Name + ` not found" // @Failure 500 {object} models.ErrorResponse "Internal server error" -// @Router /api/v1/` + data.NameLower + `/{id} [get] +// @Router /api/v1/` + singleRoutePath + `/{id} [get] func (h *` + data.Name + `Handler) Get` + data.Name + `ByID(c *gin.Context) { id := c.Param("id") @@ -343,16 +406,23 @@ func (h *` + data.Name + `Handler) Get` + data.Name + `ByID(c *gin.Context) { response := models.` + data.Name + `GetByIDResponse{ Message: "` + data.Name + ` details retrieved successfully", - Data: item, + Data: item, } c.JSON(http.StatusOK, response) -} -` +}` } func generateCreateMethod(data HandlerData) string { + var routePath string + if data.Category != "" { + routePath = data.Category + "/" + data.NamePlural + } else { + routePath = data.NamePlural + } + return ` + // Create` + data.Name + ` godoc // @Summary Create ` + data.NameLower + ` // @Description Creates a new ` + data.NameLower + ` record @@ -363,9 +433,10 @@ func generateCreateMethod(data HandlerData) string { // @Success 201 {object} models.` + data.Name + `CreateResponse "` + data.Name + ` created successfully" // @Failure 400 {object} models.ErrorResponse "Bad request or validation error" // @Failure 500 {object} models.ErrorResponse "Internal server error" -// @Router /api/v1/` + data.NamePlural + ` [post] +// @Router /api/v1/` + routePath + ` [post] func (h *` + data.Name + `Handler) Create` + data.Name + `(c *gin.Context) { var req models.` + data.Name + `CreateRequest + if err := c.ShouldBindJSON(&req); err != nil { h.respondError(c, "Invalid request body", err, http.StatusBadRequest) return @@ -394,16 +465,23 @@ func (h *` + data.Name + `Handler) Create` + data.Name + `(c *gin.Context) { response := models.` + data.Name + `CreateResponse{ Message: "` + data.Name + ` berhasil dibuat", - Data: item, + Data: item, } c.JSON(http.StatusCreated, response) -} -` +}` } func generateUpdateMethod(data HandlerData) string { + var singleRoutePath string + if data.Category != "" { + singleRoutePath = data.Category + "/" + data.NameLower + } else { + singleRoutePath = data.NameLower + } + return ` + // Update` + data.Name + ` godoc // @Summary Update ` + data.NameLower + ` // @Description Updates an existing ` + data.NameLower + ` record @@ -416,7 +494,7 @@ func generateUpdateMethod(data HandlerData) string { // @Failure 400 {object} models.ErrorResponse "Bad request or validation error" // @Failure 404 {object} models.ErrorResponse "` + data.Name + ` not found" // @Failure 500 {object} models.ErrorResponse "Internal server error" -// @Router /api/v1/` + data.NameLower + `/{id} [put] +// @Router /api/v1/` + singleRoutePath + `/{id} [put] func (h *` + data.Name + `Handler) Update` + data.Name + `(c *gin.Context) { id := c.Param("id") @@ -462,16 +540,23 @@ func (h *` + data.Name + `Handler) Update` + data.Name + `(c *gin.Context) { response := models.` + data.Name + `UpdateResponse{ Message: "` + data.Name + ` berhasil diperbarui", - Data: item, + Data: item, } c.JSON(http.StatusOK, response) -} -` +}` } func generateDeleteMethod(data HandlerData) string { + var singleRoutePath string + if data.Category != "" { + singleRoutePath = data.Category + "/" + data.NameLower + } else { + singleRoutePath = data.NameLower + } + return ` + // Delete` + data.Name + ` godoc // @Summary Delete ` + data.NameLower + ` // @Description Soft deletes a ` + data.NameLower + ` by setting status to 'deleted' @@ -483,7 +568,7 @@ func generateDeleteMethod(data HandlerData) string { // @Failure 400 {object} models.ErrorResponse "Invalid ID format" // @Failure 404 {object} models.ErrorResponse "` + data.Name + ` not found" // @Failure 500 {object} models.ErrorResponse "Internal server error" -// @Router /api/v1/` + data.NameLower + `/{id} [delete] +// @Router /api/v1/` + singleRoutePath + `/{id} [delete] func (h *` + data.Name + `Handler) Delete` + data.Name + `(c *gin.Context) { id := c.Param("id") @@ -514,16 +599,23 @@ func (h *` + data.Name + `Handler) Delete` + data.Name + `(c *gin.Context) { response := models.` + data.Name + `DeleteResponse{ Message: "` + data.Name + ` berhasil dihapus", - ID: id, + ID: id, } c.JSON(http.StatusOK, response) -} -` +}` } func generateStatsMethod(data HandlerData) string { + var routePath string + if data.Category != "" { + routePath = data.Category + "/" + data.NamePlural + } else { + routePath = data.NamePlural + } + return ` + // Get` + data.Name + `Stats godoc // @Summary Get ` + data.NameLower + ` statistics // @Description Returns comprehensive statistics about ` + data.NameLower + ` data @@ -533,7 +625,7 @@ func generateStatsMethod(data HandlerData) string { // @Param status query string false "Filter statistics by status" // @Success 200 {object} models.AggregateData "Statistics data" // @Failure 500 {object} models.ErrorResponse "Internal server error" -// @Router /api/v1/` + data.NamePlural + `/stats [get] +// @Router /api/v1/` + routePath + `/stats [get] func (h *` + data.Name + `Handler) Get` + data.Name + `Stats(c *gin.Context) { dbConn, err := h.db.GetDB("satudata") if err != nil { @@ -553,15 +645,14 @@ func (h *` + data.Name + `Handler) Get` + data.Name + `Stats(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "Statistik ` + data.NameLower + ` berhasil diambil", - "data": aggregateData, + "data": aggregateData, }) -} -` +}` } func generateHelperMethods(data HandlerData) string { - // FIXED: All method names and types properly formatted return ` + // Database operations func (h *` + data.Name + `Handler) get` + data.Name + `ByID(ctx context.Context, dbConn *sql.DB, id string) (*models.` + data.Name + `, error) { query := "SELECT id, status, date_created, date_updated, name FROM ` + data.TableName + ` WHERE id = $1 AND status != 'deleted'" @@ -609,6 +700,7 @@ func (h *` + data.Name + `Handler) update` + data.Name + `(ctx context.Context, func (h *` + data.Name + `Handler) delete` + data.Name + `(ctx context.Context, dbConn *sql.DB, id string) error { now := time.Now() + query := "UPDATE ` + data.TableName + ` SET status = 'deleted', date_updated = $2 WHERE id = $1 AND status != 'deleted'" result, err := dbConn.ExecContext(ctx, query, id, now) if err != nil { @@ -630,8 +722,8 @@ func (h *` + data.Name + `Handler) delete` + data.Name + `(ctx context.Context, func (h *` + data.Name + `Handler) fetch` + data.Name + `s(ctx context.Context, dbConn *sql.DB, filter models.` + data.Name + `Filter, limit, offset int) ([]models.` + data.Name + `, error) { whereClause, args := h.buildWhereClause(filter) query := fmt.Sprintf("SELECT id, status, date_created, date_updated, name FROM ` + data.TableName + ` WHERE %s ORDER BY date_created DESC NULLS LAST LIMIT $%d OFFSET $%d", whereClause, len(args)+1, len(args)+2) - args = append(args, limit, offset) + rows, err := dbConn.QueryContext(ctx, query, args...) if err != nil { return nil, fmt.Errorf("fetch ` + data.NamePlural + ` query failed: %w", err) @@ -658,9 +750,11 @@ func (h *` + data.Name + `Handler) fetch` + data.Name + `s(ctx context.Context, func (h *` + data.Name + `Handler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter models.` + data.Name + `Filter, total *int) error { whereClause, args := h.buildWhereClause(filter) countQuery := fmt.Sprintf("SELECT COUNT(*) FROM ` + data.TableName + ` WHERE %s", whereClause) + if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil { return fmt.Errorf("total count query failed: %w", err) } + return nil } @@ -671,7 +765,7 @@ func (h *` + data.Name + `Handler) getAggregateData(ctx context.Context, dbConn whereClause, args := h.buildWhereClause(filter) statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM ` + data.TableName + ` WHERE %s GROUP BY status ORDER BY status", whereClause) - + rows, err := dbConn.QueryContext(ctx, statusQuery, args...) if err != nil { return nil, fmt.Errorf("status query failed: %w", err) @@ -684,8 +778,8 @@ func (h *` + data.Name + `Handler) getAggregateData(ctx context.Context, dbConn if err := rows.Scan(&status, &count); err != nil { return nil, fmt.Errorf("status scan failed: %w", err) } - aggregate.ByStatus[status] = count + aggregate.ByStatus[status] = count switch status { case "active": aggregate.TotalActive = count @@ -712,9 +806,9 @@ func (h *` + data.Name + `Handler) respondError(c *gin.Context, message string, } c.JSON(statusCode, models.ErrorResponse{ - Error: errorMessage, - Code: statusCode, - Message: err.Error(), + Error: errorMessage, + Code: statusCode, + Message: err.Error(), Timestamp: time.Now(), }) } @@ -728,12 +822,15 @@ func (h *` + data.Name + `Handler) parsePaginationParams(c *gin.Context) (int, i if err != nil { return 0, 0, fmt.Errorf("invalid limit parameter: %s", limitStr) } + if parsedLimit <= 0 { return 0, 0, fmt.Errorf("limit must be greater than 0") } + if parsedLimit > 100 { return 0, 0, fmt.Errorf("limit cannot exceed 100") } + limit = parsedLimit } @@ -742,9 +839,11 @@ func (h *` + data.Name + `Handler) parsePaginationParams(c *gin.Context) (int, i if err != nil { return 0, 0, fmt.Errorf("invalid offset parameter: %s", offsetStr) } + if parsedOffset < 0 { return 0, 0, fmt.Errorf("offset cannot be negative") } + offset = parsedOffset } @@ -817,26 +916,25 @@ func (h *` + data.Name + `Handler) buildWhereClause(filter models.` + data.Name func (h *` + data.Name + `Handler) calculateMeta(limit, offset, total int) models.MetaResponse { totalPages := 0 currentPage := 1 + if limit > 0 { totalPages = (total + limit - 1) / limit // Ceiling division currentPage = (offset / limit) + 1 } return models.MetaResponse{ - Limit: limit, - Offset: offset, - Total: total, - TotalPages: totalPages, + Limit: limit, + Offset: offset, + Total: total, + TotalPages: totalPages, CurrentPage: currentPage, - HasNext: offset+limit < total, - HasPrev: offset > 0, + HasNext: offset+limit < total, + HasPrev: offset > 0, } -} -` +}` } // ================= MODEL GENERATION ===================== - func generateModelFile(data HandlerData, modelDir string) { modelContent := `package models @@ -850,7 +948,7 @@ import ( // NullableInt32 is a custom type to replace sql.NullInt32 for swagger compatibility type NullableInt32 struct { Int32 int32 ` + "`json:\"int32,omitempty\"`" + ` - Valid bool ` + "`json:\"valid\"`" + ` + Valid bool ` + "`json:\"valid\"`" + ` } // Scan implements the sql.Scanner interface for NullableInt32 @@ -859,6 +957,7 @@ func (n *NullableInt32) Scan(value interface{}) error { if err := ni.Scan(value); err != nil { return err } + n.Int32 = ni.Int32 n.Valid = ni.Valid return nil @@ -869,31 +968,32 @@ func (n NullableInt32) Value() (driver.Value, error) { if !n.Valid { return nil, nil } + return n.Int32, nil } // ` + data.Name + ` represents the data structure for the ` + data.NameLower + ` table type ` + data.Name + ` struct { - ID string ` + "`json:\"id\" db:\"id\"`" + ` - Status string ` + "`json:\"status\" db:\"status\"`" + ` - Sort NullableInt32 ` + "`json:\"sort,omitempty\" db:\"sort\"`" + ` + ID string ` + "`json:\"id\" db:\"id\"`" + ` + Status string ` + "`json:\"status\" db:\"status\"`" + ` + Sort NullableInt32 ` + "`json:\"sort,omitempty\" db:\"sort\"`" + ` UserCreated sql.NullString ` + "`json:\"user_created,omitempty\" db:\"user_created\"`" + ` - DateCreated sql.NullTime ` + "`json:\"date_created,omitempty\" db:\"date_created\"`" + ` + DateCreated sql.NullTime ` + "`json:\"date_created,omitempty\" db:\"date_created\"`" + ` UserUpdated sql.NullString ` + "`json:\"user_updated,omitempty\" db:\"user_updated\"`" + ` - DateUpdated sql.NullTime ` + "`json:\"date_updated,omitempty\" db:\"date_updated\"`" + ` - Name sql.NullString ` + "`json:\"name,omitempty\" db:\"name\"`" + ` + DateUpdated sql.NullTime ` + "`json:\"date_updated,omitempty\" db:\"date_updated\"`" + ` + Name sql.NullString ` + "`json:\"name,omitempty\" db:\"name\"`" + ` } // Custom JSON marshaling for ` + data.Name + ` func (r ` + data.Name + `) MarshalJSON() ([]byte, error) { type Alias ` + data.Name + ` aux := &struct { - Sort *int ` + "`json:\"sort,omitempty\"`" + ` - UserCreated *string ` + "`json:\"user_created,omitempty\"`" + ` + Sort *int ` + "`json:\"sort,omitempty\"`" + ` + UserCreated *string ` + "`json:\"user_created,omitempty\"`" + ` DateCreated *time.Time ` + "`json:\"date_created,omitempty\"`" + ` - UserUpdated *string ` + "`json:\"user_updated,omitempty\"`" + ` + UserUpdated *string ` + "`json:\"user_updated,omitempty\"`" + ` DateUpdated *time.Time ` + "`json:\"date_updated,omitempty\"`" + ` - Name *string ` + "`json:\"name,omitempty\"`" + ` + Name *string ` + "`json:\"name,omitempty\"`" + ` *Alias }{ Alias: (*Alias)(&r), @@ -903,18 +1003,23 @@ func (r ` + data.Name + `) MarshalJSON() ([]byte, error) { sort := int(r.Sort.Int32) aux.Sort = &sort } + if r.UserCreated.Valid { aux.UserCreated = &r.UserCreated.String } + if r.DateCreated.Valid { aux.DateCreated = &r.DateCreated.Time } + if r.UserUpdated.Valid { aux.UserUpdated = &r.UserUpdated.String } + if r.DateUpdated.Valid { aux.DateUpdated = &r.DateUpdated.Time } + if r.Name.Valid { aux.Name = &r.Name.String } @@ -928,117 +1033,117 @@ func (r *` + data.Name + `) GetName() string { return r.Name.String } return "" -} -` +}` - // Add request/response structs + // Add request/response structs based on enabled methods if data.HasGet { modelContent += ` + // Response struct for GET by ID type ` + data.Name + `GetByIDResponse struct { Message string ` + "`json:\"message\"`" + ` - Data *` + data.Name + ` ` + "`json:\"data\"`" + ` + Data *` + data.Name + ` ` + "`json:\"data\"`" + ` } // Enhanced GET response with pagination and aggregation type ` + data.Name + `GetResponse struct { - Message string ` + "`json:\"message\"`" + ` - Data []` + data.Name + ` ` + "`json:\"data\"`" + ` - Meta MetaResponse ` + "`json:\"meta\"`" + ` + Message string ` + "`json:\"message\"`" + ` + Data []` + data.Name + ` ` + "`json:\"data\"`" + ` + Meta MetaResponse ` + "`json:\"meta\"`" + ` Summary *AggregateData ` + "`json:\"summary,omitempty\"`" + ` -} -` +}` } if data.HasPost { modelContent += ` + // Request struct for create type ` + data.Name + `CreateRequest struct { - Status string ` + "`json:\"status\" validate:\"required,oneof=draft active inactive\"`" + ` - Name *string ` + "`json:\"name,omitempty\" validate:\"omitempty,min=1,max=255\"`" + ` + Status string ` + "`json:\"status\" validate:\"required,oneof=draft active inactive\"`" + ` + Name *string ` + "`json:\"name,omitempty\" validate:\"omitempty,min=1,max=255\"`" + ` } // Response struct for create type ` + data.Name + `CreateResponse struct { Message string ` + "`json:\"message\"`" + ` - Data *` + data.Name + ` ` + "`json:\"data\"`" + ` -} -` + Data *` + data.Name + ` ` + "`json:\"data\"`" + ` +}` } if data.HasPut { modelContent += ` + // Update request type ` + data.Name + `UpdateRequest struct { - ID string ` + "`json:\"-\" validate:\"required,uuid4\"`" + ` // ID dari URL path - Status string ` + "`json:\"status\" validate:\"required,oneof=draft active inactive\"`" + ` - Name *string ` + "`json:\"name,omitempty\" validate:\"omitempty,min=1,max=255\"`" + ` + ID string ` + "`json:\"-\" validate:\"required,uuid4\"`" + ` // ID dari URL path + Status string ` + "`json:\"status\" validate:\"required,oneof=draft active inactive\"`" + ` + Name *string ` + "`json:\"name,omitempty\" validate:\"omitempty,min=1,max=255\"`" + ` } // Response struct for update type ` + data.Name + `UpdateResponse struct { Message string ` + "`json:\"message\"`" + ` - Data *` + data.Name + ` ` + "`json:\"data\"`" + ` -} -` + Data *` + data.Name + ` ` + "`json:\"data\"`" + ` +}` } if data.HasDelete { modelContent += ` + // Response struct for delete type ` + data.Name + `DeleteResponse struct { Message string ` + "`json:\"message\"`" + ` - ID string ` + "`json:\"id\"`" + ` -} -` + ID string ` + "`json:\"id\"`" + ` +}` } // Add common structs modelContent += ` + // Metadata for pagination type MetaResponse struct { - Limit int ` + "`json:\"limit\"`" + ` - Offset int ` + "`json:\"offset\"`" + ` - Total int ` + "`json:\"total\"`" + ` - TotalPages int ` + "`json:\"total_pages\"`" + ` - CurrentPage int ` + "`json:\"current_page\"`" + ` - HasNext bool ` + "`json:\"has_next\"`" + ` - HasPrev bool ` + "`json:\"has_prev\"`" + ` + Limit int ` + "`json:\"limit\"`" + ` + Offset int ` + "`json:\"offset\"`" + ` + Total int ` + "`json:\"total\"`" + ` + TotalPages int ` + "`json:\"total_pages\"`" + ` + CurrentPage int ` + "`json:\"current_page\"`" + ` + HasNext bool ` + "`json:\"has_next\"`" + ` + HasPrev bool ` + "`json:\"has_prev\"`" + ` } // Aggregate data for summary type AggregateData struct { - TotalActive int ` + "`json:\"total_active\"`" + ` - TotalDraft int ` + "`json:\"total_draft\"`" + ` - TotalInactive int ` + "`json:\"total_inactive\"`" + ` - ByStatus map[string]int ` + "`json:\"by_status\"`" + ` - LastUpdated *time.Time ` + "`json:\"last_updated,omitempty\"`" + ` - CreatedToday int ` + "`json:\"created_today\"`" + ` - UpdatedToday int ` + "`json:\"updated_today\"`" + ` + TotalActive int ` + "`json:\"total_active\"`" + ` + TotalDraft int ` + "`json:\"total_draft\"`" + ` + TotalInactive int ` + "`json:\"total_inactive\"`" + ` + ByStatus map[string]int ` + "`json:\"by_status\"`" + ` + LastUpdated *time.Time ` + "`json:\"last_updated,omitempty\"`" + ` + CreatedToday int ` + "`json:\"created_today\"`" + ` + UpdatedToday int ` + "`json:\"updated_today\"`" + ` } // Error response type ErrorResponse struct { - Error string ` + "`json:\"error\"`" + ` - Code int ` + "`json:\"code\"`" + ` - Message string ` + "`json:\"message\"`" + ` + Error string ` + "`json:\"error\"`" + ` + Code int ` + "`json:\"code\"`" + ` + Message string ` + "`json:\"message\"`" + ` Timestamp time.Time ` + "`json:\"timestamp\"`" + ` } // Filter struct for query parameters type ` + data.Name + `Filter struct { - Status *string ` + "`json:\"status,omitempty\" form:\"status\"`" + ` - Search *string ` + "`json:\"search,omitempty\" form:\"search\"`" + ` + Status *string ` + "`json:\"status,omitempty\" form:\"status\"`" + ` + Search *string ` + "`json:\"search,omitempty\" form:\"search\"`" + ` DateFrom *time.Time ` + "`json:\"date_from,omitempty\" form:\"date_from\"`" + ` - DateTo *time.Time ` + "`json:\"date_to,omitempty\" form:\"date_to\"`" + ` + DateTo *time.Time ` + "`json:\"date_to,omitempty\" form:\"date_to\"`" + ` } // Validation constants const ( - StatusDraft = "draft" - StatusActive = "active" + StatusDraft = "draft" + StatusActive = "active" StatusInactive = "inactive" - StatusDeleted = "deleted" + StatusDeleted = "deleted" ) // ValidStatuses for validation @@ -1052,41 +1157,13 @@ func IsValidStatus(status string) bool { } } return false -} -` +}` writeFile(filepath.Join(modelDir, data.NameLower+".go"), modelContent) } // ================= ROUTES GENERATION ===================== - func updateRoutesFile(data HandlerData) { - // routesFile := "internal/routes/v1/routes.go" - // content, err := os.ReadFile(routesFile) - // if err != nil { - // fmt.Printf("⚠️ Could not read routes.go: %v\n", err) - // fmt.Printf("📝 Please manually add these routes to your routes.go file:\n") - // printRoutesSample(data) - // return - // } - - // routesContent := string(content) - - // // Add import - // importPattern := data.NameLower + `Handlers "` + data.ModuleName + `/internal/handlers/` + data.NameLower + `"` - // if !strings.Contains(routesContent, importPattern) { - // fmt.Printf("⚠️ Please add this import to your routes.go file:\n") - // fmt.Printf("import %sHandlers \"%s/internal/handlers/%s\"\n\n", data.NameLower, data.ModuleName, data.NameLower) - // } - - // // Check if routes already exist - // if strings.Contains(routesContent, fmt.Sprintf("New%sHandler", data.Name)) { - // fmt.Printf("⚠️ Routes for %s already exist, skipping...\n", data.Name) - // return - // } - - // fmt.Printf("📝 Please manually add these routes to your routes.go file:\n") - // printRoutesSample(data) routesFile := "internal/routes/v1/routes.go" content, err := os.ReadFile(routesFile) if err != nil { @@ -1095,43 +1172,71 @@ func updateRoutesFile(data HandlerData) { printRoutesSample(data) return } + routesContent := string(content) + // Build import path - PERBAIKAN UTAMA + var importPath, importAlias string + if data.Category != "" { + importPath = fmt.Sprintf("%s/internal/handlers/%s", data.ModuleName, data.Category) + importAlias = data.NameLower + "Handlers" + } else { + importPath = fmt.Sprintf("%s/internal/handlers", data.ModuleName) + importAlias = data.NameLower + "Handlers" + } + // import - importPattern := fmt.Sprintf("%sHandlers \"%s/internal/handlers/%s\"", - data.NameLower, data.ModuleName, data.NameLower) + importPattern := fmt.Sprintf("%s \"%s\"", importAlias, importPath) if !strings.Contains(routesContent, importPattern) { - importToAdd := fmt.Sprintf("\t%sHandlers \"%s/internal/handlers/%s\"", - data.NameLower, data.ModuleName, data.NameLower) + importToAdd := fmt.Sprintf("\t%s \"%s\"", importAlias, importPath) if strings.Contains(routesContent, "import (") { routesContent = strings.Replace(routesContent, "import (", "import (\n"+importToAdd, 1) } } + // Build route paths + var routesPath, singleRoutePath string + if data.Category != "" { + routesPath = data.Category + "/" + data.NamePlural + singleRoutePath = data.Category + "/" + data.NameLower + } else { + routesPath = data.NamePlural + singleRoutePath = data.NameLower + } + // routes newRoutes := fmt.Sprintf("\t\t// %s endpoints\n", data.Name) - newRoutes += fmt.Sprintf("\t\t%sHandler := %sHandlers.New%sHandler()\n", - data.NameLower, data.NameLower, data.Name) + newRoutes += fmt.Sprintf("\t\t%sHandler := %s.New%sHandler()\n", + data.NameLower, importAlias, data.Name) if data.HasGet { newRoutes += fmt.Sprintf("\t\tv1.GET(\"/%s\", %sHandler.Get%s)\n", - data.NamePlural, data.NameLower, data.Name) + routesPath, data.NameLower, data.Name) newRoutes += fmt.Sprintf("\t\tv1.GET(\"/%s/:id\", %sHandler.Get%sByID)\n", - data.NameLower, data.NameLower, data.Name) + singleRoutePath, data.NameLower, data.Name) } + if data.HasPost { newRoutes += fmt.Sprintf("\t\tv1.POST(\"/%s\", %sHandler.Create%s)\n", - data.NamePlural, data.NameLower, data.Name) + routesPath, data.NameLower, data.Name) } + if data.HasPut { newRoutes += fmt.Sprintf("\t\tv1.PUT(\"/%s/:id\", %sHandler.Update%s)\n", - data.NameLower, data.NameLower, data.Name) + singleRoutePath, data.NameLower, data.Name) } + if data.HasDelete { newRoutes += fmt.Sprintf("\t\tv1.DELETE(\"/%s/:id\", %sHandler.Delete%s)\n", - data.NameLower, data.NameLower, data.Name) + singleRoutePath, data.NameLower, data.Name) } + + if data.HasStats { + newRoutes += fmt.Sprintf("\t\tv1.GET(\"/%s/stats\", %sHandler.Get%sStats)\n", + routesPath, data.NameLower, data.Name) + } + newRoutes += "\n" insertMarker := "\t\tprotected := v1.Group(\"/\")" @@ -1141,6 +1246,7 @@ func updateRoutesFile(data HandlerData) { newRoutes+insertMarker, 1) } else { fmt.Printf("✅ Routes for %s already exist, skipping...\n", data.Name) + return } } @@ -1148,36 +1254,57 @@ func updateRoutesFile(data HandlerData) { fmt.Printf("Error writing routes.go: %v\n", err) return } + fmt.Printf("✅ Updated routes.go with %s endpoints\n", data.Name) } func printRoutesSample(data HandlerData) { + var routesPath, singleRoutePath string + if data.Category != "" { + routesPath = data.Category + "/" + data.NamePlural + singleRoutePath = data.Category + "/" + data.NameLower + } else { + routesPath = data.NamePlural + singleRoutePath = data.NameLower + } + + var importAlias string + if data.Category != "" { + importAlias = data.NameLower + "Handlers" + } else { + importAlias = data.NameLower + "Handlers" + } + fmt.Printf(` - // %s endpoints - %sHandler := %sHandlers.New%sHandler() -`, data.Name, data.NameLower, data.NameLower, data.Name) +// %s endpoints +%sHandler := %s.New%sHandler() +`, data.Name, data.NameLower, importAlias, data.Name) if data.HasGet { - fmt.Printf("\tv1.GET(\"/%s\", %sHandler.Get%s)\n", data.NamePlural, data.NameLower, data.Name) - fmt.Printf("\tv1.GET(\"/%s/:id\", %sHandler.Get%sByID)\n", data.NameLower, data.NameLower, data.Name) + fmt.Printf("\tv1.GET(\"/%s\", %sHandler.Get%s)\n", routesPath, data.NameLower, data.Name) + fmt.Printf("\tv1.GET(\"/%s/:id\", %sHandler.Get%sByID)\n", singleRoutePath, data.NameLower, data.Name) } + if data.HasPost { - fmt.Printf("\tv1.POST(\"/%s\", %sHandler.Create%s)\n", data.NamePlural, data.NameLower, data.Name) + fmt.Printf("\tv1.POST(\"/%s\", %sHandler.Create%s)\n", routesPath, data.NameLower, data.Name) } + if data.HasPut { - fmt.Printf("\tv1.PUT(\"/%s/:id\", %sHandler.Update%s)\n", data.NameLower, data.NameLower, data.Name) + fmt.Printf("\tv1.PUT(\"/%s/:id\", %sHandler.Update%s)\n", singleRoutePath, data.NameLower, data.Name) } + if data.HasDelete { - fmt.Printf("\tv1.DELETE(\"/%s/:id\", %sHandler.Delete%s)\n", data.NameLower, data.NameLower, data.Name) + fmt.Printf("\tv1.DELETE(\"/%s/:id\", %sHandler.Delete%s)\n", singleRoutePath, data.NameLower, data.Name) } + if data.HasStats { - fmt.Printf("\tv1.GET(\"/%s/stats\", %sHandler.Get%sStats)\n", data.NamePlural, data.NameLower, data.Name) + fmt.Printf("\tv1.GET(\"/%s/stats\", %sHandler.Get%sStats)\n", routesPath, data.NameLower, data.Name) } + fmt.Println() } // ================= UTILITY FUNCTIONS ===================== - func writeFile(filename, content string) { if err := os.WriteFile(filename, []byte(content), 0644); err != nil { fmt.Printf("❌ Error creating file %s: %v\n", filename, err)