diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e3b2d4..64f0190 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,4 +22,4 @@ jobs: run: go mod tidy - name: Run tests - run: go test -v ./... \ No newline at end of file + run: go test ./... \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index e69de29..0000000 diff --git a/cmd/new.go b/cmd/new.go index 4af1e67..91f6187 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -1,6 +1,6 @@ /* -Copyright © 2025 NAME HERE +Copyright © 2025 Saurav Upadhyay sauravup041103@gmail.com */ @@ -10,33 +10,378 @@ import ( "fmt" "io" "io/fs" + "log" "os" "path/filepath" + "strconv" "strings" "text/template" + "unicode" "github.com/spf13/cobra" + "github.com/upsaurav12/bootstrap/pkg/addons" + "github.com/upsaurav12/bootstrap/pkg/framework" + "github.com/upsaurav12/bootstrap/pkg/parser" "github.com/upsaurav12/bootstrap/templates" + + "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/common-nighthawk/go-figure" +) + +type ProjectInput struct { + Name string + Type string + Router string + Port string + DB string + Entities []string +} + +var asciiStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color(primaryBlue)). + Bold(true) + +const ( + stepName = iota + stepType + stepRouter + stepPort + stepDB + stepConfirm ) +var ( + titleStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color(softBlue)) + + labelStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color(softBlue)) + + hintStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color(mutedGray)) + + boxStyle = lipgloss.NewStyle(). + Padding(3, 2). + Border(lipgloss.RoundedBorder()). + BorderForeground(borderGray). + Width(60) +) + +func renderStep(m wizardModel, title, label, body, hint string) string { + content := lipgloss.JoinVertical( + lipgloss.Left, + titleStyle.Render(title), + "", + labelStyle.Render(label), + "", + body, + "", + hintStyle.Render(hint), + ) + + box := boxStyle. + Width(m.width - 4). + Height(m.height - 2). + Render(content) + + return box +} + +type wizardModel struct { + step int + input ProjectInput + + text textinput.Model + list list.Model + quit bool + + width int + height int +} + +type item string + +func (i item) Title() string { return string(i) } +func (i item) Description() string { return "" } +func (i item) FilterValue() string { return string(i) } + +func initialWizardModel() wizardModel { + return wizardModel{ + step: stepName, + text: newTextInput(""), + } +} + +const ( + primaryBlue = lipgloss.Color("33") // bright blue + softBlue = lipgloss.Color("39") // lighter blue + mutedGray = lipgloss.Color("241") // hints + borderGray = lipgloss.Color("238") // borders +) + +func newTextInput(placeholder string) textinput.Model { + ti := textinput.New() + ti.Prompt = "› " + ti.Placeholder = placeholder + ti.SetValue("") // ← critical + ti.Focus() + return ti +} + +func (m wizardModel) Init() tea.Cmd { + return nil +} + +func renderHeader() string { + fig := figure.NewFigure("Bootstrap CLI", "slant", true) + + ascii := strings.Trim(fig.String(), "\n") + + return asciiStyle.Render(ascii) +} + +func (m wizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + + switch msg := msg.(type) { + + // ✅ HANDLE WINDOW SIZE FIRST + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + return m, nil + + // ✅ HANDLE KEYS + case tea.KeyMsg: + switch msg.String() { + + case "ctrl+c", "esc": + m.quit = true + return m, tea.Quit + + case "enter": + switch m.step { + + case stepName: + m.input.Name = m.text.Value() + m.step = stepType + m.list = newList("Project type", []string{"rest"}) + return m, nil + + case stepType: + m.input.Type = m.list.SelectedItem().(item).Title() + m.step = stepRouter + m.list = newList("Router", []string{"gin", "chi", "echo"}) + return m, nil + + case stepRouter: + m.input.Router = m.list.SelectedItem().(item).Title() + m.step = stepPort + m.text = newTextInput("") + return m, nil + + case stepPort: + port := m.text.Value() + if port == "" { + port = "8080" + } + if _, err := strconv.Atoi(port); err != nil { + return m, nil + } + m.input.Port = port + m.step = stepDB + m.list = newList("Database", []string{"postgres", "mysql", "mongo"}) + return m, nil + + case stepDB: + m.input.DB = m.list.SelectedItem().(item).Title() + m.step = stepConfirm + return m, nil + + case stepConfirm: + return m, tea.Quit + } + } + } + + var cmd tea.Cmd + if m.step == stepName || m.step == stepPort { + m.text, cmd = m.text.Update(msg) + return m, cmd + } + + if m.step == stepType || m.step == stepRouter || m.step == stepDB { + m.list, cmd = m.list.Update(msg) + return m, cmd + } + + return m, nil +} + +func (m wizardModel) View() string { + if m.quit { + return "" + } + + switch m.step { + + case stepName: + return renderStep( + m, + renderHeader()+"\nCreate New Project", + "Project name", + m.text.View(), + "Enter to continue • Esc to quit", + ) + + case stepType: + return renderStep( + m, + "Project Type", + "Select project type", + m.list.View(), + "↑↓ navigate • Enter select • Esc quit", + ) + + case stepRouter: + return renderStep( + m, + "Router", + "Select router", + m.list.View(), + "↑↓ navigate • Enter select • Esc quit", + ) + + case stepPort: + return renderStep( + m, + "Application Port", + "Port (default: 8080)", + m.text.View(), + "Enter to continue • Esc quit", + ) + + case stepDB: + return renderStep( + m, + "Database", + "Select database", + m.list.View(), + "↑↓ navigate • Enter select • Esc quit", + ) + + case stepConfirm: + summary := fmt.Sprintf( + "Project: %s\nType: %s\nRouter: %s\nPort: %s\nDatabase: %s", + m.input.Name, + m.input.Type, + m.input.Router, + m.input.Port, + m.input.DB, + ) + + return renderStep( + m, + "Confirm Configuration", + "Review your selections", + summary, + "Enter to generate • Esc to cancel", + ) + } + + return "" +} + +func newList(title string, values []string) list.Model { + items := make([]list.Item, len(values)) + for i, v := range values { + items[i] = item(v) + } + + l := list.New(items, list.NewDefaultDelegate(), 20, 10) + l.Title = title + return l +} + +func copyProjectYAML(srcPath, destDir string) error { + if srcPath == "" { + return nil // nothing to copy + } + + content, err := os.ReadFile(srcPath) + if err != nil { + return err + } + + destPath := filepath.Join(destDir, "project.yaml") + + return os.WriteFile(destPath, content, 0644) +} + +func RunInteractiveWizard() (*ProjectInput, error) { + p := tea.NewProgram(initialWizardModel()) + model, err := p.Run() + if err != nil { + return nil, err + } + + m := model.(wizardModel) + if m.quit { + return nil, fmt.Errorf("aborted") + } + + return &m.input, nil +} + // newCmd represents the new command var newCmd = &cobra.Command{ Use: "new", Short: "command for creating a new project.", Long: `command for creating a new project.`, Run: func(cmd *cobra.Command, args []string) { + interactive, _ := cmd.Flags().GetBool("interactive") + + if interactive || (len(args) == 0 && YAMLPath == "") { + input, err := RunInteractiveWizard() + if err != nil { + fmt.Fprintln(cmd.OutOrStdout(), err) + return + } + + projectType = input.Type + projectRouter = input.Router + projectPort = input.Port + DBType = input.DB + Entities = input.Entities + + createNewProject(input.Name, input.Router, input.Type, cmd.OutOrStdout()) + return + } + + var dirName string + if len(args) < 1 && YAMLPath != "" { + yamlConfig, err := parser.ReadYAML(YAMLPath) + if err != nil { + fmt.Println("error while creating project using yaml: ", err) + return + } + + dirName = yamlConfig.Project.Name + } else { + dirName = args[0] + } + // Check if the project name is provided - if len(args) < 1 { + if len(args) < 1 && YAMLPath == "" { fmt.Fprintln(cmd.OutOrStdout(), "Error: project name is required") return } - // Get the template flag value from the command context tmpl, _ := cmd.Flags().GetString("type") - // Get the project name (first argument) - dirName := args[0] - // Create the new project createNewProject(dirName, projectRouter, tmpl, cmd.OutOrStdout()) }, @@ -46,6 +391,55 @@ var projectType string var projectPort string var projectRouter string var DBType string +var YAMLPath string +var Entitys string +var Entities []string +var yamlFile string + +type TemplateData struct { + Name string + ModuleName string + PortName string + DBType string + Imports string + Start string + Entity string + Entities []string + ContextName string + ContextType string + Router string + Bind string + JSON string + LowerEntity string + OtherImports string + UpperEntity []string + ApiGroup func(entity string, get string, lowerentity string) string + Get string + FullContext string + ToTheClient string + Response string + ImportHandler string + ImportRouter string + ServiceName string + Image string + Environment string + Port string + Volume string + VolumeName string + DBName string + DBEnvPrefix string + Import string + Driver string + DSN string + Returnable string + ReturnKeyword string + HTTPHandler string +} + +type TemplateJob struct { + TemplateDir string + DestDir string +} func init() { // Add the new command to the rootCmd @@ -56,56 +450,216 @@ func init() { newCmd.Flags().StringVar(&projectPort, "port", "", "port of the project") newCmd.Flags().StringVar(&projectRouter, "router", "", "router of the project") newCmd.Flags().StringVar(&DBType, "db", "", "data type of the project") + newCmd.Flags().StringVar(&YAMLPath, "yaml", "", "yaml file path") + newCmd.Flags().StringVar(&Entitys, "entity", "", "entity") + newCmd.Flags().StringSliceVar(&Entities, "entities", nil, "different entities") + newCmd.Flags().Bool("interactive", false, "run interactive project setup") + +} + +func buildTemplateData(projectName string, + projectPort string, + frameworkConfig framework.FrameworkConfig, + dbConfig *addons.DbAddOneConfig, // optional + yamlConfig *parser.Config, uppercase []string) TemplateData { + + data := TemplateData{ + Name: frameworkConfig.Name, + ModuleName: projectName, + PortName: projectPort, + DBType: DBType, + Imports: frameworkConfig.Imports, + Start: frameworkConfig.Start, + ContextName: frameworkConfig.ContextName, + ContextType: frameworkConfig.ContextType, + Router: frameworkConfig.Router, + Bind: frameworkConfig.Bind, + JSON: frameworkConfig.JSON, + LowerEntity: Entitys, + OtherImports: frameworkConfig.OtherImports, + ApiGroup: frameworkConfig.ApiGroup, + Get: frameworkConfig.Get, + FullContext: frameworkConfig.FullContext, + ToTheClient: frameworkConfig.ToTheClient, + Response: frameworkConfig.Response, + ImportHandler: frameworkConfig.ImportHandler, + ImportRouter: frameworkConfig.ImportRouter, + Returnable: frameworkConfig.Returnable, + ReturnKeyword: frameworkConfig.ReturnKeyword, + HTTPHandler: frameworkConfig.HTTPHandler, + Entities: Entities, + // UpperEntity: , + } + + if yamlConfig != nil { + data = TemplateData{ + Name: frameworkConfig.Name, + ModuleName: projectName, + PortName: projectPort, + DBType: yamlConfig.Project.Database, + Imports: frameworkConfig.Imports, + Start: frameworkConfig.Start, + ContextName: frameworkConfig.ContextName, + ContextType: frameworkConfig.ContextType, + Router: frameworkConfig.Router, + Bind: frameworkConfig.Bind, + JSON: frameworkConfig.JSON, + LowerEntity: Entitys, + UpperEntity: nil, + OtherImports: frameworkConfig.OtherImports, + ApiGroup: frameworkConfig.ApiGroup, + Get: frameworkConfig.Get, + FullContext: frameworkConfig.FullContext, + ToTheClient: frameworkConfig.ToTheClient, + Response: frameworkConfig.Response, + ImportHandler: frameworkConfig.ImportHandler, + ImportRouter: frameworkConfig.ImportRouter, + Returnable: frameworkConfig.Returnable, + ReturnKeyword: frameworkConfig.ReturnKeyword, + HTTPHandler: frameworkConfig.HTTPHandler, + Entities: yamlConfig.Entities, + } + } + + if dbConfig != nil { + data.ServiceName = dbConfig.ServiceName + data.DBName = dbConfig.DBName + data.DBEnvPrefix = dbConfig.DBEnvPrefix + data.Port = dbConfig.Port + data.DSN = dbConfig.DSN + data.Driver = dbConfig.Driver + data.Import = dbConfig.Import + data.Image = dbConfig.Image + data.Environment = dbConfig.Environment + data.Volume = dbConfig.Volume + data.VolumeName = dbConfig.VolumeName + } + + return data } -func createNewProject(projectName string, projectRouter string, template string, out io.Writer) { +func returnUppercase(entity string) string { + if entity == "" { + return "" + } + + runes := []rune(entity) + runes[0] = unicode.ToUpper(runes[0]) + + return string(runes) +} + +func createNewProject(projectName, projectRouter, template string, out io.Writer) { err := os.Mkdir(projectName, 0755) if err != nil { - fmt.Fprintf(out, "Error creating directory: %v\n", err) - return + log.Fatalf("error occured while creating a new project %s: ", err) } - // Print the template that was passed + err = os.MkdirAll(filepath.Join(projectName, "internal"), 0755) - // Always add README + Makefile from common - renderTemplateDir("common", projectName, TemplateData{ - ModuleName: projectName, - PortName: projectPort, - }) + if err != nil { + log.Fatalf("error occured while creating a new project %s: ", projectName) + } - renderTemplateDir("rest"+"/"+projectRouter, projectName, TemplateData{ - ModuleName: projectName, - PortName: projectPort, - DBType: DBType, - }) + // ✅ COPY project.yaml if provided + if err := copyProjectYAML(YAMLPath, projectName); err != nil { + fmt.Fprintf(out, "warning: could not copy project.yaml: %v\n", err) + } - if err != nil { - fmt.Fprintf(out, "Error rendering templates: %v\n", err) - return + // Prepare configs + + var frameworkConfig framework.FrameworkConfig + frameworkConfig = framework.FrameworkRegistory[projectRouter] + + var dbConfig *addons.DbAddOneConfig + + jobs := []TemplateJob{ + {"common", projectName}, + {"rest/clean", projectName}, } - if DBType != "" { - dbTemplatePath := "db/" + DBType - err := renderTemplateDir(dbTemplatePath, filepath.Join(projectName, "internal", "db"), TemplateData{ - ModuleName: projectName, - PortName: projectPort, - DBType: DBType, - }) - if err != nil { + var data TemplateData + + var yamlConfig *parser.Config - fmt.Fprintf(out, "Error rendering DB templates: %v\n", err) + if YAMLPath != "" { + var err error + yamlConfig, err = parser.ReadYAML(YAMLPath) + if err != nil { + fmt.Printf("error while reading yaml file: %s", err) return } + } + + if DBType != "" { + cfg := addons.DbRegistory[DBType] + dbConfig = &cfg + } + if DBType == "" && yamlConfig != nil { + DBType = yamlConfig.Project.Database + cfg := addons.DbRegistory[DBType] + dbConfig = &cfg + } + + if yamlConfig != nil { + + frameworkConfig = framework.FrameworkRegistory[yamlConfig.Project.Router] + Entities = yamlConfig.Entities + + if yamlConfig.Project.Database != "" { + + cfg := addons.DbRegistory[yamlConfig.Project.Database] + dbConfig = &cfg + + DBType = yamlConfig.Project.Database + + } + } + + var uppercase []string + + for _, entity := range Entities { + u := returnUppercase(entity) + + uppercase = append(uppercase, u) + } + + data = buildTemplateData( + projectName, + projectPort, + frameworkConfig, + dbConfig, + yamlConfig, uppercase) + + data.UpperEntity = uppercase + + // Render templates + _ = renderTemplateDir("common", projectName, data) + _ = renderTemplateDir("rest/clean", projectName, data) + + if DBType != "" { + jobs = append(jobs, + TemplateJob{"db/" + DBType, projectName}, + TemplateJob{"db/database", filepath.Join(projectName, "internal", "db")}, + ) + + // fmt.Fprintf(out, "✓ Added database support for '%s'\n", DBType) + } - fmt.Fprintf(out, "✓ Added database support for '%s'\n", DBType) + for _, job := range jobs { + if err := renderTemplateDir(job.TemplateDir, job.DestDir, data); err != nil { + fmt.Fprintf(out, "Error rendering template %s → %s: %v\n", + job.TemplateDir, job.DestDir, err) + return + } } fmt.Fprintf(out, "✓ Created '%s' successfully\n", projectName) } -type TemplateData struct { - ModuleName string - PortName string - DBType string +func IsHidden(path string) (bool, error) { + // Unix hidden check + name := filepath.Base(path) + return strings.HasPrefix(name, "."), nil } func renderTemplateDir(templatePath, destinationPath string, data TemplateData) error { @@ -114,33 +668,76 @@ func renderTemplateDir(templatePath, destinationPath string, data TemplateData) return err } - // Compute relative path (remove the base templatePath) relPath, _ := filepath.Rel(templatePath, path) - targetPath := filepath.Join(destinationPath, strings.TrimSuffix(relPath, ".tmpl")) if d.IsDir() { - return os.MkdirAll(targetPath, 0755) + return os.MkdirAll(filepath.Join(destinationPath, relPath), 0755) } - // ✅ Important: use full `path` for ReadFile content, err := templates.FS.ReadFile(path) if err != nil { return err } - // Parse template - tmpl, err := template.New(filepath.Base(path)).Parse(string(content)) - if err != nil { - return err + fileName := strings.TrimSuffix(relPath, ".tmpl") + + if templatePath == "common" { + base := filepath.Base(fileName) + if base == "env" || base == "golang-ci.yml" { + fileName = "." + fileName + } } - // Write file - outFile, err := os.Create(targetPath) - if err != nil { - return err + if len(data.Entities) == 0 { + return writeSingle(data, fileName, path, content, destinationPath) + } + + for _, entity := range data.Entities { + + // correct file renaming + newFile := strings.Replace( + fileName, + "example", + strings.ToLower(entity), + 1, // only first replacement + ) + + entityData := data + entityData.Entity = strings.Title(entity) + entityData.LowerEntity = strings.ToLower(entity) + // capture errors!! + if err := writeSingle(entityData, newFile, path, content, destinationPath); err != nil { + return err + } } - defer outFile.Close() - return tmpl.Execute(outFile, data) + return nil }) } + +func writeSingle(data TemplateData, fileName string, tmpltPath string, content []byte, destinationPath string) error { + newFile := strings.Replace( + fileName, + "example", + "user", + 1, // only first replacement + ) + + entityData := data + entityData.Entity = strings.Title("user") + entityData.LowerEntity = strings.ToLower("user") + targetPath := filepath.Join(destinationPath, newFile) + + tmpl, err := template.New(filepath.Base(tmpltPath)).Parse(string(content)) + if err != nil { + return err + } + + outFile, err := os.Create(targetPath) + if err != nil { + return err + } + defer outFile.Close() + + return tmpl.Execute(outFile, entityData) +} diff --git a/cmd/root.go b/cmd/root.go index 94bbfdf..f952a48 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,5 +1,5 @@ /* -Copyright © 2025 NAME HERE +Copyright © 2025 Saurav Upadhyay sauravup041103@gmail.com */ package cmd diff --git a/go.mod b/go.mod index 2b5bc97..1ec608b 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,68 @@ module github.com/upsaurav12/bootstrap -go 1.23.6 +go 1.24.0 require ( + github.com/gin-gonic/gin v1.11.0 + github.com/spf13/cobra v1.9.1 + github.com/stretchr/testify v1.11.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/charmbracelet/bubbles v0.21.0 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.10.1 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/spf13/cobra v1.9.1 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sahilm/fuzzy v0.1.1 // indirect github.com/spf13/pflag v1.0.6 // indirect - github.com/stretchr/testify v1.10.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/go.sum b/go.sum index 725c027..b7ced98 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,141 @@ +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= +github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= +github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 18f6996..6d9e210 100644 --- a/main.go +++ b/main.go @@ -1,5 +1,5 @@ /* -Copyright © 2025 NAME HERE +Copyright © 2025 Saurav Upadhyay sauravup041103@gmail.com */ package main diff --git a/pkg/addons/addon.go b/pkg/addons/addon.go new file mode 100644 index 0000000..1890561 --- /dev/null +++ b/pkg/addons/addon.go @@ -0,0 +1 @@ +package addons diff --git a/pkg/addons/dbAddOne.go b/pkg/addons/dbAddOne.go new file mode 100644 index 0000000..98247bd --- /dev/null +++ b/pkg/addons/dbAddOne.go @@ -0,0 +1,113 @@ +package addons + +type DbAddOneConfig struct { + ServiceName string + Image string + Environment string + Port string + Volume string + VolumeName string + DBName string + DBEnvPrefix string + Import string + Driver string + DSN string + OutputFile string +} + +var DbRegistory = map[string]DbAddOneConfig{ + "postgres": { + ServiceName: "postgres_bp", + Image: "postgres:latest", + Environment: ` POSTGRES_DB: ${GONE_DB_DATABASE} + POSTGRES_USER: ${GONE_DB_USERNAME} + POSTGRES_PASSWORD: ${GONE_DB_PASSWORD}`, + Port: "5432", + Volume: "postgres_volume_bp:/var/lib/postgresql/data", + VolumeName: "postgres_volume_bp", + DBName: "PostgreSQL", + DBEnvPrefix: "BLUEPRINT", + Import: `_ "github.com/jackc/pgx/v5/stdlib"`, + Driver: "pgx", + DSN: "postgres://%s:%s@%s:%s/%s?sslmode=disable&search_path=%s", + }, + + "mysql": { + ServiceName: "mysql_bp", + Image: "mysql:8", + Environment: ` + MYSQL_DATABASE: ${GONE_DB_DATABASE} + MYSQL_USER: ${GONE_DB_USERNAME} + MYSQL_PASSWORD: ${GONE_DB_PASSWORD} + MYSQL_ROOT_PASSWORD: ${GONE_DB_PASSWORD}`, + Port: "3306", + Volume: "mysql_volume_bp:/var/lib/mysql", + VolumeName: "mysql_volume_bp", + DBName: "MySQL", + DBEnvPrefix: "BLUEPRINT", + Import: `_ "github.com/go-sql-driver/mysql"`, + Driver: "mysql", + DSN: "%s:%s@tcp(%s:%s)/%s", + OutputFile: "mysql.go", + }, + + "mongodb": { + ServiceName: "mongo_bp", + Image: "mongo:latest", + Environment: ` + MONGO_INITDB_DATABASE: ${GONE_DB_DATABASE} + MONGO_INITDB_ROOT_USERNAME: ${GONE_DB_USERNAME} + MONGO_INITDB_ROOT_PASSWORD: ${GONE_DB_PASSWORD}`, + Port: "27017", + Volume: "mongo_volume_bp:/data/db", + VolumeName: "mongo_volume_bp", + }, + + "sqlite": { + ServiceName: "sqlite_bp", + Image: "alpine:latest", + Environment: ` + # SQLite has no environment variables`, + Port: "0", // no port needed + Volume: "sqlite_volume_bp:/data", + VolumeName: "sqlite_volume_bp", + DBName: "SQLite", + DBEnvPrefix: "BLUEPRINT", + Import: `_ "modernc.org/sqlite"`, + Driver: "sqlite", + DSN: "file:%s.db?_pragma=journal_mode(WAL)", + }, + + "cockroachdb": { + ServiceName: "cockroach_bp", + Image: "cockroachdb/cockroach:latest", + Environment: ` + # CockroachDB has no env vars needed in insecure mode`, + Port: "26257", + Volume: "cockroach_volume_bp:/cockroach/cockroach-data", + VolumeName: "cockroach_volume_bp", + DBName: "CockroachDB", + DBEnvPrefix: "BLUEPRINT", + Import: `_ "github.com/jackc/pgx/v5/stdlib"`, + Driver: "pgx", + DSN: "postgres://%s:%s@%s:%s/%s?sslmode=disable", + }, + + "mariadb": { + ServiceName: "mariadb_bp", + Image: "mariadb:latest", + Environment: ` + MARIADB_DATABASE: ${GONE_DB_DATABASE} + MARIADB_USER: ${GONE_DB_USERNAME} + MARIADB_PASSWORD: ${GONE_DB_PASSWORD} + MARIADB_ROOT_PASSWORD: ${GONE_DB_PASSWORD}`, + Port: "3306", + Volume: "mariadb_volume_bp:/var/lib/mysql", + VolumeName: "mariadb_volume_bp", + DBName: "MariaDB", + DBEnvPrefix: "BLUEPRINT", + Import: `_ "github.com/go-sql-driver/mysql"`, + Driver: "mysql", + DSN: "%s:%s@tcp(%s:%s)/%s", + }, +} diff --git a/pkg/framework/framework.go b/pkg/framework/framework.go new file mode 100644 index 0000000..12bc90b --- /dev/null +++ b/pkg/framework/framework.go @@ -0,0 +1,213 @@ +package framework + +import "fmt" + +type FrameworkConfig struct { + Name string + Imports string + Entity string + ContextName string + ContextType string + Bind string + JSON string + Router string + Start string + OtherImports string + ApiGroup func(entity string, get string, lowerentity string) string + Get string + FullContext string + ToTheClient string + Response string + ImportRouter string + ImportHandler string + Returnable string + ReturnKeyword string + HTTPHandler string + Entities []string +} + +var FrameworkRegistory = map[string]FrameworkConfig{ + "gin": { + Name: "gin", + Imports: `"github.com/gin-gonic/gin"`, + ContextName: "c", + ContextType: "*gin.Context", + Bind: "c.BindJSON", // func(obj any) error + JSON: "c.JSON", // func(code int, obj any) + Router: "*gin.Engine", + Start: "gin.Default()", + OtherImports: `"net/http"`, + FullContext: "c *gin.Context", + ApiGroup: func(entity string, get string, lowerentity string) string { + apiGroup := fmt.Sprintf(` + { + %s := api.Group("/%s") + { + %s.%s("", %sHandler.Get%ss) + } + } + `, lowerentity, lowerentity, lowerentity, get, lowerentity, entity) + + return apiGroup + }, + Get: "GET", + ToTheClient: "c.JSON(http.StatusOK, ", + Response: "(http.StatusOK,", + ImportRouter: ` + "net/http" + "github.com/gin-gonic/gin" + `, + ImportHandler: ` + "github.com/gin-gonic/gin" + "net/http" + `, + Returnable: "", + ReturnKeyword: "", + HTTPHandler: "http.Handler", + }, + + "chi": { + Name: "chi", + Imports: `"github.com/go-chi/chi/v5"`, + ContextName: "r", + ContextType: "http.ResponseWriter, *http.Request", // chi passes both + Bind: "json.NewDecoder(r.Body).Decode", // need encoding/json + JSON: "render.JSON", // from go-chi/render + Router: "chi.Router", + Start: "chi.NewRouter()", + OtherImports: ` + "encoding/json" + "github.com/go-chi/render" + "net/http" + `, + ApiGroup: func(entity string, get string, lowerentity string) string { + apiGroup := fmt.Sprintf(` + r.Group(func(r chi.Router) { + r.%s("/%s", handler.Get%ss) + }) + `, get, lowerentity, entity) + + return apiGroup + }, + Get: "Get", + FullContext: "w http.ResponseWriter, r *http.Request", + ToTheClient: "json.NewEncoder(w).Encode(", + Response: "(w, r,", + ImportRouter: ` + "encoding/json" + "net/http" + "github.com/go-chi/chi/v5" + `, + ImportHandler: ` + "net/http" + "github.com/go-chi/render" + `, + Returnable: "", + ReturnKeyword: "", + HTTPHandler: "http.Handler", + }, + + "echo": { + Name: "echo", + Imports: `"github.com/labstack/echo/v4"`, + ContextName: "c", + ContextType: "echo.Context", + Bind: "c.Bind", // func(obj interface{}) error + JSON: "c.JSON", // func(int, interface{}) error + Router: "*echo.Echo", + Start: "echo.New()", + OtherImports: `"net/http"`, + + ApiGroup: func(entity, get, lowerentity string) string { + return fmt.Sprintf(` + api := r.Group("/api/v1") + { + %s := api.Group("/%s") + { + %s.%s("", handler.Get%ss) + } + } + `, lowerentity, lowerentity, lowerentity, get, entity) + }, + + Get: "GET", + + FullContext: "c echo.Context", + + ToTheClient: "c.JSON(http.StatusOK, ", + + Response: "(http.StatusOK,", + + ImportRouter: ` + "net/http" + "github.com/labstack/echo/v4" + `, + + ImportHandler: ` + "net/http" + "github.com/labstack/echo/v4" + `, + Returnable: "error", + ReturnKeyword: "return", + HTTPHandler: "http.Handler", + }, + + "fiber": { + Name: "fiber", + Imports: `"github.com/gofiber/fiber/v2"`, + ContextName: "c", + ContextType: "*fiber.Ctx", + Bind: "c.BodyParser", // func(obj interface{}) error + JSON: "c.JSON", // func(obj interface{}) error + Router: "*fiber.App", + Start: "fiber.New()", + OtherImports: `"net/http"`, + + ApiGroup: func(entity, get, lowerentity string) string { + return fmt.Sprintf(` + api := r.Group("/api/v1") + { + %s := api.Group("/%s") + { + %s.%s("", handler.Get%ss) + } + } + `, lowerentity, lowerentity, lowerentity, get, entity) + }, + + Get: "Get", + + FullContext: "c *fiber.Ctx", + + ToTheClient: "c.JSON(", + + Response: "(fiber.StatusOK,", + + ImportRouter: ` + "github.com/gofiber/fiber/v2" + `, + + ImportHandler: ` + "github.com/gofiber/fiber/v2" + `, + + Returnable: "error", + ReturnKeyword: "return", + HTTPHandler: "*fiber.App", + }, + + "mux": { + Name: "mux", + Imports: `"github.com/gorilla/mux"`, + ContextName: "w, r", + ContextType: "http.ResponseWriter, *http.Request", + Bind: "json.NewDecoder(r.Body).Decode", + JSON: `json.NewEncoder(w).Encode`, + Router: "*mux.Router", + Start: "mux.NewRouter()", + OtherImports: ` + "encoding/json" + "net/http" + `, + }, +} diff --git a/pkg/parser/yamlParsing.go b/pkg/parser/yamlParsing.go new file mode 100644 index 0000000..7ea5126 --- /dev/null +++ b/pkg/parser/yamlParsing.go @@ -0,0 +1,58 @@ +package parser + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +type Config struct { + Project Project `yaml:"project"` + // Feature Feature `yaml:"feature"` + Entities []string `yaml:"entities"` + CustomLogic []string `yaml:"custom_logic"` +} + +type Project struct { + Name string `yaml:"name"` + Type string `yaml:"type"` + Port int `yaml:"port"` + Location string `yaml:"location"` + Database string `yaml:"db"` + Router string `yaml:"router"` +} + +// type Feature struct { +// Database FeatureItem `yaml:"database"` +// Cache FeatureItem `yaml:"cache"` +// Queue FeatureItem `yaml:"queue"` +// Auth FeatureItem `yaml:"auth"` +// } + +type FeatureItem struct { + Name string `yaml:"name"` +} + +type ProjectYAML struct { + Name string `json:"name" yaml:"name"` + Layers []string `json:"layers" yaml:"layers"` + Features []string `json:"features" yaml:"features"` +} + +func ReadYAML(yamlPath string) (*Config, error) { + yamlByte, err := os.ReadFile(yamlPath) + if err != nil { + return &Config{}, err + } + + var yamlProject Config + + err = yaml.Unmarshal(yamlByte, &yamlProject) + if err != nil { + fmt.Println("error: ", err) + return &Config{}, err + } + + return &yamlProject, nil +} diff --git a/project.yaml b/project.yaml new file mode 100644 index 0000000..e8e4e27 --- /dev/null +++ b/project.yaml @@ -0,0 +1,10 @@ +project: + name: "test1" + port: 8080 + arch: "clean" + router: "chi" # or gin / echo / fiber + +entities: +- user +- product +- payment diff --git a/templates/common/Makefile.tmpl b/templates/common/Makefile.tmpl index 2c7bea6..af53a03 100644 --- a/templates/common/Makefile.tmpl +++ b/templates/common/Makefile.tmpl @@ -1,5 +1,5 @@ # Project variables -APP_NAME := {.ModuleName} +APP_NAME := {{.ModuleName}} BIN_DIR := bin MAIN_FILE := ./cmd/main.go PKG := ./... diff --git a/templates/common/env.tmpl b/templates/common/env.tmpl new file mode 100644 index 0000000..6163151 --- /dev/null +++ b/templates/common/env.tmpl @@ -0,0 +1,8 @@ +PORT={{.PortName}} +APP_ENV=local +GONE_DB_HOST=localhost +GONE_DB_PORT=5431 +GONE_DB_DATABASE=gone +GONE_DB_USERNAME=example_username +GONE_DB_PASSWORD=password1234 +GONE_DB_SCHEMA=public \ No newline at end of file diff --git a/templates/common/golang-ci.yml.tmpl b/templates/common/golang-ci.yml.tmpl new file mode 100644 index 0000000..abd171c --- /dev/null +++ b/templates/common/golang-ci.yml.tmpl @@ -0,0 +1,12 @@ +run: + timeout: 5m + tests: true + +linters: + enable: + - govet + - errcheck + - staticcheck + - gofmt + - ineffassign + - unused diff --git a/templates/common/project.yaml.tmpl b/templates/common/project.yaml.tmpl new file mode 100644 index 0000000..e75ab15 --- /dev/null +++ b/templates/common/project.yaml.tmpl @@ -0,0 +1,14 @@ +project: + name: "{{ .ModuleName }}" + port: {{ .Port }} + router: "{{ .Router }}" + database: "{{ .DBType }}" + +entities: +{{- if .Entities }} +{{- range .Entities }} + - {{ . }} +{{- end }} +{{- else }} + - user +{{- end }} diff --git a/templates/db/database/database.go.tmpl b/templates/db/database/database.go.tmpl new file mode 100644 index 0000000..259a580 --- /dev/null +++ b/templates/db/database/database.go.tmpl @@ -0,0 +1,69 @@ +package database + +import ( + "{{.ModuleName}}/internal/model" + "fmt" + "log" + "os" + + _ "github.com/jackc/pgx/v5/stdlib" + _ "github.com/joho/godotenv/autoload" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +// Service represents a service that interacts with a database. +type Service interface { + Health() map[string]string + Close() error + GetDB() *gorm.DB +} + +type service struct { + db *gorm.DB +} + +var ( + database = os.Getenv("GONE_DB_DATABASE") + password = os.Getenv("GONE_DB_PASSWORD") + username = os.Getenv("GONE_DB_USERNAME") + port = os.Getenv("GONE_DB_PORT") + host = os.Getenv("GONE_DB_HOST") + schema = os.Getenv("GONE_DB_SCHEMA") + dbInstance *service +) + +func New() Service { + dsn := fmt.Sprintf( + "host=%s user=%s password=%s dbname=%s port=%s sslmode=disable", + host, username, password, database, port, + ) + + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + log.Fatalf("failed to connect database: %v", err) + } + + db.AutoMigrate(model.Registry...) + + dbInstance = &service{db: db} + return dbInstance +} + +func (s *service) GetDB() *gorm.DB { + return s.db +} + +func (s *service) Health() map[string]string { + sqlDB, _ := s.db.DB() + err := sqlDB.Ping() + if err != nil { + return map[string]string{"status": "down"} + } + return map[string]string{"status": "up"} +} + +func (s *service) Close() error { + sqlDB, _ := s.db.DB() + return sqlDB.Close() +} diff --git a/templates/db/mongo/db.go.tmpl b/templates/db/mongo/db.go.tmpl deleted file mode 100644 index ec2a96f..0000000 --- a/templates/db/mongo/db.go.tmpl +++ /dev/null @@ -1,34 +0,0 @@ -package db - -import ( - "context" - "log" - "time" - - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" -) - -var Client *mongo.Client - -func Connect() { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017")) - if err != nil { - log.Fatalf("✖ Failed to connect to MongoDB: %v", err) - } - - Client = client - log.Println("✓ Connected to MongoDB") -} - -func Close() { - if Client != nil { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - Client.Disconnect(ctx) - log.Println("🔌 MongoDB connection closed") - } -} diff --git a/templates/db/mongo/docker-compose.yml.tmpl b/templates/db/mongo/docker-compose.yml.tmpl new file mode 100644 index 0000000..e07d532 --- /dev/null +++ b/templates/db/mongo/docker-compose.yml.tmpl @@ -0,0 +1,15 @@ +services: + mongo_bp: + image: mongo:latest + restart: unless-stopped + environment: + MONGO_INITDB_DATABASE: ${GONE_DB_DATABASE} + MONGO_INITDB_ROOT_USERNAME: ${GONE_DB_USERNAME} + MONGO_INITDB_ROOT_PASSWORD: ${GONE_DB_PASSWORD} + ports: + - "${GONE_DB_PORT}:27017" + volumes: + - mongo_volume_bp:/data/db + +volumes: + mongo_volume_bp: \ No newline at end of file diff --git a/templates/db/mysql/db.go.tmpl b/templates/db/mysql/db.go.tmpl deleted file mode 100644 index 31260de..0000000 --- a/templates/db/mysql/db.go.tmpl +++ /dev/null @@ -1,27 +0,0 @@ -package db - -import ( - "database/sql" - "log" - - _ "github.com/go-sql-driver/mysql" -) - -var DB *sql.DB - -func Connect() { - connStr := "user:password@tcp(localhost:3306)/app_db" - var err error - DB, err = sql.Open("mysql", connStr) - if err != nil { - log.Fatalf("✖ Failed to connect to MySQL: %v", err) - } - log.Println("✓ Connected to MySQL database") -} - -func Close() { - if DB != nil { - DB.Close() - log.Println("🔌 MySQL connection closed") - } -} diff --git a/templates/db/mysql/docker-compose.yml.tmpl b/templates/db/mysql/docker-compose.yml.tmpl new file mode 100644 index 0000000..fee41d9 --- /dev/null +++ b/templates/db/mysql/docker-compose.yml.tmpl @@ -0,0 +1,16 @@ +services: + mysql_bp: + image: mysql:8 + restart: unless-stopped + environment: + MYSQL_DATABASE: ${GONE_DB_DATABASE} + MYSQL_USER: ${GONE_DB_USERNAME} + MYSQL_PASSWORD: ${GONE_DB_PASSWORD} + MYSQL_ROOT_PASSWORD: ${GONE_DB_PASSWORD} + ports: + - "${GONE_DB_PORT}:3306" + volumes: + - mysql_volume_bp:/var/lib/mysql + +volumes: + mysql_volume_bp: diff --git a/templates/db/postgres/db.go.tmpl b/templates/db/postgres/db.go.tmpl deleted file mode 100644 index 3422984..0000000 --- a/templates/db/postgres/db.go.tmpl +++ /dev/null @@ -1,27 +0,0 @@ -package db - -import ( - "database/sql" - "log" - - _ "github.com/lib/pq" -) - -var DB *sql.DB - -func Connect() { - connStr := "user=postgres password=postgres dbname=app_db sslmode=disable" - var err error - DB, err = sql.Open("postgres", connStr) - if err != nil { - log.Fatalf("✖ Failed to connect to PostgreSQL: %v", err) - } - log.Println("✓ Connected to PostgreSQL database") -} - -func Close() { - if DB != nil { - DB.Close() - log.Println("🔌 PostgreSQL connection closed") - } -} diff --git a/templates/db/postgres/docker-compose.yml.tmpl b/templates/db/postgres/docker-compose.yml.tmpl new file mode 100644 index 0000000..311b70c --- /dev/null +++ b/templates/db/postgres/docker-compose.yml.tmpl @@ -0,0 +1,15 @@ +services: + psql_bp: + image: postgres:latest + restart: unless-stopped + environment: + POSTGRES_DB: ${GONE_DB_DATABASE} + POSTGRES_USER: ${GONE_DB_USERNAME} + POSTGRES_PASSWORD: ${GONE_DB_PASSWORD} + ports: + - "${GONE_DB_PORT}:5432" + volumes: + - psql_volume_bp:/var/lib/postgresql + +volumes: + psql_volume_bp: \ No newline at end of file diff --git a/templates/db/sqllite/db.go.tmpl b/templates/db/sqllite/db.go.tmpl deleted file mode 100644 index 2693850..0000000 --- a/templates/db/sqllite/db.go.tmpl +++ /dev/null @@ -1,26 +0,0 @@ -package db - -import ( - "database/sql" - "log" - - _ "github.com/mattn/go-sqlite3" -) - -var DB *sql.DB - -func Connect() { - var err error - DB, err = sql.Open("sqlite3", "app.db") - if err != nil { - log.Fatalf("✖ Failed to connect to SQLite: %v", err) - } - log.Println("✓ Connected to SQLite database") -} - -func Close() { - if DB != nil { - DB.Close() - log.Println("🔌 SQLite connection closed") - } -} diff --git a/templates/embed.go b/templates/embed.go index 5d7a80e..068db6d 100644 --- a/templates/embed.go +++ b/templates/embed.go @@ -7,7 +7,8 @@ import "embed" // Since this file is located in the 'templates' directory, // the paths 'common' and 'rest' correctly refer to the template folders. // -//go:embed common -//go:embed rest -//go:embed db + +//go:embed common/** +//go:embed rest/** +//go:embed db/** var FS embed.FS diff --git a/templates/rest/chi/cmd/main.go.tmpl b/templates/rest/chi/cmd/main.go.tmpl deleted file mode 100644 index 933fb29..0000000 --- a/templates/rest/chi/cmd/main.go.tmpl +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "log" - "net/http" - - "{{.ModuleName}}/internal/config" - "{{.ModuleName}}/internal/router" - - "github.com/go-chi/chi/v5" -) - -func main() { - cfg := config.Load() - - r := chi.NewRouter() - router.RegisterRoutes(r) - - port := cfg.Port - if port == "" { - port = "8080" - } - - log.Printf("Starting server on :%s", port) - if err := http.ListenAndServe(":"+port, r); err != nil { - log.Fatal(err) - } -} diff --git a/templates/rest/chi/go.mod.tmpl b/templates/rest/chi/go.mod.tmpl deleted file mode 100644 index 84b2fb3..0000000 --- a/templates/rest/chi/go.mod.tmpl +++ /dev/null @@ -1,5 +0,0 @@ -module {{.ModuleName}} - -go 1.22 - -require github.com/go-chi/chi/v5 v5.0.11 diff --git a/templates/rest/chi/internal/config/config.go.tmpl b/templates/rest/chi/internal/config/config.go.tmpl deleted file mode 100644 index 326241b..0000000 --- a/templates/rest/chi/internal/config/config.go.tmpl +++ /dev/null @@ -1,20 +0,0 @@ -package config - -import "os" - -type Config struct { - Port string -} - -func Load() Config { - return Config{ - Port: getEnv("APP_PORT", "8080"), - } -} - -func getEnv(key, fallback string) string { - if value, ok := os.LookupEnv(key); ok { - return value - } - return fallback -} diff --git a/templates/rest/chi/internal/handler/user_handler.go.tmpl b/templates/rest/chi/internal/handler/user_handler.go.tmpl deleted file mode 100644 index 6c5ca19..0000000 --- a/templates/rest/chi/internal/handler/user_handler.go.tmpl +++ /dev/null @@ -1,16 +0,0 @@ -package handler - -import ( - "encoding/json" - "net/http" - - "github.com/go-chi/chi/v5" -) - -func GetUser(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") - json.NewEncoder(w).Encode(map[string]string{ - "id": id, - "name": "John Doe", - }) -} diff --git a/templates/rest/chi/internal/router/routes.go.tmpl b/templates/rest/chi/internal/router/routes.go.tmpl deleted file mode 100644 index 9cd014d..0000000 --- a/templates/rest/chi/internal/router/routes.go.tmpl +++ /dev/null @@ -1,11 +0,0 @@ -package router - -import ( - handler "{{.ModuleName}}/internal/handler" - - "github.com/go-chi/chi/v5" -) - -func RegisterRoutes(r *chi.Mux) { - r.Get("/users/{id}", handler.GetUser) -} diff --git a/templates/rest/clean/cmd/main.go.tmpl b/templates/rest/clean/cmd/main.go.tmpl new file mode 100644 index 0000000..1008f94 --- /dev/null +++ b/templates/rest/clean/cmd/main.go.tmpl @@ -0,0 +1,58 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os/signal" + "syscall" + "time" + "{{.ModuleName}}/internal/server" +) + +func gracefulShutdown(apiServer *http.Server, done chan bool) { + // Create context that listens for the interrupt signal from the OS. + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + // Listen for the interrupt signal. + <-ctx.Done() + + log.Println("shutting down gracefully, press Ctrl+C again to force") + stop() // Allow Ctrl+C to force shutdown + + // The context is used to inform the server it has 5 seconds to finish + // the request it is currently handling + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := apiServer.Shutdown(ctx); err != nil { + log.Printf("Server forced to shutdown with error: %v", err) + } + + log.Println("Server exiting") + + // Notify the main goroutine that the shutdown is complete + done <- true +} + +func main() { + + server := router.NewServer() + + // Create a done channel to signal when the shutdown is complete + done := make(chan bool, 1) + + // Run graceful shutdown in a separate goroutine + go gracefulShutdown(server, done) + + err := server.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + panic(fmt.Sprintf("http server error: %s", err)) + } + + // Wait for the graceful shutdown to complete + <-done + log.Println("Graceful shutdown complete.") +} + diff --git a/templates/rest/clean/docker-compose.yml.tmpl b/templates/rest/clean/docker-compose.yml.tmpl new file mode 100644 index 0000000..1a16578 --- /dev/null +++ b/templates/rest/clean/docker-compose.yml.tmpl @@ -0,0 +1,15 @@ +services: + {{.ServiceName}}: + image: {{.Image}} + restart: unless-stopped + + environment: | + + ports: + - "${GONE_DB_PORT}:5432" + + volumes: + - {{.Volume}} + +volumes: + {{.VolumeName}}: diff --git a/templates/rest/gin/go.mod.tmpl b/templates/rest/clean/go.mod.tmpl similarity index 100% rename from templates/rest/gin/go.mod.tmpl rename to templates/rest/clean/go.mod.tmpl diff --git a/templates/rest/gin/internal/config/config.go.tmpl b/templates/rest/clean/internal/config/config.go.tmpl similarity index 100% rename from templates/rest/gin/internal/config/config.go.tmpl rename to templates/rest/clean/internal/config/config.go.tmpl diff --git a/templates/rest/clean/internal/handler/example_handler.go.tmpl b/templates/rest/clean/internal/handler/example_handler.go.tmpl new file mode 100644 index 0000000..71f31dc --- /dev/null +++ b/templates/rest/clean/internal/handler/example_handler.go.tmpl @@ -0,0 +1,21 @@ +package handler + +import ( + "{{.ModuleName}}/internal/service" + "net/http" + + "github.com/gin-gonic/gin" +) + +type {{.Entity}}Handler struct { + Service *service.{{.Entity}}Service +} + +func New{{.Entity}}Handler(s *service.{{.Entity}}Service) *{{.Entity}}Handler { + return &{{.Entity}}Handler{Service: s} +} + +func (h *{{.Entity}}Handler) Get{{.Entity}}s(c *gin.Context) { + {{.LowerEntity}}s, _ := h.Service.Get{{.Entity}}s() + c.JSON(http.StatusOK, {{.LowerEntity}}s) +} diff --git a/templates/rest/clean/internal/model/example_model.go.tmpl b/templates/rest/clean/internal/model/example_model.go.tmpl new file mode 100644 index 0000000..076a318 --- /dev/null +++ b/templates/rest/clean/internal/model/example_model.go.tmpl @@ -0,0 +1,15 @@ +package model + +import ( + "gorm.io/gorm" +) + +type {{.Entity}} struct { + gorm.Model + ID int `json:"id"` + Name string `json:"name"` +} + +func init() { + Register(&{{.Entity}}{}) +} \ No newline at end of file diff --git a/templates/rest/clean/internal/model/registory.go.tmpl b/templates/rest/clean/internal/model/registory.go.tmpl new file mode 100644 index 0000000..9648607 --- /dev/null +++ b/templates/rest/clean/internal/model/registory.go.tmpl @@ -0,0 +1,7 @@ +package model + +var Registry []interface{} + +func Register(m interface{}) { + Registry = append(Registry, m) +} diff --git a/templates/rest/clean/internal/repository/example_repo.go.tmpl b/templates/rest/clean/internal/repository/example_repo.go.tmpl new file mode 100644 index 0000000..d4fe1d1 --- /dev/null +++ b/templates/rest/clean/internal/repository/example_repo.go.tmpl @@ -0,0 +1,24 @@ +package repository + +import ( + "{{.ModuleName}}/internal/model" + "gorm.io/gorm" +) + +type {{.Entity}}Repo struct { + DB *gorm.DB +} + +func New{{.Entity}}Repo(db *gorm.DB) *{{.Entity}}Repo { + return &{{.Entity}}Repo{DB: db} +} + +func (r *{{.Entity}}Repo) FindAll() ([]model.{{.Entity}}, error) { + var {{.LowerEntity}}s []model.{{.Entity}} + err := r.DB.Find(&{{.LowerEntity}}s).Error + return {{.LowerEntity}}s, err +} + +func (r *{{.Entity}}Repo) Create({{.LowerEntity}}*model.{{.Entity}}) error { + return r.DB.Create({{.LowerEntity}}).Error +} diff --git a/templates/rest/clean/internal/server/routes.go.tmpl b/templates/rest/clean/internal/server/routes.go.tmpl new file mode 100644 index 0000000..7083f8e --- /dev/null +++ b/templates/rest/clean/internal/server/routes.go.tmpl @@ -0,0 +1,58 @@ +package router + +import ( + "{{.ModuleName}}/internal/handler" + "{{.ModuleName}}/internal/repository" + "{{.ModuleName}}/internal/service" + {{.ImportRouter}} +) + +func (s *Server) RegisterRoutes() {{.HTTPHandler}} { + r := {{.Start}} + + r.{{.Get}}("/", s.HelloWorldHandler) + + r.{{.Get}}("/health", s.healthHandler) + + gormDB := s.db.GetDB() + + + api := r.Group("/api/v1") + + {{ if .Entities }} + {{ range $i, $entity := .Entities }} + {{ $upper := index $.UpperEntity $i }} + {{ $lower := $entity }} + + {{ printf "%sRepo := repository.New%sRepo(gormDB)" $lower $upper }} + {{ printf "%sService := service.New%sService(%sRepo)" $lower $upper $lower }} + {{ printf "%sHandler := handler.New%sHandler(%sService)" $lower $upper $lower }} + + {{ call $.ApiGroup $upper $.Get $lower }} + {{ end }} + {{ else }} + userRepo := repository.NewUserRepo(gormDB) + userService := service.NewUserService(userRepo) + userHandler := handler.NewUserHandler(userService) + + { + user := api.Group("/user") + { + user.GET("", userHandler.GetUsers) + } + } + {{ end }} + + return r +} + +func (s *Server) HelloWorldHandler({{.FullContext}}) {{.Returnable}} { + resp := make(map[string]string) + resp["message"] = "Hello World" + + {{.ReturnKeyword}} {{.ToTheClient}} resp) +} + +func (s *Server) healthHandler({{.FullContext}}) {{.Returnable}} { + {{.ReturnKeyword}} {{.ToTheClient}} s.db.Health()) +} diff --git a/templates/rest/clean/internal/server/server.go.tmpl b/templates/rest/clean/internal/server/server.go.tmpl new file mode 100644 index 0000000..00614e3 --- /dev/null +++ b/templates/rest/clean/internal/server/server.go.tmpl @@ -0,0 +1,50 @@ +package router + +import ( + "fmt" + "net/http" + "os" + "strconv" + {{- if .DBType }} + database "{{.ModuleName}}/internal/db" + {{- end }} + "time" + + "gorm.io/gorm" +) + +type Server struct { + port int + db database.Service + gormDB *gorm.DB +} + +func NewServer() *http.Server { + port, _ := strconv.Atoi(os.Getenv("PORT")) + + dbService := database.New() + if dbService == nil { + panic("database.New() returned nil — DB initialization failed") + } + + gormDB := dbService.GetDB() + if gormDB == nil { + panic("dbService.GetDB() returned nil — Gorm DB is not initialized") + } + + srv := &Server{ + port: port, + db: dbService, + gormDB: gormDB, + } + + httpServer := &http.Server{ + Addr: fmt.Sprintf(":%d", srv.port), + Handler: srv.RegisterRoutes(), + IdleTimeout: time.Minute, + ReadTimeout: 10 * time.Second, + WriteTimeout: 30 * time.Second, + } + + return httpServer +} diff --git a/templates/rest/clean/internal/service/example_service.go.tmpl b/templates/rest/clean/internal/service/example_service.go.tmpl new file mode 100644 index 0000000..d1d493f --- /dev/null +++ b/templates/rest/clean/internal/service/example_service.go.tmpl @@ -0,0 +1,18 @@ +package service + +import ( + "{{.ModuleName}}/internal/model" + "{{.ModuleName}}/internal/repository" +) + +type {{.Entity}}Service struct { + Repo *repository.{{.Entity}}Repo +} + +func New{{.Entity}}Service(repo *repository.{{.Entity}}Repo) *{{.Entity}}Service { + return &{{.Entity}}Service{Repo: repo} +} + +func (s *{{.Entity}}Service) Get{{.Entity}}s() ([]model.{{.Entity}}, error) { + return s.Repo.FindAll() +} \ No newline at end of file diff --git a/templates/rest/gin/cmd/main.go.tmpl b/templates/rest/gin/cmd/main.go.tmpl deleted file mode 100644 index f321e65..0000000 --- a/templates/rest/gin/cmd/main.go.tmpl +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "log" - - "{{.ModuleName}}/internal/config" - "{{.ModuleName}}/internal/router" - - "github.com/gin-gonic/gin" - - {{- if .DBType }} - "{{.ModuleName}}/internal/db" - {{- end }} -) - -func main() { - cfg := config.New() - - {{- if .DBType }} - db.Connect() - defer db.Close() - {{- end }} - - r := gin.Default() - router.RegisterRoutes(r) - - port := cfg.Port - if port == "" { - port = "{{.PortName}}" - } - - log.Printf("🚀 starting server on :%s", port) - if err := r.Run(":" + port); err != nil { - log.Fatal(err) - } -} diff --git a/templates/rest/gin/internal/handler/user_handler.go.tmpl b/templates/rest/gin/internal/handler/user_handler.go.tmpl deleted file mode 100644 index 07af570..0000000 --- a/templates/rest/gin/internal/handler/user_handler.go.tmpl +++ /dev/null @@ -1,32 +0,0 @@ -package handler - -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - -type User struct { - ID int `json:"id"` - Name string `json:"name"` -} - -var users = []User{ - {ID: 1, Name: "Alice"}, - {ID: 2, Name: "Bob"}, -} - -func GetUsers(c *gin.Context) { - c.JSON(http.StatusOK, users) -} - -func CreateUser(c *gin.Context) { - var newUser User - if err := c.BindJSON(&newUser); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - newUser.ID = len(users) + 1 - users = append(users, newUser) - c.JSON(http.StatusCreated, newUser) -} diff --git a/templates/rest/gin/internal/router/routes.go.tmpl b/templates/rest/gin/internal/router/routes.go.tmpl deleted file mode 100644 index 50a950e..0000000 --- a/templates/rest/gin/internal/router/routes.go.tmpl +++ /dev/null @@ -1,15 +0,0 @@ -package router - -import ( - "{{ .ModuleName }}/internal/handler" - - "github.com/gin-gonic/gin" -) - -func RegisterRoutes(r *gin.Engine) { - v1 := r.Group("/api/v1") - { - v1.GET("/users", handler.GetUsers) - v1.POST("/users", handler.CreateUser) - } -}