initial import
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
Ctx context.Context
|
||||
Request *http.Request
|
||||
Writer http.ResponseWriter
|
||||
PathMap map[string]string
|
||||
}
|
||||
|
||||
func NewContext(writer http.ResponseWriter, request *http.Request) *Context {
|
||||
ctx := context.Background()
|
||||
rctx := &Context{
|
||||
Writer: writer,
|
||||
Request: request,
|
||||
Ctx: ctx,
|
||||
PathMap: make(map[string]string),
|
||||
}
|
||||
return rctx
|
||||
}
|
||||
|
||||
func (rctx *Context) SetStatus(httpStatus int) {
|
||||
rctx.Writer.WriteHeader(httpStatus)
|
||||
}
|
||||
|
||||
func (rctx *Context) SendJSON(payload any) {
|
||||
rctx.Writer.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rctx.Writer).Encode(payload)
|
||||
}
|
||||
|
||||
func (rctx *Context) SendText(payload string) {
|
||||
rctx.Writer.Header().Set("Content-Type", "text/plain")
|
||||
rctx.Writer.Write([]byte(payload))
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
compContextPlain int = iota
|
||||
compContextRegex
|
||||
|
||||
startRegex byte = '{'
|
||||
stopRegex byte = '}'
|
||||
)
|
||||
|
||||
func pathCompiler(path string) (string, error) {
|
||||
var err error
|
||||
res := make([]byte, 0)
|
||||
var pos int = compContextPlain
|
||||
var depth int = 0
|
||||
pattern := make([]byte, 0)
|
||||
for _, b := range []byte(path) {
|
||||
switch pos {
|
||||
case compContextPlain:
|
||||
switch b {
|
||||
case stopRegex:
|
||||
depth -= 1
|
||||
res = append(res, b)
|
||||
case startRegex:
|
||||
depth += 1
|
||||
pos = compContextRegex // pattern started
|
||||
pattern = make([]byte, 0)
|
||||
default:
|
||||
res = append(res, b)
|
||||
}
|
||||
case compContextRegex:
|
||||
switch b {
|
||||
case startRegex:
|
||||
depth += 1
|
||||
case stopRegex:
|
||||
depth -= 1
|
||||
if depth == 0 {
|
||||
pattern = convertRegexp(pattern)
|
||||
res = append(res, pattern...)
|
||||
pos = compContextPlain // pattern ended
|
||||
}
|
||||
default:
|
||||
pattern = append(pattern, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
if depth != 0 {
|
||||
err = fmt.Errorf("Unbalanced brackets into pattern")
|
||||
}
|
||||
return string(res), err
|
||||
}
|
||||
|
||||
const (
|
||||
defaultRegexp = `[a-zA-Z0-9_][\-\.a-zA-Z0-9_%%=:~]+`
|
||||
)
|
||||
|
||||
func convertRegexp(src []byte) []byte {
|
||||
var res string
|
||||
const patternSeps = ":"
|
||||
parts := strings.SplitN(string(src), patternSeps, 2)
|
||||
if len(parts) == 1 {
|
||||
parts = append(parts, defaultRegexp)
|
||||
}
|
||||
res = fmt.Sprintf("(?<%s>%s)", parts[0], parts[1])
|
||||
return []byte(res)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func tDebugf(msg string, args ...any) {
|
||||
fmt.Printf("debug: ")
|
||||
fmt.Printf(msg, args...)
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func TestPatchCompilerA(t *testing.T) {
|
||||
var err error
|
||||
srcPath := `/v1/file/{collection:[a-zA-Z]+}/{name}`
|
||||
reSource, err := pathCompiler(srcPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
tDebugf("re: %s\n", reSource)
|
||||
|
||||
re, err := regexp.Compile(reSource)
|
||||
require.NoError(t, err)
|
||||
reqPath := `/v1/file/foo/bare`
|
||||
match := re.MatchString(reqPath)
|
||||
require.True(t, match)
|
||||
|
||||
submatch := re.FindStringSubmatch(reqPath)
|
||||
subnames := re.SubexpNames()
|
||||
|
||||
submap := make(map[string]string)
|
||||
for i, val := range subnames {
|
||||
tDebugf("subname: %d = %s", i, val)
|
||||
}
|
||||
for i, val := range submatch {
|
||||
key := subnames[i]
|
||||
if key != "" {
|
||||
submap[key] = val
|
||||
}
|
||||
tDebugf("sub: %d = %s", i, val)
|
||||
}
|
||||
tDebugf("submap: %v", submap)
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
ServeHTTP(rctx *Context)
|
||||
}
|
||||
|
||||
type HandlerFunc func(rctx *Context)
|
||||
|
||||
func (handlerFunc HandlerFunc) ServeHTTP(rctx *Context) {
|
||||
handlerFunc(rctx)
|
||||
}
|
||||
|
||||
type Router struct {
|
||||
routeHandler *Selector
|
||||
}
|
||||
|
||||
func NewRouter() *Router {
|
||||
return &Router{
|
||||
routeHandler: NewSelector(),
|
||||
}
|
||||
}
|
||||
|
||||
func (rout *Router) AddRoute(method, path string, handlerFunc HandlerFunc) {
|
||||
rout.routeHandler.AddRoute(method, path, handlerFunc)
|
||||
}
|
||||
|
||||
func (rout *Router) Get(path string, handlerFunc HandlerFunc) {
|
||||
rout.routeHandler.AddRoute("GET", path, handlerFunc)
|
||||
}
|
||||
|
||||
func (rout *Router) Post(path string, handlerFunc HandlerFunc) {
|
||||
rout.routeHandler.AddRoute("POST", path, handlerFunc)
|
||||
}
|
||||
|
||||
func (rout *Router) ServeHTTP(writer http.ResponseWriter, req *http.Request) {
|
||||
rctx := NewContext(writer, req)
|
||||
rout.routeHandler.ServeHTTP(rctx)
|
||||
}
|
||||
|
||||
func (rout *Router) NotFound(handlerFunc HandlerFunc) {
|
||||
rout.routeHandler.notFound = handlerFunc
|
||||
}
|
||||
|
||||
type Selector struct {
|
||||
Routes []*Route
|
||||
notFound Handler
|
||||
}
|
||||
|
||||
func NewSelector() *Selector {
|
||||
notFound := HandlerFunc(func(ctx *Context) {
|
||||
http.NotFound(ctx.Writer, ctx.Request)
|
||||
})
|
||||
return &Selector{
|
||||
Routes: make([]*Route, 0),
|
||||
notFound: notFound,
|
||||
}
|
||||
}
|
||||
|
||||
const globalRoutePattern = `^%s$`
|
||||
|
||||
func (hand *Selector) AddRoute(method, path string, handlerFunc HandlerFunc) error {
|
||||
var err error
|
||||
path, err = pathCompiler(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path = fmt.Sprintf(globalRoutePattern, path)
|
||||
re, err := regexp.Compile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
route := &Route{
|
||||
Method: method,
|
||||
Path: path,
|
||||
Handler: handlerFunc,
|
||||
Regexp: re,
|
||||
}
|
||||
hand.Routes = append(hand.Routes, route)
|
||||
return err
|
||||
}
|
||||
|
||||
func (hand *Selector) ServeHTTP(rctx *Context) {
|
||||
realHandler := hand.notFound
|
||||
for _, route := range hand.Routes {
|
||||
match := route.Match(rctx.Request)
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
subvals := route.Regexp.FindStringSubmatch(rctx.Request.URL.Path)
|
||||
subkeys := route.Regexp.SubexpNames()
|
||||
for i, val := range subvals {
|
||||
key := subkeys[i]
|
||||
if key != "" {
|
||||
rctx.PathMap[key] = val
|
||||
}
|
||||
}
|
||||
realHandler = route.Handler
|
||||
break
|
||||
}
|
||||
realHandler.ServeHTTP(rctx)
|
||||
}
|
||||
|
||||
type Route struct {
|
||||
Method string
|
||||
Regexp *regexp.Regexp
|
||||
Path string
|
||||
Handler HandlerFunc
|
||||
}
|
||||
|
||||
func (route Route) Match(req *http.Request) bool {
|
||||
if req.Method != route.Method {
|
||||
return false
|
||||
}
|
||||
match := route.Regexp.MatchString(req.URL.Path)
|
||||
if match {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRouterStatus(t *testing.T) {
|
||||
|
||||
reqPath := "/hello"
|
||||
handler := NewRouter()
|
||||
helloHandler := func(rctx *Context) {
|
||||
rctx.SetStatus(http.StatusOK)
|
||||
}
|
||||
handler.Get(reqPath, helloHandler)
|
||||
|
||||
request, err := http.NewRequest("GET", reqPath, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
fmt.Printf("Response code: %d\n", recorder.Code)
|
||||
}
|
||||
|
||||
func TestRouterSendText(t *testing.T) {
|
||||
testText := "hello, world"
|
||||
reqPath := "/hello"
|
||||
rout := NewRouter()
|
||||
helloHandler := func(rctx *Context) {
|
||||
rctx.SendText(testText)
|
||||
}
|
||||
rout.Get(reqPath, helloHandler)
|
||||
|
||||
request, err := http.NewRequest("GET", reqPath, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
rout.ServeHTTP(recorder, request)
|
||||
fmt.Printf("Response code: %d\n", recorder.Code)
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
bodyReader := recorder.Body
|
||||
bodyBytes, err := io.ReadAll(bodyReader)
|
||||
|
||||
fmt.Printf("Response body: %s\n", string(bodyBytes))
|
||||
require.Equal(t, string(bodyBytes), testText)
|
||||
}
|
||||
|
||||
func TestRouterSendJSON(t *testing.T) {
|
||||
|
||||
type testStruct struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
testVar := testStruct{
|
||||
Message: "hello, world",
|
||||
}
|
||||
testData, err := json.Marshal(testVar)
|
||||
require.NoError(t, err)
|
||||
|
||||
reqPath := "/hello"
|
||||
handler := NewRouter()
|
||||
helloHandler := func(rctx *Context) {
|
||||
rctx.SendJSON(&testVar)
|
||||
}
|
||||
handler.Get(reqPath, helloHandler)
|
||||
|
||||
request, err := http.NewRequest("GET", reqPath, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
fmt.Printf("Response code: %d\n", recorder.Code)
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
bodyReader := recorder.Body
|
||||
bodyBytes, err := io.ReadAll(bodyReader)
|
||||
require.NoError(t, err)
|
||||
bodyBytes = bytes.Trim(bodyBytes, "\n\r")
|
||||
|
||||
fmt.Printf("Response body: %s\n", string(bodyBytes))
|
||||
require.Equal(t, string(testData), string(bodyBytes))
|
||||
}
|
||||
|
||||
func BenchmarkLoggerL(b *testing.B) {
|
||||
reqPath := "/hello"
|
||||
helloHandler := func(rctx *Context) {
|
||||
rctx.SetStatus(http.StatusOK)
|
||||
}
|
||||
handler := NewRouter()
|
||||
handler.Get(reqPath, helloHandler)
|
||||
|
||||
request, err := http.NewRequest("GET", reqPath, nil)
|
||||
require.NoError(b, err)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
require.Equal(b, http.StatusOK, recorder.Code)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user