Compare commits
No commits in common. "main" and "v1.0.2" have entirely different histories.
14 changed files with 76 additions and 717 deletions
|
@ -1,81 +0,0 @@
|
||||||
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 .
|
|
|
@ -1,20 +0,0 @@
|
||||||
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
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,3 +1,2 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode
|
.vscode
|
||||||
.idea
|
|
38
.gitlab-ci.yml
Normal file
38
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
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
72
.releaserc
|
@ -1,72 +0,0 @@
|
||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
63
README.md
Normal file → Executable file
63
README.md
Normal file → Executable file
|
@ -1,77 +1,36 @@
|
||||||
# httprouter
|
# GoHTTPRouter
|
||||||
|
|
||||||
[](https://git.martin-riedl.de/golang/httprouter/tags)
|
|
||||||
[](https://git.martin-riedl.de/golang/httprouter/actions)
|
|
||||||
[](https://godoc.org/gitlab.com/martinr92/gohttprouter)
|
[](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)
|
[](https://goreportcard.com/report/gitlab.com/martinr92/gohttprouter)
|
||||||
|
|
||||||
httprouter is a framework used for HTTP request routing.
|
GoHTTPRouter is a framework used for HTTP request routing.
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
|
|
||||||
## Simple Routing
|
## Simple Routing
|
||||||
|
|
||||||
Just replace the standard router of golang with this one:
|
Just replace the standard router of golang with this one:
|
||||||
```golang
|
```golang
|
||||||
import "git.martin-riedl.de/golang/httprouter/v4"
|
router := gohttprouter.New()
|
||||||
```
|
router.HandleFunc(http.MethodGet, "/home", func(response http.ResponseWriter, request *http.Request, info gohttprouter.RoutingInfo) {
|
||||||
```golang
|
|
||||||
router := httprouter.New()
|
|
||||||
router.HandleFunc(http.MethodGet, "/home", func(response http.ResponseWriter, request *http.Request, info httprouter.RoutingInfo) {
|
|
||||||
response.Write([]byte("Home"))
|
response.Write([]byte("Home"))
|
||||||
})
|
})
|
||||||
err := http.ListenAndServe("localhost:8080", router)
|
err := http.ListenAndServe("localhost:8080", router)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Routing with placeholder
|
## 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.
|
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
|
```golang
|
||||||
import "git.martin-riedl.de/golang/httprouter/v4"
|
router := gohttprouter.New()
|
||||||
```
|
router.HandleFunc(http.MethodGet, "/user/:id", func(response http.ResponseWriter, request *http.Request, info gohttprouter.RoutingInfo) {
|
||||||
```golang
|
|
||||||
router := httprouter.New()
|
|
||||||
router.HandleFunc(http.MethodGet, "/user/:id", handleUserPages)
|
|
||||||
router.HandleFunc(http.MethodGet, "/user/:id/settings", handleUserPages)
|
|
||||||
func handleUserPages(response http.ResponseWriter, request *http.Request, info httprouter.RoutingInfo) {
|
|
||||||
id := info.Parameters["id"]
|
id := info.Parameters["id"]
|
||||||
response.Write([]byte(id))
|
})
|
||||||
}
|
|
||||||
err := http.ListenAndServe("localhost:8080", router)
|
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 "git.martin-riedl.de/golang/httprouter/v4"
|
|
||||||
|
|
||||||
//go:embed files/static/*
|
|
||||||
var staticFiles embed.FS
|
|
||||||
```
|
|
||||||
```golang
|
|
||||||
staticFS := httprouter.NewFS(&staticFiles)
|
|
||||||
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.
|
|
||||||
```golang
|
|
||||||
import "git.martin-riedl.de/golang/httprouter/v4"
|
|
||||||
```
|
|
||||||
```golang
|
|
||||||
staticFS := httprouter.NewFS(&staticFiles)
|
|
||||||
staticFS.UseLocalFolder = true
|
|
||||||
staticFS.LocalFolderPrefix = "some/folder" // optional
|
|
||||||
```
|
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
```
|
```
|
||||||
Copyright 2018-2025 Martin Riedl
|
Copyright 2019 Martin Riedl
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
185
fs.go
185
fs.go
|
@ -1,185 +0,0 @@
|
||||||
// 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.
|
|
||||||
// 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"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"mime"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FS file system for serving static content.
|
|
||||||
type FS struct {
|
|
||||||
StaticFiles *embed.FS
|
|
||||||
FolderPrefix string
|
|
||||||
UseLocalFolder bool
|
|
||||||
LocalFolderPrefix string
|
|
||||||
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,
|
|
||||||
HeaderHandler: DefaultHeaderHandler,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *FS) ServeHTTP(w http.ResponseWriter, r *http.Request, info RoutingInfo) {
|
|
||||||
// use wildcard placeholder value for the path (if available)
|
|
||||||
url, found := info.Parameters["*"]
|
|
||||||
if !found {
|
|
||||||
url = r.URL.Path
|
|
||||||
}
|
|
||||||
|
|
||||||
// search for file in static file system
|
|
||||||
file, err := fs.findFile(url)
|
|
||||||
if err != nil {
|
|
||||||
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
|
|
||||||
fs.serve(w, r, info, file)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *FS) findFile(url string) (fs.File, error) {
|
|
||||||
// build file path
|
|
||||||
filePath := path.Join(fs.FolderPrefix, url)
|
|
||||||
filePath = strings.TrimPrefix(filePath, "/")
|
|
||||||
|
|
||||||
// check, if file exists in local folder
|
|
||||||
if fs.UseLocalFolder {
|
|
||||||
localFilePath := path.Join(fs.LocalFolderPrefix, filePath)
|
|
||||||
localFilePath = strings.TrimPrefix(localFilePath, "/")
|
|
||||||
if localFilePath == "" {
|
|
||||||
localFilePath = "."
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := os.Open(localFilePath)
|
|
||||||
if err == nil {
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// use static file system
|
|
||||||
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.HeaderHandler != nil {
|
|
||||||
fs.HeaderHandler(w, r, info, file)
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
|
|
||||||
// check for head request
|
|
||||||
if r.Method == http.MethodHead {
|
|
||||||
// return no content
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// send file content
|
|
||||||
io.Copy(w, file)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HeaderHandler for custom http header handling
|
|
||||||
type HeaderHandler func(http.ResponseWriter, *http.Request, RoutingInfo, fs.File)
|
|
||||||
|
|
||||||
// DefaultHeaderHandler is the default implementation for file headers
|
|
||||||
func DefaultHeaderHandler(w http.ResponseWriter, r *http.Request, info RoutingInfo, file fs.File) {
|
|
||||||
ContentLengthHeaderHandler(w, r, info, file)
|
|
||||||
ContentTypeHeaderHandler(w, r, info, file)
|
|
||||||
CacheControlHeaderHandler(w, r, info, file)
|
|
||||||
ContentTypeOptionHeaderHandler(w, r, info, file)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContentLengthHeaderHandler sends the file size
|
|
||||||
func ContentLengthHeaderHandler(w http.ResponseWriter, r *http.Request, info RoutingInfo, file fs.File) {
|
|
||||||
// send file size as header
|
|
||||||
stat, err := file.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Length", fmt.Sprint(stat.Size()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContentTypeHeaderHandler sends content type based on the file name extension
|
|
||||||
func ContentTypeHeaderHandler(w http.ResponseWriter, r *http.Request, info RoutingInfo, file fs.File) {
|
|
||||||
stat, err := file.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mimeHeader := mime.TypeByExtension(path.Ext(stat.Name()))
|
|
||||||
if mimeHeader != "" {
|
|
||||||
w.Header().Set("Content-Type", mimeHeader)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CacheControlHeaderHandler send cache control header of 7 days for the browser and 30 days for the CDN
|
|
||||||
func CacheControlHeaderHandler(w http.ResponseWriter, r *http.Request, info RoutingInfo, file fs.File) {
|
|
||||||
w.Header().Set("Cache-Control", fmt.Sprintf("public, s-maxage=%d, max-age=%d", 60*60*24*30, 60*60*24*7))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContentTypeOptionHeaderHandler for nosniff content type option
|
|
||||||
func ContentTypeOptionHeaderHandler(w http.ResponseWriter, r *http.Request, info RoutingInfo, file fs.File) {
|
|
||||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
||||||
}
|
|
212
fs_test.go
212
fs_test.go
|
@ -1,212 +0,0 @@
|
||||||
// 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"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed README.md LICENSE.txt
|
|
||||||
var staticFiles embed.FS
|
|
||||||
|
|
||||||
type testWriter struct {
|
|
||||||
header http.Header
|
|
||||||
written []byte
|
|
||||||
statusCode int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tw *testWriter) Header() http.Header {
|
|
||||||
if tw.header == nil {
|
|
||||||
tw.header = make(http.Header)
|
|
||||||
}
|
|
||||||
return tw.header
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tw *testWriter) Write(bytes []byte) (int, error) {
|
|
||||||
tw.written = append(tw.written, bytes...)
|
|
||||||
return len(bytes), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tw *testWriter) WriteHeader(statusCode int) {
|
|
||||||
tw.statusCode = statusCode
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFSReadmeFile(t *testing.T) {
|
|
||||||
// create new FS
|
|
||||||
fs := NewFS(&staticFiles)
|
|
||||||
|
|
||||||
// serve the request
|
|
||||||
tw := &testWriter{}
|
|
||||||
tr := &http.Request{
|
|
||||||
Method: http.MethodGet,
|
|
||||||
URL: &url.URL{
|
|
||||||
Path: "/README.md",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
fs.ServeHTTP(tw, tr, RoutingInfo{})
|
|
||||||
|
|
||||||
// check data
|
|
||||||
if tw.statusCode != http.StatusOK {
|
|
||||||
t.Error("received invalid http status code", tw.statusCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFSReadmeFileLocal(t *testing.T) {
|
|
||||||
// create new FS
|
|
||||||
fs := NewFS(&staticFiles)
|
|
||||||
fs.UseLocalFolder = true
|
|
||||||
fs.LocalFolderPrefix = "/"
|
|
||||||
|
|
||||||
// serve the request
|
|
||||||
tw := &testWriter{}
|
|
||||||
tr := &http.Request{
|
|
||||||
Method: http.MethodGet,
|
|
||||||
URL: &url.URL{
|
|
||||||
Path: "/README.md",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
fs.ServeHTTP(tw, tr, RoutingInfo{})
|
|
||||||
|
|
||||||
// check data
|
|
||||||
if tw.statusCode != http.StatusOK {
|
|
||||||
t.Error("received invalid http status code", tw.statusCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFSReadmeFileHead(t *testing.T) {
|
|
||||||
// create new FS
|
|
||||||
fs := NewFS(&staticFiles)
|
|
||||||
|
|
||||||
// serve the request
|
|
||||||
tw := &testWriter{}
|
|
||||||
tr := &http.Request{
|
|
||||||
Method: http.MethodHead,
|
|
||||||
URL: &url.URL{
|
|
||||||
Path: "/README.md",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
fs.ServeHTTP(tw, tr, RoutingInfo{})
|
|
||||||
|
|
||||||
// check data
|
|
||||||
if tw.statusCode != http.StatusOK {
|
|
||||||
t.Error("received invalid http status code", tw.statusCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFSLicense(t *testing.T) {
|
|
||||||
// create new FS
|
|
||||||
fs := NewFS(&staticFiles)
|
|
||||||
|
|
||||||
// serve the request
|
|
||||||
tw := &testWriter{}
|
|
||||||
tr := &http.Request{
|
|
||||||
Method: http.MethodHead,
|
|
||||||
URL: &url.URL{
|
|
||||||
Path: "/LICENSE.txt",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
fs.ServeHTTP(tw, tr, RoutingInfo{})
|
|
||||||
|
|
||||||
// check data
|
|
||||||
if tw.statusCode != http.StatusOK {
|
|
||||||
t.Error("received invalid http status code", tw.statusCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
// serve the request
|
|
||||||
tw := &testWriter{}
|
|
||||||
tr := &http.Request{
|
|
||||||
Method: http.MethodGet,
|
|
||||||
URL: &url.URL{
|
|
||||||
Path: "/not-exists.txt",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
fs.ServeHTTP(tw, tr, RoutingInfo{})
|
|
||||||
|
|
||||||
// check data
|
|
||||||
if tw.statusCode != http.StatusNotFound {
|
|
||||||
t.Error("received invalid http status code", tw.statusCode)
|
|
||||||
}
|
|
||||||
}
|
|
4
go.mod
4
go.mod
|
@ -1,3 +1 @@
|
||||||
module git.martin-riedl.de/golang/httprouter/v4
|
module gitlab.com/martinr92/gohttprouter
|
||||||
|
|
||||||
go 1.19
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
||||||
"extends": [
|
|
||||||
"local>ci/renovate//configs/base"
|
|
||||||
],
|
|
||||||
"baseBranches": [
|
|
||||||
"develop"
|
|
||||||
]
|
|
||||||
}
|
|
33
route.go
Normal file → Executable file
33
route.go
Normal file → Executable file
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2018-2025 Martin Riedl
|
// Copyright 2018-2019 Martin Riedl
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with 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
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package httprouter
|
package gohttprouter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -21,15 +21,11 @@ import (
|
||||||
type route struct {
|
type route struct {
|
||||||
name string
|
name string
|
||||||
placeholderName string
|
placeholderName string
|
||||||
isWildcard bool
|
|
||||||
handler Handler
|
handler Handler
|
||||||
routes map[string]*route
|
routes map[string]*route
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const parameterPrefix = ":"
|
||||||
parameterPrefix = ":"
|
|
||||||
wildcard = "*"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newRoute() *route {
|
func newRoute() *route {
|
||||||
return &route{routes: make(map[string]*route)}
|
return &route{routes: make(map[string]*route)}
|
||||||
|
@ -45,13 +41,6 @@ func (r *route) parsePath(path string, create bool) (*route, bool, map[string]st
|
||||||
parameters[r.placeholderName] = parts[0]
|
parameters[r.placeholderName] = parts[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for wildcard match
|
|
||||||
if !create && r.isWildcard {
|
|
||||||
parameters = make(map[string]string)
|
|
||||||
parameters[wildcard] = path
|
|
||||||
return r, false, parameters
|
|
||||||
}
|
|
||||||
|
|
||||||
// more path is available
|
// more path is available
|
||||||
if len(parts) == 2 {
|
if len(parts) == 2 {
|
||||||
// parse next part element
|
// parse next part element
|
||||||
|
@ -72,17 +61,12 @@ func (r *route) parsePath(path string, create bool) (*route, bool, map[string]st
|
||||||
subRoute, ok = r.routes[parameterPrefix]
|
subRoute, ok = r.routes[parameterPrefix]
|
||||||
}
|
}
|
||||||
|
|
||||||
// route still not found, try to use wildcard path
|
|
||||||
if !create && !ok {
|
|
||||||
subRoute, ok = r.routes[wildcard]
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for creation
|
// check for creation
|
||||||
subRouteCreated := false
|
subRouteCreated := false
|
||||||
if !ok && !create {
|
if !ok && !create {
|
||||||
// no route found
|
// no route found
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
} else if !ok {
|
} else if !ok && create {
|
||||||
subRoute = newRoute()
|
subRoute = newRoute()
|
||||||
subRoute.parseName(subName)
|
subRoute.parseName(subName)
|
||||||
r.routes[subRoute.name] = subRoute
|
r.routes[subRoute.name] = subRoute
|
||||||
|
@ -91,7 +75,7 @@ func (r *route) parsePath(path string, create bool) (*route, bool, map[string]st
|
||||||
|
|
||||||
// parse sub-route
|
// parse sub-route
|
||||||
matchingRoute, created, subParameters := subRoute.parsePath(parts[1], create)
|
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
|
// last element in path
|
||||||
|
@ -102,9 +86,6 @@ func (r *route) parseName(name string) {
|
||||||
if isParameter(name) {
|
if isParameter(name) {
|
||||||
r.name = parameterPrefix
|
r.name = parameterPrefix
|
||||||
r.placeholderName = name[1:]
|
r.placeholderName = name[1:]
|
||||||
} else if isWildcard(name) {
|
|
||||||
r.name = wildcard
|
|
||||||
r.isWildcard = true
|
|
||||||
} else {
|
} else {
|
||||||
r.name = name
|
r.name = name
|
||||||
}
|
}
|
||||||
|
@ -127,7 +108,3 @@ func mergeParameterMaps(m1, m2 map[string]string) map[string]string {
|
||||||
func isParameter(s string) bool {
|
func isParameter(s string) bool {
|
||||||
return strings.Index(s, parameterPrefix) == 0 && len(s) > 1
|
return strings.Index(s, parameterPrefix) == 0 && len(s) > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func isWildcard(s string) bool {
|
|
||||||
return s == wildcard
|
|
||||||
}
|
|
||||||
|
|
32
route_test.go
Normal file → Executable file
32
route_test.go
Normal file → Executable file
|
@ -1,18 +1,4 @@
|
||||||
// Copyright 2025 Martin Riedl
|
package gohttprouter
|
||||||
//
|
|
||||||
// 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 (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -73,7 +59,7 @@ func TestRoutePlaceholder(t *testing.T) {
|
||||||
t.Error("error during placeholder name validation;", placeholderElement.placeholderName)
|
t.Error("error during placeholder name validation;", placeholderElement.placeholderName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check pathfinder
|
// check path finder
|
||||||
foundRoute, _, foundParameters := route.parsePath("/user/123/home/martin", false)
|
foundRoute, _, foundParameters := route.parsePath("/user/123/home/martin", false)
|
||||||
if foundRoute != homePathElement.routes[":"] {
|
if foundRoute != homePathElement.routes[":"] {
|
||||||
t.Error("wrong path element returned form path finding")
|
t.Error("wrong path element returned form path finding")
|
||||||
|
@ -82,17 +68,3 @@ func TestRoutePlaceholder(t *testing.T) {
|
||||||
t.Error("wrong parameter value parsed")
|
t.Error("wrong parameter value parsed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRouteWildcard(t *testing.T) {
|
|
||||||
route := newRoute()
|
|
||||||
route.parsePath("/static/*", true)
|
|
||||||
|
|
||||||
// check path
|
|
||||||
foundRoute, _, foundParameters := route.parsePath("/static/js/my.js", false)
|
|
||||||
if foundRoute == nil {
|
|
||||||
t.Error("no route found for wildcard path")
|
|
||||||
}
|
|
||||||
if foundParameters["*"] != "js/my.js" {
|
|
||||||
t.Error("wrong parameter value for wildcard parsed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
10
router.go
Normal file → Executable file
10
router.go
Normal file → Executable file
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2018-2025 Martin Riedl
|
// Copyright 2018-2019 Martin Riedl
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with 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
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package httprouter
|
package gohttprouter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -85,7 +85,7 @@ func (router *Router) findRoute(method string, path string, create bool) (route
|
||||||
methodRoute, ok := router.registry[methodUpper]
|
methodRoute, ok := router.registry[methodUpper]
|
||||||
if !ok && !create {
|
if !ok && !create {
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
} else if !ok {
|
} else if !ok && create {
|
||||||
methodRoute = newRoute()
|
methodRoute = newRoute()
|
||||||
router.registry[methodUpper] = methodRoute
|
router.registry[methodUpper] = methodRoute
|
||||||
created = true
|
created = true
|
||||||
|
@ -115,9 +115,7 @@ func (router *Router) ServeHTTP(response http.ResponseWriter, request *http.Requ
|
||||||
// check for exact matching
|
// check for exact matching
|
||||||
route, _, parameter := router.findRoute(request.Method, request.URL.Path, false)
|
route, _, parameter := router.findRoute(request.Method, request.URL.Path, false)
|
||||||
if route != nil && route.handler != nil {
|
if route != nil && route.handler != nil {
|
||||||
route.handler.ServeHTTP(response, request, RoutingInfo{
|
route.handler.ServeHTTP(response, request, RoutingInfo{Parameters: parameter})
|
||||||
Parameters: parameter,
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,4 @@
|
||||||
// Copyright 2025 Martin Riedl
|
package gohttprouter
|
||||||
//
|
|
||||||
// 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 (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
@ -21,6 +7,17 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func testWebServer(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
Must(router.HandleFunc(http.MethodGet, "/:name", func(response http.ResponseWriter, request *http.Request, info RoutingInfo) {
|
||||||
|
response.Write([]byte("name: " + info.Parameters["name"]))
|
||||||
|
}))
|
||||||
|
err := http.ListenAndServe("localhost:8080", router)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRouterInvalidPath(t *testing.T) {
|
func TestRouterInvalidPath(t *testing.T) {
|
||||||
router := New()
|
router := New()
|
||||||
if err := router.HandleFunc(http.MethodGet, "missing/slash", emptyHandlerFunction); err == nil {
|
if err := router.HandleFunc(http.MethodGet, "missing/slash", emptyHandlerFunction); err == nil {
|
||||||
|
@ -77,7 +74,7 @@ func TestRouterServer(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait for completion
|
// wait for completion
|
||||||
<-quitChan
|
_ = <-quitChan
|
||||||
listener.Close()
|
listener.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +96,7 @@ func TestRouterServerHandler(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait for completion
|
// wait for completion
|
||||||
<-testHandler.CloseChannel
|
_ = <-testHandler.CloseChannel
|
||||||
listener.Close()
|
listener.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue