Skip to content

Commit

Permalink
Add websocket endpoint to advanced options (#192)
Browse files Browse the repository at this point in the history
Add websocket endpoint to advanced options (#192)
  • Loading branch information
MitchellBerend authored Mar 14, 2024
1 parent 4299a43 commit 076a576
Show file tree
Hide file tree
Showing 40 changed files with 825 additions and 244 deletions.
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Check the official Go-blueprint [docs](https://go-blueprint.net).
- [License](#license)

<a id="install"></a>

<h2>
<picture>
<img src="./public/install.gif?raw=true" width="60px" style="margin-right: 1px;">
Expand Down Expand Up @@ -58,6 +59,7 @@ go-blueprint create --name my-project --framework gin --driver postgres
See `go-blueprint create -h` for all the options and shorthands.

<a id="frameworks-supported"></a>

<h2>
<picture>
<img src="./public/frameworks.gif?raw=true" width="60px" style="margin-right: 1px;">
Expand Down Expand Up @@ -94,22 +96,24 @@ Choose from a variety of supported database drivers:
- [Redis](https://github.com/redis/go-redis)

<a id="advanced-features"></a>

<h2>
<picture>
<img src="./public/advanced.gif?raw=true" width="70px" style="margin-right: 1px;">
</picture>
Advanced Features
</h2>

Blueprint is focused on being as minimalistic as possible. That being said, we wanted to offer the ability to add other features people may want without bloating the overall experience.
Blueprint is focused on being as minimalistic as possible. That being said, we wanted to offer the ability to add other features people may want without bloating the overall experience.

You can now use the `--advanced` flag when running the `create` command to get access to the following features. This is a multi-option prompt; one or more features can be used at the same time:

- [HTMX](https://htmx.org/) support using [Templ](https://templ.guide/)
- CI/CD workflow setup using [Github Actions](https://docs.github.com/en/actions)

- [Websocket](https://pkg.go.dev/nhooyr.io/websocket) sets up a websocket endpoint

<a id="usage-example"></a>

<h2>
<picture>
<img src="./public/example.gif?raw=true" width="60px" style="margin-right: 1px;">
Expand All @@ -133,7 +137,7 @@ Advanced features are accessible with the --advanced flag
go-blueprint create --advanced
```

Both advanced features can be enabled using the `--feature` flag along with the `--advanced` flag.
Advanced features can be enabled using the `--feature` flag along with the `--advanced` flag.

For HTMX:
```bash
Expand All @@ -145,16 +149,24 @@ For the CI/CD workflow:
go-blueprint create --advanced --feature githubaction
```

Or for both:
For the websocket:
```bash
go-blueprint create --advanced --feature htmx --feature githubaction
go-blueprint create --advanced --feature websocket
```

Or all features at once:
```bash
go-blueprint create --name my-project --framework chi --driver mysql --advanced --feature htmx --feature githubaction --feature websocket
```

<p align="center">
<img src="./public/blueprint_2.png" alt="Starter Image" width="800"/>
<img src="./public/blueprint_advanced.png" alt="Advanced Options" width="800"/>
</p>

**Visit [documentation](https://go-blueprint.net) to learn more about blueprint and its features.**

<a id="github-stats"></a>

<h2>
<picture>
<img src="./public/stats.gif?raw=true" width="45px" style="margin-right: 10px;">
Expand All @@ -167,6 +179,7 @@ go-blueprint create --advanced --feature htmx --feature githubaction
</p>

<a id="license"></a>

<h2>
<picture>
<img src="./public/license.gif?raw=true" width="50px" style="margin-right: 1px;">
Expand Down
3 changes: 2 additions & 1 deletion cmd/flags/advancedFeatures.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ type AdvancedFeatures []string
const (
Htmx string = "htmx"
GoProjectWorkflow string = "githubaction"
Websocket string = "websocket"
)

var AllowedAdvancedFeatures = []string{string(Htmx), string(GoProjectWorkflow)}
var AllowedAdvancedFeatures = []string{string(Htmx), string(GoProjectWorkflow), string(Websocket)}

func (f AdvancedFeatures) String() string {
return strings.Join(f, ",")
Expand Down
48 changes: 43 additions & 5 deletions cmd/program/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ package program
import (
"bytes"
"fmt"
"html/template"
"log"
"os"
"path/filepath"
"strings"
"text/template"

tea "github.com/charmbracelet/bubbletea"
"github.com/melkeydev/go-blueprint/cmd/flags"
Expand Down Expand Up @@ -39,8 +39,8 @@ type Project struct {
}

type AdvancedTemplates struct {
TemplateRoutes template.HTML
TemplateImports template.HTML
TemplateRoutes string
TemplateImports string
}

// A Framework contains the name and templater for a
Expand Down Expand Up @@ -69,6 +69,7 @@ type Templater interface {
TestHandler() []byte
HtmxTemplRoutes() []byte
HtmxTemplImports() []byte
WebsocketImports() []byte
}

type DBDriverTemplater interface {
Expand Down Expand Up @@ -522,6 +523,14 @@ func (p *Project) CreateMainFile() error {
}
}

// if the websocket option is checked, a websocket dependency needs to
// be added to the routes depending on the framework choosen.
// Only fiber uses a different websocket library, the other frameworks
// all work with the same one
if p.AdvancedOptions[string(flags.Websocket)] {
p.CreateWebsocketImports(projectPath)
}

err = p.CreateFileWithInjection(internalServerPath, projectPath, "routes.go", "routes")
if err != nil {
log.Printf("Error injecting routes.go file: %v", err)
Expand Down Expand Up @@ -711,6 +720,35 @@ func (p *Project) CreateHtmxTemplates() {
if err != nil {
log.Fatal(err)
}
p.AdvancedTemplates.TemplateRoutes = template.HTML(routeBuffer.String())
p.AdvancedTemplates.TemplateImports = template.HTML(importBuffer.String())
p.AdvancedTemplates.TemplateRoutes = routeBuffer.String()
p.AdvancedTemplates.TemplateImports = importBuffer.String()
}

func (p *Project) CreateWebsocketImports(appDir string) {
websocketDependency := []string{"nhooyr.io/websocket"}
if p.ProjectType == flags.Fiber {
websocketDependency = []string{"github.com/gofiber/contrib/websocket"}
}

// Websockets require a different package depending on what framework is
// choosen. The application calls go mod tidy at the end so we don't
// have to here
err := utils.GoGetPackage(appDir, websocketDependency)
if err != nil {
log.Fatal(err)
}

importsPlaceHolder := string(p.FrameworkMap[p.ProjectType].templater.WebsocketImports())

importTmpl, err := template.New("imports").Parse(importsPlaceHolder)
if err != nil {
log.Fatalf("CreateWebsocketImports failed to create template: %v", err)
}
var importBuffer bytes.Buffer
err = importTmpl.Execute(&importBuffer, p)
if err != nil {
log.Fatalf("CreateWebsocketImports failed write template: %v", err)
}
newImports := strings.Join([]string{string(p.AdvancedTemplates.TemplateImports), importBuffer.String()}, "\n")
p.AdvancedTemplates.TemplateImports = newImports
}
5 changes: 5 additions & 0 deletions cmd/steps/steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ func InitSteps(projectType flags.Framework, databaseType flags.Database) *Steps
Title: "Go Project Workflow",
Desc: "Workflow templates for testing, cross-compiling and releasing Go projects",
},
{
Flag: "Websocket",
Title: "Weboscket endpoint",
Desc: "Add a websocket endpoint",
},
},
},
},
Expand Down
1 change: 1 addition & 0 deletions cmd/template/advanced/files/websocket/imports/fiber.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"github.com/gofiber/contrib/websocket"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"nhooyr.io/websocket"
16 changes: 16 additions & 0 deletions cmd/template/advanced/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ var stdLibHtmxTemplRoutes []byte
//go:embed files/htmx/imports/standard_library.tmpl
var stdLibHtmxTemplImports []byte

//go:embed files/websocket/imports/standard_library.tmpl
var stdLibWebsocketImports []byte

//go:embed files/htmx/routes/chi.tmpl
var chiHtmxTemplRoutes []byte

Expand All @@ -49,6 +52,10 @@ var fiberHtmxTemplRoutes []byte
//go:embed files/htmx/imports/fiber.tmpl
var fiberHtmxTemplImports []byte

//go:embed files/websocket/imports/fiber.tmpl
var fiberWebsocketTemplImports []byte


func EchoHtmxTemplRoutesTemplate() []byte {
return echoHtmxTemplRoutes
}
Expand Down Expand Up @@ -77,6 +84,10 @@ func StdLibHtmxTemplImportsTemplate() []byte {
return stdLibHtmxTemplImports
}

func StdLibWebsocketTemplImportsTemplate() []byte {
return stdLibWebsocketImports
}

func HelloTemplTemplate() []byte {
return helloTemplTemplate
}
Expand Down Expand Up @@ -108,3 +119,8 @@ func FiberHtmxTemplRoutesTemplate() []byte {
func FiberHtmxTemplImportsTemplate() []byte {
return fiberHtmxTemplImports
}

func FiberWebsocketTemplImportsTemplate() []byte {
return fiberWebsocketTemplImports
}

4 changes: 4 additions & 0 deletions cmd/template/framework/chiRoutes.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ func (c ChiTemplates) HtmxTemplImports() []byte {
func (c ChiTemplates) HtmxTemplRoutes() []byte {
return advanced.ChiHtmxTemplRoutesTemplate()
}

func (c ChiTemplates) WebsocketImports() []byte {
return advanced.StdLibWebsocketTemplImportsTemplate()
}
4 changes: 4 additions & 0 deletions cmd/template/framework/echoRoutes.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ func (e EchoTemplates) HtmxTemplImports() []byte {
func (e EchoTemplates) HtmxTemplRoutes() []byte {
return advanced.EchoHtmxTemplRoutesTemplate()
}

func (e EchoTemplates) WebsocketImports() []byte {
return advanced.StdLibWebsocketTemplImportsTemplate()
}
4 changes: 4 additions & 0 deletions cmd/template/framework/fiberServer.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,7 @@ func (f FiberTemplates) HtmxTemplImports() []byte {
func (f FiberTemplates) HtmxTemplRoutes() []byte {
return advanced.FiberHtmxTemplRoutesTemplate()
}

func (f FiberTemplates) WebsocketImports() []byte {
return advanced.FiberWebsocketTemplImportsTemplate()
}
34 changes: 34 additions & 0 deletions cmd/template/framework/files/routes/chi.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import (
"encoding/json"
"log"
"net/http"
{{if .AdvancedOptions.websocket}}
"fmt"
"time"
{{end}}

"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
Expand All @@ -19,6 +23,9 @@ func (s *Server) RegisterRoutes() http.Handler {
{{if ne .DBDriver "none"}}
r.Get("/health", s.healthHandler)
{{end}}
{{if .AdvancedOptions.websocket}}
r.Get("/websocket", s.websocketHandler)
{{end}}
{{.AdvancedTemplates.TemplateRoutes}}

return r
Expand All @@ -42,3 +49,30 @@ func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(jsonResp)
}
{{end}}

{{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}}
48 changes: 43 additions & 5 deletions cmd/template/framework/files/routes/echo.go.tmpl
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package server

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

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
{{.AdvancedTemplates.TemplateImports}}
)
)
func (s *Server) RegisterRoutes() http.Handler {
e := echo.New()
e.Use(middleware.Logger())
Expand All @@ -17,6 +22,9 @@ func (s *Server) RegisterRoutes() http.Handler {
{{if ne .DBDriver "none"}}
e.GET("/health", s.healthHandler)
{{end}}
{{if .AdvancedOptions.websocket}}
e.GET("/websocket", s.websocketHandler)
{{end}}

return e
}
Expand All @@ -34,3 +42,33 @@ func (s *Server) healthHandler(c echo.Context) error {
return c.JSON(http.StatusOK, s.db.Health())
}
{{end}}

{{if .AdvancedOptions.websocket}}
func (s *Server) websocketHandler(c echo.Context) error {
w := c.Response().Writer
r := c.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 nil
}

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)
}
return nil
}
{{end}}
Loading

0 comments on commit 076a576

Please sign in to comment.