Public Access
feat: basalt 0.0.1
This commit is contained in:
22 files changed
+1364
-1
No files matched your search
@@ -1,7 +1,11 @@
|
||||
.DS_Store
|
||||
|
||||
# ---> Go
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
|
||||
assets/embedded/
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
@@ -25,3 +29,5 @@ go.work.sum
|
||||
# env file
|
||||
.env
|
||||
|
||||
# Binary
|
||||
/basalt
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "assets/testdata/api_tmpl"]
|
||||
path = assets/testdata/api_tmpl
|
||||
url = [email protected]:FernandoJVideira/api_tmpl.git
|
||||
@@ -0,0 +1,31 @@
|
||||
PKG_NAME="gitea.fernandovideira.com/FernandoJVideira/keystone"
|
||||
|
||||
version:
|
||||
@go run -ldflags "-X ${PKG_NAME}/cmd.version=v1.0.0" cmd/basalt/main.go version
|
||||
|
||||
generate-templates:
|
||||
@echo "Initializing submodules..."
|
||||
@git submodule update --init --recursive
|
||||
@echo "Generating embedded templates..."
|
||||
@go run tools/sync_templates.go
|
||||
|
||||
build: prepare-embed
|
||||
@echo "Building for $(GOOS)/$(GOARCH)..."
|
||||
@GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags "-X ${PKG_NAME}/cmd.version=v0.0.1" -o $(OUTPUT) ./cmd/basalt/main.go
|
||||
|
||||
windows:
|
||||
@$(MAKE) build GOOS=windows GOARCH=amd64 OUTPUT=basalt.exe
|
||||
|
||||
win-arm:
|
||||
@$(MAKE) build GOOS=windows GOARCH=arm64 OUTPUT=basalt.exe
|
||||
|
||||
linux:
|
||||
@$(MAKE) build GOOS=linux GOARCH=amd64 OUTPUT=basalt
|
||||
|
||||
mac:
|
||||
@$(MAKE) build GOOS=darwin GOARCH=arm64 OUTPUT=basalt
|
||||
|
||||
prepare-embed:
|
||||
@git submodule update --init --recursive
|
||||
@mkdir -p assets/embedded
|
||||
@go run tools/sync_templates.go
|
||||
@@ -1,2 +1,160 @@
|
||||
# keystone
|
||||
# Keystone
|
||||
|
||||
A powerful CLI tool for bootstrapping Go API projects with best practices built-in. Keystone helps you quickly scaffold production-ready Go APIs with support for both HTTP (REST) and gRPC architectures.
|
||||
|
||||
## Features
|
||||
|
||||
- 🚀 **Quick Project Scaffolding** - Generate complete API projects with a single command
|
||||
- 🔄 **Multiple API Types** - Support for both HTTP (REST) and gRPC architectures
|
||||
- 📦 **Service Generation** - Easily create new services with proper structure
|
||||
- 🎯 **Handler Generation** - Generate HTTP or gRPC handlers on-demand
|
||||
- 🔐 **Cryptographic Key Generator** - Built-in secure key generation
|
||||
- 🛠️ **Pre-configured Tooling** - Automatic installation of essential Go tools
|
||||
- 📝 **Template-based** - Consistent code generation using embedded templates
|
||||
|
||||
## Installation
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yourusername/keystone.git
|
||||
cd keystone
|
||||
make build
|
||||
```
|
||||
|
||||
### Build for Different Platforms
|
||||
|
||||
```bash
|
||||
# macOS (ARM64)
|
||||
make mac
|
||||
|
||||
# Linux (AMD64)
|
||||
make linux
|
||||
|
||||
# Windows (AMD64)
|
||||
make windows
|
||||
|
||||
# Windows (ARM64)
|
||||
make win-arm
|
||||
```
|
||||
|
||||
The compiled binary will be named `basalt` (or `basalt.exe` on Windows).
|
||||
|
||||
## Usage
|
||||
|
||||
### Create a New API Project
|
||||
|
||||
```bash
|
||||
keystone new api
|
||||
```
|
||||
|
||||
This interactive command will:
|
||||
1. Prompt you for a project name (module name)
|
||||
2. Ask you to select an API type (HTTP REST or gRPC)
|
||||
3. Scaffold the complete project structure
|
||||
4. Install required development tools
|
||||
5. Run `go mod tidy` to organize dependencies
|
||||
|
||||
**Tools Automatically Installed:**
|
||||
- `air` - Live reload for Go apps
|
||||
- `golangci-lint` - Go linters aggregator
|
||||
- `mockgen` - Mock generation for testing
|
||||
- `sqlc` - SQL compiler for type-safe database code
|
||||
- `migrate` - Database migration tool
|
||||
- `swag` - Swagger documentation (HTTP only)
|
||||
- `protoc-gen-*` - Protocol Buffer generators (gRPC only)
|
||||
|
||||
### Generate a New Service
|
||||
|
||||
```bash
|
||||
keystone new service
|
||||
```
|
||||
|
||||
Creates a new service file in `internal/service/` with:
|
||||
- Service interface definition
|
||||
- Service struct implementation
|
||||
- Compile-time interface checks
|
||||
|
||||
### Generate a New Handler
|
||||
|
||||
```bash
|
||||
keystone new handler
|
||||
```
|
||||
|
||||
Creates a new handler in the appropriate directory:
|
||||
- HTTP handlers → `internal/handlers/http/`
|
||||
- gRPC handlers → `internal/handlers/grpc/`
|
||||
|
||||
### Generate a Cryptographic Key
|
||||
|
||||
```bash
|
||||
keystone new key
|
||||
```
|
||||
|
||||
Generates a secure random key for use in your application (default: 32 characters).
|
||||
|
||||
### Check Version
|
||||
|
||||
```bash
|
||||
keystone version
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
When you create a new project, Keystone generates a structured Go API with:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── cmd/
|
||||
│ └── api/
|
||||
│ └── main.go
|
||||
├── internal/
|
||||
│ ├── handlers/
|
||||
│ │ ├── http/ (if HTTP selected)
|
||||
│ │ └── grpc/ (if gRPC selected)
|
||||
│ └── service/
|
||||
├── proto/ (if gRPC selected)
|
||||
├── pb/ (if gRPC selected)
|
||||
└── go.mod
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Go 1.25.5 or higher
|
||||
- Make (for building from source)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- [Cobra](https://github.com/spf13/cobra) - CLI framework
|
||||
- [promptui](https://github.com/manifoldco/promptui) - Interactive prompts
|
||||
- [golang.org/x/text](https://golang.org/x/text) - Text processing utilities
|
||||
|
||||
## Development
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
### Run with Version Flag
|
||||
|
||||
```bash
|
||||
make version
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
## Author
|
||||
|
||||
Built by LKResuts for internal usage to bootstrap Go APIs.
|
||||
|
||||
---
|
||||
|
||||
**Note:** The binary is compiled as `basalt`, but the CLI tool is referred to as `keystone` in documentation and usage.
|
||||
@@ -0,0 +1,7 @@
|
||||
package assets
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:generate go run ../tools/sync_templates.go
|
||||
//go:embed templates/* embedded/*
|
||||
var EmbedFS embed.FS
|
||||
@@ -0,0 +1,39 @@
|
||||
package gapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"{{ .ModuleName }}/pb" // We need the module name to import pb correctly!
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/genproto/googleapis/rpc/errdetails"
|
||||
)
|
||||
|
||||
func (s *Server) {{ .Name }}(ctx context.Context, req *pb.{{ .Name }}Request) (*pb.{{ .Name }}Response, error) {
|
||||
violations := validate{{ .Name }}Request(req)
|
||||
if violations != nil {
|
||||
return nil, invalidArgumentError(violations)
|
||||
}
|
||||
|
||||
// TODO: Call service layer
|
||||
// result, err := s.service.{{ .Name }}(ctx, ...)
|
||||
|
||||
/*
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to execute: %v", err)
|
||||
}
|
||||
*/
|
||||
|
||||
resp := &pb.{{ .Name }}Response{
|
||||
// TODO: Map result to response
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func validate{{ .Name }}Request(req *pb.{{ .Name }}Request) (violations []*errdetails.BadRequest_FieldViolation) {
|
||||
// TODO: Add validation logic
|
||||
// if err := val.ValidateString(req.GetField()); err != nil {
|
||||
// violations = append(violations, fieldViolation("field", err))
|
||||
// }
|
||||
return violations
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
// We can assume imports like 'response' are available in the same package
|
||||
// or we might need to fix imports if this file is in a subfolder.
|
||||
)
|
||||
|
||||
type {{ .Name }}Request struct {
|
||||
// TODO: Add request fields
|
||||
// Username string `json:"username" binding:"required"`
|
||||
}
|
||||
|
||||
type {{ .Name }}Response struct {
|
||||
// TODO: Add response fields
|
||||
}
|
||||
|
||||
// {{ .Name }} godoc
|
||||
// @Summary {{ .Name }}
|
||||
// @Description {{ .Name }} handler
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body {{ .Name }}Request true "Request body"
|
||||
// @Success 200 {object} response.JSONResponse{data={{ .Name }}Response}
|
||||
// @Router /v1/{{ .Name }} [post]
|
||||
func (s *Server) create{{ .Name }}(ctx *gin.Context) {
|
||||
var req {{ .Name }}Request
|
||||
// We assume response.JSONResponse is available or imported
|
||||
var resp response.JSONResponse
|
||||
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
resp = response.JSONResponse{
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
}
|
||||
ctx.JSON(http.StatusBadRequest, resp)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Call your service here
|
||||
// result, err := s.service.{{ .Name }}(ctx, req)
|
||||
|
||||
/*
|
||||
if err != nil {
|
||||
resp = response.JSONResponse{
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
}
|
||||
ctx.JSON(http.StatusInternalServerError, resp)
|
||||
return
|
||||
}
|
||||
*/
|
||||
|
||||
resp = response.JSONResponse{
|
||||
Success: true,
|
||||
Message: "Operation successful",
|
||||
Data: nil, // Replace with actual data
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
{{ if .UseGRPC }}
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
{{ end }}
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
{{ if .UseGRPC }}
|
||||
"github.com/rs/cors"
|
||||
{{ end }}
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"{{ .ModuleName }}/internal/auth"
|
||||
"{{ .ModuleName }}/internal/config"
|
||||
db "{{ .ModuleName }}/internal/db/sqlc"
|
||||
{{ if .UseGRPC }}
|
||||
gapi "{{ .ModuleName }}/internal/handlers/grpc"
|
||||
{{ end }}
|
||||
{{ if not .UseGRPC }}
|
||||
api "{{ .ModuleName }}/internal/handlers/http"
|
||||
{{ end }}
|
||||
"{{ .ModuleName }}/internal/mailer"
|
||||
"{{ .ModuleName }}/internal/service"
|
||||
"{{ .ModuleName }}/internal/worker"
|
||||
{{ if .UseGRPC }}
|
||||
"{{ .ModuleName }}/pb"
|
||||
{{ end }}
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
{{ if .UseGRPC }}
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"github.com/rakyll/statik/fs"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/reflection"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
|
||||
_ "{{ .ModuleName }}/docs/statik"
|
||||
{{ end }}
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
|
||||
|
||||
)
|
||||
|
||||
var interruptSignals = []os.Signal{
|
||||
os.Interrupt,
|
||||
syscall.SIGTERM,
|
||||
syscall.SIGINT,
|
||||
}
|
||||
|
||||
// @title Simple Bank API
|
||||
// @version 1.0
|
||||
// @description API for Simple Bank application
|
||||
// @termsOfService http://swagger.io/terms/
|
||||
|
||||
// @contact.name API Support
|
||||
// @contact.email [email protected]
|
||||
|
||||
// @license.name MIT
|
||||
// @license.url https://opensource.org/licenses/MIT
|
||||
|
||||
// @host localhost:8080
|
||||
// @BasePath /v1
|
||||
// @schemes http https
|
||||
|
||||
// @securityDefinitions.apikey BearerAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @description Type "Bearer" followed by a space and JWT token.
|
||||
func main() {
|
||||
config, err := config.LoadConfig(".")
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("cannot load config")
|
||||
}
|
||||
|
||||
if config.Environment == "development" {
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), interruptSignals...)
|
||||
defer cancel()
|
||||
|
||||
connPool, err := pgxpool.New(ctx, config.DBAddr)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("cannot connect to db")
|
||||
}
|
||||
|
||||
runDBMigration(config.MigrationURL, config.DBAddr)
|
||||
|
||||
redisOpt := asynq.RedisClientOpt{
|
||||
Addr: config.RedisAddr,
|
||||
Password: config.RedisPassword,
|
||||
DB: config.RedisDB,
|
||||
}
|
||||
|
||||
taskDist := worker.NewRedisTaskDistributor(redisOpt)
|
||||
store := db.NewPostgresStore(connPool)
|
||||
|
||||
tokenMaker, err := auth.NewPasetoMaker(config)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("cannot create token maker")
|
||||
}
|
||||
|
||||
userService := service.NewUserService(store, taskDist)
|
||||
authService := service.NewAuthService(store, tokenMaker, config)
|
||||
|
||||
wg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
{{ if not .UseGRPC }}
|
||||
runGinServer(ctx, wg, config, store, taskDist, tokenMaker, userService, authService)
|
||||
{{ else }}
|
||||
// Assuming you want Gateway AND gRPC when gRPC is selected
|
||||
runGatewayServer(ctx, wg, config, store, taskDist, tokenMaker, userService, authService)
|
||||
runGrpcServer(ctx, wg, config, store, taskDist, tokenMaker, userService, authService)
|
||||
{{ end }}
|
||||
|
||||
runTaskProcessor(ctx, wg, redisOpt, store, config)
|
||||
|
||||
err = wg.Wait()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("application terminated with error")
|
||||
}
|
||||
}
|
||||
|
||||
func runTaskProcessor(ctx context.Context, wg *errgroup.Group, redisOpt asynq.RedisClientOpt, store db.Store, config config.Config) {
|
||||
mailer := mailer.NewGmailSender(config.EmailSenderName, config.EmailSender, config.EmailPassword)
|
||||
taskProcessor := worker.NewRedisTaskProcessor(redisOpt, store, mailer, config)
|
||||
log.Info().Msg("start task processor")
|
||||
if err := taskProcessor.Start(); err != nil {
|
||||
log.Fatal().Err(err).Msg("cannot start task processor")
|
||||
}
|
||||
|
||||
wg.Go(func() error {
|
||||
<-ctx.Done()
|
||||
log.Info().Msg("gracefully shutting down task processor")
|
||||
taskProcessor.Shutdown()
|
||||
log.Info().Msg("task processor shut down")
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func runDBMigration(migrationURL, dbSource string) {
|
||||
migration, err := migrate.New(migrationURL, dbSource)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("cannot create new migrate instance")
|
||||
}
|
||||
|
||||
if err = migration.Up(); err != nil && err != migrate.ErrNoChange {
|
||||
log.Fatal().Err(err).Msg("failed to run migrate up")
|
||||
}
|
||||
|
||||
log.Info().Msg("db migrated successfully")
|
||||
}
|
||||
|
||||
{{ if not .UseGRPC }}
|
||||
func runGinServer(ctx context.Context, wg *errgroup.Group, config config.Config, store db.Store, td worker.TaskDistributor, tokenMaker auth.Maker, userService service.UserService, authService service.AuthService) {
|
||||
s, err := api.NewServer(config, store, td, tokenMaker, userService, authService)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("cannot create server")
|
||||
}
|
||||
|
||||
wg.Go(func() error {
|
||||
err = s.Start(config.HTTPServerAddr)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("cannot start server:")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
wg.Go(func() error {
|
||||
<-ctx.Done()
|
||||
log.Info().Msg("shutting down server")
|
||||
err := s.Shutdown(ctx)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("cannot shutdown server")
|
||||
return err
|
||||
}
|
||||
log.Info().Msg("server shut down")
|
||||
return nil
|
||||
})
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
{{ if .UseGRPC }}
|
||||
func runGrpcServer(ctx context.Context, wg *errgroup.Group, config config.Config, store db.Store, taskDistributor worker.TaskDistributor, tokenMaker auth.Maker, userService service.UserService, authService service.AuthService) {
|
||||
server, err := gapi.NewServer(config, store, taskDistributor, tokenMaker, userService, authService)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("cannot create server:")
|
||||
}
|
||||
|
||||
grpcLogger := grpc.UnaryInterceptor(gapi.GrpcLogger)
|
||||
|
||||
grpcServer := grpc.NewServer(grpcLogger)
|
||||
pb.RegisterSimpleBankServer(grpcServer, server)
|
||||
reflection.Register(grpcServer)
|
||||
|
||||
listener, err := net.Listen("tcp", config.GrpcServerAddr)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("cannot create listener")
|
||||
}
|
||||
|
||||
wg.Go(func() error {
|
||||
log.Info().Msgf("start gRPC server at %s", listener.Addr().String())
|
||||
err = grpcServer.Serve(listener)
|
||||
if err != nil {
|
||||
if errors.Is(err, grpc.ErrServerStopped) {
|
||||
return nil
|
||||
}
|
||||
log.Error().Err(err).Msg("gRPC server failed to serve")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
wg.Go(func() error {
|
||||
<-ctx.Done()
|
||||
log.Info().Msg("gracefully shutting down gRPC server")
|
||||
grpcServer.GracefulStop()
|
||||
log.Info().Msg("gRPC server shut down")
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func runGatewayServer(ctx context.Context, wg *errgroup.Group, config config.Config, store db.Store, taskDistributor worker.TaskDistributor, tokenMaker auth.Maker, userService service.UserService, authService service.AuthService) {
|
||||
server, err := gapi.NewServer(config, store, taskDistributor, tokenMaker, userService, authService)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("cannot create server:")
|
||||
}
|
||||
|
||||
opts := runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{
|
||||
MarshalOptions: protojson.MarshalOptions{
|
||||
UseProtoNames: true,
|
||||
},
|
||||
UnmarshalOptions: protojson.UnmarshalOptions{
|
||||
DiscardUnknown: true,
|
||||
},
|
||||
})
|
||||
|
||||
grpcMux := runtime.NewServeMux(opts)
|
||||
|
||||
err = pb.RegisterSimpleBankHandlerServer(ctx, grpcMux, server)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("cannot register handler server")
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", grpcMux)
|
||||
|
||||
statikFS, err := fs.New()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("cannot create statik fs")
|
||||
}
|
||||
|
||||
swaggerHandler := http.StripPrefix("/swagger/", http.FileServer(statikFS))
|
||||
mux.Handle("/swagger/", swaggerHandler)
|
||||
|
||||
c := cors.New(
|
||||
cors.Options{
|
||||
AllowedOrigins: config.AllowedOrigins,
|
||||
AllowedMethods: []string{
|
||||
http.MethodGet,
|
||||
http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodPatch,
|
||||
http.MethodDelete,
|
||||
http.MethodOptions,
|
||||
},
|
||||
AllowedHeaders: []string{
|
||||
"Accept",
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"X-CSRF-Token",
|
||||
},
|
||||
AllowCredentials: true,
|
||||
Debug: config.Environment == "development",
|
||||
},
|
||||
)
|
||||
handler := c.Handler(gapi.HTTPLogger(mux))
|
||||
httpServer := &http.Server{
|
||||
Handler: handler,
|
||||
Addr: config.HTTPServerAddr,
|
||||
}
|
||||
|
||||
wg.Go(func() error {
|
||||
log.Info().Msgf("start HTTP gateway server at %s", httpServer.Addr)
|
||||
|
||||
err = httpServer.ListenAndServe()
|
||||
if err != nil {
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
return nil
|
||||
}
|
||||
log.Error().Err(err).Msg("cannot start http gateway server")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
wg.Go(func() error {
|
||||
<-ctx.Done()
|
||||
log.Info().Msg("gracefully shutting down HTTP gateway server")
|
||||
err := httpServer.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("cannot shut down http gateway server")
|
||||
return err
|
||||
}
|
||||
log.Info().Msg("HTTP gateway server shut down")
|
||||
return nil
|
||||
})
|
||||
|
||||
}
|
||||
{{ end }}
|
||||
@@ -0,0 +1,23 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
db "{{ .ModuleName }}/internal/db/sqlc"
|
||||
// TODO: Add other imports as needed
|
||||
// "{{ .ModuleName }}/internal/worker"
|
||||
)
|
||||
|
||||
type {{ .LowerName }}Service struct {
|
||||
store db.Store
|
||||
// TODO: Add other dependencies
|
||||
// taskDistributor worker.TaskDistributor
|
||||
}
|
||||
|
||||
// New{{ .Name }}Service creates a new {{ .LowerName }} service
|
||||
func New{{ .Name }}Service(store db.Store) {{ .Name }}Service {
|
||||
return &{{ .LowerName }}Service{
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
+1
Submodule assets/testdata/api_tmpl added at 961bdd81d4.
+178
@@ -0,0 +1,178 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitea.fernandovideira.com/FernandoJVideira/keystone/assets"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
newCmd.AddCommand(apiCmd)
|
||||
}
|
||||
|
||||
var apiCmd = &cobra.Command{
|
||||
Use: "api",
|
||||
Short: "Create a new Keystone API project",
|
||||
Long: `Create a new Keystone API project with the necessary structure and files.`,
|
||||
Run: genProject,
|
||||
}
|
||||
|
||||
func genProject(cmd *cobra.Command, args []string) {
|
||||
|
||||
projectNamePrompt := promptui.Prompt{
|
||||
Label: "Project Name",
|
||||
}
|
||||
|
||||
rawInput, err := projectNamePrompt.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Prompt failed %v\n", err)
|
||||
return
|
||||
}
|
||||
moduleName := strings.TrimSpace(rawInput)
|
||||
projectDir := path.Base(strings.TrimSuffix(moduleName, "/"))
|
||||
|
||||
apiType := promptui.Select{
|
||||
Label: "Select API Type",
|
||||
Items: []string{"HTTP (REST)", "gRPC"},
|
||||
}
|
||||
|
||||
_, result, err := apiType.Run()
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Prompt failed %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Logic to map the selection to a boolean
|
||||
useGRPC := strings.Contains(result, "gRPC")
|
||||
|
||||
fmt.Printf("🚀 Scaffolding '%s' as %s...\n", moduleName, result)
|
||||
if err := scaffoldProject(projectDir, moduleName, useGRPC); err != nil {
|
||||
fmt.Printf("error scaffolding project: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("📦 Installing tools...")
|
||||
if err := installTools(useGRPC); err != nil {
|
||||
fmt.Printf("error installing tools: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("🧹 Tidying up Go modules...")
|
||||
if err := runCommand(projectDir, "go", "mod", "tidy"); err != nil {
|
||||
fmt.Printf("error running 'go mod tidy': %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("✅ Done!")
|
||||
}
|
||||
|
||||
func scaffoldProject(projectDir, moduleName string, useGRPC bool) error {
|
||||
|
||||
err := os.MkdirAll(projectDir, 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating project directory: %v", err)
|
||||
}
|
||||
|
||||
root := "embedded"
|
||||
err = fs.WalkDir(assets.EmbedFS, root, func(path string, d fs.DirEntry, err error) error {
|
||||
|
||||
relPath, _ := filepath.Rel(root, path)
|
||||
|
||||
if !useGRPC {
|
||||
// If User chose HTTP: Remove gRPC stuff
|
||||
if strings.HasPrefix(relPath, "pb") ||
|
||||
strings.HasPrefix(relPath, "proto") ||
|
||||
strings.Contains(relPath, "handlers/grpc") ||
|
||||
strings.HasPrefix(relPath, "docs/statik") ||
|
||||
strings.HasPrefix(relPath, "docs/swagger") {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
// If User chose gRPC: Remove HTTP stuff
|
||||
if strings.Contains(relPath, "handlers/http") {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasSuffix(relPath, "cmd/api/main.go") {
|
||||
return nil
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
contentBytes, err := assets.EmbedFS.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading embedded file %s: %v", path, err)
|
||||
}
|
||||
content := string(contentBytes)
|
||||
|
||||
targetPath := filepath.Join(projectDir, relPath)
|
||||
|
||||
targetPath = strings.TrimSuffix(targetPath, ".tmpl")
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||
return fmt.Errorf("error creating directory %s: %v", filepath.Dir(targetPath), err)
|
||||
}
|
||||
|
||||
newContent := strings.ReplaceAll(content, oldModuleName, moduleName)
|
||||
newContent = strings.ReplaceAll(newContent, "{{ .ModuleName }}", moduleName)
|
||||
|
||||
return os.WriteFile(targetPath, []byte(newContent), 0644)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error copying template files: %v\n", err)
|
||||
}
|
||||
|
||||
// Generate main.go from template
|
||||
return genFile(
|
||||
"templates/main.go.tpl",
|
||||
"main",
|
||||
filepath.Join(projectDir, "cmd/api", "main.go"),
|
||||
MainTemplateData{
|
||||
ModuleName: moduleName,
|
||||
UseGRPC: useGRPC,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func installTools(useGrpc bool) error {
|
||||
tools := []string{
|
||||
"github.com/air-verse/air@latest",
|
||||
"github.com/golangci/golangci-lint/cmd/golangci-lint@latest",
|
||||
"go.uber.org/mock/mockgen@latest",
|
||||
"github.com/sqlc-dev/sqlc/cmd/sqlc@latest",
|
||||
"github.com/golang-migrate/migrate/v4/cmd/migrate@latest",
|
||||
}
|
||||
|
||||
if useGrpc {
|
||||
tools = append(tools,
|
||||
"google.golang.org/protobuf/cmd/protoc-gen-go@latest",
|
||||
"google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest",
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest",
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest",
|
||||
)
|
||||
} else {
|
||||
tools = append(tools,
|
||||
"github.com/swaggo/swag/cmd/swag@latest",
|
||||
)
|
||||
}
|
||||
|
||||
for _, tool := range tools {
|
||||
if err := runCommand("", "go", "install", tool); err != nil {
|
||||
return fmt.Errorf("error installing tool %s: %v", tool, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "gitea.fernandovideira.com/FernandoJVideira/keystone/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var handlerTypes = map[string]string{
|
||||
"HTTP (REST)": "http",
|
||||
"gRPC": "grpc",
|
||||
}
|
||||
|
||||
func init() {
|
||||
newCmd.AddCommand(handlerCmd)
|
||||
}
|
||||
|
||||
var handlerCmd = &cobra.Command{
|
||||
Use: "handler",
|
||||
Short: "Create a new handler",
|
||||
Long: `Create a new handler for your Keystone project.`,
|
||||
Run: genHandler,
|
||||
}
|
||||
|
||||
type HandlerTemplateData struct {
|
||||
ModuleName string
|
||||
Name string
|
||||
}
|
||||
|
||||
func genHandler(cmd *cobra.Command, args []string) {
|
||||
namePrompt := promptui.Prompt{
|
||||
Label: "Handler Name",
|
||||
}
|
||||
|
||||
handlerNameInput, err := namePrompt.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Prompt failed %v\n", err)
|
||||
return
|
||||
}
|
||||
handlerTypeInput := promptui.Select{
|
||||
Label: "Select API Type",
|
||||
Items: []string{"HTTP (REST)", "gRPC"},
|
||||
}
|
||||
|
||||
_, result, err := handlerTypeInput.Run()
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Prompt failed %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
moduleName, err := getModuleName()
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error retrieving module name: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
mappedHandlerType := handlerTypes[result]
|
||||
handlerName := strings.ToLower(handlerNameInput)
|
||||
cases := cases.Title(language.English)
|
||||
|
||||
fmt.Printf("🚀 Creating handler '%s' as %s...\n", handlerNameInput, result)
|
||||
|
||||
if err := genFile(
|
||||
fmt.Sprintf("templates/handler_%s.go.tpl", mappedHandlerType),
|
||||
handlerName,
|
||||
filepath.Join("internal/handlers", mappedHandlerType, fmt.Sprintf("%s.go", handlerName)),
|
||||
HandlerTemplateData{
|
||||
ModuleName: moduleName,
|
||||
Name: cases.String(handlerNameInput),
|
||||
},
|
||||
); err != nil {
|
||||
fmt.Printf("❌ Error creating handler: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Handler '%s' created successfully at %s\n", handlerNameInput, filepath.Join("internal/handlers", mappedHandlerType, fmt.Sprintf("%s.go", handlerName)))
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"gitea.fernandovideira.com/FernandoJVideira/keystone/assets"
|
||||
)
|
||||
|
||||
const (
|
||||
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_+"
|
||||
)
|
||||
|
||||
func randomString(length int) string {
|
||||
s, r := make([]rune, length), []rune(chars)
|
||||
|
||||
for i := range s {
|
||||
p, _ := rand.Prime(rand.Reader, len(r))
|
||||
x, y := p.Uint64(), uint64(len(r))
|
||||
s[i] = r[x%y]
|
||||
}
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func genFile(tplPath, tplName, outputPath string, data any) error {
|
||||
tplContent, err := assets.EmbedFS.ReadFile(tplPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading %s template: %w", tplPath, err)
|
||||
}
|
||||
|
||||
tpl, err := template.New(tplName).Parse(string(tplContent))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing %s template: %w", tplPath, err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
|
||||
return fmt.Errorf("error ensuring directory for %s: %w", outputPath, err)
|
||||
}
|
||||
|
||||
outFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating %s: %w", outputPath, err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
return tpl.Execute(outFile, data)
|
||||
}
|
||||
|
||||
func getModuleName() (string, error) {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get working directory: %w", err)
|
||||
}
|
||||
|
||||
for {
|
||||
goModPath := filepath.Join(dir, "go.mod")
|
||||
file, err := os.Open(goModPath)
|
||||
if err == nil {
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if str, ok := strings.CutPrefix(line, "module "); ok {
|
||||
return strings.TrimSpace(str), nil
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return "", fmt.Errorf("failed to read %s: %w", goModPath, err)
|
||||
}
|
||||
return "", fmt.Errorf("module directive not found in %s", goModPath)
|
||||
}
|
||||
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return "", fmt.Errorf("failed to open %s: %w", goModPath, err)
|
||||
}
|
||||
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
return "", fmt.Errorf("go.mod not found starting from %s", dir)
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
|
||||
func runCommand(dir, name string, args ...string) error {
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
newCmd.AddCommand(keyCmd)
|
||||
}
|
||||
|
||||
var size int
|
||||
var defaultSize = 32
|
||||
|
||||
var keyCmd = &cobra.Command{
|
||||
Use: "key [size]",
|
||||
Short: "Generate a new cryptographic key",
|
||||
Long: `Generate a new cryptographic key for use in Keystone projects.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var keyLength int
|
||||
if cmd.Flags().Lookup("size").Changed {
|
||||
keyLength = size
|
||||
} else {
|
||||
keyLength = defaultSize
|
||||
}
|
||||
|
||||
if keyLength <= 32 {
|
||||
fmt.Println("Warning: It is recommended to use a key size greater than 32 for better security.")
|
||||
fmt.Println("Generating a key of default size:", defaultSize)
|
||||
keyLength = defaultSize
|
||||
}
|
||||
|
||||
key := randomString(keyLength)
|
||||
println("Generated Key:", key)
|
||||
},
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const oldModuleName = "github.com/FernandoJVideira/simple_bank"
|
||||
const srcDir = "embedded/api_tmpl"
|
||||
|
||||
type MainTemplateData struct {
|
||||
ModuleName string
|
||||
UseGRPC bool
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(newCmd)
|
||||
}
|
||||
|
||||
var newCmd = &cobra.Command{
|
||||
Use: "new",
|
||||
Short: "Create a new Keystone project",
|
||||
Long: `Create a new Keystone project with the necessary structure and files.`,
|
||||
Run: genProject,
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "keystone",
|
||||
Short: "Keystone is a CLI tool for bootstrapping Go APIs",
|
||||
Long: `Keystone is a CLI tool built by LKResuts for internal usage to bootstrap Go APIs`,
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
func init() {
|
||||
newCmd.AddCommand(serviceCmd)
|
||||
}
|
||||
|
||||
type ServiceTemplateData struct {
|
||||
ModuleName string
|
||||
Name string
|
||||
LowerName string
|
||||
}
|
||||
|
||||
var serviceCmd = &cobra.Command{
|
||||
Use: "service",
|
||||
Short: "Create a new service",
|
||||
Long: `Create a new service for your Keystone project.`,
|
||||
Run: genService,
|
||||
}
|
||||
|
||||
func genService(cmd *cobra.Command, args []string) {
|
||||
namePrompt := promptui.Prompt{
|
||||
Label: "Service Name",
|
||||
}
|
||||
|
||||
serviceNameInput, err := namePrompt.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Prompt failed %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
moduleName, err := getModuleName()
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error retrieving module name: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
print(moduleName)
|
||||
|
||||
serviceName := strings.ToLower(strings.TrimSpace(serviceNameInput))
|
||||
cases := cases.Title(language.English)
|
||||
|
||||
fmt.Printf("🚀 Creating service '%s'...\n", serviceName)
|
||||
|
||||
if err := genFile(
|
||||
"templates/service.go.tpl",
|
||||
serviceName,
|
||||
fmt.Sprintf("internal/service/%s.go", serviceName),
|
||||
ServiceTemplateData{
|
||||
ModuleName: moduleName,
|
||||
Name: cases.String(serviceName),
|
||||
LowerName: serviceName,
|
||||
},
|
||||
); err != nil {
|
||||
fmt.Printf("❌ Error creating service: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
updateServiceFile(serviceName, cases)
|
||||
|
||||
fmt.Printf("✅ Service '%s' created successfully!\n", serviceNameInput)
|
||||
|
||||
}
|
||||
|
||||
func updateServiceFile(serviceName string, cases cases.Caser) {
|
||||
currDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error getting current directory: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
serviceFilePath := fmt.Sprintf("%s/internal/service/service.go", currDir)
|
||||
|
||||
content, err := os.ReadFile(serviceFilePath)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error reading service file: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fileContent := string(content)
|
||||
srvc := cases.String(serviceName)
|
||||
|
||||
interfaceSnippet := fmt.Sprintf(`// %sService defines operations for %s
|
||||
type %sService interface {
|
||||
// TODO: Add service methods
|
||||
}
|
||||
|
||||
`, srvc, srvc, srvc)
|
||||
|
||||
compileTimeCheck := fmt.Sprintf("\t_ %sService = (*%sService)(nil)",
|
||||
srvc, serviceName)
|
||||
|
||||
fileContent = insertInterface(fileContent, interfaceSnippet)
|
||||
fileContent = strings.Replace(fileContent, "var (", "var (\n"+compileTimeCheck, 1)
|
||||
|
||||
err = os.WriteFile(serviceFilePath, []byte(fileContent), 0644)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error writing to service file: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Service interface for '%s' added successfully!\n", serviceName)
|
||||
}
|
||||
|
||||
func insertInterface(fileContent, interfaceSnippet string) string {
|
||||
parts := strings.SplitN(fileContent, "var (", 2)
|
||||
|
||||
if len(parts) != 2 {
|
||||
return fileContent
|
||||
}
|
||||
|
||||
return parts[0] + interfaceSnippet + "var (" + parts[1]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Default version (we will change this dynamically later!)
|
||||
var version = "dev"
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Display the version of Keystone",
|
||||
Long: `Display the current version of the Keystone CLI tool.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("Keystone CLI version: %s\n", version)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
module gitea.fernandovideira.com/FernandoJVideira/keystone
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
golang.org/x/text v0.31.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -0,0 +1,69 @@
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
sourceDir = "assets/testdata/api_tmpl"
|
||||
targetDir = "assets/embedded"
|
||||
oldModuleName = "github.com/FernandoJVideira/simple_bank"
|
||||
moduleTemplate = "{{ .ModuleName }}"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.Println("Syncing templates...")
|
||||
syncTemplates()
|
||||
}
|
||||
|
||||
func syncTemplates() {
|
||||
|
||||
err := filepath.WalkDir(sourceDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relPath, _ := filepath.Rel(sourceDir, path)
|
||||
destPath := filepath.Join(targetDir, relPath)
|
||||
|
||||
if strings.HasPrefix(d.Name(), ".") && d.Name() != "." {
|
||||
if d.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return os.MkdirAll(destPath, 0755)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.Name() == "go.mod" || d.Name() == "go.sum" {
|
||||
destPath += ".tmpl"
|
||||
}
|
||||
|
||||
if d.Name() == "go.mod" {
|
||||
strContent := string(content)
|
||||
strContent = strings.Replace(strContent, oldModuleName, moduleTemplate, 1)
|
||||
content = []byte(strContent)
|
||||
}
|
||||
|
||||
return os.WriteFile(destPath, content, 0644)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error walking source: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Templates synced successfully!")
|
||||
}
|
||||
Reference in new issue
Block a user