Compare commits
28 commits
Author | SHA1 | Date | |
---|---|---|---|
0a0fc645fc | |||
107c873b4b | |||
851ecb345a | |||
f4ceca90ab | |||
b4cff38dca | |||
7b5e1d0254 | |||
18a84b9b92 | |||
8d8f8b0c99 | |||
203438dc8c | |||
ceb7d04edd | |||
2fb1b35713 | |||
c0e04495a4 | |||
ac2d0bd827 | |||
1613a02b8e | |||
ff36e5eb1d | |||
a6d67cf60a | |||
aa0a11f3c5 | |||
773209071c | |||
144d640578 | |||
85ab140761 | |||
fda5e96ceb | |||
f7dc11aabc | |||
8225706f4c | |||
e491412f07 | |||
78a018b4f6 | |||
5c80024022 | |||
b7dc1d7f6e | |||
36efa38103 |
14 changed files with 370 additions and 72 deletions
81
.forgejo/workflows/build.yml
Normal file
81
.forgejo/workflows/build.yml
Normal file
|
@ -0,0 +1,81 @@
|
|||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- develop
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
name: Checks
|
||||
runs-on: docker
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23.x'
|
||||
check-latest: true
|
||||
- name: Run go fmt and go vet
|
||||
run: |
|
||||
go fmt $(go list ./...)
|
||||
go vet $(go list ./...)
|
||||
|
||||
code-coverage:
|
||||
name: Code Coverage
|
||||
runs-on: docker
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23.x'
|
||||
check-latest: true
|
||||
- name: Run tests and generate coverage report
|
||||
run: |
|
||||
go test -covermode=count -coverprofile coverage.cov $(go list ./...)
|
||||
go tool cover -func=coverage.cov
|
||||
go tool cover -html=coverage.cov -o coverage.html
|
||||
- name: Upload coverage artifacts
|
||||
uses: https://code.forgejo.org/forgejo/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-reports
|
||||
path: |
|
||||
coverage.cov
|
||||
coverage.html
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: docker
|
||||
strategy:
|
||||
matrix:
|
||||
go:
|
||||
- GOOS: darwin
|
||||
GOARCH: amd64
|
||||
- GOOS: darwin
|
||||
GOARCH: arm64
|
||||
- GOOS: linux
|
||||
GOARCH: amd64
|
||||
- GOOS: linux
|
||||
GOARCH: arm64
|
||||
- GOOS: windows
|
||||
GOARCH: amd64
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23.x'
|
||||
check-latest: true
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
echo "GOOS=${{ matrix.go.GOOS }}" >> $GITHUB_ENV
|
||||
echo "GOARCH=${{ matrix.go.GOARCH }}" >> $GITHUB_ENV
|
||||
- name: Build
|
||||
run: go build .
|
20
.forgejo/workflows/release.yml
Normal file
20
.forgejo/workflows/release.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Semantic Release
|
||||
runs-on: docker
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Semantic Release
|
||||
shell: bash
|
||||
run: |
|
||||
npm install -g semantic-release@23 conventional-changelog-conventionalcommits@7
|
||||
semantic-release
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
.DS_Store
|
||||
.vscode
|
||||
.idea
|
|
@ -1,38 +0,0 @@
|
|||
image: golang:latest
|
||||
|
||||
stages:
|
||||
- test
|
||||
|
||||
validate lint:
|
||||
stage: test
|
||||
script:
|
||||
- go get -u golang.org/x/lint/golint
|
||||
- golint -set_exit_status ./...
|
||||
|
||||
execute tests:
|
||||
stage: test
|
||||
script:
|
||||
- go test ./...
|
||||
|
||||
race detection:
|
||||
stage: test
|
||||
script:
|
||||
- go test -race ./...
|
||||
|
||||
code coverage:
|
||||
stage: test
|
||||
script:
|
||||
- go test -covermode=count -coverprofile coverage.cov ./...
|
||||
- go tool cover -func=coverage.cov
|
||||
- mkdir $CI_PROJECT_DIR/report
|
||||
- go tool cover -html=coverage.cov -o $CI_PROJECT_DIR/report/coverage.html
|
||||
coverage: '/\(statements\)\W+\d+\.\d+%/'
|
||||
artifacts:
|
||||
paths:
|
||||
- report/
|
||||
|
||||
codecov.io:
|
||||
stage: test
|
||||
script:
|
||||
- go test -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
- bash <(curl -s https://codecov.io/bash)
|
72
.releaserc
Normal file
72
.releaserc
Normal file
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
"branches": [
|
||||
"main",
|
||||
{
|
||||
"name": "beta",
|
||||
"prerelease": true
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"@semantic-release/commit-analyzer",
|
||||
{
|
||||
"preset": "conventionalcommits",
|
||||
"releaseRules": [
|
||||
{
|
||||
"type": "chore",
|
||||
"release": "patch"
|
||||
},
|
||||
{
|
||||
"type": "build",
|
||||
"release": "patch"
|
||||
},
|
||||
{
|
||||
"type": "ci",
|
||||
"release": "patch"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/release-notes-generator",
|
||||
{
|
||||
"preset": "conventionalcommits",
|
||||
"presetConfig": {
|
||||
"types": [
|
||||
{
|
||||
"type": "feat",
|
||||
"section": "Features"
|
||||
},
|
||||
{
|
||||
"type": "feature",
|
||||
"section": "Features"
|
||||
},
|
||||
{
|
||||
"type": "fix",
|
||||
"section": "Bug Fixes"
|
||||
},
|
||||
{
|
||||
"type": "perf",
|
||||
"section": "Performance Improvements"
|
||||
},
|
||||
{
|
||||
"type": "revert",
|
||||
"section": "Reverts"
|
||||
},
|
||||
{
|
||||
"type": "chore",
|
||||
"section": "Miscellaneous Chores"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
"successCommentCondition": false,
|
||||
"failTitle": false
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
32
README.md
32
README.md
|
@ -1,17 +1,19 @@
|
|||
# goHTTPRouter
|
||||
# httprouter
|
||||
|
||||
[](https://git.martin-riedl.de/golang/httprouter/tags)
|
||||
[](https://git.martin-riedl.de/golang/httprouter/actions)
|
||||
[](https://godoc.org/gitlab.com/martinr92/gohttprouter)
|
||||
[](https://gitlab.com/martinr92/gohttprouter/commits/master)
|
||||
[](https://gitlab.com/martinr92/gohttprouter/commits/master)
|
||||
[](https://codecov.io/gl/martinr92/gohttprouter)
|
||||
[](https://goreportcard.com/report/gitlab.com/martinr92/gohttprouter)
|
||||
|
||||
goHTTPRouter is a framework used for HTTP request routing.
|
||||
httprouter is a framework used for HTTP request routing.
|
||||
|
||||
# Examples
|
||||
|
||||
## Simple Routing
|
||||
|
||||
Just replace the standard router of golang with this one:
|
||||
```golang
|
||||
import httprouter "gitlab.com/martinr92/gohttprouter/v2"
|
||||
import "git.martin-riedl.de/golang/httprouter/v4"
|
||||
```
|
||||
```golang
|
||||
router := httprouter.New()
|
||||
|
@ -22,9 +24,10 @@ err := http.ListenAndServe("localhost:8080", router)
|
|||
```
|
||||
|
||||
## Routing with placeholder
|
||||
|
||||
A path can also contain placeholder (like :id). If then a request is sent to the url "/user/123" the method gets executed and the URL part (in this case "123") is passed as parameter into your handler function.
|
||||
```golang
|
||||
import httprouter "gitlab.com/martinr92/gohttprouter/v2"
|
||||
import "git.martin-riedl.de/golang/httprouter/v4"
|
||||
```
|
||||
```golang
|
||||
router := httprouter.New()
|
||||
|
@ -38,11 +41,12 @@ err := http.ListenAndServe("localhost:8080", router)
|
|||
```
|
||||
|
||||
## Serve Static Content
|
||||
|
||||
Static files (like JavaScript or CSS) can be served automatically (including caching header and MIME type). It uses the `embed.FS` (since go 1.16) to serve static content.
|
||||
```golang
|
||||
import httprouter "gitlab.com/martinr92/gohttprouter/v2"
|
||||
import "git.martin-riedl.de/golang/httprouter/v4"
|
||||
|
||||
//go:embed files/statc/*
|
||||
//go:embed files/static/*
|
||||
var staticFiles embed.FS
|
||||
```
|
||||
```golang
|
||||
|
@ -51,9 +55,12 @@ router := httprouter.New()
|
|||
router.Handle(http.MethodGet, "/static/*", staticFS)
|
||||
```
|
||||
|
||||
For development purpose you can enable the local file serving additionally. The framework checks first, if the file exists locally and serves it directly. If not, the file is served from the `embed.FS`. This helps you during local development so you can modify a CSS file without recompiling everything.
|
||||
For development purpose you can enable the local file serving additionally.
|
||||
The framework checks first, if the file exists locally and serves it directly.
|
||||
If not, the file is served from the `embed.FS`.
|
||||
This helps you during local development so you can modify a CSS file without recompiling everything.
|
||||
```golang
|
||||
import httprouter "gitlab.com/martinr92/gohttprouter/v2"
|
||||
import "git.martin-riedl.de/golang/httprouter/v4"
|
||||
```
|
||||
```golang
|
||||
staticFS := httprouter.NewFS(&staticFiles)
|
||||
|
@ -62,8 +69,9 @@ staticFS.LocalFolderPrefix = "some/folder" // optional
|
|||
```
|
||||
|
||||
# License
|
||||
|
||||
```
|
||||
Copyright 2018-2021 Martin Riedl
|
||||
Copyright 2018-2025 Martin Riedl
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
54
fs.go
54
fs.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2021 Martin Riedl
|
||||
// Copyright 2021-2025 Martin Riedl
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gohttprouter
|
||||
package httprouter
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
@ -32,14 +32,15 @@ type FS struct {
|
|||
FolderPrefix string
|
||||
UseLocalFolder bool
|
||||
LocalFolderPrefix string
|
||||
HeaderHeandler HeaderHandler
|
||||
HeaderHandler HeaderHandler
|
||||
IndexFileNames []string
|
||||
}
|
||||
|
||||
// NewFS creates a new instance of the http file system used for serving static files.
|
||||
func NewFS(staticFiles *embed.FS) *FS {
|
||||
return &FS{
|
||||
StaticFiles: staticFiles,
|
||||
HeaderHeandler: DefaultHeaderHandler,
|
||||
HeaderHandler: DefaultHeaderHandler,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,6 +57,12 @@ func (fs *FS) ServeHTTP(w http.ResponseWriter, r *http.Request, info RoutingInfo
|
|||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
// check, if the file is a folder
|
||||
file, ok := fs.checkFolder(file, url)
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// serve file content
|
||||
|
@ -71,6 +78,9 @@ func (fs *FS) findFile(url string) (fs.File, error) {
|
|||
if fs.UseLocalFolder {
|
||||
localFilePath := path.Join(fs.LocalFolderPrefix, filePath)
|
||||
localFilePath = strings.TrimPrefix(localFilePath, "/")
|
||||
if localFilePath == "" {
|
||||
localFilePath = "."
|
||||
}
|
||||
|
||||
file, err := os.Open(localFilePath)
|
||||
if err == nil {
|
||||
|
@ -79,13 +89,43 @@ func (fs *FS) findFile(url string) (fs.File, error) {
|
|||
}
|
||||
|
||||
// use static file system
|
||||
return fs.StaticFiles.Open(filePath)
|
||||
if filePath == "" {
|
||||
filePath = "."
|
||||
}
|
||||
file, err := fs.StaticFiles.Open(filePath)
|
||||
return file, err
|
||||
}
|
||||
|
||||
func (fs *FS) checkFolder(file fs.File, url string) (fs.File, bool) {
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return file, false
|
||||
}
|
||||
|
||||
// check for folder / root location
|
||||
if fileInfo.IsDir() {
|
||||
// close folder handler (we don't need it anymore)
|
||||
_ = file.Close()
|
||||
|
||||
// check for index files
|
||||
for _, indexFileName := range fs.IndexFileNames {
|
||||
newFullPath := path.Join(url, indexFileName)
|
||||
if newFile, err := fs.findFile(newFullPath); err == nil {
|
||||
return newFile, true
|
||||
}
|
||||
}
|
||||
|
||||
return file, false
|
||||
}
|
||||
|
||||
return file, true
|
||||
}
|
||||
|
||||
func (fs *FS) serve(w http.ResponseWriter, r *http.Request, info RoutingInfo, file fs.File) {
|
||||
// execute header handler
|
||||
if fs.HeaderHeandler != nil {
|
||||
fs.HeaderHeandler(w, r, info, file)
|
||||
if fs.HeaderHandler != nil {
|
||||
fs.HeaderHandler(w, r, info, file)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
|
|
79
fs_test.go
79
fs_test.go
|
@ -1,4 +1,18 @@
|
|||
package gohttprouter
|
||||
// Copyright 2025 Martin Riedl
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httprouter
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
@ -114,6 +128,69 @@ func TestFSLicense(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestFSFolder(t *testing.T) {
|
||||
// create new FS
|
||||
fs := NewFS(&staticFiles)
|
||||
|
||||
// serve the request
|
||||
tw := &testWriter{}
|
||||
tr := &http.Request{
|
||||
Method: http.MethodHead,
|
||||
URL: &url.URL{
|
||||
Path: "/",
|
||||
},
|
||||
}
|
||||
fs.ServeHTTP(tw, tr, RoutingInfo{})
|
||||
|
||||
// check data
|
||||
if tw.statusCode != http.StatusNotFound {
|
||||
t.Error("received invalid http status code", tw.statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFSFolderLocal(t *testing.T) {
|
||||
// create new FS
|
||||
fs := NewFS(&staticFiles)
|
||||
fs.UseLocalFolder = true
|
||||
fs.LocalFolderPrefix = "/"
|
||||
|
||||
// serve the request
|
||||
tw := &testWriter{}
|
||||
tr := &http.Request{
|
||||
Method: http.MethodHead,
|
||||
URL: &url.URL{
|
||||
Path: "/",
|
||||
},
|
||||
}
|
||||
fs.ServeHTTP(tw, tr, RoutingInfo{})
|
||||
|
||||
// check data
|
||||
if tw.statusCode != http.StatusNotFound {
|
||||
t.Error("received invalid http status code", tw.statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFSFolderIndex(t *testing.T) {
|
||||
// create new FS
|
||||
fs := NewFS(&staticFiles)
|
||||
fs.IndexFileNames = []string{"README.md"}
|
||||
|
||||
// serve the request
|
||||
tw := &testWriter{}
|
||||
tr := &http.Request{
|
||||
Method: http.MethodHead,
|
||||
URL: &url.URL{
|
||||
Path: "/",
|
||||
},
|
||||
}
|
||||
fs.ServeHTTP(tw, tr, RoutingInfo{})
|
||||
|
||||
// check data
|
||||
if tw.statusCode != http.StatusOK {
|
||||
t.Error("received invalid http status code", tw.statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFSNotFound(t *testing.T) {
|
||||
// create new FS
|
||||
fs := NewFS(&staticFiles)
|
||||
|
|
4
go.mod
4
go.mod
|
@ -1,3 +1,3 @@
|
|||
module gitlab.com/martinr92/gohttprouter/v2
|
||||
module git.martin-riedl.de/golang/httprouter/v4
|
||||
|
||||
go 1.16
|
||||
go 1.19
|
9
renovate.json
Normal file
9
renovate.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"local>ci/renovate//configs/base"
|
||||
],
|
||||
"baseBranches": [
|
||||
"develop"
|
||||
]
|
||||
}
|
8
route.go
8
route.go
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2018-2021 Martin Riedl
|
||||
// Copyright 2018-2025 Martin Riedl
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gohttprouter
|
||||
package httprouter
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
@ -82,7 +82,7 @@ func (r *route) parsePath(path string, create bool) (*route, bool, map[string]st
|
|||
if !ok && !create {
|
||||
// no route found
|
||||
return nil, false, nil
|
||||
} else if !ok && create {
|
||||
} else if !ok {
|
||||
subRoute = newRoute()
|
||||
subRoute.parseName(subName)
|
||||
r.routes[subRoute.name] = subRoute
|
||||
|
@ -91,7 +91,7 @@ func (r *route) parsePath(path string, create bool) (*route, bool, map[string]st
|
|||
|
||||
// parse sub-route
|
||||
matchingRoute, created, subParameters := subRoute.parsePath(parts[1], create)
|
||||
return matchingRoute, (subRouteCreated || created), mergeParameterMaps(parameters, subParameters)
|
||||
return matchingRoute, subRouteCreated || created, mergeParameterMaps(parameters, subParameters)
|
||||
}
|
||||
|
||||
// last element in path
|
||||
|
|
|
@ -1,4 +1,18 @@
|
|||
package gohttprouter
|
||||
// Copyright 2025 Martin Riedl
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httprouter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2018-2021 Martin Riedl
|
||||
// Copyright 2018-2025 Martin Riedl
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gohttprouter
|
||||
package httprouter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -85,7 +85,7 @@ func (router *Router) findRoute(method string, path string, create bool) (route
|
|||
methodRoute, ok := router.registry[methodUpper]
|
||||
if !ok && !create {
|
||||
return nil, false, nil
|
||||
} else if !ok && create {
|
||||
} else if !ok {
|
||||
methodRoute = newRoute()
|
||||
router.registry[methodUpper] = methodRoute
|
||||
created = true
|
||||
|
|
|
@ -1,4 +1,18 @@
|
|||
package gohttprouter
|
||||
// Copyright 2025 Martin Riedl
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httprouter
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
|
Loading…
Add table
Reference in a new issue