Compare commits
No commits in common. "main" and "v1.0.1" have entirely different histories.
14 changed files with 78 additions and 719 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
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,2 @@
|
|||
.DS_Store
|
||||
.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
|
||||
|
||||
[](https://git.martin-riedl.de/golang/httprouter/tags)
|
||||
[](https://git.martin-riedl.de/golang/httprouter/actions)
|
||||
# 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)
|
||||
|
||||
httprouter is a framework used for HTTP request routing.
|
||||
GoHTTPRouter is a framework used for HTTP request routing.
|
||||
|
||||
# Examples
|
||||
|
||||
## Simple Routing
|
||||
|
||||
Just replace the standard router of golang with this one:
|
||||
```golang
|
||||
import "git.martin-riedl.de/golang/httprouter/v4"
|
||||
```
|
||||
```golang
|
||||
router := httprouter.New()
|
||||
router.HandleFunc(http.MethodGet, "/home", func(response http.ResponseWriter, request *http.Request, info httprouter.RoutingInfo) {
|
||||
router := gohttprouter.New()
|
||||
router.HandleFunc(http.MethodGet, "/home", func(response http.ResponseWriter, request *http.Request, info gohttprouter.RoutingInfo) {
|
||||
response.Write([]byte("Home"))
|
||||
})
|
||||
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 "git.martin-riedl.de/golang/httprouter/v4"
|
||||
```
|
||||
```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) {
|
||||
router := gohttprouter.New()
|
||||
router.HandleFunc(http.MethodGet, "/user/:id", func(response http.ResponseWriter, request *http.Request, info gohttprouter.RoutingInfo) {
|
||||
id := info.Parameters["id"]
|
||||
response.Write([]byte(id))
|
||||
}
|
||||
})
|
||||
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
|
||||
|
||||
```
|
||||
Copyright 2018-2025 Martin Riedl
|
||||
Copyright 2019 Martin Riedl
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (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
|
||||
|
||||
go 1.19
|
||||
module gitlab.com/martinr92/gohttprouter
|
||||
|
|
|
@ -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");
|
||||
// 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 httprouter
|
||||
package gohttprouter
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
@ -21,15 +21,11 @@ import (
|
|||
type route struct {
|
||||
name string
|
||||
placeholderName string
|
||||
isWildcard bool
|
||||
handler Handler
|
||||
routes map[string]*route
|
||||
}
|
||||
|
||||
const (
|
||||
parameterPrefix = ":"
|
||||
wildcard = "*"
|
||||
)
|
||||
const parameterPrefix = ":"
|
||||
|
||||
func newRoute() *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]
|
||||
}
|
||||
|
||||
// check for wildcard match
|
||||
if !create && r.isWildcard {
|
||||
parameters = make(map[string]string)
|
||||
parameters[wildcard] = path
|
||||
return r, false, parameters
|
||||
}
|
||||
|
||||
// more path is available
|
||||
if len(parts) == 2 {
|
||||
// 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]
|
||||
}
|
||||
|
||||
// route still not found, try to use wildcard path
|
||||
if !create && !ok {
|
||||
subRoute, ok = r.routes[wildcard]
|
||||
}
|
||||
|
||||
// check for creation
|
||||
subRouteCreated := false
|
||||
if !ok && !create {
|
||||
// no route found
|
||||
return nil, false, nil
|
||||
} else if !ok {
|
||||
} else if !ok && create {
|
||||
subRoute = newRoute()
|
||||
subRoute.parseName(subName)
|
||||
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
|
||||
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
|
||||
|
@ -102,9 +86,6 @@ func (r *route) parseName(name string) {
|
|||
if isParameter(name) {
|
||||
r.name = parameterPrefix
|
||||
r.placeholderName = name[1:]
|
||||
} else if isWildcard(name) {
|
||||
r.name = wildcard
|
||||
r.isWildcard = true
|
||||
} else {
|
||||
r.name = name
|
||||
}
|
||||
|
@ -127,7 +108,3 @@ func mergeParameterMaps(m1, m2 map[string]string) map[string]string {
|
|||
func isParameter(s string) bool {
|
||||
return strings.Index(s, parameterPrefix) == 0 && len(s) > 1
|
||||
}
|
||||
|
||||
func isWildcard(s string) bool {
|
||||
return s == wildcard
|
||||
}
|
||||
|
|
30
route_test.go
Normal file → Executable file
30
route_test.go
Normal file → Executable file
|
@ -1,18 +1,4 @@
|
|||
// 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
|
||||
package gohttprouter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
@ -82,17 +68,3 @@ func TestRoutePlaceholder(t *testing.T) {
|
|||
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")
|
||||
}
|
||||
}
|
||||
|
|
12
router.go
Normal file → Executable file
12
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");
|
||||
// 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 httprouter
|
||||
package gohttprouter
|
||||
|
||||
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 {
|
||||
} else if !ok && create {
|
||||
methodRoute = newRoute()
|
||||
router.registry[methodUpper] = methodRoute
|
||||
created = true
|
||||
|
@ -113,11 +113,9 @@ func (router *Router) validateRoute(path string) error {
|
|||
|
||||
func (router *Router) ServeHTTP(response http.ResponseWriter, request *http.Request) {
|
||||
// check for exact matching
|
||||
route, _, parameter := router.findRoute(request.Method, request.URL.Path, false)
|
||||
route, _, parameter := router.findRoute(request.Method, request.RequestURI, false)
|
||||
if route != nil && route.handler != nil {
|
||||
route.handler.ServeHTTP(response, request, RoutingInfo{
|
||||
Parameters: parameter,
|
||||
})
|
||||
route.handler.ServeHTTP(response, request, RoutingInfo{Parameters: parameter})
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +1,4 @@
|
|||
// 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
|
||||
package gohttprouter
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
@ -21,6 +7,17 @@ import (
|
|||
"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) {
|
||||
router := New()
|
||||
if err := router.HandleFunc(http.MethodGet, "missing/slash", emptyHandlerFunction); err == nil {
|
||||
|
@ -77,7 +74,7 @@ func TestRouterServer(t *testing.T) {
|
|||
}
|
||||
|
||||
// wait for completion
|
||||
<-quitChan
|
||||
_ = <-quitChan
|
||||
listener.Close()
|
||||
}
|
||||
|
||||
|
@ -93,13 +90,13 @@ func TestRouterServerHandler(t *testing.T) {
|
|||
go http.Serve(listener, router)
|
||||
|
||||
// call method
|
||||
_, err = http.Get("http://localhost:8081/?test=false")
|
||||
_, err = http.Get("http://localhost:8081/")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// wait for completion
|
||||
<-testHandler.CloseChannel
|
||||
_ = <-testHandler.CloseChannel
|
||||
listener.Close()
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue