Compare commits

..

21 Commits

Author SHA1 Message Date
8aceb5ff8c fix crash 2024-09-30 19:44:10 +02:00
9da3012789 Integrate cobra into the framework part1 2024-09-30 19:38:16 +02:00
fc4f94e967 GoWeb: add cobra 2024-09-30 14:02:36 +02:00
7c4a95ac04 Repo: Add binary to gitignore 2024-09-30 13:53:00 +02:00
17b19e3a10 Repo: reorganize the repo structure 2024-09-30 13:42:37 +02:00
c3fc59b8f1 Gin: add container into Gin 2024-09-30 09:32:33 +02:00
f0fc7ee2cb Gin: add gin into the framework 2024-09-29 23:50:00 +02:00
f0b688ff8c Container: Add Gin web service provider 2024-09-28 22:54:00 +02:00
f866f18cfd Container: create service container and service provider 2024-09-28 22:54:00 +02:00
722e8191a9 GoWeb: Remove everything to use Gin 2024-09-28 22:53:41 +02:00
c5ab1debdb stop: use graceful shutdown for 5s 2024-09-26 23:05:05 +02:00
c0fea38790 stop: use graceful shutdown 2024-09-26 22:58:55 +02:00
b98513ad36 stop: catch signals 2024-09-26 22:27:26 +02:00
0786a97a77 context: Get params from url 2024-09-26 21:15:15 +02:00
cb1ea9f701 request/response: add implementations for response writer 2024-09-26 09:32:44 +02:00
8f4b378fdd request/response: add more implementations 2024-09-25 22:45:25 +02:00
099d1aeb0f request/response: add interfaces and implementations 2024-09-25 22:20:00 +02:00
3b16d6b16a middleware: refactor tests 2024-09-25 14:09:29 +02:00
fc4103cff3 middleware: add tests 2024-09-25 13:52:12 +02:00
e770731643 middleware: Add recovery middleware 2024-09-25 13:35:38 +02:00
3bf14b9c04 middleware: Integrate middlewares into router functions 2024-09-25 13:31:37 +02:00
34 changed files with 969 additions and 715 deletions

1
.gitignore vendored
View File

@ -21,3 +21,4 @@
# Go workspace file
go.work
go-web

6
.gitmodules vendored Normal file
View File

@ -0,0 +1,6 @@
[submodule "framework/gin"]
path = framework/gin
url = gitea@git.vinchent.xyz:vinchent/gin.git
[submodule "framework/cobra"]
path = framework/cobra
url = gitea@git.vinchent.xyz:vinchent/cobra.git

30
app/console/kernel.go Normal file
View File

@ -0,0 +1,30 @@
package console
import (
"git.vinchent.xyz/vinchent/go-web/framework"
"git.vinchent.xyz/vinchent/go-web/framework/commands"
"github.com/spf13/cobra"
)
func RunCommand(container framework.Container) error {
rootCmd := &cobra.Command{
Use: "goweb",
Short: "goweb command",
Long: "the command line tool for goweb framework.",
RunE: func(cmd *cobra.Command, args []string) error {
cmd.InitDefaultHelpFlag()
return cmd.Help()
},
CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true},
}
rootCmd.SetContainer(container)
commands.AddKernelCommands(rootCmd)
AddAppCommand(rootCmd)
return rootCmd.Execute()
}
func AddAppCommand(rootCmd *cobra.Command) {
// rootCmd.AddCommand(demo.InitFoo())
}

14
app/web/kernel.go Normal file
View File

@ -0,0 +1,14 @@
package web
import "github.com/gin-gonic/gin"
func NewHttpEngine() (*gin.Engine, error) {
// Use ReleaseMode by default to not ouput debug info
gin.SetMode(gin.ReleaseMode)
r := gin.Default()
Routes(r)
return r, nil
}

View File

9
app/web/route.go Normal file
View File

@ -0,0 +1,9 @@
package web
import "github.com/gin-gonic/gin"
func Routes(r *gin.Engine) {
r.Static("/dist/", "./dist/")
// demo.Register(r)
}

1
framework/cobra Submodule

Submodule framework/cobra added at 6d888d3313

View File

@ -0,0 +1,8 @@
package commands
import "github.com/spf13/cobra"
func AddKernelCommands(root *cobra.Command) {
// root.AddCommand(DemoCommand)
// root.AddCommand(initAppCommand())
}

189
framework/container.go Normal file
View File

@ -0,0 +1,189 @@
package framework
import (
"fmt"
"io"
"sync"
)
type Container interface {
// Bind binds a service provider to the container.
//
// If the service exists, the old one will be replaced by the new one.
// No error is returned in this case.
//
// Return any Init() error.
Bind(provider ServiceProvider) error
// IsBound returns true the provider is bound to the container.
IsBound(name string) bool
// Make gets or creates a service by its name.
//
// Return error if the service cannot be initiated or doesn't exist.
Make(name string) (interface{}, error)
// MustMake makes a service, supposing that it is not instantiated before.
// Return nil if error.
MustMake(name string) interface{}
// MakeNew creates a new instance of the service with different parameters.
//
// Used for non singleton services.
MakeNew(name string, params []interface{}) (interface{}, error)
}
type GoWebContainer struct {
// Must implement Container interfaces.
Container
// Service providers by name.
providers map[string]ServiceProvider
// Instantiated services by name.
instances map[string]interface{}
// RWMutex to protect the changes in container.
mu sync.RWMutex
}
func errUnknownService(name string) error {
return fmt.Errorf("service %q is not registered in the container", name)
}
func NewGoWebContainer() *GoWebContainer {
return &GoWebContainer{
providers: map[string]ServiceProvider{},
instances: map[string]interface{}{},
mu: sync.RWMutex{},
}
}
func (c *GoWebContainer) PrintProviders(w io.Writer) {
ret := make([]string, len(c.providers))
for _, provider := range c.providers {
name := provider.Name()
ret = append(ret, name)
}
fmt.Fprintf(w, "%v", ret)
}
func (c *GoWebContainer) Bind(provider ServiceProvider) error {
c.bindProvider(provider)
// Instantiate Later ?
if provider.InstantiateLater() {
return nil
}
// Instantiate now !
_, err := c.instantiate(provider, nil)
if err != nil {
return err
}
return nil
}
func (c *GoWebContainer) bindProvider(provider ServiceProvider) {
c.mu.Lock()
defer c.mu.Unlock()
name := provider.Name()
c.providers[name] = provider
}
func (c *GoWebContainer) IsBound(name string) bool {
return c.getServiceProvider(name) != nil
}
func (c *GoWebContainer) getServiceProvider(name string) ServiceProvider {
c.mu.RLock()
defer c.mu.RUnlock()
provider, ok := c.providers[name]
if ok {
return provider
}
return nil
}
func (c *GoWebContainer) Make(name string) (interface{}, error) {
return c.makeServiceInstance(name, nil, false)
}
func (c *GoWebContainer) MustMake(name string) interface{} {
ins, err := c.makeServiceInstance(name, nil, false)
if err != nil {
return nil
}
return ins
}
func (c *GoWebContainer) MakeNew(name string, params []interface{}) (interface{}, error) {
return c.makeServiceInstance(name, params, true)
}
func (c *GoWebContainer) getServiceInstance(name string) interface{} {
c.mu.RLock()
defer c.mu.RUnlock()
instance, ok := c.instances[name]
if ok {
return instance
}
return nil
}
func (c *GoWebContainer) makeServiceInstance(
name string,
params []interface{},
forceNew bool,
) (interface{}, error) {
provider := c.getServiceProvider(name)
if provider == nil {
return nil, errUnknownService(name)
}
if forceNew {
return c.instantiate(provider, params)
}
instance := c.getServiceInstance(name)
if instance != nil {
return instance, nil
}
// create a new instance
return c.instantiate(provider, params)
}
// instantiate instantiates a new instance.
// If no params are provided, then default params are used.
func (c *GoWebContainer) instantiate(
provider ServiceProvider,
params []interface{},
) (interface{}, error) {
c.mu.Lock()
defer c.mu.Unlock()
if err := provider.Init(c); err != nil {
// TODO: Error should be wrapped.
return nil, err
}
if params == nil {
params = provider.Params(c)
}
construct := provider.Register(c)
instance, err := construct(params...)
if err != nil {
// TODO: Error should be wrapped.
return nil, err
}
c.instances[provider.Name()] = instance
return instance, nil
}

View File

@ -0,0 +1,88 @@
package framework
import (
"log"
"reflect"
"testing"
)
// {{{ contract
const DummyProviderName = "container:test"
type IDummyService interface {
GetDummy() DummyStruct
}
type DummyStruct struct {
Name string
}
// }}}
// {{{ provider
type DummyServiceProvider struct{}
func (p *DummyServiceProvider) Name() string {
return DummyProviderName
}
func (p *DummyServiceProvider) Register(c Container) NewInstance {
return NewDummyService
}
func (p *DummyServiceProvider) Init(c Container) error {
log.Println("init")
return nil
}
func (p *DummyServiceProvider) InstantiateLater() bool {
return true
}
func (p *DummyServiceProvider) Params(c Container) []interface{} {
return []interface{}{c}
}
// }}}
// {{{ service
type DummyService struct {
IDummyService
// parameters
c Container
}
func (d *DummyService) GetDummy() DummyStruct {
return DummyStruct{
Name: "Dummy!",
}
}
func NewDummyService(params ...interface{}) (interface{}, error) {
c := params[0].(Container)
log.Println("new dummy service")
return &DummyService{c: c}, nil
}
// }}}
func TestBind(t *testing.T) {
container := NewGoWebContainer()
provider := &DummyServiceProvider{}
container.Bind(provider)
dummyService := container.MustMake(DummyProviderName).(IDummyService)
want := DummyStruct{
Name: "Dummy!",
}
got := dummyService.GetDummy()
if !reflect.DeepEqual(want, got) {
t.Errorf("want %v, got %v ", want, got)
}
}

View File

