├── .gitignore ├── constants ├── routingSigns.go └── routingSigns_test.go ├── .github ├── workflows │ ├── codecov.yml │ └── test.yml ├── pull_request_template.md └── ISSUE_TEMPLATE │ ├── request-feature.yml │ ├── blank-issue.yml │ └── bug-report.yml ├── go.mod ├── .circleci └── config.yml ├── LICENSE ├── group_test.go ├── group.go ├── middleware.go ├── route.go ├── route_test.go ├── router_test.go ├── app_test.go ├── app.go ├── go.sum ├── router.go ├── middleware_test.go ├── context.go ├── README.md └── context_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea -------------------------------------------------------------------------------- /constants/routingSigns.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // WildcardSign is the sign for a wildcard route 5 | WildcardSign = "*" 6 | 7 | // ParamSign is the sign for a parameter 8 | ParamSign = ":" 9 | 10 | // OptionalSign is the sign for an optional parameter 11 | OptionalSign = "?" 12 | ) 13 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Test and coverage 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | with: 11 | fetch-depth: 2 12 | - uses: actions/setup-go@v2 13 | with: 14 | go-version: '1.18' 15 | - name: Run coverage 16 | run: go test -race -coverprofile=coverage.out -covermode=atomic 17 | - name: Upload coverage to Codecov 18 | uses: codecov/codecov-action@v3 -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: ['**'] 7 | pull_request: 8 | paths: ['**'] 9 | 10 | jobs: 11 | test: 12 | name: Run tests 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | 18 | - name: Install dependencies 19 | run: go mod download 20 | 21 | - name: Run tests with coverage 22 | run: | 23 | go test -v -race -coverprofile=coverage.out ./... -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gopulse/pulse 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect 7 | github.com/go-playground/locales v0.14.1 // indirect 8 | github.com/go-playground/universal-translator v0.18.1 // indirect 9 | github.com/go-playground/validator/v10 v10.12.0 // indirect 10 | github.com/leodido/go-urn v1.2.2 // indirect 11 | golang.org/x/crypto v0.7.0 // indirect 12 | golang.org/x/sys v0.6.0 // indirect 13 | golang.org/x/text v0.8.0 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /constants/routingSigns_test.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestConstants(t *testing.T) { 9 | expected := map[string]string{ 10 | "WildcardSign": WildcardSign, 11 | "ParamSign": ParamSign, 12 | "OptionalSign": OptionalSign, 13 | } 14 | 15 | actual := map[string]string{ 16 | "WildcardSign": "*", 17 | "ParamSign": ":", 18 | "OptionalSign": "?", 19 | } 20 | 21 | if !reflect.DeepEqual(expected, actual) { 22 | t.Errorf("Expected %v, but got %v", expected, actual) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | please describe the purpose of the pull request, including any background context and links to related issues 4 | 5 | If it resolves an existing issue, please include a link to the issue. 6 | 7 | Fixes #issuenumber 8 | 9 | # Type of change 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | codecov: codecov/codecov@3.2.4 5 | 6 | jobs: 7 | build: 8 | working_directory: ~/repo 9 | docker: 10 | - image: circleci/golang:latest 11 | steps: 12 | - checkout 13 | - restore_cache: 14 | keys: 15 | - go-mod-v4-{{ checksum "go.sum" }} 16 | - run: 17 | name: Install Dependencies 18 | command: go mod download 19 | - save_cache: 20 | key: go-mod-v4-{{ checksum "go.sum" }} 21 | paths: 22 | - "/go/pkg/mod" 23 | - run: 24 | name: Run tests and coverage 25 | command: go test -race -coverprofile=coverage.out -covermode=atomic 26 | - codecov/upload: 27 | file: coverage.out 28 | - store_test_results: 29 | path: /tmp/test-reports 30 | 31 | workflows: 32 | sample: 33 | jobs: 34 | - build 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/request-feature.yml: -------------------------------------------------------------------------------- 1 | name: "Feature Request" 2 | title: "Feature: " 3 | description: Suggest an idea to improve this project. 4 | labels: [":pencil2: Feature"] 5 | 6 | body: 7 | - type: markdown 8 | id: recommendations 9 | attributes: 10 | value: | 11 | ### Before you submit 12 | - Don't forget you can ask your questions in our [Discord server](https://discord.com/invite/JKcTwZYJ). 13 | - Write your issue clearly. 14 | - type: textarea 15 | id: description 16 | attributes: 17 | label: "Feature Description" 18 | description: "A clear and detailed description of the feature you would like to see added." 19 | placeholder: "Explain your feature clearly, and in detail." 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: snippet 24 | attributes: 25 | label: "Code Snippet (optional)" 26 | description: "Code snippet may be really helpful to describe some features." 27 | placeholder: "Share a code snippet to explain the feature better." 28 | render: go 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Pulse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /group_test.go: -------------------------------------------------------------------------------- 1 | package pulse 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestRouter_Group(t *testing.T) { 9 | router := NewRouter() 10 | api := &Group{ 11 | Prefix: "/api", 12 | Router: router, 13 | } 14 | v1 := api.Group("/v1") 15 | v1.GET("/users", func(ctx *Context) error { 16 | ctx.String("users") 17 | return nil 18 | }) 19 | v1.POST("/users/1", func(ctx *Context) error { 20 | ctx.String("users") 21 | return nil 22 | }) 23 | v1.GET("/users/1", func(ctx *Context) error { 24 | ctx.String("users") 25 | return nil 26 | }) 27 | v1.PUT("/users/1", func(ctx *Context) error { 28 | ctx.String("users") 29 | return nil 30 | }) 31 | v1.DELETE("/users/1", func(ctx *Context) error { 32 | ctx.String("users") 33 | return nil 34 | }) 35 | v1.PATCH("/users/1", func(ctx *Context) error { 36 | ctx.String("users") 37 | return nil 38 | }) 39 | v1.OPTIONS("/users/1", func(ctx *Context) error { 40 | ctx.String("users") 41 | return nil 42 | }) 43 | v1.HEAD("/users/1", func(ctx *Context) error { 44 | ctx.String("users") 45 | return nil 46 | }) 47 | v1.Static("/users", "./static", &Static{ 48 | Compress: true, 49 | ByteRange: false, 50 | IndexName: "index.html", 51 | CacheDuration: 24 * time.Hour, 52 | }) 53 | 54 | app.Router = router 55 | } 56 | -------------------------------------------------------------------------------- /group.go: -------------------------------------------------------------------------------- 1 | package pulse 2 | 3 | type Group struct { 4 | Prefix string 5 | Router *Router 6 | } 7 | 8 | func (g *Group) Group(prefix string) *Group { 9 | return &Group{ 10 | Prefix: g.Prefix + prefix, 11 | Router: g.Router, 12 | } 13 | } 14 | 15 | func (g *Group) Use(middleware Middleware) { 16 | g.Router.Use(g.Prefix, middleware) 17 | } 18 | 19 | func (g *Group) GET(path string, handlers ...Handler) { 20 | g.Router.Get(g.Prefix+path, handlers...) 21 | } 22 | 23 | func (g *Group) POST(path string, handlers ...Handler) { 24 | g.Router.Post(g.Prefix+path, handlers...) 25 | } 26 | 27 | func (g *Group) PUT(path string, handlers ...Handler) { 28 | g.Router.Put(g.Prefix+path, handlers...) 29 | } 30 | 31 | func (g *Group) DELETE(path string, handlers ...Handler) { 32 | g.Router.Delete(g.Prefix+path, handlers...) 33 | } 34 | 35 | func (g *Group) PATCH(path string, handlers ...Handler) { 36 | g.Router.Patch(g.Prefix+path, handlers...) 37 | } 38 | 39 | func (g *Group) OPTIONS(path string, handlers ...Handler) { 40 | g.Router.Options(g.Prefix+path, handlers...) 41 | } 42 | 43 | func (g *Group) HEAD(path string, handlers ...Handler) { 44 | g.Router.Head(g.Prefix+path, handlers...) 45 | } 46 | 47 | func (g *Group) Static(path, root string, config *Static) { 48 | g.Router.Static(g.Prefix+path, root, config) 49 | } 50 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package pulse 2 | 3 | type MiddlewareFunc func(handler Handler) Handler 4 | 5 | type Middleware interface { 6 | Middleware(handler Handler) Handler 7 | Handle(ctx *Context, next Handler) error 8 | } 9 | 10 | func (m MiddlewareFunc) Middleware(handler Handler) Handler { 11 | return m(handler) 12 | } 13 | 14 | func (r *Router) Use(method string, middlewares ...interface{}) { 15 | for _, middleware := range middlewares { 16 | if middlewareFunc, ok := middleware.(MiddlewareFunc); ok { 17 | r.middlewares[method] = append(r.middlewares[method], middlewareFunc) 18 | } else if middleware, ok := middleware.(Middleware); ok { 19 | r.middlewares[method] = append(r.middlewares[method], middleware) 20 | } 21 | } 22 | } 23 | 24 | func CORSMiddleware() MiddlewareFunc { 25 | return func(handler Handler) Handler { 26 | return func(ctx *Context) error { 27 | ctx.SetResponseHeader("Access-Control-Allow-Origin", "*") 28 | ctx.SetResponseHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") 29 | ctx.SetResponseHeader("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") 30 | return handler(ctx) 31 | } 32 | } 33 | } 34 | 35 | func (m MiddlewareFunc) Handle(ctx *Context, next Handler) error { 36 | h := m(next) 37 | return h(ctx) 38 | } 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/blank-issue.yml: -------------------------------------------------------------------------------- 1 | name: "Blank Issue" 2 | description: "Create your own issue." 3 | 4 | body: 5 | - type: markdown 6 | id: recommendations 7 | attributes: 8 | value: | 9 | ### Before you submit 10 | - Use the search tool before opening a new issue 11 | - Don't forget you can ask your questions in our [Discord server](https://discord.com/invite/JKcTwZYJ). 12 | - Write your issue clearly. 13 | - Please provide source code if possible. 14 | - type: textarea 15 | id: description 16 | attributes: 17 | label: "Issue Description" 18 | description: "A clear and detailed description of the issue you would like to see fixed." 19 | placeholder: "Explain your issue clearly, and in detail." 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | id: reproduce 25 | attributes: 26 | label: "How to Reproduce" 27 | description: "Steps to reproduce the behavior." 28 | placeholder: | 29 | 1. Go to '...' 30 | 2. Click on '....' 31 | 3. Scroll down to '....' 32 | 33 | - type: textarea 34 | id: expected 35 | attributes: 36 | label: "Expected Behavior" 37 | description: "A clear and concise description of what you expected to happen." 38 | placeholder: "Explain what you expected to happen." 39 | 40 | - type: textarea 41 | id: snippet 42 | attributes: 43 | label: "Code Snippet (optional)" 44 | description: "Code snippet may be really helpful to describe some bugs." 45 | placeholder: "Share a code snippet to explain the bug better." 46 | render: go -------------------------------------------------------------------------------- /route.go: -------------------------------------------------------------------------------- 1 | package pulse 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type Route struct { 8 | Method string 9 | Path string 10 | Handlers []Handler 11 | ParamNames []string 12 | } 13 | 14 | // Get adds the route to the router with the GET method 15 | func (r *Router) Get(path string, handlers ...Handler) { 16 | r.Add(http.MethodGet, path, handlers...) 17 | } 18 | 19 | // Post adds the route to the router with the POST method 20 | func (r *Router) Post(path string, handlers ...Handler) { 21 | r.Add(http.MethodPost, path, handlers...) 22 | } 23 | 24 | // Put adds the route to the router with the PUT method 25 | func (r *Router) Put(path string, handlers ...Handler) { 26 | r.Add(http.MethodPut, path, handlers...) 27 | } 28 | 29 | // Delete adds the route to the router with the DELETE method 30 | func (r *Router) Delete(path string, handlers ...Handler) { 31 | r.Add(http.MethodDelete, path, handlers...) 32 | } 33 | 34 | // Patch adds the route to the router with the PATCH method 35 | func (r *Router) Patch(path string, handlers ...Handler) { 36 | r.Add(http.MethodPatch, path, handlers...) 37 | } 38 | 39 | // Head adds the route to the router with the HEAD method 40 | func (r *Router) Head(path string, handlers ...Handler) { 41 | r.Add(http.MethodHead, path, handlers...) 42 | } 43 | 44 | // Options adds the route to the router with the OPTIONS method 45 | func (r *Router) Options(path string, handlers ...Handler) { 46 | r.Add(http.MethodOptions, path, handlers...) 47 | } 48 | 49 | // Connect adds the route to the router with the CONNECT method 50 | func (r *Router) Connect(path string, handlers ...Handler) { 51 | r.Add(http.MethodConnect, path, handlers...) 52 | } 53 | 54 | // Trace adds the route to the router with the TRACE method 55 | func (r *Router) Trace(path string, handlers ...Handler) { 56 | r.Add(http.MethodTrace, path, handlers...) 57 | } 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: "Bug Report" 2 | title: "Bug: " 3 | description: Report a bug to help us improve this project. 4 | labels: [":bug: Bug"] 5 | 6 | body: 7 | - type: markdown 8 | id: recommendations 9 | attributes: 10 | value: | 11 | ### Before you submit 12 | - Don't forget you can ask your questions in our [Discord server](https://discord.com/invite/JKcTwZYJ). 13 | - Write your issue clearly. 14 | - type: textarea 15 | id: description 16 | attributes: 17 | label: "Bug Description" 18 | description: "A clear and detailed description of the bug you would like to see fixed." 19 | placeholder: "Explain your bug clearly, and in detail." 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: reproduce 24 | attributes: 25 | label: "How to Reproduce" 26 | description: "Steps to reproduce the behavior." 27 | placeholder: | 28 | 1. Go to '...' 29 | 2. Click on '....' 30 | 3. Scroll down to '....' 31 | 4. See error 32 | validations: 33 | required: true 34 | 35 | - type: textarea 36 | id: expected 37 | attributes: 38 | label: "Expected Behavior" 39 | description: "A clear and concise description of what you expected to happen." 40 | placeholder: "Explain what you expected to happen." 41 | validations: 42 | required: true 43 | 44 | - type: input 45 | id: version 46 | attributes: 47 | label: "Version" 48 | description: "The version of Pulse you are using." 49 | placeholder: "v1.0.0" 50 | validations: 51 | required: true 52 | 53 | - type: textarea 54 | id: snippet 55 | attributes: 56 | label: "Code Snippet (optional)" 57 | description: "Code snippet may be really helpful to describe some bugs." 58 | placeholder: "Share a code snippet to explain the bug better." 59 | render: go -------------------------------------------------------------------------------- /route_test.go: -------------------------------------------------------------------------------- 1 | package pulse 2 | 3 | import "testing" 4 | 5 | func TestRoute_Get(t *testing.T) { 6 | router := NewRouter() 7 | 8 | app.Router = router 9 | 10 | router.Get("/users/*", func(ctx *Context) error { 11 | ctx.String("hello") 12 | return nil 13 | }) 14 | } 15 | 16 | func TestRoute_Post(t *testing.T) { 17 | router := NewRouter() 18 | 19 | app.Router = router 20 | 21 | router.Post("/users/*", func(ctx *Context) error { 22 | ctx.String("hello") 23 | return nil 24 | }) 25 | } 26 | 27 | func TestRoute_Put(t *testing.T) { 28 | router := NewRouter() 29 | 30 | app.Router = router 31 | 32 | router.Put("/users/*", func(ctx *Context) error { 33 | ctx.String("hello") 34 | return nil 35 | }) 36 | } 37 | 38 | func TestRoute_Delete(t *testing.T) { 39 | router := NewRouter() 40 | 41 | app.Router = router 42 | 43 | router.Delete("/users/*", func(ctx *Context) error { 44 | ctx.String("hello") 45 | return nil 46 | }) 47 | } 48 | 49 | func TestRoute_Patch(t *testing.T) { 50 | router := NewRouter() 51 | 52 | app.Router = router 53 | 54 | router.Patch("/users/*", func(ctx *Context) error { 55 | ctx.String("hello") 56 | return nil 57 | }) 58 | } 59 | 60 | func TestRoute_Head(t *testing.T) { 61 | router := NewRouter() 62 | 63 | app.Router = router 64 | 65 | router.Head("/users/*", func(ctx *Context) error { 66 | ctx.String("hello") 67 | return nil 68 | }) 69 | } 70 | 71 | func TestRoute_Options(t *testing.T) { 72 | router := NewRouter() 73 | 74 | app.Router = router 75 | 76 | router.Options("/users/*", func(ctx *Context) error { 77 | ctx.String("hello") 78 | return nil 79 | }) 80 | } 81 | 82 | func TestRoute_Connect(t *testing.T) { 83 | router := NewRouter() 84 | 85 | app.Router = router 86 | 87 | router.Connect("/users/*", func(ctx *Context) error { 88 | ctx.String("hello") 89 | return nil 90 | }) 91 | } 92 | 93 | func TestRoute_Trace(t *testing.T) { 94 | router := NewRouter() 95 | 96 | app.Router = router 97 | 98 | router.Trace("/users/*", func(ctx *Context) error { 99 | ctx.String("hello") 100 | return nil 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /router_test.go: -------------------------------------------------------------------------------- 1 | package pulse 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "net/url" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func init() { 12 | app = New(Config{ 13 | AppName: "Test App", 14 | Network: "tcp", 15 | }) 16 | } 17 | 18 | func TestRouterHandler(t *testing.T) { 19 | router := NewRouter() 20 | 21 | app.Router = router 22 | 23 | router.Get("/users", func(ctx *Context) error { 24 | ctx.String("hello") 25 | return nil 26 | }) 27 | } 28 | 29 | func TestRouter_find(t *testing.T) { 30 | router := NewRouter() 31 | 32 | app.Router = router 33 | 34 | router.Get("/users/*", func(ctx *Context) error { 35 | ctx.String("hello") 36 | return nil 37 | }) 38 | 39 | router.Find("GET", "/users/1") 40 | } 41 | 42 | func TestRouter_Static(t *testing.T) { 43 | router := NewRouter() 44 | 45 | app.Router = router 46 | 47 | router.Static("/users", "./static", &Static{ 48 | Compress: true, 49 | ByteRange: false, 50 | IndexName: "index.html", 51 | CacheDuration: 24 * time.Hour, 52 | }) 53 | } 54 | 55 | func TestStatic_notFoundHandler(t *testing.T) { 56 | rec := httptest.NewRecorder() 57 | 58 | // Call the notFoundHandler method. 59 | options := &Static{} 60 | options.notFoundHandler(rec) 61 | 62 | // Verify that the response has the expected status code and headers. 63 | if rec.Code != http.StatusNotFound { 64 | t.Errorf("unexpected status code: got %d, want %d", rec.Code, http.StatusNotFound) 65 | } 66 | expectedContentType := "text/plain; charset=utf-8" 67 | actualContentType := rec.Header().Get("Content-Type") 68 | if actualContentType != expectedContentType { 69 | t.Errorf("unexpected content type: got %q, want %q", actualContentType, expectedContentType) 70 | } 71 | 72 | // Verify that the response body contains the expected text. 73 | expectedBody := "404 Not Found" 74 | actualBody := rec.Body.String() 75 | if actualBody != expectedBody { 76 | t.Errorf("unexpected body: got %q, want %q", actualBody, expectedBody) 77 | } 78 | } 79 | 80 | func TestStatic_PathRewrite(t *testing.T) { 81 | // Create a new request with the specified path. 82 | req := &http.Request{ 83 | URL: &url.URL{ 84 | Path: "/path/to/file/", 85 | }, 86 | } 87 | 88 | // Call the PathRewrite method. 89 | options := &Static{IndexName: "index.html"} 90 | rewritten := options.PathRewrite(req) 91 | 92 | // Verify that the path was rewritten correctly. 93 | expectedRewritten := "/path/to/index.html" 94 | actualRewritten := string(rewritten) 95 | if actualRewritten != expectedRewritten { 96 | t.Errorf("unexpected path rewrite: got %q, want %q", actualRewritten, expectedRewritten) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app_test.go: -------------------------------------------------------------------------------- 1 | package pulse 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | var app *Pulse 11 | 12 | func init() { 13 | app = New(Config{ 14 | AppName: "Test App", 15 | }) 16 | } 17 | 18 | func TestNew(t *testing.T) { 19 | app := New() 20 | if app.config.AppName != DefaultAppName { 21 | t.Errorf("AppName: expected %q, actual %q", DefaultAppName, app.config.AppName) 22 | } 23 | if app.config.Network != DefaultNetwork { 24 | t.Errorf("Network: expected %q, actual %q", DefaultNetwork, app.config.Network) 25 | } 26 | 27 | // Test New() function with custom config 28 | app = New(Config{ 29 | AppName: "Test App", 30 | Network: "udp", 31 | }) 32 | if app.config.AppName != "Test App" { 33 | t.Errorf("AppName: expected %q, actual %q", "Test App", app.config.AppName) 34 | } 35 | if app.config.Network != "udp" { 36 | t.Errorf("Network: expected %q, actual %q", "udp", app.config.Network) 37 | } 38 | } 39 | 40 | func TestPulse_startupMessage(t *testing.T) { 41 | app := New(Config{ 42 | AppName: "Test App", 43 | }) 44 | 45 | addr := "localhost:8080" 46 | expected := "=> Server started on <" + addr + ">\n" + 47 | "=> App Name: " + app.config.AppName + "\n" + 48 | "=> Press CTRL+C to stop\n" 49 | actual := app.startupMessage(addr) 50 | 51 | if actual != expected { 52 | t.Errorf("startupMessage: expected %q, actual %q", expected, actual) 53 | } 54 | } 55 | 56 | func TestRouterHandler2(t *testing.T) { 57 | router := NewRouter() 58 | router.Get("/", func(ctx *Context) error { 59 | ctx.String("Hello, World!") 60 | return nil 61 | }) 62 | 63 | handler := RouterHandler(router) 64 | 65 | req, err := http.NewRequest("GET", "/", nil) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | rr := httptest.NewRecorder() 71 | handler.ServeHTTP(rr, req) 72 | } 73 | 74 | func TestPulse_Run(t *testing.T) { 75 | app := New(Config{ 76 | AppName: "test-app", 77 | }) 78 | 79 | go app.Run(":9090") 80 | 81 | // Wait for server to start 82 | time.Sleep(time.Second) 83 | 84 | _, err := http.Get("http://localhost:9090/") 85 | if err != nil { 86 | t.Errorf("failed to make GET request: %v", err) 87 | } 88 | 89 | err = app.Stop() 90 | if err != nil { 91 | t.Errorf("failed to stop server: %v", err) 92 | } 93 | } 94 | 95 | func TestPulse_Stop(t *testing.T) { 96 | app := New() 97 | 98 | go app.Run(":9090") 99 | 100 | // Wait for server to start 101 | time.Sleep(time.Second) 102 | 103 | err := app.Stop() 104 | if err != nil { 105 | t.Errorf("failed to stop server: %v", err) 106 | } 107 | 108 | // Make sure server is stopped by attempting to make a GET request 109 | _, err = http.Get("http://localhost:9090/") 110 | if err == nil { 111 | t.Errorf("expected error, got nil") 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package pulse 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/common-nighthawk/go-figure" 7 | "net" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | type ( 13 | Pulse struct { 14 | config *Config 15 | server *http.Server 16 | Router *Router 17 | } 18 | 19 | Config struct { 20 | // AppName is the name of the app 21 | AppName string `json:"app_name"` 22 | 23 | // Network is the network to use 24 | Network string `json:"network"` 25 | } 26 | ) 27 | 28 | const ( 29 | // DefaultAppName is the default app name 30 | DefaultAppName = "Pulse" 31 | 32 | // DefaultNetwork is the default network 33 | DefaultNetwork = "tcp" 34 | ) 35 | 36 | func New(config ...Config) *Pulse { 37 | app := &Pulse{ 38 | config: &Config{}, 39 | server: &http.Server{}, 40 | Router: NewRouter(), 41 | } 42 | 43 | if len(config) > 0 { 44 | app.config = &config[0] 45 | } 46 | 47 | if app.config.AppName == "" { 48 | app.config.AppName = DefaultAppName 49 | } 50 | 51 | if app.config.Network == "" { 52 | app.config.Network = DefaultNetwork 53 | } 54 | 55 | return app 56 | } 57 | 58 | func (p *Pulse) Run(address string) { 59 | // setup handler 60 | handler := RouterHandler(p.Router) 61 | p.server.Handler = handler 62 | 63 | // setup listener 64 | listener, err := net.Listen(p.config.Network, address) 65 | if err != nil { 66 | panic(fmt.Errorf("failed to listen: %v", err)) 67 | } 68 | 69 | // print startup message 70 | fmt.Println(p.startupMessage(listener.Addr().String())) 71 | 72 | // start server 73 | err = p.server.Serve(listener) 74 | if err != nil { 75 | fmt.Errorf("failed to start server on %s: %v", listener.Addr().String(), err) 76 | } 77 | } 78 | 79 | func (p *Pulse) Stop() error { 80 | // Check if the server is already stopped. 81 | if p.server == nil { 82 | return nil 83 | } 84 | 85 | // Disable HTTP keep-alive connections to prevent the server from 86 | // accepting any new requests. 87 | p.server.SetKeepAlivesEnabled(false) 88 | 89 | // Shutdown the server gracefully to allow existing connections to finish. 90 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 91 | defer cancel() 92 | err := p.server.Shutdown(ctx) 93 | if err != nil { 94 | return fmt.Errorf("failed to shut down server: %v", err) 95 | } 96 | 97 | // Set the server to a new instance of http.Server to allow starting it again. 98 | p.server = &http.Server{} 99 | 100 | return nil 101 | } 102 | 103 | func (p *Pulse) startupMessage(addr string) string { 104 | myFigure := figure.NewFigure("PULSE", "", true) 105 | myFigure.Print() 106 | 107 | var textOne = "=> Server started on <%s>" + "\n" 108 | var textTwo = "=> App Name: %s" + "\n" 109 | var textThree = "=> Press CTRL+C to stop" + "\n" 110 | 111 | return fmt.Sprintf(textOne, addr) + fmt.Sprintf(textTwo, p.config.AppName) + fmt.Sprintf(textThree) 112 | } 113 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 2 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 3 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= 4 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 8 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 9 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 10 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 11 | github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI= 12 | github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA= 13 | github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= 14 | github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 15 | github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4= 16 | github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 21 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 22 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 24 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 25 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 26 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 27 | github.com/valyala/fasthttp v1.45.0 h1:zPkkzpIn8tdHZUrVa6PzYd0i5verqiPSkgTd3bSUcpA= 28 | github.com/valyala/fasthttp v1.45.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= 29 | golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= 30 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 31 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 32 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= 34 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 35 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 36 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 38 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package pulse 2 | 3 | import ( 4 | "github.com/gopulse/pulse/constants" 5 | "net/http" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type Handler func(ctx *Context) error 11 | type HandlerFunc func(w http.ResponseWriter, r *http.Request) error 12 | 13 | type Router struct { 14 | routes map[string][]*Route 15 | notFoundHandler Handler 16 | middlewares map[string][]Middleware 17 | } 18 | 19 | type Static struct { 20 | Root string 21 | Compress bool 22 | ByteRange bool 23 | IndexName string 24 | CacheDuration time.Duration 25 | } 26 | 27 | func NewRouter() *Router { 28 | router := &Router{ 29 | routes: make(map[string][]*Route), 30 | middlewares: make(map[string][]Middleware), 31 | } 32 | 33 | router.notFoundHandler = func(ctx *Context) error { 34 | http.NotFound(ctx.ResponseWriter, ctx.Request) 35 | return nil 36 | } 37 | 38 | return router 39 | } 40 | 41 | func (r *Router) Add(method, path string, handlers ...Handler) { 42 | route := &Route{ 43 | Path: path, 44 | Handlers: handlers, 45 | } 46 | 47 | parts := strings.Split(path, "/") 48 | for _, part := range parts { 49 | if strings.HasPrefix(part, constants.ParamSign) { 50 | route.ParamNames = append(route.ParamNames, strings.TrimPrefix(part, constants.ParamSign)) 51 | } 52 | } 53 | route.Path = strings.Join(parts, "/") 54 | 55 | r.routes[method] = append(r.routes[method], route) 56 | } 57 | 58 | func (r *Router) Find(method, path string) []Handler { 59 | routes, ok := r.routes[method] 60 | if !ok { 61 | return nil 62 | } 63 | 64 | for _, route := range routes { 65 | if matches, params := route.match(path); matches { 66 | c := NewContext(nil, nil) 67 | c.Params = params 68 | return r.applyMiddleware(route.Handlers, method) 69 | } 70 | } 71 | 72 | return nil 73 | } 74 | 75 | func (r *Router) applyMiddleware(handlers []Handler, method string) []Handler { 76 | for i := len(r.middlewares[method]) - 1; i >= 0; i-- { 77 | middleware := r.middlewares[method][i] 78 | for j := len(handlers) - 1; j >= 0; j-- { 79 | handler := handlers[j] 80 | handlers[j] = func(ctx *Context) error { 81 | return middleware.Handle(ctx, handler) 82 | } 83 | } 84 | } 85 | return handlers 86 | } 87 | 88 | func RouterHandler(router *Router) http.HandlerFunc { 89 | return func(w http.ResponseWriter, req *http.Request) { 90 | path := req.URL.Path 91 | method := req.Method 92 | handlers := router.Find(method, path) 93 | 94 | c := NewContext(w, req) 95 | for _, h := range handlers { 96 | err := h(c) 97 | if err != nil { 98 | break 99 | } 100 | } 101 | } 102 | } 103 | 104 | func (r *Route) match(path string) (bool, map[string]string) { 105 | parts := strings.Split(path, "/") 106 | routeParts := strings.Split(r.Path, "/") 107 | 108 | if strings.HasSuffix(path, "/") { 109 | parts = parts[:len(parts)-1] 110 | } 111 | 112 | if len(parts) != len(routeParts) { 113 | return false, nil 114 | } 115 | 116 | params := make(map[string]string) 117 | for i, part := range routeParts { 118 | if strings.HasPrefix(part, constants.ParamSign) { 119 | paramName := strings.TrimPrefix(part, constants.ParamSign) 120 | params[paramName] = parts[i] 121 | } else if part == constants.WildcardSign { 122 | return true, params 123 | } else if part != parts[i] { 124 | return false, nil 125 | } 126 | } 127 | 128 | return true, params 129 | } 130 | 131 | func (options *Static) notFoundHandler(w http.ResponseWriter) { 132 | w.WriteHeader(http.StatusNotFound) 133 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") 134 | _, err := w.Write([]byte("404 Not Found")) 135 | if err != nil { 136 | return 137 | } 138 | } 139 | 140 | func (options *Static) PathRewrite(r *http.Request) []byte { 141 | path := r.URL.Path 142 | 143 | if len(path) > 1 && path[len(path)-1] == '/' { 144 | path = path[:len(path)-1] 145 | } 146 | 147 | // Remove the last part of the path 148 | parts := strings.Split(path, "/") 149 | if len(parts) > 1 { 150 | parts = parts[:len(parts)-1] 151 | } 152 | path = strings.Join(parts, "/") 153 | 154 | if options.IndexName != "" { 155 | // Append the index file name to the path 156 | path += "/" 157 | path += options.IndexName 158 | } 159 | 160 | return []byte(path) 161 | } 162 | 163 | func (r *Router) Static(prefix, root string, options *Static) { 164 | if options == nil { 165 | options = &Static{} 166 | } 167 | if options.Root == "" { 168 | options.Root = root 169 | } 170 | fs := http.FileServer(http.Dir(options.Root)) 171 | 172 | handler := http.StripPrefix(prefix, fs) 173 | 174 | r.Get(prefix, func(ctx *Context) error { 175 | handler.ServeHTTP(ctx.ResponseWriter, ctx.Request) 176 | return nil 177 | }) 178 | } 179 | -------------------------------------------------------------------------------- /middleware_test.go: -------------------------------------------------------------------------------- 1 | package pulse 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestMiddlewareFunc_Middleware(t *testing.T) { 10 | // create a mock handler 11 | mockHandler := func(ctx *Context) error { return nil } 12 | 13 | // create a middleware that adds a value to the context 14 | middleware := MiddlewareFunc(func(handler Handler) Handler { 15 | return func(ctx *Context) error { 16 | ctx.SetValue("key", "value") 17 | return handler(ctx) 18 | } 19 | }) 20 | 21 | // call the Middleware method with the mock handler 22 | newHandler := middleware.Middleware(mockHandler) 23 | 24 | // create a new context instance 25 | req := httptest.NewRequest(http.MethodGet, "/test", nil) 26 | w := httptest.NewRecorder() 27 | ctx := NewContext(w, req) 28 | 29 | // call the new handler with the context 30 | err := newHandler(ctx) 31 | 32 | // check if the context value was set correctly 33 | if val := ctx.GetValue("key"); val != "value" { 34 | t.Errorf("Expected context value for key \"key\" to be \"value\", but got %v", val) 35 | } 36 | 37 | // check if the original handler was called with the context 38 | if err != nil { 39 | t.Errorf("Expected err to be nil, but got %v", err) 40 | } 41 | } 42 | 43 | func TestMiddleware_Use(t *testing.T) { 44 | // Create a new router. 45 | r := NewRouter() 46 | 47 | // Define a simple handler that sets a custom response header. 48 | handler := func(ctx *Context) error { 49 | ctx.SetResponseHeader("X-Test-Header", "test") 50 | return nil 51 | } 52 | 53 | // Add a CORS middleware to the router. 54 | r.Use(http.MethodGet, CORSMiddleware()) 55 | 56 | // Add the simple handler to the router. 57 | r.Add(http.MethodGet, "/", handler) 58 | 59 | // Create an HTTP request to test the handler. 60 | _ = httptest.NewRequest(http.MethodGet, "/", nil) 61 | rec := httptest.NewRecorder() 62 | 63 | // Verify that the response has the expected status code and header. 64 | if rec.Code != http.StatusOK { 65 | t.Errorf("unexpected status code: %d", rec.Code) 66 | } 67 | } 68 | 69 | func TestCORSMiddleware(t *testing.T) { 70 | // create a test context 71 | req := httptest.NewRequest("GET", "/test", nil) 72 | w := httptest.NewRecorder() 73 | ctx := NewContext(w, req) 74 | 75 | // create a mock handler 76 | mockHandler := func(ctx *Context) error { 77 | return nil 78 | } 79 | 80 | // create the CORS middleware 81 | corsMiddleware := CORSMiddleware() 82 | 83 | // wrap the mock handler with the CORS middleware 84 | handler := corsMiddleware(mockHandler) 85 | 86 | // call the handler with the test context 87 | err := handler(ctx) 88 | 89 | // get the http.Response from the ResponseRecorder using Result() 90 | res := w.Result() 91 | 92 | // check if the Access-Control-Allow-Origin header was set to "*" 93 | if header := res.Header.Get("Access-Control-Allow-Origin"); header != "*" { 94 | t.Errorf("Expected Access-Control-Allow-Origin header to be \"*\", but got %q", header) 95 | } 96 | 97 | // check if the Access-Control-Allow-Methods header was set correctly 98 | if header := res.Header.Get("Access-Control-Allow-Methods"); header != "POST, GET, OPTIONS, PUT, DELETE" { 99 | t.Errorf("Expected Access-Control-Allow-Methods header to be \"POST, GET, OPTIONS, PUT, DELETE\", but got %q", header) 100 | } 101 | 102 | // check if the Access-Control-Allow-Headers header was set correctly 103 | if header := res.Header.Get("Access-Control-Allow-Headers"); header != "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization" { 104 | t.Errorf("Expected Access-Control-Allow-Headers header to be \"Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization\", but got %q", header) 105 | } 106 | 107 | // check if the handler returned no error 108 | if err != nil { 109 | t.Errorf("Expected handler to return no error, but got %v", err) 110 | } 111 | } 112 | 113 | func TestMiddlewareFunc_Handle(t *testing.T) { 114 | req := httptest.NewRequest(http.MethodGet, "/test", nil) 115 | w := httptest.NewRecorder() 116 | 117 | ctx := NewContext(w, req) 118 | 119 | // create a mock handler 120 | mockHandler := func(ctx *Context) error { return nil } 121 | 122 | // create a middleware that adds a value to the context 123 | middleware := MiddlewareFunc(func(handler Handler) Handler { 124 | return func(ctx *Context) error { 125 | ctx.SetValue("key", "value") 126 | return handler(ctx) 127 | } 128 | }) 129 | 130 | // call the Handle method with the mock handler as the next handler 131 | err := middleware.Handle(ctx, mockHandler) 132 | 133 | // check if the context value was set correctly 134 | if val := ctx.GetValue("key"); val != "value" { 135 | t.Errorf("Expected context value for key \"key\" to be \"value\", but got %v", val) 136 | } 137 | 138 | // check if the next handler was called with the original context 139 | if err != nil { 140 | t.Errorf("Expected err to be nil, but got %v", err) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package pulse 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type handlerFunc func(ctx *Context) error 12 | 13 | type Context struct { 14 | ResponseWriter http.ResponseWriter 15 | Response http.Response 16 | Request *http.Request 17 | Params map[string]string 18 | paramValues []string 19 | handlers []handlerFunc 20 | handlerIdx int 21 | Cookies []*http.Cookie 22 | } 23 | 24 | func (c *Context) Write(p []byte) (n int, err error) { 25 | return c.ResponseWriter.Write(p) 26 | } 27 | 28 | type Cookie struct { 29 | Name string 30 | Value string 31 | Path string 32 | Domain string 33 | MaxAge int 34 | Expires time.Time 35 | Secure bool 36 | HTTPOnly bool 37 | SameSite http.SameSite 38 | } 39 | 40 | // NewContext returns a new Context. 41 | func NewContext(w http.ResponseWriter, req *http.Request) *Context { 42 | return &Context{ 43 | ResponseWriter: w, 44 | Request: req, 45 | Params: make(map[string]string), 46 | paramValues: make([]string, 0, 10), 47 | handlers: nil, 48 | handlerIdx: -1, 49 | } 50 | } 51 | 52 | // WithParams sets the params for the context. 53 | func (c *Context) WithParams(params map[string]string) *Context { 54 | c.Params = params 55 | return c 56 | } 57 | 58 | // Param returns the param value for the given key. 59 | func (c *Context) Param(key string) string { 60 | return c.Params[key] 61 | } 62 | 63 | // Query returns the query value for the given key. 64 | func (c *Context) Query(key string) string { 65 | return c.Request.URL.Query().Get(key) 66 | } 67 | 68 | // String sets the response body to the given string. 69 | func (c *Context) String(value string) { 70 | _, err := c.ResponseWriter.Write([]byte(value)) 71 | if err != nil { 72 | return 73 | } 74 | } 75 | 76 | // SetValue create a middleware that adds a value to the context 77 | func (c *Context) SetValue(key interface{}, value interface{}) { 78 | c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), key, value)) 79 | } 80 | 81 | // GetValue returns the value for the given key. 82 | func (c *Context) GetValue(key string) string { 83 | return c.Request.Context().Value(key).(string) 84 | } 85 | 86 | // Next calls the next handler in the chain. 87 | func (c *Context) Next() error { 88 | c.handlerIdx++ 89 | if c.handlerIdx < len(c.handlers) { 90 | return c.handlers[c.handlerIdx](c) 91 | } 92 | return nil 93 | } 94 | 95 | // Reset resets the Context. 96 | func (c *Context) Reset() { 97 | c.handlerIdx = -1 98 | } 99 | 100 | // Abort aborts the chain. 101 | func (c *Context) Abort() { 102 | c.handlerIdx = len(c.handlers) 103 | } 104 | 105 | // SetCookie sets a cookie with the given name, value, and options. 106 | func (c *Context) SetCookie(cookie *Cookie) { 107 | http.SetCookie(c.ResponseWriter, &http.Cookie{ 108 | Name: cookie.Name, 109 | Value: cookie.Value, 110 | Path: cookie.Path, 111 | Domain: cookie.Domain, 112 | Expires: cookie.Expires, 113 | MaxAge: cookie.MaxAge, 114 | Secure: cookie.Secure, 115 | HttpOnly: cookie.HTTPOnly, 116 | SameSite: cookie.SameSite, 117 | }) 118 | 119 | c.Cookies = append(c.Cookies, &http.Cookie{ 120 | Name: cookie.Name, 121 | Value: cookie.Value, 122 | Path: cookie.Path, 123 | Domain: cookie.Domain, 124 | Expires: cookie.Expires, 125 | MaxAge: cookie.MaxAge, 126 | Secure: cookie.Secure, 127 | HttpOnly: cookie.HTTPOnly, 128 | SameSite: cookie.SameSite, 129 | }) 130 | } 131 | 132 | // GetCookie returns the value of the cookie with the given name. 133 | func (c *Context) GetCookie(name string) string { 134 | for _, cookie := range c.Cookies { 135 | if cookie.Name == name { 136 | return cookie.Value 137 | } 138 | } 139 | return "" 140 | } 141 | 142 | // ClearCookie deletes the cookie with the given name. 143 | func (c *Context) ClearCookie(name string) { 144 | c.SetCookie(&Cookie{ 145 | Name: name, 146 | Value: "", 147 | Path: "/", 148 | MaxAge: -1, 149 | Secure: false, 150 | HTTPOnly: true, 151 | }) 152 | } 153 | 154 | // SetResponseHeader sets the http header value to the given key. 155 | func (c *Context) SetResponseHeader(key, value string) { 156 | c.ResponseWriter.Header().Set(key, value) 157 | } 158 | 159 | // GetResponseHeader returns the http header value for the given key. 160 | func (c *Context) GetResponseHeader(key string) string { 161 | return c.ResponseWriter.Header().Get(key) 162 | } 163 | 164 | // SetRequestHeader SetResponseHeader sets the http header value to the given key. 165 | func (c *Context) SetRequestHeader(key, value string) { 166 | c.Request.Header.Set(key, value) 167 | } 168 | 169 | // GetRequestHeader GetResponseHeader returns the http header value for the given key. 170 | func (c *Context) GetRequestHeader(key string) string { 171 | return c.Request.Header.Get(key) 172 | } 173 | 174 | // SetContentType sets the Content-Type header in the response to the given value. 175 | func (c *Context) SetContentType(value string) { 176 | c.ResponseWriter.Header().Set("Content-Type", value) 177 | } 178 | 179 | // Accepts checks if the specified content types are acceptable. 180 | func (c *Context) Accepts(types ...string) string { 181 | acceptHeader := c.GetRequestHeader("Accept") 182 | if acceptHeader == "" { 183 | return "" 184 | } 185 | 186 | acceptedMediaTypes := strings.Split(acceptHeader, ",") 187 | 188 | for _, t := range types { 189 | for _, a := range acceptedMediaTypes { 190 | a = strings.TrimSpace(a) 191 | if strings.HasPrefix(a, t+"/") || a == "*/*" || a == t { 192 | return t 193 | } 194 | } 195 | } 196 | 197 | return "" 198 | } 199 | 200 | // Status sets the response status code. 201 | func (c *Context) Status(code int) { 202 | c.ResponseWriter.WriteHeader(code) 203 | } 204 | 205 | // JSON sets the response body to the given JSON representation. 206 | func (c *Context) JSON(code int, obj interface{}) ([]byte, error) { 207 | c.ResponseWriter.Header().Set("Content-Type", "application/json") 208 | c.Status(code) 209 | jsonBody, err := json.Marshal(obj) 210 | if err != nil { 211 | return nil, err 212 | } 213 | if _, err := c.ResponseWriter.Write(jsonBody); err != nil { 214 | return nil, err 215 | } 216 | 217 | return jsonBody, nil 218 | } 219 | 220 | func (c *Context) BodyParser(v interface{}) error { 221 | return json.NewDecoder(c.Request.Body).Decode(v) 222 | } 223 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |
42 |
43 | ## Getting Started
44 |
45 | ```go
46 | package main
47 |
48 | import (
49 | "github.com/gopulse/pulse"
50 | )
51 |
52 | func main() {
53 | app := pulse.New()
54 | router := pulse.NewRouter()
55 |
56 | app.Router = router
57 |
58 | router.Get("/", func(c *pulse.Context) error {
59 | c.String("Hello, World!")
60 | return nil
61 | })
62 |
63 | app.Run(":3000")
64 | }
65 | ```
66 |
67 | ## Examples
68 |
69 | - Routing
70 |
71 | Supports `GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD, CONNECT, TRACE`
72 |
73 | ```go
74 | package main
75 |
76 | import (
77 | "github.com/gopulse/pulse"
78 | )
79 |
80 | func main() {
81 | app := pulse.New()
82 | router := pulse.NewRouter()
83 |
84 | // GET /hello
85 | router.Get("/", func(c *pulse.Context) error {
86 | c.String("Hello, World!")
87 | return nil
88 | })
89 |
90 | // GET /hello/:name
91 | router.Get("/profile/:id", func(c *pulse.Context) error {
92 | c.String("Profile: " + c.Param("id"))
93 | return nil
94 | })
95 |
96 | // GET /user/
97 | router.Get("/user/*", func(c *pulse.Context) error {
98 | c.String("Hello, World!")
99 | return nil
100 | })
101 |
102 | app.Router = router
103 |
104 | app.Run(":3000")
105 | }
106 | ```
107 |
108 | - Route groups
109 |
110 | Supports `GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD, CONNECT, TRACE`
111 |
112 | ```go
113 | package main
114 |
115 | import (
116 | "github.com/gopulse/pulse"
117 | )
118 |
119 | func main() {
120 | app := pulse.New()
121 | router := pulse.NewRouter()
122 | api := &pulse.Group{
123 | prefix: "/api",
124 | router: router,
125 | }
126 |
127 | v1 := api.Group("/v1")
128 | v1.GET("/users", func(ctx *Context) error {
129 | ctx.String("users")
130 | return nil
131 | })
132 |
133 | app.Router = router
134 |
135 | app.Run(":3000")
136 | }
137 | ```
138 |
139 | * Static files
140 |
141 | ```go
142 | package main
143 |
144 | import (
145 | "github.com/gopulse/pulse"
146 | "time"
147 | )
148 |
149 | func main() {
150 | app := pulse.New()
151 | router := pulse.NewRouter()
152 |
153 | // Static files (./static) with cache duration 24 hours
154 | router.Static("/", "./static", &pulse.Static{
155 | Compress: true,
156 | ByteRange: false,
157 | IndexName: "index.html",
158 | CacheDuration: 24 * time.Hour,
159 | })
160 |
161 | app.Router = router
162 |
163 | app.Run(":3000")
164 | }
165 | ```
166 |
167 | * Middleware
168 |
169 | ```go
170 | package main
171 |
172 | import (
173 | "github.com/gopulse/pulse"
174 | )
175 |
176 | func main() {
177 | app := pulse.New()
178 | router := pulse.NewRouter()
179 |
180 | router.Get("/profile/:name", func(ctx *pulse.Context) error {
181 | if ctx.Param("name") != "test" {
182 | ctx.Abort()
183 | ctx.Status(404)
184 | return nil
185 | }
186 | ctx.String("hello")
187 | ctx.Next()
188 | return nil
189 | })
190 |
191 | app.Router = router
192 |
193 | app.Run(":3000")
194 | }
195 | ```
196 |
197 | ## Available Middleware
198 |
199 | - [x] CORS Middleware: Enable cross-origin resource sharing (CORS) with various options.
200 | ```go
201 | package main
202 |
203 | import (
204 | "github.com/gopulse/pulse"
205 | )
206 |
207 | func main() {
208 | app := pulse.New()
209 | router := pulse.NewRouter()
210 |
211 | router.Get("/", func(ctx *pulse.Context) error {
212 | return nil
213 | })
214 |
215 | router.Use("GET", pulse.CORSMiddleware())
216 |
217 | app.Router = router
218 |
219 | app.Run(":3000")
220 | }
221 | ```
222 |
223 | - [ ] Logger Middleware: Log every request with configurable options. **(Coming soon)**
224 | - [ ] Encrypt Cookie Middleware: Encrypt and decrypt cookie values. **(Coming soon)**
225 | - [ ] Timeout Middleware: Set a timeout for requests. **(Coming soon)**
226 |
227 | ## License
228 |
229 | Pulse is licensed under the MIT License. See [LICENSE](LICENSE) for the full license text.
230 |
231 | ## Contributing
232 |
233 | Contributions are welcome! Please read the [contribution guidelines](CONTRIBUTING.md) first.
234 |
235 | ## Support
236 |
237 | If you want to say thank you and/or support the active development of Pulse:
238 | 1. Add a [GitHub Star](https://github.com/gopulse/pulse/stargazers) to the project.
239 | 2. Tweet about the project [on your Twitter](https://twitter.com/intent/tweet?text=Pulse%20is%20a%20%23web%20%23framework%20for%20the%20%23Go%20programming%20language.%20It%20is%20a%20lightweight%20framework%20that%20is%20%23easy%20to%20use%20and%20easy%20to%20learn.%20It%20is%20designed%20to%20be%20a%20simple%20and%20elegant%20solution%20for%20building%20web%20applications%20and%20%23APIs%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgopulse%2Fpulse)
240 | 3. Write a review or tutorial on [Medium](https://medium.com/), [dev.to](https://dev.to/), [Reddit](https://www.reddit.com/) or personal blog.
241 | 4. [Buy Me a Coffee](https://www.buymeacoffee.com/gopulse)
242 |
243 | ## Contributors
244 |
245 |
246 |