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
|
# ---> Go
|
||||||
# If you prefer the allow list template instead of the deny list, see community template:
|
# 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
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
#
|
#
|
||||||
|
|
||||||
|
assets/embedded/
|
||||||
# Binaries for programs and plugins
|
# Binaries for programs and plugins
|
||||||
*.exe
|
*.exe
|
||||||
*.exe~
|
*.exe~
|
||||||
@@ -25,3 +29,5 @@ go.work.sum
|
|||||||
# env file
|
# env file
|
||||||
.env
|
.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