@ -1,291 +0,0 @@
package framework
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"strconv"
"sync"
"time"
)
// Context type is the customized context of Araneae framework
//
// It packages the internal context.Context with basic "wr" couple.
type Context struct {
ctx context.Context
request *http.Request
responseWriter http.ResponseWriter
handlers []ControllerHandler
// current handler index
index int
hasTimeout bool
writerMux *sync.Mutex
}
// NewContext create a new context
func NewContext(w http.ResponseWriter, r *http.Request) *Context {
return &Context{
ctx: r.Context(),
request: r,
responseWriter: w,
writerMux: &sync.Mutex{},
index: -1, // will be set to 0 when at the beginning
}
}
// {{{ Basic functions
// WriterMux returns the writer mutex.
//
// This is useful when goroutines concurrently write into responseWriter,
// while at the same time we are writing into the responseWriter for a
// panic or timeout.
// We can protect it at the context level.
func (ctx *Context) WriterMux() *sync.Mutex {
return ctx.writerMux
}
// GetRequest returns the original request
func (ctx *Context) GetRequest() *http.Request {
return ctx.request
}
// GetResponseWriter returns the original response writer
func (ctx *Context) GetResponseWriter() http.ResponseWriter {
return ctx.responseWriter
}
// SetHasTimeout indicates that the context has timeout.
//
// So that other goroutines won't write into the responseWriter anymore
func (ctx *Context) SetHasTimeout() {
ctx.hasTimeout = true
}
// HasTimeout returns whether the context has timeout.
func (ctx *Context) HasTimeout() bool {
return ctx.hasTimeout
}
// }}}
// {{{ Implements context interface
// BaseContext return a request default Context
func (ctx *Context) BaseContext() context.Context {
return ctx.request.Context()
}
// Done calls the base function
func (ctx *Context) Done() <-chan struct{} {
return ctx.BaseContext().Done()
}
// Deadline calls the base function
func (ctx *Context) Deadline() (deadline time.Time, ok bool) {
return ctx.BaseContext().Deadline()
}
// Err calls the base function
func (ctx *Context) Err() error {
return ctx.BaseContext().Err()
}
// Value calls the base function
func (ctx *Context) Value(key any) any {
return ctx.BaseContext().Value(key)
}
// Next runs the next function in the function chain
func (ctx *Context) Next() error {
ctx.index++
if ctx.index >= len(ctx.handlers) {
// This is the end of the chain
return nil
}
// Run this handler
if err := ctx.handlers[ctx.index](ctx); err != nil {
return err
}
return nil
}
// SetHandlers sets handlers for context
func (ctx *Context) SetHandlers(handlers []ControllerHandler) {
ctx.handlers = handlers
}
// }}}
// {{{ Implements request functions
// {{{ Request URI
// QueryInt gets an int value from the query request
func (ctx *Context) QueryInt(key string, defval int) (int, error) {
params, err := ctx.QueryAll()
if err != nil {
return defval, err
}
if vals, ok := params[key]; ok {
len := len(vals)
if len > 0 {
intval, err := strconv.Atoi(vals[len-1]) // return the last elem
if err != nil {
return defval, err
}
return intval, nil
}
}
return defval, errors.New("key not found")
}
// QueryString gets a string value from the query request
func (ctx *Context) QueryString(key string, defval string) (string, error) {
params, err := ctx.QueryAll()
if err != nil {
return defval, err
}
if vals, ok := params[key]; ok {
len := len(vals)
if len > 0 {
return vals[len-1], nil // return the last elem
}
}
return defval, errors.New("key not found")
}
// QueryArray gets an array of string values from the query request
func (ctx *Context) QueryArray(key string, defval []string) ([]string, error) {
params, err := ctx.QueryAll()
if err != nil {
return defval, err
}
if vals, ok := params[key]; ok {
return vals, nil // return the last elem
}
return defval, errors.New("key not found")
}
// QueryAll returns all queries in a request URL
func (ctx *Context) QueryAll() (url.Values, error) {
if ctx.request != nil {
return map[string][]string(ctx.request.URL.Query()), nil
}
return url.Values{}, errors.New("missing request in the context")
}
// }}}
// {{{ Post form
// FormInt gets an int value from the submitted form
func (ctx *Context) FormInt(key string, defval int) (int, error) {
vals, err := ctx.FormAll()
if err != nil {
return defval, err
}
valStrs, ok := vals[key]
if !ok {
return defval, errors.New("key not found")
}
valInt, err := strconv.Atoi(valStrs[0]) // Get the first one as result
if err != nil {
return defval, err
}
return valInt, nil
}
// FormString gets a string value from the submitted form
func (ctx *Context) FormString(key string, defval string) (string, error) {
vals, err := ctx.FormAll()
if err != nil {
return defval, err
}
valStrs, ok := vals[key]
if !ok {
return defval, errors.New("key not found")
}
return valStrs[0], nil
}
// FormArray gets an array of string values from the submitted form
func (ctx *Context) FormArray(key string, defval []string) ([]string, error) {
vals, err := ctx.FormAll()
if err != nil {
return defval, err
}
valStrs, ok := vals[key]
if !ok {
return defval, errors.New("key not found")
}
return valStrs, nil
}
// FormAll gets everything from the submitted form
func (ctx *Context) FormAll() (url.Values, error) {
if ctx.request != nil {
err := ctx.request.ParseForm()
if err != nil {
return url.Values{}, err
}
return ctx.request.PostForm, err
}
return url.Values{}, errors.New("missing request in the context")
}
// }}}
// {{{ application/json
// ReadJSON binds the request JSON body to an object.
//
// A pointer of obj should be passed.
func (ctx *Context) ReadJSON(obj any) error {
if ctx.request == nil {
return errors.New("missing request in the context")
}
dec := json.NewDecoder(ctx.request.Body)
err := dec.Decode(obj)
if err != nil {
return err
}
err = dec.Decode(&struct{}{})
if err != io.EOF {
return errors.New("body must have only a single JSON value")
}
return nil
}
// WriteJSON send back an object in JSON format with the status code
func (ctx *Context) WriteJSON(status int, obj any) error {
// There is a timeout, some error message data must have already been
// written to the output. Stop writing anything into the responseWriter.
if ctx.HasTimeout() {
return nil
}
data, err := json.Marshal(obj)
if err != nil {
return err
}
ctx.responseWriter.Header().Set("Content-type", "application/json")
ctx.responseWriter.WriteHeader(status)
_, err = ctx.responseWriter.Write(data)
if err != nil {
return err
}
return nil
}
// }}}
// }}}

