go-dart: v1.0.0 - initial release
This commit is contained in:
commit
8886b71c69
22
.devcontainer/devcontainer.json
Normal file
22
.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/go
|
||||||
|
{
|
||||||
|
"name": "Go",
|
||||||
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/go:1-1.23-bookworm"
|
||||||
|
|
||||||
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
|
// "features": {},
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
// "forwardPorts": [],
|
||||||
|
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
// "postCreateCommand": "go version",
|
||||||
|
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
// "customizations": {},
|
||||||
|
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "root"
|
||||||
|
}
|
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for more information:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
# https://containers.dev/guide/dependabot
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "devcontainers"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# 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
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Timo Riegebauer
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
51
cloud_context.go
Normal file
51
cloud_context.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package dart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CloudContext struct {
|
||||||
|
database *Database
|
||||||
|
websocketManager *WebsocketManager
|
||||||
|
state map[string]any
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CloudContext) Database() (*Database, error) {
|
||||||
|
if c.database == nil {
|
||||||
|
return nil, errors.New("database is not enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.database, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CloudContext) WebsocketManager() (*WebsocketManager, error) {
|
||||||
|
if c.websocketManager == nil {
|
||||||
|
return nil, errors.New("websocket manager is not enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.websocketManager, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CloudContext) Set(key string, value any) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
c.state[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CloudContext) Get(key string) (any, bool) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
val, exists := c.state[key]
|
||||||
|
return val, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CloudContext) Delete(key string) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
delete(c.state, key)
|
||||||
|
}
|
9
cloud_function.go
Normal file
9
cloud_function.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package dart
|
||||||
|
|
||||||
|
import "github.com/robfig/cron/v3"
|
||||||
|
|
||||||
|
type CloudFunction struct {
|
||||||
|
Name string
|
||||||
|
EntryID cron.EntryID
|
||||||
|
Handler CloudHandler
|
||||||
|
}
|
3
cloud_handler.go
Normal file
3
cloud_handler.go
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package dart
|
||||||
|
|
||||||
|
type CloudHandler func(ctx *CloudContext) error
|
21
cmd/main.go
Normal file
21
cmd/main.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.trcreatives.com/triegebauer/go-dart"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := dart.New()
|
||||||
|
app.WithCloudFunctions()
|
||||||
|
|
||||||
|
cloudFunction := dart.CloudFunction{
|
||||||
|
Name: "Loading Osu Data",
|
||||||
|
Handler: func(ctx *dart.CloudContext) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
app.ScheduleCloudFunction("* * * * * *", &cloudFunction)
|
||||||
|
|
||||||
|
app.Start(":8123")
|
||||||
|
}
|
168
context.go
Normal file
168
context.go
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
package dart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/schema"
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Context struct {
|
||||||
|
response http.ResponseWriter
|
||||||
|
request *http.Request
|
||||||
|
params httprouter.Params
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
|
database *Database
|
||||||
|
websocketManager *WebsocketManager
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContextKey string
|
||||||
|
|
||||||
|
func (c *Context) Request() *http.Request {
|
||||||
|
return c.request
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Response() http.ResponseWriter {
|
||||||
|
return c.response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Param(key string) string {
|
||||||
|
return c.params.ByName(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) FormFile(field string) (multipart.File, error) {
|
||||||
|
file, _, err := c.request.FormFile(field)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Query(key string) string {
|
||||||
|
return c.request.URL.Query().Get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Header(key string) string {
|
||||||
|
return c.request.Header.Get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Cookie(name string) (*http.Cookie, error) {
|
||||||
|
return c.request.Cookie(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Value(key ContextKey) any {
|
||||||
|
return c.ctx.Value(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) ClientIP() string {
|
||||||
|
ip := c.request.Header.Get("X-Forwarded-For")
|
||||||
|
if ip == "" {
|
||||||
|
ip = c.request.RemoteAddr
|
||||||
|
}
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) UserAgent() string {
|
||||||
|
return c.request.UserAgent()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) SetHeader(key, value string) {
|
||||||
|
c.response.Header().Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) SetCookie(cookie *http.Cookie) {
|
||||||
|
http.SetCookie(c.response, cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) SetValue(key ContextKey, value any) {
|
||||||
|
c.ctx = context.WithValue(c.ctx, key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Status(statusCode int) {
|
||||||
|
c.response.WriteHeader(statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Text(statusCode int, message string) error {
|
||||||
|
c.response.Header().Set("Content-Type", "text/plain")
|
||||||
|
c.response.WriteHeader(statusCode)
|
||||||
|
_, err := c.response.Write([]byte(message))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) JSON(statusCode int, v any) error {
|
||||||
|
c.response.Header().Set("Content-Type", "application/json")
|
||||||
|
c.response.WriteHeader(statusCode)
|
||||||
|
return json.NewEncoder(c.response).Encode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Bytes(statusCode int, data []byte, contentType string) error {
|
||||||
|
c.response.Header().Set("Content-Type", contentType)
|
||||||
|
c.response.WriteHeader(statusCode)
|
||||||
|
_, err := c.response.Write(data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) HTML(statusCode int, html string) error {
|
||||||
|
c.response.Header().Set("Content-Type", "text/html")
|
||||||
|
c.response.WriteHeader(statusCode)
|
||||||
|
_, err := c.response.Write([]byte(html))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) File(filePath string) error {
|
||||||
|
http.ServeFile(c.response, c.request, filePath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) ParseJSON(v any) error {
|
||||||
|
return json.NewDecoder(c.request.Body).Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) ParseXML(v any) error {
|
||||||
|
return xml.NewDecoder(c.request.Body).Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) ParseForm(v any) error {
|
||||||
|
if err := c.request.ParseForm(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return schema.NewDecoder().Decode(v, c.request.Form)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) ParseMultipartForm(v any, maxMemory int64) error {
|
||||||
|
if err := c.request.ParseMultipartForm(maxMemory); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return schema.NewDecoder().Decode(v, c.request.MultipartForm.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) ParseQuery(v any) error {
|
||||||
|
return schema.NewDecoder().Decode(v, c.request.URL.Query())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Redirect(statusCode int, url string) error {
|
||||||
|
http.Redirect(c.response, c.request, url, statusCode)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) DB() (*Database, error) {
|
||||||
|
if c.database == nil {
|
||||||
|
return nil, errors.New("database is not enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.database, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) WebsocketManager() (*WebsocketManager, error) {
|
||||||
|
if c.websocketManager == nil {
|
||||||
|
return nil, errors.New("websocket manager is not enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.websocketManager, nil
|
||||||
|
}
|
228
dart.go
Normal file
228
dart.go
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
package dart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dart struct {
|
||||||
|
router *httprouter.Router
|
||||||
|
errorHandler ErrorHandler
|
||||||
|
middlewares []Middleware
|
||||||
|
logger *log.Logger
|
||||||
|
|
||||||
|
database *Database
|
||||||
|
enableDatabase bool
|
||||||
|
|
||||||
|
websocketManager *WebsocketManager
|
||||||
|
enableWebsocketManager bool
|
||||||
|
|
||||||
|
cron *cron.Cron
|
||||||
|
enableCloudFunctions bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Dart {
|
||||||
|
return &Dart{
|
||||||
|
router: httprouter.New(),
|
||||||
|
errorHandler: defaultErrorHandler,
|
||||||
|
middlewares: []Middleware{},
|
||||||
|
logger: log.New(os.Stdout, "[Wasp] ", log.LstdFlags),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) WithMySQLDatabase(cfg MySQLConfig) {
|
||||||
|
d.database = NewMySQLDatabase(cfg)
|
||||||
|
d.enableDatabase = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) WithPostgreSQLDatabase(cfg PostgreSQLConfig) {
|
||||||
|
d.database = NewPostgreSQLDatabase(cfg)
|
||||||
|
d.enableDatabase = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) WithSQLiteDatabase(cfg SQLiteConfig) {
|
||||||
|
d.database = NewSQLiteDatabase(cfg)
|
||||||
|
d.enableDatabase = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) DB() (*Database, error) {
|
||||||
|
if d.database == nil {
|
||||||
|
return nil, errors.New("database is not enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.database, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) WithWebsocketManager() {
|
||||||
|
d.websocketManager = &WebsocketManager{
|
||||||
|
conns: make(map[string]map[*websocket.Conn]bool),
|
||||||
|
}
|
||||||
|
d.enableWebsocketManager = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) WithCloudFunctions() {
|
||||||
|
d.cron = cron.New(cron.WithSeconds())
|
||||||
|
d.enableCloudFunctions = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) ScheduleCloudFunction(spec string, cf *CloudFunction) {
|
||||||
|
if !d.enableCloudFunctions {
|
||||||
|
d.logger.Println("Cloud function scheduling attempted but cloud functions are disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := &CloudContext{
|
||||||
|
database: d.database,
|
||||||
|
websocketManager: d.websocketManager,
|
||||||
|
state: make(map[string]any),
|
||||||
|
}
|
||||||
|
|
||||||
|
entryId, err := d.cron.AddFunc(spec, func() {
|
||||||
|
d.logger.Printf("Executing cloud function: %s", cf.Name)
|
||||||
|
if err := cf.Handler(ctx); err != nil {
|
||||||
|
d.logger.Printf("Error executing cloud function '%s': %v\n", cf.Name, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Printf("Error scheduling cloud function '%s': '%v\n", cf.Name, entryId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cf.EntryID = entryId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) Start(listenAddr string) error {
|
||||||
|
browsableUrl := listenAddr
|
||||||
|
if strings.HasPrefix(browsableUrl, ":") {
|
||||||
|
browsableUrl = fmt.Sprintf("http://localhost%s", browsableUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.enableCloudFunctions {
|
||||||
|
d.cron.Start()
|
||||||
|
defer d.cron.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
d.logger.Printf("Starting Dart app at %s\n", browsableUrl)
|
||||||
|
return http.ListenAndServe(listenAddr, d.router)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) StartTLS(listenAddr, certFile, keyFile string) error {
|
||||||
|
browsableUrl := listenAddr
|
||||||
|
if strings.HasPrefix(browsableUrl, ":") {
|
||||||
|
browsableUrl = fmt.Sprintf("http://localhost%s", browsableUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.enableCloudFunctions {
|
||||||
|
d.cron.Start()
|
||||||
|
defer d.cron.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
d.logger.Printf("Starting Dart app at %s\n", browsableUrl)
|
||||||
|
return http.ListenAndServeTLS(listenAddr, certFile, keyFile, d.router)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) SetErrorHandler(errorHandler ErrorHandler) {
|
||||||
|
d.errorHandler = errorHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) Group(prefix string) *Group {
|
||||||
|
return &Group{
|
||||||
|
prefix: prefix,
|
||||||
|
dart: d,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) Websocket(path string, middlewares ...Middleware) {
|
||||||
|
if !d.enableWebsocketManager {
|
||||||
|
d.logger.Println("Websocket registration attempted but websockets are disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.register(http.MethodGet, path, d.websocketManager.createHandler(), middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) Static(prefix, root string, middlewares ...Middleware) {
|
||||||
|
fileServer := http.FileServer(http.Dir(root))
|
||||||
|
wrappedHandler := d.createHandlerChain(func(ctx *Context) error {
|
||||||
|
http.StripPrefix(prefix, fileServer).ServeHTTP(ctx.response, ctx.request)
|
||||||
|
return nil
|
||||||
|
}, middlewares)
|
||||||
|
|
||||||
|
d.router.Handle(http.MethodGet, prefix+"/*filepath", wrappedHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) Use(middlewares ...Middleware) {
|
||||||
|
d.middlewares = append(d.middlewares, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) GET(path string, h Handler, middlewares ...Middleware) {
|
||||||
|
d.register(http.MethodGet, path, h, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) POST(path string, h Handler, middlewares ...Middleware) {
|
||||||
|
d.register(http.MethodPost, path, h, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) PUT(path string, h Handler, middlewares ...Middleware) {
|
||||||
|
d.register(http.MethodPut, path, h, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) DELETE(path string, h Handler, middlewares ...Middleware) {
|
||||||
|
d.register(http.MethodDelete, path, h, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) PATCH(path string, h Handler, middlewares ...Middleware) {
|
||||||
|
d.register(http.MethodPatch, path, h, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) HEAD(path string, h Handler, middlewares ...Middleware) {
|
||||||
|
d.register(http.MethodHead, path, h, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) OPTIONS(path string, h Handler, middlewares ...Middleware) {
|
||||||
|
d.register(http.MethodOptions, path, h, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) register(method string, path string, h Handler, middlewares ...Middleware) {
|
||||||
|
handlers := slices.Clone(middlewares)
|
||||||
|
|
||||||
|
finalHandler := d.createHandlerChain(h, handlers)
|
||||||
|
d.router.Handle(method, path, finalHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dart) createHandlerChain(h Handler, middlewares []Middleware) httprouter.Handle {
|
||||||
|
return func(rw http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||||
|
ctx := &Context{
|
||||||
|
response: rw,
|
||||||
|
request: r,
|
||||||
|
params: params,
|
||||||
|
ctx: context.Background(),
|
||||||
|
database: d.database,
|
||||||
|
websocketManager: d.websocketManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
finalHandler := h
|
||||||
|
|
||||||
|
for i := len(middlewares) - 1; i >= 0; i-- {
|
||||||
|
finalHandler = middlewares[i](finalHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(d.middlewares) - 1; i >= 0; i-- {
|
||||||
|
finalHandler = d.middlewares[i](finalHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := finalHandler(ctx); err != nil {
|
||||||
|
d.logger.Printf("Error in request %s %s: %v\n", r.Method, r.URL.Path, err)
|
||||||
|
d.errorHandler(err, ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
70
database.go
Normal file
70
database.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package dart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Database struct {
|
||||||
|
Conn *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type MySQLConfig struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
Database string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostgreSQLConfig struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
Database string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SQLiteConfig struct {
|
||||||
|
Filename string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMySQLDatabase(cfg MySQLConfig) *Database {
|
||||||
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
|
||||||
|
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Database{
|
||||||
|
Conn: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPostgreSQLDatabase(cfg PostgreSQLConfig) *Database {
|
||||||
|
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disabled TimeZone=Europe/Vienna", cfg.Host, cfg.Username, cfg.Password, cfg.Database, cfg.Port)
|
||||||
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Database{
|
||||||
|
Conn: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSQLiteDatabase(cfg SQLiteConfig) *Database {
|
||||||
|
db, err := gorm.Open(sqlite.Open(cfg.Filename), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Database{
|
||||||
|
Conn: db,
|
||||||
|
}
|
||||||
|
}
|
9
error_handler.go
Normal file
9
error_handler.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package dart
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type ErrorHandler func(err error, ctx *Context)
|
||||||
|
|
||||||
|
func defaultErrorHandler(err error, c *Context) {
|
||||||
|
c.JSON(http.StatusInternalServerError, Json{"error": err.Error()})
|
||||||
|
}
|
28
go.mod
Normal file
28
go.mod
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
module git.trcreatives.com/triegebauer/go-dart
|
||||||
|
|
||||||
|
go 1.23.8
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gorilla/schema v1.4.1
|
||||||
|
github.com/gorilla/websocket v1.5.3
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0
|
||||||
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
|
gorm.io/driver/mysql v1.5.7
|
||||||
|
gorm.io/driver/postgres v1.5.11
|
||||||
|
gorm.io/driver/sqlite v1.5.7
|
||||||
|
gorm.io/gorm v1.25.12
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
|
golang.org/x/crypto v0.17.0 // indirect
|
||||||
|
golang.org/x/sync v0.13.0 // indirect
|
||||||
|
golang.org/x/text v0.24.0 // indirect
|
||||||
|
)
|
53
go.sum
Normal file
53
go.sum
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
|
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
|
||||||
|
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||||
|
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||||
|
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
|
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||||
|
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||||
|
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||||
|
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||||
|
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
|
||||||
|
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||||
|
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
|
||||||
|
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||||
|
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
|
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||||
|
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
97
group.go
Normal file
97
group.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package dart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Group struct {
|
||||||
|
prefix string
|
||||||
|
middlewares []Middleware
|
||||||
|
parent *Group
|
||||||
|
dart *Dart
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) Group(prefix string) *Group {
|
||||||
|
return &Group{
|
||||||
|
prefix: prefix,
|
||||||
|
parent: g,
|
||||||
|
dart: g.dart,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) Static(prefix, root string, middlewares ...Middleware) {
|
||||||
|
fileServer := http.FileServer(http.Dir(root))
|
||||||
|
fullPath := joinPaths(g.getFullPrefix(), prefix)
|
||||||
|
g.register(http.MethodGet, prefix+"/*filepath", func(ctx *Context) error {
|
||||||
|
http.StripPrefix(fullPath, fileServer).ServeHTTP(ctx.response, ctx.request)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) Use(middlewares ...Middleware) {
|
||||||
|
g.middlewares = append(g.middlewares, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) GET(path string, h Handler, middlewares ...Middleware) {
|
||||||
|
g.register(http.MethodGet, path, h, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) POST(path string, h Handler, middlewares ...Middleware) {
|
||||||
|
g.register(http.MethodPost, path, h, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) PUT(path string, h Handler, middlewares ...Middleware) {
|
||||||
|
g.register(http.MethodPut, path, h, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) DELETE(path string, h Handler, middlewares ...Middleware) {
|
||||||
|
g.register(http.MethodDelete, path, h, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) PATCH(path string, h Handler, middlewares ...Middleware) {
|
||||||
|
g.register(http.MethodPatch, path, h, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) HEAD(path string, h Handler, middlewares ...Middleware) {
|
||||||
|
g.register(http.MethodHead, path, h, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) OPTIONS(path string, h Handler, middlewares ...Middleware) {
|
||||||
|
g.register(http.MethodOptions, path, h, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) getFullPrefix() string {
|
||||||
|
if g.parent != nil {
|
||||||
|
return joinPaths(g.parent.getFullPrefix(), g.prefix)
|
||||||
|
}
|
||||||
|
return g.prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) getAllMiddlewares() []Middleware {
|
||||||
|
middlewares := make([]Middleware, 0)
|
||||||
|
current := g
|
||||||
|
|
||||||
|
for current != nil {
|
||||||
|
middlewares = append(current.middlewares, middlewares...)
|
||||||
|
current = current.parent
|
||||||
|
}
|
||||||
|
return middlewares
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) register(method string, path string, h Handler, middlewares ...Middleware) {
|
||||||
|
fullPath := joinPaths(g.getFullPrefix(), path)
|
||||||
|
allMiddlewares := append(g.getAllMiddlewares(), middlewares...)
|
||||||
|
g.dart.register(method, fullPath, h, allMiddlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinPaths(a, b string) string {
|
||||||
|
a = strings.TrimSuffix(a, "/")
|
||||||
|
b = strings.TrimPrefix(b, "/")
|
||||||
|
|
||||||
|
if b == "" {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
return a + "/" + b
|
||||||
|
}
|
3
handler.go
Normal file
3
handler.go
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package dart
|
||||||
|
|
||||||
|
type Handler func(ctx *Context) error
|
3
middleware.go
Normal file
3
middleware.go
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package dart
|
||||||
|
|
||||||
|
type Middleware func(next Handler) Handler
|
81
websocket_manager.go
Normal file
81
websocket_manager.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package dart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebsocketManager struct {
|
||||||
|
conns map[string]map[*websocket.Conn]bool
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *WebsocketManager) createHandler() Handler {
|
||||||
|
return func(ctx *Context) error {
|
||||||
|
upgrader := websocket.Upgrader{
|
||||||
|
CheckOrigin: func(r *http.Request) bool { return true },
|
||||||
|
}
|
||||||
|
conn, err := upgrader.Upgrade(ctx.response, ctx.request, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
wm.register(ctx.request.URL.Path, conn)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if _, _, err := conn.NextReader(); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wm.unregister(ctx.request.URL.Path, conn)
|
||||||
|
conn.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *WebsocketManager) register(path string, conn *websocket.Conn) {
|
||||||
|
wm.mutex.Lock()
|
||||||
|
defer wm.mutex.Unlock()
|
||||||
|
|
||||||
|
if _, exists := wm.conns[path]; !exists {
|
||||||
|
wm.conns[path] = make(map[*websocket.Conn]bool)
|
||||||
|
}
|
||||||
|
wm.conns[path][conn] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *WebsocketManager) unregister(path string, conn *websocket.Conn) {
|
||||||
|
wm.mutex.Lock()
|
||||||
|
defer wm.mutex.Unlock()
|
||||||
|
|
||||||
|
if conns, exists := wm.conns[path]; exists {
|
||||||
|
delete(conns, conn)
|
||||||
|
if len(conns) == 0 {
|
||||||
|
delete(wm.conns, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *WebsocketManager) Broadcast(path string, msg []byte) error {
|
||||||
|
wm.mutex.RLock()
|
||||||
|
conns, exists := wm.conns[path]
|
||||||
|
wm.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("there is no websocket on path '%s'", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
for conn := range conns {
|
||||||
|
go func(c *websocket.Conn) {
|
||||||
|
if err := c.WriteMessage(websocket.TextMessage, msg); err != nil {
|
||||||
|
wm.unregister(path, conn)
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
}(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user