Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ See `go-blueprint create -h` for all the options and shorthands.
- [HttpRouter](https://github.com/julienschmidt/httprouter)
- [Gorilla/mux](https://github.com/gorilla/mux)
- [Echo](https://github.com/labstack/echo)
- [Huma](https://github.com/danielgtaylor/huma) (built on Chi, with OpenAPI 3.1 docs and request/response validation)

<a id="database-support"></a>

Expand Down
3 changes: 2 additions & 1 deletion cmd/flags/frameworks.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ const (
HttpRouter Framework = "httprouter"
StandardLibrary Framework = "standard-library"
Echo Framework = "echo"
Huma Framework = "huma"
)

var AllowedProjectTypes = []string{string(Chi), string(Gin), string(Fiber), string(GorillaMux), string(HttpRouter), string(StandardLibrary), string(Echo)}
var AllowedProjectTypes = []string{string(Chi), string(Gin), string(Fiber), string(GorillaMux), string(HttpRouter), string(StandardLibrary), string(Echo), string(Huma)}

func (f Framework) String() string {
return string(f)
Expand Down
6 changes: 6 additions & 0 deletions cmd/program/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ var (
ginPackage = []string{"github.com/gin-gonic/gin"}
fiberPackage = []string{"github.com/gofiber/fiber/v2"}
echoPackage = []string{"github.com/labstack/echo/v4", "github.com/labstack/echo/v4/middleware"}
humaPackage = []string{"github.com/danielgtaylor/huma/v2", "github.com/danielgtaylor/huma/v2/adapters/humachi", "github.com/go-chi/chi/v5"}

mysqlDriver = []string{"github.com/go-sql-driver/mysql"}
postgresDriver = []string{"github.com/jackc/pgx/v5/stdlib"}
Expand Down Expand Up @@ -185,6 +186,11 @@ func (p *Project) createFrameworkMap() {
packageName: echoPackage,
templater: framework.EchoTemplates{},
}

p.FrameworkMap[flags.Huma] = Framework{
packageName: humaPackage,
templater: framework.HumaTemplates{},
}
}

func (p *Project) createDBDriverMap() {
Expand Down
4 changes: 4 additions & 0 deletions cmd/steps/steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ func InitSteps(projectType flags.Framework, databaseType flags.Database) *Steps
Title: "Echo",
Desc: "High performance, extensible, minimalist Go web framework",
},
{
Title: "Huma",
Desc: "A modern, simple to use, and fast REST API framework with built-in OpenAPI 3.1 docs and validation (built on Chi)",
},
},
Headers: "What framework do you want to use in your Go project?",
Field: projectType.String(),
Expand Down
103 changes: 103 additions & 0 deletions cmd/template/framework/files/routes/huma.go.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package server

import (
"context"
"net/http"
{{if .AdvancedOptions.websocket}}
"fmt"
"log"
"time"
{{end}}

"github.com/danielgtaylor/huma/v2"
"github.com/danielgtaylor/huma/v2/adapters/humachi"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
{{.AdvancedTemplates.TemplateImports}}

)

type HelloWorldOutput struct {
Body struct {
Message string `json:"message" example:"Hello World" doc:"Greeting message"`
}
}

{{if ne .DBDriver "none"}}
type HealthOutput struct {
Body map[string]string
}
{{end}}

func (s *Server) RegisterRoutes() http.Handler {
r := chi.NewRouter()
r.Use(middleware.Logger)

r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"https://*", "http://*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
AllowCredentials: true,
MaxAge: 300,
}))

api := humachi.New(r, huma.DefaultConfig("{{.ProjectName}}", "1.0.0"))

huma.Register(api, huma.Operation{
OperationID: "get-hello-world",
Method: http.MethodGet,
Path: "/",
Summary: "Hello World",
}, func(ctx context.Context, input *struct{}) (*HelloWorldOutput, error) {
resp := &HelloWorldOutput{}
resp.Body.Message = "Hello World"
return resp, nil
})
{{if ne .DBDriver "none"}}

huma.Register(api, huma.Operation{
OperationID: "get-health",
Method: http.MethodGet,
Path: "/health",
Summary: "Health check",
}, func(ctx context.Context, input *struct{}) (*HealthOutput, error) {
resp := &HealthOutput{}
resp.Body = s.db.Health()
return resp, nil
})
{{end}}
{{if .AdvancedOptions.websocket}}
r.Get("/websocket", s.websocketHandler)
{{end}}
{{.AdvancedTemplates.TemplateRoutes}}

return r
}

{{if .AdvancedOptions.websocket}}
func (s *Server) websocketHandler(w http.ResponseWriter, r *http.Request) {
socket, err := websocket.Accept(w, r, nil)

if err != nil {
log.Printf("could not open websocket: %v", err)
_, _ = w.Write([]byte("could not open websocket"))
w.WriteHeader(http.StatusInternalServerError)
return
}

defer socket.Close(websocket.StatusGoingAway, "server closing websocket")

ctx := r.Context()
socketCtx := socket.CloseRead(ctx)

for {
payload := fmt.Sprintf("server timestamp: %d", time.Now().UnixNano())
err := socket.Write(socketCtx, websocket.MessageText, []byte(payload))
if err != nil {
break
}
time.Sleep(time.Second * 2)
}
}
{{end}}
40 changes: 40 additions & 0 deletions cmd/template/framework/files/tests/huma-test.go.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package server

import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
)

func TestHelloWorldHandler(t *testing.T) {
s := &Server{}
server := httptest.NewServer(s.RegisterRoutes())
defer server.Close()

resp, err := http.Get(server.URL + "/")
if err != nil {
t.Fatalf("error making request to server. Err: %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
t.Errorf("expected status OK; got %v", resp.Status)
}

body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("error reading response body. Err: %v", err)
}

var result map[string]string
if err := json.Unmarshal(body, &result); err != nil {
t.Fatalf("error unmarshalling response body. Err: %v", err)
}

expected := "Hello World"
if result["message"] != expected {
t.Errorf("expected message to be %v; got %v", expected, result["message"])
}
}
45 changes: 45 additions & 0 deletions cmd/template/framework/humaRoutes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package framework

import (
_ "embed"

"github.com/melkeydev/go-blueprint/cmd/template/advanced"
)

//go:embed files/routes/huma.go.tmpl
var humaRoutesTemplate []byte

//go:embed files/tests/huma-test.go.tmpl
var humaTestHandlerTemplate []byte

// HumaTemplates contains the methods used for building
// an app that uses [github.com/danielgtaylor/huma] on top of [github.com/go-chi/chi]
type HumaTemplates struct{}

func (h HumaTemplates) Main() []byte {
return mainTemplate
}

func (h HumaTemplates) Server() []byte {
return standardServerTemplate
}

func (h HumaTemplates) Routes() []byte {
return humaRoutesTemplate
}

func (h HumaTemplates) TestHandler() []byte {
return humaTestHandlerTemplate
}

func (h HumaTemplates) HtmxTemplImports() []byte {
return advanced.StdLibHtmxTemplImportsTemplate()
}

func (h HumaTemplates) HtmxTemplRoutes() []byte {
return advanced.ChiHtmxTemplRoutesTemplate()
}

func (h HumaTemplates) WebsocketImports() []byte {
return advanced.StdLibWebsocketTemplImportsTemplate()
}
Loading