15
framework/contract/app.go Normal file
View File

@ -0,0 +1,15 @@
package contract
const AppName = "goweb:app"
type App interface {
Version() string
BaseFolder() string
ConfigFolder() string
LogFolder() string
ProvidersFolder() string
MiddlewaresFolder() string
CommandsFolder() string
RuntimeFolder() string
TestsFolder() string
}

View File

@ -0,0 +1,10 @@
package contract
import "net/http"
const KernelName = "goweb:kernel"
type Kernel interface {
// HttpEngine we use actually gin.Engine, but it is open to change.
HttpEngine() http.Handler
}

View File

@ -1,3 +0,0 @@
package framework
type ControllerHandler func(c *Context) error

View File

@ -1,107 +0,0 @@
package framework
import (
"log"
"net/http"
"strings"
)
// Core is the core struct of the framework
type Core struct {
router map[string]*Trie
}
// NewCore initialize the Core.
func NewCore() *Core {
getRouter := NewTrie()
postRouter := NewTrie()
putRouter := NewTrie()
deleteRouter := NewTrie()
router := map[string]*Trie{}
router["GET"] = getRouter
router["POST"] = postRouter
router["PUT"] = putRouter
router["DELETE"] = deleteRouter
return &Core{router: router}
}
// Get is a simple get router
func (c *Core) Get(url string, handler ControllerHandler) {
upperUrl := strings.ToUpper(url)
if err := c.router["GET"].AddRouter(upperUrl, handler); err != nil {
log.Println(err)
}
}
// Post is a simple post router
func (c *Core) Post(url string, handler ControllerHandler) {
upperUrl := strings.ToUpper(url)
if err := c.router["POST"].AddRouter(upperUrl, handler); err != nil {
log.Println(err)
}
}
// Put is a simple put router
func (c *Core) Put(url string, handler ControllerHandler) {
upperUrl := strings.ToUpper(url)
if err := c.router["PUT"].AddRouter(upperUrl, handler); err != nil {
log.Println(err)
}
}
// Delete is a simple delete router
func (c *Core) Delete(url string, handler ControllerHandler) {
upperUrl := strings.ToUpper(url)
if err := c.router["DELETE"].AddRouter(upperUrl, handler); err != nil {
log.Println(err)
}
}
// FindRouteByRequest finds route using the request
func (c *Core) FindRouteByRequest(r *http.Request) []ControllerHandler {
upperUri := strings.ToUpper(r.URL.Path)
upperMethod := strings.ToUpper(r.Method)
mapper, ok := c.router[upperMethod]
if !ok {
log.Printf("Method %q is not recognized\n", upperMethod)
return nil
}
controllers := mapper.FindRoute(upperUri)
if controllers == nil {
log.Printf("URI %q is not recognized\n", r.URL.Path)
return nil
}
return controllers
}
func (c *Core) Group(prefix string) IGroup {
return &Group{
core: c,
prefix: prefix,
}
}
// ServeHTTP implements the Handler interface
func (c *Core) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Println("Welcome to the Araneae framework")
ctx := NewContext(w, r)
handlers := c.FindRouteByRequest(r)
if handlers == nil {
ctx.WriteJSON(http.StatusNotFound, "Request not found")
return
}
ctx.SetHandlers(handlers)
if err := ctx.Next(); err != nil {
ctx.WriteJSON(http.StatusInternalServerError, "Internal error")
return
}
}

1
framework/gin Submodule

Submodule framework/gin added at a5849d15be

View File

@ -1,43 +0,0 @@
package framework
// IGroup prefix routes
type IGroup interface {
Get(string, ControllerHandler)
Post(string, ControllerHandler)
Put(string, ControllerHandler)
Delete(string, ControllerHandler)
}
// Group is the implementation of IGroup interface
type Group struct {
core *Core
prefix string
}
// NewGroup create a new prefix group
func NewGroup(core *Core, prefix string) *Group {
return &Group{
core: core,
prefix: prefix,
}
}
// Get is a simple get router of the group
func (g *Group) Get(url string, handler ControllerHandler) {
g.core.Get(g.prefix+url, handler)
}
// Post is a simple post router of the group
func (g *Group) Post(url string, handler ControllerHandler) {
g.core.Post(g.prefix+url, handler)
}
// Put is a simple put router of the group
func (g *Group) Put(url string, handler ControllerHandler) {
g.core.Put(g.prefix+url, handler)
}
// Delete is a simple delete router of the group
func (g *Group) Delete(url string, handler ControllerHandler) {
g.core.Delete(g.prefix+url, handler)
}

View File

@ -1,25 +0,0 @@
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"git.vinchent.xyz/vinchent/go-web/framework"
)
func TestTimeout(t *testing.T) {
t.Run("Test timeout handler", func(t *testing.T) {
timeoutHandler := Timeout(1 * time.Millisecond)
request := httptest.NewRequest(http.MethodGet, "/", nil)
response := httptest.NewRecorder()
c := framework.NewContext(response, request)
err := timeoutHandler(c)
if err != nil {
t.Fatal(err)
}
})
}

View File

@ -0,0 +1,20 @@
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
)
// Recovery is a middleware that recovers from the panic
func Recovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
c.JSON(http.StatusInternalServerError, err)
}
}()
c.Next()
}
}

View File

@ -0,0 +1,31 @@
package middleware
import (
"log"
"github.com/gin-gonic/gin"
)
func Test1() gin.HandlerFunc {
return func(c *gin.Context) {
log.Println("middleware test1 pre")
c.Next()
log.Println("middleware test1 post")
}
}
func Test2() gin.HandlerFunc {
return func(c *gin.Context) {
log.Println("middleware test2 pre")
c.Next()
log.Println("middleware test2 post")
}
}
func Test3() gin.HandlerFunc {
return func(c *gin.Context) {
log.Println("middleware test3 pre")
c.Next()
log.Println("middleware test3 post")
}
}

View File

@ -6,15 +6,15 @@ import (
"net/http"
"time"
"git.vinchent.xyz/vinchent/go-web/framework"
"github.com/gin-gonic/gin"
)
func Timeout(d time.Duration) framework.ControllerHandler {
return func(c *framework.Context) error {
func Timeout(d time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
finish := make(chan struct{}, 1)
panicChan := make(chan interface{}, 1)
durationCtx, cancel := context.WithTimeout(c.BaseContext(), d)
durationCtx, cancel := context.WithTimeout(c.Request.Context(), d)
defer cancel()
go func() {
@ -35,14 +35,12 @@ func Timeout(d time.Duration) framework.ControllerHandler {
case p := <-panicChan:
// panic
log.Println(p)
c.GetResponseWriter().WriteHeader(http.StatusInternalServerError)
c.Status(http.StatusInternalServerError)
case <-finish:
// finish normally
log.Println("finish")
case <-durationCtx.Done():
c.SetHasTimeout()
c.GetResponseWriter().Write([]byte("time out"))
c.JSON(http.StatusRequestTimeout, "time out")
}
return nil
}
}

View File

@ -0,0 +1,98 @@
package middleware
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
)
func TestTimeout(t *testing.T) {
t.Run("Test timeout handler", func(t *testing.T) {
timeoutHandler := Timeout(1 * time.Millisecond)
longHandler := func(c *gin.Context) {
time.Sleep(2 * time.Millisecond)
}
res := prepareMiddlewareTest(t, timeoutHandler, longHandler)
assertCode(t, res.StatusCode, http.StatusRequestTimeout)
assertBody(t, res.Body, "\"time out\"")
})
t.Run("Test no timeout", func(t *testing.T) {
timeoutHandler := Timeout(2 * time.Millisecond)
quickHandler := func(c *gin.Context) {
// time.Sleep(1 * time.Millisecond)
c.JSON(http.StatusOK, "ok")
}
res := prepareMiddlewareTest(t, timeoutHandler, quickHandler)
assertCode(t, res.StatusCode, http.StatusOK)
})
}
func TestRecover(t *testing.T) {
t.Run("Test panic", func(t *testing.T) {
recoverer := Recovery()
panicHandler := func(c *gin.Context) {
panic("panic")
}
res := prepareMiddlewareTest(t, recoverer, panicHandler)
assertCode(t, res.StatusCode, http.StatusInternalServerError)
})
t.Run("Test no panic", func(t *testing.T) {
recoverer := Recovery()
normalHandler := func(c *gin.Context) {
c.JSON(http.StatusOK, "ok")
}
res := prepareMiddlewareTest(t, recoverer, normalHandler)
assertCode(t, res.StatusCode, http.StatusOK)
})
}
func prepareMiddlewareTest(
t testing.TB,
mid gin.HandlerFunc,
in gin.HandlerFunc,
) *http.Response {
t.Helper()
request := httptest.NewRequest(http.MethodGet, "/", nil)
response := httptest.NewRecorder()
_, r := gin.CreateTestContext(response)
r.Use(mid)
r.GET("/", in)
r.ServeHTTP(response, request)
res := response.Result()
return res
}
func assertCode(t testing.TB, got int, want int) {
t.Helper()
if got != want {
t.Errorf("status code got %d, want %d", got, want)
}
}
func assertBody(t testing.TB, got io.Reader, want string) {
t.Helper()
buf, _ := io.ReadAll(got)
if cmp := bytes.Compare(buf, []byte(want)); cmp != 0 {
t.Errorf("got %q, want %q", string(buf), want)
}
}

24
framework/provider.go Normal file
View File

@ -0,0 +1,24 @@
package framework
// NewInstance defines the function to create a new instance for a service.
type NewInstance func(...interface{}) (interface{}, error)
// ServiceProvider is the interface for a service provider
type ServiceProvider interface {
// Register a service into the service container, return a NewInstance constructer
Register(Container) NewInstance
// Init is called when instantiating the service.
Init(Container) error
// InstantiateLater decides if the service is instantiated at register phase.
// If true, then this service shall be instantiated later.
InstantiateLater() bool
// Params defines the parameters needed to instantiate a service.
// NOTE: First one should always be the container.
Params(Container) []interface{}
// Name is the name of the service provider
Name() string
}

View File

@ -0,0 +1,30 @@
package app
import (
"git.vinchent.xyz/vinchent/go-web/framework"
"git.vinchent.xyz/vinchent/go-web/framework/contract"
)
type GoWebAppProvider struct {
BaseFolder string
}
func (goweb *GoWebAppProvider) Register(c framework.Container) framework.NewInstance {
return NewGoWebApp
}
func (goweb *GoWebAppProvider) Init(c framework.Container) error {
return nil
}
func (goweb *GoWebAppProvider) InstantiateLater() bool {
return false
}
func (goweb *GoWebAppProvider) Params(c framework.Container) []interface{} {
return []interface{}{c, goweb.BaseFolder}
}
func (goweb *GoWebAppProvider) Name() string {
return contract.AppName
}

View File

@ -0,0 +1,102 @@
package app
import (
"flag"
"fmt"
"path/filepath"
"git.vinchent.xyz/vinchent/go-web/framework"
"git.vinchent.xyz/vinchent/go-web/framework/utils"
)
type GoWebApp struct {
container framework.Container
baseFolder string
}
const AppVersion = "0.0.1"
func ErrParamsAmount(want int, got int) error {
return fmt.Errorf("want %d arguments, got %d", want, got)
}
// NewGoWebApp creates a new GoWeb app.
//
// \params[in]: container
// \params[in]: baseFolder
func NewGoWebApp(params ...interface{}) (interface{}, error) {
if len(params) != 2 {
return nil, ErrParamsAmount(2, len(params))
}
container := params[0].(framework.Container)
baseFolder := params[1].(string)
return &GoWebApp{
container: container,
baseFolder: baseFolder,
}, nil
}
func (goweb *GoWebApp) Version() string {
return AppVersion
}
func (goweb *GoWebApp) BaseFolder() string {
if goweb.baseFolder != "" {
return goweb.baseFolder
}
// not defined yet
var baseFolder string
flag.StringVar(
&baseFolder,
"base_folder",
utils.GetExecDirectory(),
"base_folder of the app. Use the current path as default value",
)
return baseFolder
}
func (goweb *GoWebApp) ConfigFolder() string {
return filepath.Join(goweb.BaseFolder(), "config")
}
func (goweb *GoWebApp) StorageFolder() string {
return filepath.Join(goweb.BaseFolder(), "storage")
}
func (goweb *GoWebApp) LogFolder() string {
// storage/log
return filepath.Join(goweb.StorageFolder(), "log")
}
func (goweb *GoWebApp) ProvidersFolder() string {
return filepath.Join(goweb.BaseFolder(), "providers")
}
func (goweb *GoWebApp) WebFolder() string {
return filepath.Join(goweb.BaseFolder(), "web")
}
func (goweb *GoWebApp) MiddlewaresFolder() string {
// http/middlewares
return filepath.Join(goweb.WebFolder(), "middlewares")
}
func (goweb *GoWebApp) ConsoleFolder() string {
return filepath.Join(goweb.BaseFolder(), "console")
}
func (goweb *GoWebApp) CommandsFolder() string {
// console/commands
return filepath.Join(goweb.ConsoleFolder(), "commands")
}
func (goweb *GoWebApp) RuntimeFolder() string {
return filepath.Join(goweb.StorageFolder(), "runtime")
}
func (goweb *GoWebApp) TestsFolder() string {
return filepath.Join(goweb.BaseFolder(), "tests")
}

View File

@ -0,0 +1,31 @@
package kernel
import (
"git.vinchent.xyz/vinchent/go-web/framework"
"git.vinchent.xyz/vinchent/go-web/framework/contract"
"github.com/gin-gonic/gin"
)
type GoWebKernelProvider struct {
HttpEngine *gin.Engine
}
func (goweb *GoWebKernelProvider) Register(c framework.Container) framework.NewInstance {
return NewGoWebKernelService
}
func (goweb *GoWebKernelProvider) Init(c framework.Container) error {
return nil
}
func (goweb *GoWebKernelProvider) InstantiateLater() bool {
return false
}
func (goweb *GoWebKernelProvider) Params(c framework.Container) []interface{} {
return []interface{}{goweb.HttpEngine}
}
func (goweb *GoWebKernelProvider) Name() string {
return contract.AppName
}

View File

@ -0,0 +1,22 @@
package kernel
import (
"net/http"
"github.com/gin-gonic/gin"
)
type GoWebKernelService struct {
// Since the container is included in the engine, we don't need to pass
// it as an argument.
engine *gin.Engine
}
func NewGoWebKernelService(params ...interface{}) (interface{}, error) {
httpEngine := params[0].(*gin.Engine)
return &GoWebKernelService{engine: httpEngine}, nil
}
func (goweb *GoWebKernelService) HttpEngine() http.Handler {
return goweb.engine
}

View File

@ -1,154 +0,0 @@
package framework
import (
"errors"
"fmt"
"strings"
)
type Trie struct {
root *node
}
func NewTrie() *Trie {
return &Trie{root: newNode("")}
}
func (t *Trie) FindRoute(uri string) []ControllerHandler {
uri = strings.TrimPrefix(uri, "/")
if uri == "" {
return t.root.handlers
}
found := t.root.findRoute(uri)
if found == nil {
return nil
}
return found.handlers
}
func (t *Trie) AddRouter(uri string, handler ControllerHandler) error {
uri = strings.TrimPrefix(uri, "/")
if uri == "" {
t.root.isLast = true
t.root.handlers = append(t.root.handlers, handler)
return nil
}
upperUri := strings.ToUpper(uri)
match := t.FindRoute(upperUri)
if match != nil {
// existing route
return fmt.Errorf("existing route for %q", uri)
}
// The route does not exist, add it to the tree
err := t.root.addRoute(upperUri, handler)
if err != nil {
return err
}
return nil
}
type node struct {
isLast bool
segment string
handlers []ControllerHandler
children []*node
}
func newNode(segment string) *node {
return &node{segment: segment}
}
func isWildcard(s string) bool {
return strings.HasPrefix(s, ":")
}
// /user/name
// /user/:id/name
// /user/3/name
// findRoute finds the handler for the uri if exists.
//
// we suppose that uri passed here doesn't begin with "/"
func (n *node) findRoute(uri string) *node {
splitted := strings.SplitN(uri, "/", 2)
splittedLen := len(splitted)
if isWildcard(splitted[0]) {
// input is a wildcard, check if this endpoint has already children
// if so, return the first one which is not nil.
nbChildren := len(n.children)
if nbChildren > 0 && n.children[0].segment != splitted[0] {
// several nodes exist, return the first one that is not nil
return n.children[0]
}
}
// try to find the value in childre
for _, child := range n.children {
if isWildcard(child.segment) || child.segment == splitted[0] {
if splittedLen == 1 {
// This is the last value, do the check and return
if child.isLast {
// if isLast, that means we have already registered the endpoint
return child
} else {
// otherwise, take it as not registered
return nil
}
}
// More segments to check
return child.findRoute(splitted[1])
}
}
// nothing found in the children
return nil
}
func (n *node) addRoute(uri string, handler ControllerHandler) error {
splitted := strings.SplitN(uri, "/", 2)
splittedLen := len(splitted)
isLast := splittedLen == 1
// try to find the value in childre
for _, child := range n.children {
if isWildcard(child.segment) || child.segment == splitted[0] {
if isLast {
// This is the last value, do the check and return
if child.isLast {
// if isLast, that means we have already registered the endpoint
return errors.New("node exists")
} else {
// otherwise, set the child
child.isLast = true
child.handlers = append(child.handlers, handler)
return nil
}
}
// More segments to check
return child.addRoute(splitted[1], handler)
}
}
// create a new node
new := newNode(splitted[0])
if isLast {
// this is the end
new.handlers = append(new.handlers, handler)
new.isLast = true
n.children = append(n.children, new)
return nil
}
// continue
new.isLast = false
err := new.addRoute(splitted[1], handler)
if err != nil {
return err
}
n.children = append(n.children, new)
return nil
}

12
framework/utils/exec.go Normal file
View File

@ -0,0 +1,12 @@
package utils
import "os"
// Get the current folder with / suffix
func GetExecDirectory() string {
path, err := os.Getwd()
if err != nil {
return ""
}
return path + "/"
}

50
go.mod
View File

@ -1,3 +1,53 @@
module git.vinchent.xyz/vinchent/go-web
go 1.22.5
require (
github.com/gin-gonic/gin v1.10.0
github.com/spf13/cobra v1.8.1
)
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/quic-go v0.43.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/mock v0.4.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/gin-gonic/gin => ./framework/gin
replace github.com/spf13/cobra => ./framework/cobra

132
go.sum
View File

@ -0,0 +1,132 @@
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/quic-go v0.43.1 h1:fLiMNfQVe9q2JvSsiXo4fXOEguXHGGl9+6gLp4RPeZQ=
github.com/quic-go/quic-go v0.43.1/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -1,83 +1,29 @@
package main
import (
"context"
"log"
"net/http"
"time"
"git.vinchent.xyz/vinchent/go-web/framework"
"github.com/gin-gonic/gin"
)
// func FooControllerHandler(ctx *framework.Context) error {
// return ctx.WriteJSON(http.StatusOK, map[string]any{
// "code": 0,
// })
// }
func FooControllerHandler(ctx *framework.Context) error {
durationCtx, cancel := context.WithTimeout(ctx.BaseContext(), time.Duration(1*time.Second))
defer cancel()
finish := make(chan struct{}, 1)
panicChan := make(chan interface{}, 1)
// some long task
go func() {
// Deal with the panic during the work
defer func() {
if p := recover(); p != nil {
panicChan <- p
}
}()
// do the business
func UserLoginController(ctx *gin.Context) {
time.Sleep(10 * time.Second)
ctx.WriteJSON(http.StatusOK, "ok")
finish <- struct{}{}
}()
select {
case <-panicChan:
// Protect ResponseWriter for concurrently writing from different
// goroutines if there are any
ctx.WriterMux().Lock()
defer ctx.WriterMux().Unlock()
log.Println("panicked")
ctx.WriteJSON(http.StatusInternalServerError, "panicked")
case <-finish:
log.Println("finished")
case <-durationCtx.Done():
ctx.WriterMux().Lock()
defer ctx.WriterMux().Unlock()
log.Println("Timeout")
ctx.WriteJSON(http.StatusInternalServerError, "time out")
ctx.SetHasTimeout()
}
return nil
ctx.JSON(http.StatusOK, "ok")
}
func UserLoginController(ctx *framework.Context) error {
ctx.WriteJSON(http.StatusOK, "ok")
return nil
func SubjectDelController(ctx *gin.Context) {
ctx.JSON(http.StatusAccepted, "deleted")
}
func SubjectDelController(ctx *framework.Context) error {
ctx.WriteJSON(http.StatusAccepted, "deleted")
return nil
func SubjectUpdateController(ctx *gin.Context) {
ctx.JSON(http.StatusAccepted, "updated")
}
func SubjectUpdateController(ctx *framework.Context) error {
ctx.WriteJSON(http.StatusAccepted, "updated")
return nil
func SubjectGetController(ctx *gin.Context) {
ctx.JSON(http.StatusAccepted, "got")
}
func SubjectGetController(ctx *framework.Context) error {
ctx.WriteJSON(http.StatusAccepted, "got")
return nil
}
func SubjectListController(ctx *framework.Context) error {
ctx.WriteJSON(http.StatusAccepted, "list")
return nil
func SubjectListController(ctx *gin.Context) {
ctx.JSON(http.StatusAccepted, "list")
}

25
main.go
View File

@ -1,21 +1,24 @@
package main
import (
"log"
"net/http"
"git.vinchent.xyz/vinchent/go-web/app/console"
"git.vinchent.xyz/vinchent/go-web/app/web"
"git.vinchent.xyz/vinchent/go-web/framework"
"git.vinchent.xyz/vinchent/go-web/framework/providers/app"
"git.vinchent.xyz/vinchent/go-web/framework/providers/kernel"
)
func main() {
core := framework.NewCore()
registerRouter(core)
server := &http.Server{
Addr: ":8080",
Handler: core,
container := framework.NewGoWebContainer()
container.Bind(&app.GoWebAppProvider{})
engine, err := web.NewHttpEngine()
if err != nil {
panic("Cannot start http server")
}
if err := server.ListenAndServe(); err != nil {
log.Panic(err)
}
container.Bind(&kernel.GoWebKernelProvider{HttpEngine: engine})
console.RunCommand(container)
}

View File

@ -1,15 +1,21 @@
package main
import "git.vinchent.xyz/vinchent/go-web/framework"
import (
"git.vinchent.xyz/vinchent/go-web/framework/middlewares"
"github.com/gin-gonic/gin"
)
func registerRouter(core *framework.Core) {
core.Get("/user/login", UserLoginController)
func registerRouter(core *gin.Engine) {
core.Use(middleware.Test1(), middleware.Test2())
core.GET("/user/login", UserLoginController)
subjectApi := core.Group("/subject")
{
subjectApi.Delete("/:id", SubjectDelController)
subjectApi.Put("/:id", SubjectUpdateController)
subjectApi.Get("/:id", SubjectGetController)
subjectApi.Get("/list/all", SubjectListController)
subjectApi.Use(middleware.Test3())
subjectApi.DELETE("/:id", SubjectDelController)
subjectApi.PUT("/:id", SubjectUpdateController)
subjectApi.GET("/:id", SubjectGetController)
subjectApi.GET("/:id/test", SubjectGetController)
subjectApi.GET("/list/all", SubjectListController)
}
}