Compare commits
No commits in common. "dev" and "master" have entirely different histories.
62
.github/workflows/nfctl.yml
vendored
Normal file
62
.github/workflows/nfctl.yml
vendored
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
name: Auto Build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'release/nfctl/*'
|
||||||
|
|
||||||
|
env:
|
||||||
|
RELEASE_VERSION: v24.09.23-r1
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-job:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
repository-projects: write
|
||||||
|
steps:
|
||||||
|
- name: checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: install golang
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '1.20'
|
||||||
|
|
||||||
|
- name: build linux amd64
|
||||||
|
run: CGO_ENABLE=0 GOOS=linux GOARCH=amd64 go build -ldflags='-s -w' -o "dist/nfctl-linux_amd64-${{ env.RELEASE_VERSION }}" nft/nfctl/main.go
|
||||||
|
|
||||||
|
- name: build linux arm64
|
||||||
|
run: CGO_ENABLE=0 GOOS=linux GOARCH=arm64 go build -ldflags='-s -w' -o "dist/nfctl-linux_arm64-${{ env.RELEASE_VERSION }}" nft/nfctl/main.go
|
||||||
|
|
||||||
|
- name: build windows amd64
|
||||||
|
run: CGO_ENABLE=0 GOOS=windows GOARCH=amd64 go build -ldflags='-s -w' -o "dist/nfctl-win_amd64-${{ env.RELEASE_VERSION }}.exe" nft/nfctl/main.go
|
||||||
|
|
||||||
|
- name: build windows arm64
|
||||||
|
run: CGO_ENABLE=0 GOOS=windows GOARCH=arm64 go build -ldflags='-s -w' -o "dist/nfctl-win_arm64-${{ env.RELEASE_VERSION }}.exe" nft/nfctl/main.go
|
||||||
|
|
||||||
|
- name: build darwin amd64
|
||||||
|
run: CGO_ENABLE=0 GOOS=darwin GOARCH=amd64 go build -ldflags='-s -w' -o "dist/nfctl-darwin_amd64-${{ env.RELEASE_VERSION }}" nft/nfctl/main.go
|
||||||
|
|
||||||
|
- name: build darwin arm64
|
||||||
|
run: CGO_ENABLE=0 GOOS=darwin GOARCH=arm64 go build -ldflags='-s -w' -o "dist/nfctl-darwin_arm64-${{ env.RELEASE_VERSION }}" nft/nfctl/main.go
|
||||||
|
|
||||||
|
- name: show all builds
|
||||||
|
run: ls -lash dist
|
||||||
|
|
||||||
|
- name: create releases
|
||||||
|
id: create_releases
|
||||||
|
uses: "marvinpinto/action-automatic-releases@latest"
|
||||||
|
with:
|
||||||
|
automatic_release_tag: "Release-nfctl-${{ env.RELEASE_VERSION }}"
|
||||||
|
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
title: "Release_${{ env.RELEASE_VERSION }}"
|
||||||
|
prerelease: false
|
||||||
|
files: |
|
||||||
|
dist/nfctl-linux_amd64-${{ env.RELEASE_VERSION }}
|
||||||
|
dist/nfctl-linux_arm64-${{ env.RELEASE_VERSION }}
|
||||||
|
dist/nfctl-win_amd64-${{ env.RELEASE_VERSION }}.exe
|
||||||
|
dist/nfctl-win_arm64-${{ env.RELEASE_VERSION }}.exe
|
||||||
|
dist/nfctl-darwin_amd64-${{ env.RELEASE_VERSION }}
|
||||||
|
dist/nfctl-darwin_arm64-${{ env.RELEASE_VERSION }}
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
xtest
|
309
app.go
309
app.go
@ -1,62 +1,303 @@
|
|||||||
package nf
|
package nf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/loveuer/nf/internal/bytesconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ IRouter = (*App)(nil)
|
||||||
|
|
||||||
|
regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
|
||||||
|
regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
*RouterGroup
|
RouterGroup
|
||||||
router *router
|
config *Config
|
||||||
groups []*RouterGroup
|
groups []*RouterGroup
|
||||||
|
server *http.Server
|
||||||
|
|
||||||
|
trees methodTrees
|
||||||
|
|
||||||
|
pool *sync.Pool
|
||||||
|
|
||||||
|
maxParams uint16
|
||||||
|
maxSections uint16
|
||||||
|
|
||||||
|
redirectTrailingSlash bool // true
|
||||||
|
redirectFixedPath bool // false
|
||||||
|
handleMethodNotAllowed bool // false
|
||||||
|
useRawPath bool // false
|
||||||
|
unescapePathValues bool // true
|
||||||
|
removeExtraSlash bool // false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) allocateContext() *Ctx {
|
||||||
|
var (
|
||||||
|
skippedNodes = make([]skippedNode, 0, a.maxSections)
|
||||||
|
v = make(Params, 0, a.maxParams)
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := Ctx{
|
||||||
|
lock: sync.Mutex{},
|
||||||
|
app: a,
|
||||||
|
index: -1,
|
||||||
|
locals: make(map[string]any),
|
||||||
|
handlers: make([]HandlerFunc, 0),
|
||||||
|
skippedNodes: &skippedNodes,
|
||||||
|
params: &v,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
func (a *App) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||||
c := newContext(writer, request)
|
var (
|
||||||
for _, group := range a.groups {
|
err error
|
||||||
if strings.HasPrefix(request.URL.Path, group.prefix) {
|
c = a.pool.Get().(*Ctx)
|
||||||
c.handlers = append(c.handlers, group.middlewares...)
|
nfe = new(Err)
|
||||||
}
|
)
|
||||||
|
|
||||||
|
c.reset(writer, request)
|
||||||
|
|
||||||
|
if err = c.verify(); err != nil {
|
||||||
|
if errors.As(err, nfe) {
|
||||||
|
_ = c.Status(nfe.Status).SendString(nfe.Msg)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := a.router.handle(c); err != nil {
|
_ = c.Status(500).SendString(err.Error())
|
||||||
writer.WriteHeader(500)
|
return
|
||||||
_, _ = writer.Write([]byte(err.Error()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Get(path string, handlers ...HandlerFunc) {
|
a.handleHTTPRequest(c)
|
||||||
verifyHandlers(path, handlers...)
|
|
||||||
a.router.addRoute(http.MethodGet, path, handlers...)
|
a.pool.Put(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Post(path string, handlers ...HandlerFunc) {
|
func (a *App) run(ln net.Listener) error {
|
||||||
verifyHandlers(path, handlers...)
|
srv := &http.Server{Handler: a}
|
||||||
a.router.addRoute(http.MethodPost, path, handlers...)
|
|
||||||
|
if a.config.DisableHttpErrorLog {
|
||||||
|
srv.ErrorLog = log.New(io.Discard, "", 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Delete(path string, handlers ...HandlerFunc) {
|
a.server = srv
|
||||||
verifyHandlers(path, handlers...)
|
|
||||||
a.router.addRoute(http.MethodDelete, path, handlers...)
|
if !a.config.DisableBanner {
|
||||||
|
fmt.Println(banner + "nf serve at: " + ln.Addr().String() + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Put(path string, handlers ...HandlerFunc) {
|
err := a.server.Serve(ln)
|
||||||
verifyHandlers(path, handlers...)
|
if !errors.Is(err, http.ErrServerClosed) || a.config.ErrServeClose {
|
||||||
a.router.addRoute(http.MethodPut, path, handlers...)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Patch(path string, handlers ...HandlerFunc) {
|
return nil
|
||||||
verifyHandlers(path, handlers...)
|
|
||||||
a.router.addRoute(http.MethodPatch, path, handlers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) Head(path string, handlers ...HandlerFunc) {
|
|
||||||
verifyHandlers(path, handlers...)
|
|
||||||
a.router.addRoute(http.MethodHead, path, handlers...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Run(address string) error {
|
func (a *App) Run(address string) error {
|
||||||
fmt.Println(banner + "nf serve at: " + address + "\n")
|
ln, err := net.Listen("tcp", address)
|
||||||
return http.ListenAndServe(address, a)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.run(ln)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) RunTLS(address string, tlsConfig *tls.Config) error {
|
||||||
|
ln, err := tls.Listen("tcp", address, tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.run(ln)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) RunListener(ln net.Listener) error {
|
||||||
|
a.server = &http.Server{Addr: ln.Addr().String()}
|
||||||
|
|
||||||
|
return a.run(ln)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Shutdown(ctx context.Context) error {
|
||||||
|
return a.server.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) addRoute(method, path string, handlers ...HandlerFunc) {
|
||||||
|
elsePanic(path[0] == '/', "path must begin with '/'")
|
||||||
|
elsePanic(method != "", "HTTP method can not be empty")
|
||||||
|
elsePanic(len(handlers) > 0, "without enable not implement, there must be at least one handler")
|
||||||
|
|
||||||
|
if !a.config.DisableMessagePrint {
|
||||||
|
fmt.Printf("[NF] Add Route: %-8s - %-25s (%2d handlers)\n", method, path, len(handlers))
|
||||||
|
}
|
||||||
|
|
||||||
|
root := a.trees.get(method)
|
||||||
|
if root == nil {
|
||||||
|
root = new(node)
|
||||||
|
root.fullPath = "/"
|
||||||
|
a.trees = append(a.trees, methodTree{method: method, root: root})
|
||||||
|
}
|
||||||
|
|
||||||
|
root.addRoute(path, handlers...)
|
||||||
|
|
||||||
|
if paramsCount := countParams(path); paramsCount > a.maxParams {
|
||||||
|
a.maxParams = paramsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
if sectionsCount := countSections(path); sectionsCount > a.maxSections {
|
||||||
|
a.maxSections = sectionsCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleHTTPRequest(c *Ctx) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
httpMethod := c.Request.Method
|
||||||
|
rPath := c.Request.URL.Path
|
||||||
|
unescape := false
|
||||||
|
if a.useRawPath && len(c.Request.URL.RawPath) > 0 {
|
||||||
|
rPath = c.Request.URL.RawPath
|
||||||
|
unescape = a.unescapePathValues
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.removeExtraSlash {
|
||||||
|
rPath = cleanPath(rPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find root of the tree for the given HTTP method
|
||||||
|
t := a.trees
|
||||||
|
for i, tl := 0, len(t); i < tl; i++ {
|
||||||
|
if t[i].method != httpMethod {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
root := t[i].root
|
||||||
|
// Find route in tree
|
||||||
|
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
|
||||||
|
if value.params != nil {
|
||||||
|
c.params = value.params
|
||||||
|
}
|
||||||
|
|
||||||
|
if value.handlers != nil {
|
||||||
|
c.handlers = value.handlers
|
||||||
|
c.fullPath = value.fullPath
|
||||||
|
|
||||||
|
if err = c.Next(); err != nil {
|
||||||
|
serveError(c, errorHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if httpMethod != http.MethodConnect && rPath != "/" {
|
||||||
|
if value.tsr && a.redirectTrailingSlash {
|
||||||
|
redirectTrailingSlash(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if a.redirectFixedPath && redirectFixedPath(c, root, a.redirectFixedPath) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.handleMethodNotAllowed {
|
||||||
|
// According to RFC 7231 section 6.5.5, MUST generate an Allow header field in response
|
||||||
|
// containing a list of the target resource's currently supported methods.
|
||||||
|
allowed := make([]string, 0, len(t)-1)
|
||||||
|
for _, tree := range a.trees {
|
||||||
|
if tree.method == httpMethod {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
|
||||||
|
allowed = append(allowed, tree.method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allowed) > 0 {
|
||||||
|
c.handlers = a.combineHandlers(a.config.MethodNotAllowedHandler)
|
||||||
|
|
||||||
|
_ = c.Next()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.handlers = a.combineHandlers(a.config.NotFoundHandler)
|
||||||
|
|
||||||
|
_ = c.Next()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorHandler(c *Ctx) error {
|
||||||
|
return c.Status(500).SendString(_500)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveError(c *Ctx, handler HandlerFunc) {
|
||||||
|
err := c.Next()
|
||||||
|
|
||||||
|
if c.writermem.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = handler(c)
|
||||||
|
_ = err
|
||||||
|
}
|
||||||
|
|
||||||
|
func redirectTrailingSlash(c *Ctx) {
|
||||||
|
req := c.Request
|
||||||
|
p := req.URL.Path
|
||||||
|
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
|
||||||
|
prefix = regSafePrefix.ReplaceAllString(prefix, "")
|
||||||
|
prefix = regRemoveRepeatedChar.ReplaceAllString(prefix, "/")
|
||||||
|
|
||||||
|
p = prefix + "/" + req.URL.Path
|
||||||
|
}
|
||||||
|
req.URL.Path = p + "/"
|
||||||
|
if length := len(p); length > 1 && p[length-1] == '/' {
|
||||||
|
req.URL.Path = p[:length-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectRequest(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func redirectFixedPath(c *Ctx, root *node, trailingSlash bool) bool {
|
||||||
|
req := c.Request
|
||||||
|
rPath := req.URL.Path
|
||||||
|
|
||||||
|
if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(rPath), trailingSlash); ok {
|
||||||
|
req.URL.Path = bytesconv.BytesToString(fixedPath)
|
||||||
|
redirectRequest(c)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func redirectRequest(c *Ctx) {
|
||||||
|
req := c.Request
|
||||||
|
// rPath := req.URL.Path
|
||||||
|
rURL := req.URL.String()
|
||||||
|
|
||||||
|
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
|
||||||
|
if req.Method != http.MethodGet {
|
||||||
|
code = http.StatusTemporaryRedirect
|
||||||
|
}
|
||||||
|
|
||||||
|
// debugPrint("redirecting request %d: %s --> %s", code, rPath, rURL)
|
||||||
|
|
||||||
|
http.Redirect(c.Writer, req, rURL, code)
|
||||||
|
c.writermem.WriteHeaderNow()
|
||||||
}
|
}
|
||||||
|
343
ctx.go
343
ctx.go
@ -1,40 +1,69 @@
|
|||||||
package nf
|
package nf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/loveuer/nf/internal/sse"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Map map[string]interface{}
|
var forwardHeaders = []string{"CF-Connecting-IP", "X-Forwarded-For", "X-Real-Ip"}
|
||||||
|
|
||||||
type Ctx struct {
|
type Ctx struct {
|
||||||
// origin objects
|
lock sync.Mutex
|
||||||
Writer http.ResponseWriter
|
writermem responseWriter
|
||||||
|
Writer ResponseWriter
|
||||||
Request *http.Request
|
Request *http.Request
|
||||||
// request info
|
|
||||||
path string
|
path string
|
||||||
Method string
|
method string
|
||||||
// response info
|
|
||||||
StatusCode int
|
StatusCode int
|
||||||
|
|
||||||
params map[string]string
|
app *App
|
||||||
|
params *Params
|
||||||
index int
|
index int
|
||||||
handlers []HandlerFunc
|
handlers []HandlerFunc
|
||||||
locals map[string]any
|
locals map[string]interface{}
|
||||||
|
skippedNodes *[]skippedNode
|
||||||
|
fullPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newContext(writer http.ResponseWriter, request *http.Request) *Ctx {
|
func (c *Ctx) reset(w http.ResponseWriter, r *http.Request) {
|
||||||
return &Ctx{
|
traceId := r.Header.Get(TraceKey)
|
||||||
Writer: writer,
|
if traceId == "" {
|
||||||
Request: request,
|
traceId = uuid.Must(uuid.NewV7()).String()
|
||||||
path: request.URL.Path,
|
|
||||||
Method: request.Method,
|
|
||||||
index: -1,
|
|
||||||
locals: map[string]any{},
|
|
||||||
handlers: make([]HandlerFunc, 0),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Ctx) Locals(key string, value ...any) any {
|
c.writermem.reset(w)
|
||||||
|
|
||||||
|
c.Request = r.WithContext(context.WithValue(r.Context(), TraceKey, traceId))
|
||||||
|
c.Writer = &c.writermem
|
||||||
|
c.handlers = nil
|
||||||
|
c.index = -1
|
||||||
|
c.path = r.URL.Path
|
||||||
|
c.method = r.Method
|
||||||
|
c.StatusCode = 200
|
||||||
|
|
||||||
|
c.fullPath = ""
|
||||||
|
*c.params = (*c.params)[:0]
|
||||||
|
*c.skippedNodes = (*c.skippedNodes)[:0]
|
||||||
|
for key := range c.locals {
|
||||||
|
delete(c.locals, key)
|
||||||
|
}
|
||||||
|
c.writermem.Header().Set(TraceKey, traceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) Locals(key string, value ...interface{}) interface{} {
|
||||||
data := c.locals[key]
|
data := c.locals[key]
|
||||||
if len(value) > 0 {
|
if len(value) > 0 {
|
||||||
c.locals[key] = value[0]
|
c.locals[key] = value[0]
|
||||||
@ -43,6 +72,16 @@ func (c *Ctx) Locals(key string, value ...any) any {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) Method(overWrite ...string) string {
|
||||||
|
method := c.Request.Method
|
||||||
|
|
||||||
|
if len(overWrite) > 0 && overWrite[0] != "" {
|
||||||
|
c.Request.Method = overWrite[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return method
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Ctx) Path(overWrite ...string) string {
|
func (c *Ctx) Path(overWrite ...string) string {
|
||||||
path := c.Request.URL.Path
|
path := c.Request.URL.Path
|
||||||
if len(overWrite) > 0 && overWrite[0] != "" {
|
if len(overWrite) > 0 && overWrite[0] != "" {
|
||||||
@ -52,14 +91,274 @@ func (c *Ctx) Path(overWrite ...string) string {
|
|||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) Cookies(key string, defaultValue ...string) string {
|
||||||
|
dv := ""
|
||||||
|
|
||||||
|
if len(defaultValue) > 0 {
|
||||||
|
dv = defaultValue[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
cookie, err := c.Request.Cookie(key)
|
||||||
|
if err != nil || cookie.Value == "" {
|
||||||
|
return dv
|
||||||
|
}
|
||||||
|
|
||||||
|
return cookie.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) Context() context.Context {
|
||||||
|
return c.Request.Context()
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Ctx) Next() error {
|
func (c *Ctx) Next() error {
|
||||||
c.index++
|
c.index++
|
||||||
s := len(c.handlers)
|
|
||||||
for ; c.index < s; c.index++ {
|
if c.index >= len(c.handlers) {
|
||||||
if err := c.handlers[c.index](c); err != nil {
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
handler = c.handlers[c.index]
|
||||||
|
)
|
||||||
|
|
||||||
|
if handler != nil {
|
||||||
|
if err = handler(c); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.index++
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===============================================================
|
||||||
|
|| Handle Ctx Request Part
|
||||||
|
=============================================================== */
|
||||||
|
|
||||||
|
func (c *Ctx) verify() error {
|
||||||
|
// 验证 body size
|
||||||
|
if c.app.config.BodyLimit != -1 && c.Request.ContentLength > c.app.config.BodyLimit {
|
||||||
|
return NewNFError(413, "Content Too Large")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) Param(key string) string {
|
||||||
|
return c.params.ByName(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) SetParam(key, value string) {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
params := append(*c.params, Param{Key: key, Value: value})
|
||||||
|
c.params = ¶ms
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) Form(key string) string {
|
||||||
|
return c.Request.FormValue(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormValue fiber ctx function
|
||||||
|
func (c *Ctx) FormValue(key string) string {
|
||||||
|
return c.Request.FormValue(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) FormFile(key string) (*multipart.FileHeader, error) {
|
||||||
|
_, fh, err := c.Request.FormFile(key)
|
||||||
|
return fh, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) MultipartForm() (*multipart.Form, error) {
|
||||||
|
if err := c.Request.ParseMultipartForm(c.app.config.BodyLimit); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Request.MultipartForm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) Query(key string) string {
|
||||||
|
return c.Request.URL.Query().Get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) Get(key string, defaultValue ...string) string {
|
||||||
|
value := c.Request.Header.Get(key)
|
||||||
|
if value == "" && len(defaultValue) > 0 {
|
||||||
|
return defaultValue[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) IP(useProxyHeader ...bool) string {
|
||||||
|
ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(useProxyHeader) > 0 && useProxyHeader[0] {
|
||||||
|
for _, h := range forwardHeaders {
|
||||||
|
for _, rip := range strings.Split(c.Request.Header.Get(h), ",") {
|
||||||
|
realIP := net.ParseIP(strings.Replace(rip, " ", "", -1))
|
||||||
|
if check := net.ParseIP(realIP.String()); check != nil {
|
||||||
|
ip = realIP.String()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) BodyParser(out interface{}) error {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
ctype = strings.ToLower(c.Request.Header.Get("Content-Type"))
|
||||||
|
)
|
||||||
|
|
||||||
|
ctype = parseVendorSpecificContentType(ctype)
|
||||||
|
|
||||||
|
ctypeEnd := strings.IndexByte(ctype, ';')
|
||||||
|
if ctypeEnd != -1 {
|
||||||
|
ctype = ctype[:ctypeEnd]
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(ctype, "json") {
|
||||||
|
bs, err := io.ReadAll(c.Request.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = c.Request.Body.Close()
|
||||||
|
|
||||||
|
c.Request.Body = io.NopCloser(bytes.NewReader(bs))
|
||||||
|
|
||||||
|
return json.Unmarshal(bs, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(ctype, MIMEApplicationForm) {
|
||||||
|
|
||||||
|
if err = c.Request.ParseForm(); err != nil {
|
||||||
|
return NewNFError(400, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseToStruct("form", out, c.Request.Form)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(ctype, MIMEMultipartForm) {
|
||||||
|
if err = c.Request.ParseMultipartForm(c.app.config.BodyLimit); err != nil {
|
||||||
|
return NewNFError(400, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseToStruct("form", out, c.Request.PostForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewNFError(422, "Unprocessable Content")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) QueryParser(out interface{}) error {
|
||||||
|
return parseToStruct("query", out, c.Request.URL.Query())
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===============================================================
|
||||||
|
|| Handle Ctx Response Part
|
||||||
|
=============================================================== */
|
||||||
|
|
||||||
|
func (c *Ctx) Status(code int) *Ctx {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
c.Writer.WriteHeader(code)
|
||||||
|
c.StatusCode = c.writermem.status
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set set response header
|
||||||
|
func (c *Ctx) Set(key string, value string) {
|
||||||
|
c.Writer.Header().Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddHeader add response header
|
||||||
|
func (c *Ctx) AddHeader(key string, value string) {
|
||||||
|
c.Writer.Header().Add(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHeader set response header
|
||||||
|
func (c *Ctx) SetHeader(key string, value string) {
|
||||||
|
c.Writer.Header().Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) SendStatus(code int) error {
|
||||||
|
c.Status(code)
|
||||||
|
c.Writer.WriteHeaderNow()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) SendString(data string) error {
|
||||||
|
c.SetHeader("Content-Type", "text/plain")
|
||||||
|
_, err := c.Write([]byte(data))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) Writef(format string, values ...interface{}) (int, error) {
|
||||||
|
c.SetHeader("Content-Type", "text/plain")
|
||||||
|
return c.Write([]byte(fmt.Sprintf(format, values...)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) JSON(data interface{}) error {
|
||||||
|
c.SetHeader("Content-Type", MIMEApplicationJSON)
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(c.Writer)
|
||||||
|
|
||||||
|
if err := encoder.Encode(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) SSEvent(event string, data interface{}) error {
|
||||||
|
c.Set("Content-Type", "text/event-stream")
|
||||||
|
c.Set("Cache-Control", "no-cache")
|
||||||
|
c.Set("Transfer-Encoding", "chunked")
|
||||||
|
|
||||||
|
return sse.Encode(c.Writer, sse.Event{Event: event, Data: data})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) Flush() error {
|
||||||
|
if f, ok := c.Writer.(http.Flusher); ok {
|
||||||
|
f.Flush()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("http.Flusher is not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) HTML(html string) error {
|
||||||
|
c.SetHeader("Content-Type", "text/html")
|
||||||
|
_, err := c.Write([]byte(html))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) RenderHTML(name, html string, obj any) error {
|
||||||
|
c.SetHeader("Content-Type", "text/html")
|
||||||
|
t, err := template.New(name).Parse(html)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Execute(c.Writer, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) Redirect(url string, code int) error {
|
||||||
|
http.Redirect(c.Writer, c.Request, url, code)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Ctx) Write(data []byte) (int, error) {
|
||||||
|
return c.Writer.Write(data)
|
||||||
|
}
|
||||||
|
16
error.go
Normal file
16
error.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package nf
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
type Err struct {
|
||||||
|
Status int
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n Err) Error() string {
|
||||||
|
return strconv.Itoa(n.Status) + " " + n.Msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNFError(status int, msg string) Err {
|
||||||
|
return Err{Status: status, Msg: msg}
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"nf"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := nf.New()
|
|
||||||
|
|
||||||
app.Post("/data", func(c *nf.Ctx) error {
|
|
||||||
type Req struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Age int `json:"age"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
req = new(Req)
|
|
||||||
rm = make(map[string]any)
|
|
||||||
)
|
|
||||||
|
|
||||||
if err = c.BodyParser(req); err != nil {
|
|
||||||
log.Print("err 1:", err)
|
|
||||||
return c.Status(500).SendString(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = c.BodyParser(&rm); err != nil {
|
|
||||||
log.Print("err 2:", err)
|
|
||||||
return c.Status(500).SendString(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(nf.Map{"rm": rm, "req": req})
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Fatal(app.Run(":19991"))
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"nf"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Logger() nf.HandlerFunc {
|
|
||||||
return func(c *nf.Ctx) error {
|
|
||||||
// Start timer
|
|
||||||
t := time.Now()
|
|
||||||
// Process request
|
|
||||||
err := c.Next()
|
|
||||||
// Calculate resolution time
|
|
||||||
log.Printf("[%d] %s in %v", c.StatusCode, c.Request.RequestURI, time.Since(t))
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRecovery() nf.HandlerFunc {
|
|
||||||
return func(c *nf.Ctx) error {
|
|
||||||
defer func() {
|
|
||||||
if err := recover(); err != nil {
|
|
||||||
log.Printf("[recovery] %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"nf"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := nf.New()
|
|
||||||
|
|
||||||
app.Get("/hello", func(c *nf.Ctx) error {
|
|
||||||
return c.SendString("world")
|
|
||||||
})
|
|
||||||
|
|
||||||
app.Get("/world", func(c *nf.Ctx) error {
|
|
||||||
return errors.New("nice")
|
|
||||||
})
|
|
||||||
|
|
||||||
v1 := app.Group("/v1")
|
|
||||||
v1.Use(NewRecovery())
|
|
||||||
v1.Get("/hello", func(c *nf.Ctx) error {
|
|
||||||
return c.JSON(nf.Map{"version": "v1", "date": time.Now()})
|
|
||||||
})
|
|
||||||
|
|
||||||
v1.Get(
|
|
||||||
"/name",
|
|
||||||
func(c *nf.Ctx) error {
|
|
||||||
c.Locals("name", "zyp")
|
|
||||||
panic("name")
|
|
||||||
return c.Next()
|
|
||||||
},
|
|
||||||
func(c *nf.Ctx) error {
|
|
||||||
return c.SendString(fmt.Sprintf("hi, %s", c.Locals("name1")))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
v2 := v1.Group("/v2")
|
|
||||||
v2.Use(Logger())
|
|
||||||
v2.Get(
|
|
||||||
"/name",
|
|
||||||
func(c *nf.Ctx) error {
|
|
||||||
return c.SendString("hi, loveuer")
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
log.Fatal(app.Run(":7733"))
|
|
||||||
}
|
|
63
go.mod
63
go.mod
@ -1,44 +1,39 @@
|
|||||||
module nf
|
module github.com/loveuer/nf
|
||||||
|
|
||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/fatih/color v1.17.0
|
||||||
github.com/gofiber/fiber/v2 v2.52.0
|
github.com/go-git/go-git/v5 v5.12.0
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/spf13/cobra v1.8.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
github.com/bytedance/sonic v1.9.1 // indirect
|
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
github.com/cloudflare/circl v1.3.7 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/go-resty/resty/v2 v2.16.2
|
||||||
github.com/google/uuid v1.5.0 // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.17.0 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
github.com/leodido/go-urn v1.2.4 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/skeema/knownhosts v1.2.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
golang.org/x/crypto v0.25.0 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
golang.org/x/mod v0.12.0 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
golang.org/x/net v0.27.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
golang.org/x/tools v0.13.0 // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
golang.org/x/crypto v0.14.0 // indirect
|
|
||||||
golang.org/x/net v0.17.0 // indirect
|
|
||||||
golang.org/x/sys v0.15.0 // indirect
|
|
||||||
golang.org/x/text v0.13.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.30.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
226
go.sum
226
go.sum
@ -1,107 +1,151 @@
|
|||||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
|
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||||
|
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||||
|
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||||
|
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
|
||||||
|
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
|
||||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
|
||||||
github.com/gofiber/fiber/v2 v2.52.0 h1:S+qXi7y+/Pgvqq4DrSmREGiFwtB7Bu6+QFLuIHYw/UE=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
github.com/gofiber/fiber/v2 v2.52.0/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||||
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
||||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||||
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
|
||||||
|
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
||||||
|
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||||
|
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
|
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||||
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||||
|
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
|
||||||
|
60
group.go
60
group.go
@ -1,60 +0,0 @@
|
|||||||
package nf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RouterGroup struct {
|
|
||||||
prefix string
|
|
||||||
middlewares []HandlerFunc // support middleware
|
|
||||||
parent *RouterGroup // support nesting
|
|
||||||
app *App // all groups share a Engine instance
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group is defined to create a new RouterGroup
|
|
||||||
// remember all groups share the same Engine instance
|
|
||||||
func (group *RouterGroup) Group(prefix string) *RouterGroup {
|
|
||||||
app := group.app
|
|
||||||
newGroup := &RouterGroup{
|
|
||||||
prefix: group.prefix + prefix,
|
|
||||||
parent: group,
|
|
||||||
app: app,
|
|
||||||
}
|
|
||||||
app.groups = append(app.groups, newGroup)
|
|
||||||
return newGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
func (group *RouterGroup) addRoute(method string, comp string, handlers ...HandlerFunc) {
|
|
||||||
pattern := group.prefix + comp
|
|
||||||
log.Printf("Add Route %4s - %s", method, pattern)
|
|
||||||
group.app.router.addRoute(method, pattern, handlers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (group *RouterGroup) Get(pattern string, handlers ...HandlerFunc) {
|
|
||||||
group.addRoute(http.MethodGet, pattern, handlers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (group *RouterGroup) Post(pattern string, handlers ...HandlerFunc) {
|
|
||||||
group.addRoute(http.MethodPost, pattern, handlers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (group *RouterGroup) Put(pattern string, handlers ...HandlerFunc) {
|
|
||||||
group.addRoute(http.MethodPut, pattern, handlers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (group *RouterGroup) Delete(pattern string, handlers ...HandlerFunc) {
|
|
||||||
group.addRoute(http.MethodDelete, pattern, handlers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (group *RouterGroup) Patch(pattern string, handlers ...HandlerFunc) {
|
|
||||||
group.addRoute(http.MethodPatch, pattern, handlers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (group *RouterGroup) Head(pattern string, handlers ...HandlerFunc) {
|
|
||||||
group.addRoute(http.MethodHead, pattern, handlers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
|
|
||||||
group.middlewares = append(group.middlewares, middlewares...)
|
|
||||||
}
|
|
@ -1,3 +1,9 @@
|
|||||||
package nf
|
package nf
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
type HandlerFunc func(*Ctx) error
|
type HandlerFunc func(*Ctx) error
|
||||||
|
|
||||||
|
func ToDoHandler(c *Ctx) error {
|
||||||
|
return c.Status(501).SendString(fmt.Sprintf("%s - %s Not Implemented", c.Method(), c.Path()))
|
||||||
|
}
|
||||||
|
26
internal/bytesconv/bytesconv_1.19.go
Normal file
26
internal/bytesconv/bytesconv_1.19.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright 2020 Gin Core Team. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build !go1.20
|
||||||
|
|
||||||
|
package bytesconv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StringToBytes converts string to byte slice without a memory allocation.
|
||||||
|
func StringToBytes(s string) []byte {
|
||||||
|
return *(*[]byte)(unsafe.Pointer(
|
||||||
|
&struct {
|
||||||
|
string
|
||||||
|
Cap int
|
||||||
|
}{s, len(s)},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// BytesToString converts byte slice to string without a memory allocation.
|
||||||
|
func BytesToString(b []byte) string {
|
||||||
|
return *(*string)(unsafe.Pointer(&b))
|
||||||
|
}
|
23
internal/bytesconv/bytesconv_1.20.go
Normal file
23
internal/bytesconv/bytesconv_1.20.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// Copyright 2023 Gin Core Team. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build go1.20
|
||||||
|
|
||||||
|
package bytesconv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StringToBytes converts string to byte slice without a memory allocation.
|
||||||
|
// For more details, see https://github.com/golang/go/issues/53003#issuecomment-1140276077.
|
||||||
|
func StringToBytes(s string) []byte {
|
||||||
|
return unsafe.Slice(unsafe.StringData(s), len(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// BytesToString converts byte slice to string without a memory allocation.
|
||||||
|
// For more details, see https://github.com/golang/go/issues/53003#issuecomment-1140276077.
|
||||||
|
func BytesToString(b []byte) string {
|
||||||
|
return unsafe.String(unsafe.SliceData(b), len(b))
|
||||||
|
}
|
99
internal/bytesconv/bytesconv_test.go
Normal file
99
internal/bytesconv/bytesconv_test.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
// Copyright 2020 Gin Core Team. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package bytesconv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testString = "Albert Einstein: Logic will get you from A to B. Imagination will take you everywhere."
|
||||||
|
var testBytes = []byte(testString)
|
||||||
|
|
||||||
|
func rawBytesToStr(b []byte) string {
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rawStrToBytes(s string) []byte {
|
||||||
|
return []byte(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// go test -v
|
||||||
|
|
||||||
|
func TestBytesToString(t *testing.T) {
|
||||||
|
data := make([]byte, 1024)
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
rand.Read(data)
|
||||||
|
if rawBytesToStr(data) != BytesToString(data) {
|
||||||
|
t.Fatal("don't match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
const (
|
||||||
|
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||||
|
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||||
|
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||||
|
)
|
||||||
|
|
||||||
|
var src = rand.NewSource(time.Now().UnixNano())
|
||||||
|
|
||||||
|
func RandStringBytesMaskImprSrcSB(n int) string {
|
||||||
|
sb := strings.Builder{}
|
||||||
|
sb.Grow(n)
|
||||||
|
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
|
||||||
|
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
|
||||||
|
if remain == 0 {
|
||||||
|
cache, remain = src.Int63(), letterIdxMax
|
||||||
|
}
|
||||||
|
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
|
||||||
|
sb.WriteByte(letterBytes[idx])
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
cache >>= letterIdxBits
|
||||||
|
remain--
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringToBytes(t *testing.T) {
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
s := RandStringBytesMaskImprSrcSB(64)
|
||||||
|
if !bytes.Equal(rawStrToBytes(s), StringToBytes(s)) {
|
||||||
|
t.Fatal("don't match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// go test -v -run=none -bench=^BenchmarkBytesConv -benchmem=true
|
||||||
|
|
||||||
|
func BenchmarkBytesConvBytesToStrRaw(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
rawBytesToStr(testBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkBytesConvBytesToStr(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
BytesToString(testBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkBytesConvStrToBytesRaw(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
rawStrToBytes(testString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkBytesConvStrToBytes(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
StringToBytes(testString)
|
||||||
|
}
|
||||||
|
}
|
27
internal/schema/LICENSE
Normal file
27
internal/schema/LICENSE
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
Copyright (c) 2012 Rodrigo Moraes. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
305
internal/schema/cache.go
Normal file
305
internal/schema/cache.go
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errInvalidPath = errors.New("schema: invalid path")
|
||||||
|
|
||||||
|
// newCache returns a new cache.
|
||||||
|
func newCache() *cache {
|
||||||
|
c := cache{
|
||||||
|
m: make(map[reflect.Type]*structInfo),
|
||||||
|
regconv: make(map[reflect.Type]Converter),
|
||||||
|
tag: "schema",
|
||||||
|
}
|
||||||
|
return &c
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache caches meta-data about a struct.
|
||||||
|
type cache struct {
|
||||||
|
l sync.RWMutex
|
||||||
|
m map[reflect.Type]*structInfo
|
||||||
|
regconv map[reflect.Type]Converter
|
||||||
|
tag string
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerConverter registers a converter function for a custom type.
|
||||||
|
func (c *cache) registerConverter(value interface{}, converterFunc Converter) {
|
||||||
|
c.regconv[reflect.TypeOf(value)] = converterFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePath parses a path in dotted notation verifying that it is a valid
|
||||||
|
// path to a struct field.
|
||||||
|
//
|
||||||
|
// It returns "path parts" which contain indices to fields to be used by
|
||||||
|
// reflect.Value.FieldByString(). Multiple parts are required for slices of
|
||||||
|
// structs.
|
||||||
|
func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) {
|
||||||
|
var struc *structInfo
|
||||||
|
var field *fieldInfo
|
||||||
|
var index64 int64
|
||||||
|
var err error
|
||||||
|
parts := make([]pathPart, 0)
|
||||||
|
path := make([]string, 0)
|
||||||
|
keys := strings.Split(p, ".")
|
||||||
|
for i := 0; i < len(keys); i++ {
|
||||||
|
if t.Kind() != reflect.Struct {
|
||||||
|
return nil, errInvalidPath
|
||||||
|
}
|
||||||
|
if struc = c.get(t); struc == nil {
|
||||||
|
return nil, errInvalidPath
|
||||||
|
}
|
||||||
|
if field = struc.get(keys[i]); field == nil {
|
||||||
|
return nil, errInvalidPath
|
||||||
|
}
|
||||||
|
// Valid field. Append index.
|
||||||
|
path = append(path, field.name)
|
||||||
|
if field.isSliceOfStructs && (!field.unmarshalerInfo.IsValid || (field.unmarshalerInfo.IsValid && field.unmarshalerInfo.IsSliceElement)) {
|
||||||
|
// Parse a special case: slices of structs.
|
||||||
|
// i+1 must be the slice index.
|
||||||
|
//
|
||||||
|
// Now that struct can implements TextUnmarshaler interface,
|
||||||
|
// we don't need to force the struct's fields to appear in the path.
|
||||||
|
// So checking i+2 is not necessary anymore.
|
||||||
|
i++
|
||||||
|
if i+1 > len(keys) {
|
||||||
|
return nil, errInvalidPath
|
||||||
|
}
|
||||||
|
if index64, err = strconv.ParseInt(keys[i], 10, 0); err != nil {
|
||||||
|
return nil, errInvalidPath
|
||||||
|
}
|
||||||
|
parts = append(parts, pathPart{
|
||||||
|
path: path,
|
||||||
|
field: field,
|
||||||
|
index: int(index64),
|
||||||
|
})
|
||||||
|
path = make([]string, 0)
|
||||||
|
|
||||||
|
// Get the next struct type, dropping ptrs.
|
||||||
|
if field.typ.Kind() == reflect.Ptr {
|
||||||
|
t = field.typ.Elem()
|
||||||
|
} else {
|
||||||
|
t = field.typ
|
||||||
|
}
|
||||||
|
if t.Kind() == reflect.Slice {
|
||||||
|
t = t.Elem()
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if field.typ.Kind() == reflect.Ptr {
|
||||||
|
t = field.typ.Elem()
|
||||||
|
} else {
|
||||||
|
t = field.typ
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add the remaining.
|
||||||
|
parts = append(parts, pathPart{
|
||||||
|
path: path,
|
||||||
|
field: field,
|
||||||
|
index: -1,
|
||||||
|
})
|
||||||
|
return parts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// get returns a cached structInfo, creating it if necessary.
|
||||||
|
func (c *cache) get(t reflect.Type) *structInfo {
|
||||||
|
c.l.RLock()
|
||||||
|
info := c.m[t]
|
||||||
|
c.l.RUnlock()
|
||||||
|
if info == nil {
|
||||||
|
info = c.create(t, "")
|
||||||
|
c.l.Lock()
|
||||||
|
c.m[t] = info
|
||||||
|
c.l.Unlock()
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
// create creates a structInfo with meta-data about a struct.
|
||||||
|
func (c *cache) create(t reflect.Type, parentAlias string) *structInfo {
|
||||||
|
info := &structInfo{}
|
||||||
|
var anonymousInfos []*structInfo
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
if f := c.createField(t.Field(i), parentAlias); f != nil {
|
||||||
|
info.fields = append(info.fields, f)
|
||||||
|
if ft := indirectType(f.typ); ft.Kind() == reflect.Struct && f.isAnonymous {
|
||||||
|
anonymousInfos = append(anonymousInfos, c.create(ft, f.canonicalAlias))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, a := range anonymousInfos {
|
||||||
|
others := []*structInfo{info}
|
||||||
|
others = append(others, anonymousInfos[:i]...)
|
||||||
|
others = append(others, anonymousInfos[i+1:]...)
|
||||||
|
for _, f := range a.fields {
|
||||||
|
if !containsAlias(others, f.alias) {
|
||||||
|
info.fields = append(info.fields, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
// createField creates a fieldInfo for the given field.
|
||||||
|
func (c *cache) createField(field reflect.StructField, parentAlias string) *fieldInfo {
|
||||||
|
alias, options := fieldAlias(field, c.tag)
|
||||||
|
if alias == "-" {
|
||||||
|
// Ignore this field.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
canonicalAlias := alias
|
||||||
|
if parentAlias != "" {
|
||||||
|
canonicalAlias = parentAlias + "." + alias
|
||||||
|
}
|
||||||
|
// Check if the type is supported and don't cache it if not.
|
||||||
|
// First let's get the basic type.
|
||||||
|
isSlice, isStruct := false, false
|
||||||
|
ft := field.Type
|
||||||
|
m := isTextUnmarshaler(reflect.Zero(ft))
|
||||||
|
if ft.Kind() == reflect.Ptr {
|
||||||
|
ft = ft.Elem()
|
||||||
|
}
|
||||||
|
if isSlice = ft.Kind() == reflect.Slice; isSlice {
|
||||||
|
ft = ft.Elem()
|
||||||
|
if ft.Kind() == reflect.Ptr {
|
||||||
|
ft = ft.Elem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ft.Kind() == reflect.Array {
|
||||||
|
ft = ft.Elem()
|
||||||
|
if ft.Kind() == reflect.Ptr {
|
||||||
|
ft = ft.Elem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isStruct = ft.Kind() == reflect.Struct; !isStruct {
|
||||||
|
if c.converter(ft) == nil && builtinConverters[ft.Kind()] == nil {
|
||||||
|
// Type is not supported.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &fieldInfo{
|
||||||
|
typ: field.Type,
|
||||||
|
name: field.Name,
|
||||||
|
alias: alias,
|
||||||
|
canonicalAlias: canonicalAlias,
|
||||||
|
unmarshalerInfo: m,
|
||||||
|
isSliceOfStructs: isSlice && isStruct,
|
||||||
|
isAnonymous: field.Anonymous,
|
||||||
|
isRequired: options.Contains("required"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// converter returns the converter for a type.
|
||||||
|
func (c *cache) converter(t reflect.Type) Converter {
|
||||||
|
return c.regconv[t]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type structInfo struct {
|
||||||
|
fields []*fieldInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *structInfo) get(alias string) *fieldInfo {
|
||||||
|
for _, field := range i.fields {
|
||||||
|
if strings.EqualFold(field.alias, alias) {
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsAlias(infos []*structInfo, alias string) bool {
|
||||||
|
for _, info := range infos {
|
||||||
|
if info.get(alias) != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type fieldInfo struct {
|
||||||
|
typ reflect.Type
|
||||||
|
// name is the field name in the struct.
|
||||||
|
name string
|
||||||
|
alias string
|
||||||
|
// canonicalAlias is almost the same as the alias, but is prefixed with
|
||||||
|
// an embedded struct field alias in dotted notation if this field is
|
||||||
|
// promoted from the struct.
|
||||||
|
// For instance, if the alias is "N" and this field is an embedded field
|
||||||
|
// in a struct "X", canonicalAlias will be "X.N".
|
||||||
|
canonicalAlias string
|
||||||
|
// unmarshalerInfo contains information regarding the
|
||||||
|
// encoding.TextUnmarshaler implementation of the field type.
|
||||||
|
unmarshalerInfo unmarshaler
|
||||||
|
// isSliceOfStructs indicates if the field type is a slice of structs.
|
||||||
|
isSliceOfStructs bool
|
||||||
|
// isAnonymous indicates whether the field is embedded in the struct.
|
||||||
|
isAnonymous bool
|
||||||
|
isRequired bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fieldInfo) paths(prefix string) []string {
|
||||||
|
if f.alias == f.canonicalAlias {
|
||||||
|
return []string{prefix + f.alias}
|
||||||
|
}
|
||||||
|
return []string{prefix + f.alias, prefix + f.canonicalAlias}
|
||||||
|
}
|
||||||
|
|
||||||
|
type pathPart struct {
|
||||||
|
field *fieldInfo
|
||||||
|
path []string // path to the field: walks structs using field names.
|
||||||
|
index int // struct index in slices of structs.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func indirectType(typ reflect.Type) reflect.Type {
|
||||||
|
if typ.Kind() == reflect.Ptr {
|
||||||
|
return typ.Elem()
|
||||||
|
}
|
||||||
|
return typ
|
||||||
|
}
|
||||||
|
|
||||||
|
// fieldAlias parses a field tag to get a field alias.
|
||||||
|
func fieldAlias(field reflect.StructField, tagName string) (alias string, options tagOptions) {
|
||||||
|
if tag := field.Tag.Get(tagName); tag != "" {
|
||||||
|
alias, options = parseTag(tag)
|
||||||
|
}
|
||||||
|
if alias == "" {
|
||||||
|
alias = field.Name
|
||||||
|
}
|
||||||
|
return alias, options
|
||||||
|
}
|
||||||
|
|
||||||
|
// tagOptions is the string following a comma in a struct field's tag, or
|
||||||
|
// the empty string. It does not include the leading comma.
|
||||||
|
type tagOptions []string
|
||||||
|
|
||||||
|
// parseTag splits a struct field's url tag into its name and comma-separated
|
||||||
|
// options.
|
||||||
|
func parseTag(tag string) (string, tagOptions) {
|
||||||
|
s := strings.Split(tag, ",")
|
||||||
|
return s[0], s[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains checks whether the tagOptions contains the specified option.
|
||||||
|
func (o tagOptions) Contains(option string) bool {
|
||||||
|
for _, s := range o {
|
||||||
|
if s == option {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
145
internal/schema/converter.go
Normal file
145
internal/schema/converter.go
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Converter func(string) reflect.Value
|
||||||
|
|
||||||
|
var (
|
||||||
|
invalidValue = reflect.Value{}
|
||||||
|
boolType = reflect.Bool
|
||||||
|
float32Type = reflect.Float32
|
||||||
|
float64Type = reflect.Float64
|
||||||
|
intType = reflect.Int
|
||||||
|
int8Type = reflect.Int8
|
||||||
|
int16Type = reflect.Int16
|
||||||
|
int32Type = reflect.Int32
|
||||||
|
int64Type = reflect.Int64
|
||||||
|
stringType = reflect.String
|
||||||
|
uintType = reflect.Uint
|
||||||
|
uint8Type = reflect.Uint8
|
||||||
|
uint16Type = reflect.Uint16
|
||||||
|
uint32Type = reflect.Uint32
|
||||||
|
uint64Type = reflect.Uint64
|
||||||
|
)
|
||||||
|
|
||||||
|
// Default converters for basic types.
|
||||||
|
var builtinConverters = map[reflect.Kind]Converter{
|
||||||
|
boolType: convertBool,
|
||||||
|
float32Type: convertFloat32,
|
||||||
|
float64Type: convertFloat64,
|
||||||
|
intType: convertInt,
|
||||||
|
int8Type: convertInt8,
|
||||||
|
int16Type: convertInt16,
|
||||||
|
int32Type: convertInt32,
|
||||||
|
int64Type: convertInt64,
|
||||||
|
stringType: convertString,
|
||||||
|
uintType: convertUint,
|
||||||
|
uint8Type: convertUint8,
|
||||||
|
uint16Type: convertUint16,
|
||||||
|
uint32Type: convertUint32,
|
||||||
|
uint64Type: convertUint64,
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertBool(value string) reflect.Value {
|
||||||
|
if value == "on" {
|
||||||
|
return reflect.ValueOf(true)
|
||||||
|
} else if v, err := strconv.ParseBool(value); err == nil {
|
||||||
|
return reflect.ValueOf(v)
|
||||||
|
}
|
||||||
|
return invalidValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertFloat32(value string) reflect.Value {
|
||||||
|
if v, err := strconv.ParseFloat(value, 32); err == nil {
|
||||||
|
return reflect.ValueOf(float32(v))
|
||||||
|
}
|
||||||
|
return invalidValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertFloat64(value string) reflect.Value {
|
||||||
|
if v, err := strconv.ParseFloat(value, 64); err == nil {
|
||||||
|
return reflect.ValueOf(v)
|
||||||
|
}
|
||||||
|
return invalidValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertInt(value string) reflect.Value {
|
||||||
|
if v, err := strconv.ParseInt(value, 10, 0); err == nil {
|
||||||
|
return reflect.ValueOf(int(v))
|
||||||
|
}
|
||||||
|
return invalidValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertInt8(value string) reflect.Value {
|
||||||
|
if v, err := strconv.ParseInt(value, 10, 8); err == nil {
|
||||||
|
return reflect.ValueOf(int8(v))
|
||||||
|
}
|
||||||
|
return invalidValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertInt16(value string) reflect.Value {
|
||||||
|
if v, err := strconv.ParseInt(value, 10, 16); err == nil {
|
||||||
|
return reflect.ValueOf(int16(v))
|
||||||
|
}
|
||||||
|
return invalidValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertInt32(value string) reflect.Value {
|
||||||
|
if v, err := strconv.ParseInt(value, 10, 32); err == nil {
|
||||||
|
return reflect.ValueOf(int32(v))
|
||||||
|
}
|
||||||
|
return invalidValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertInt64(value string) reflect.Value {
|
||||||
|
if v, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||||||
|
return reflect.ValueOf(v)
|
||||||
|
}
|
||||||
|
return invalidValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertString(value string) reflect.Value {
|
||||||
|
return reflect.ValueOf(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertUint(value string) reflect.Value {
|
||||||
|
if v, err := strconv.ParseUint(value, 10, 0); err == nil {
|
||||||
|
return reflect.ValueOf(uint(v))
|
||||||
|
}
|
||||||
|
return invalidValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertUint8(value string) reflect.Value {
|
||||||
|
if v, err := strconv.ParseUint(value, 10, 8); err == nil {
|
||||||
|
return reflect.ValueOf(uint8(v))
|
||||||
|
}
|
||||||
|
return invalidValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertUint16(value string) reflect.Value {
|
||||||
|
if v, err := strconv.ParseUint(value, 10, 16); err == nil {
|
||||||
|
return reflect.ValueOf(uint16(v))
|
||||||
|
}
|
||||||
|
return invalidValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertUint32(value string) reflect.Value {
|
||||||
|
if v, err := strconv.ParseUint(value, 10, 32); err == nil {
|
||||||
|
return reflect.ValueOf(uint32(v))
|
||||||
|
}
|
||||||
|
return invalidValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertUint64(value string) reflect.Value {
|
||||||
|
if v, err := strconv.ParseUint(value, 10, 64); err == nil {
|
||||||
|
return reflect.ValueOf(v)
|
||||||
|
}
|
||||||
|
return invalidValue
|
||||||
|
}
|
534
internal/schema/decoder.go
Normal file
534
internal/schema/decoder.go
Normal file
@ -0,0 +1,534 @@
|
|||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewDecoder returns a new Decoder.
|
||||||
|
func NewDecoder() *Decoder {
|
||||||
|
return &Decoder{cache: newCache()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decoder decodes values from a map[string][]string to a struct.
|
||||||
|
type Decoder struct {
|
||||||
|
cache *cache
|
||||||
|
zeroEmpty bool
|
||||||
|
ignoreUnknownKeys bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAliasTag changes the tag used to locate custom field aliases.
|
||||||
|
// The default tag is "schema".
|
||||||
|
func (d *Decoder) SetAliasTag(tag string) {
|
||||||
|
d.cache.tag = tag
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZeroEmpty controls the behaviour when the decoder encounters empty values
|
||||||
|
// in a map.
|
||||||
|
// If z is true and a key in the map has the empty string as a value
|
||||||
|
// then the corresponding struct field is set to the zero value.
|
||||||
|
// If z is false then empty strings are ignored.
|
||||||
|
//
|
||||||
|
// The default value is false, that is empty values do not change
|
||||||
|
// the value of the struct field.
|
||||||
|
func (d *Decoder) ZeroEmpty(z bool) {
|
||||||
|
d.zeroEmpty = z
|
||||||
|
}
|
||||||
|
|
||||||
|
// IgnoreUnknownKeys controls the behaviour when the decoder encounters unknown
|
||||||
|
// keys in the map.
|
||||||
|
// If i is true and an unknown field is encountered, it is ignored. This is
|
||||||
|
// similar to how unknown keys are handled by encoding/json.
|
||||||
|
// If i is false then Decode will return an error. Note that any valid keys
|
||||||
|
// will still be decoded in to the target struct.
|
||||||
|
//
|
||||||
|
// To preserve backwards compatibility, the default value is false.
|
||||||
|
func (d *Decoder) IgnoreUnknownKeys(i bool) {
|
||||||
|
d.ignoreUnknownKeys = i
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterConverter registers a converter function for a custom type.
|
||||||
|
func (d *Decoder) RegisterConverter(value interface{}, converterFunc Converter) {
|
||||||
|
d.cache.registerConverter(value, converterFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes a map[string][]string to a struct.
|
||||||
|
//
|
||||||
|
// The first parameter must be a pointer to a struct.
|
||||||
|
//
|
||||||
|
// The second parameter is a map, typically url.Values from an HTTP request.
|
||||||
|
// Keys are "paths" in dotted notation to the struct fields and nested structs.
|
||||||
|
//
|
||||||
|
// See the package documentation for a full explanation of the mechanics.
|
||||||
|
func (d *Decoder) Decode(dst interface{}, src map[string][]string) error {
|
||||||
|
v := reflect.ValueOf(dst)
|
||||||
|
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
|
||||||
|
return errors.New("schema: interface must be a pointer to struct")
|
||||||
|
}
|
||||||
|
v = v.Elem()
|
||||||
|
t := v.Type()
|
||||||
|
multiError := MultiError{}
|
||||||
|
for path, values := range src {
|
||||||
|
if parts, err := d.cache.parsePath(path, t); err == nil {
|
||||||
|
if err = d.decode(v, path, parts, values); err != nil {
|
||||||
|
multiError[path] = err
|
||||||
|
}
|
||||||
|
} else if !d.ignoreUnknownKeys {
|
||||||
|
multiError[path] = UnknownKeyError{Key: path}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
multiError.merge(d.checkRequired(t, src))
|
||||||
|
if len(multiError) > 0 {
|
||||||
|
return multiError
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkRequired checks whether required fields are empty
|
||||||
|
//
|
||||||
|
// check type t recursively if t has struct fields.
|
||||||
|
//
|
||||||
|
// src is the source map for decoding, we use it here to see if those required fields are included in src
|
||||||
|
func (d *Decoder) checkRequired(t reflect.Type, src map[string][]string) MultiError {
|
||||||
|
m, errs := d.findRequiredFields(t, "", "")
|
||||||
|
for key, fields := range m {
|
||||||
|
if isEmptyFields(fields, src) {
|
||||||
|
errs[key] = EmptyFieldError{Key: key}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// findRequiredFields recursively searches the struct type t for required fields.
|
||||||
|
//
|
||||||
|
// canonicalPrefix and searchPrefix are used to resolve full paths in dotted notation
|
||||||
|
// for nested struct fields. canonicalPrefix is a complete path which never omits
|
||||||
|
// any embedded struct fields. searchPrefix is a user-friendly path which may omit
|
||||||
|
// some embedded struct fields to point promoted fields.
|
||||||
|
func (d *Decoder) findRequiredFields(t reflect.Type, canonicalPrefix, searchPrefix string) (map[string][]fieldWithPrefix, MultiError) {
|
||||||
|
struc := d.cache.get(t)
|
||||||
|
if struc == nil {
|
||||||
|
// unexpect, cache.get never return nil
|
||||||
|
return nil, MultiError{canonicalPrefix + "*": errors.New("cache fail")}
|
||||||
|
}
|
||||||
|
|
||||||
|
m := map[string][]fieldWithPrefix{}
|
||||||
|
errs := MultiError{}
|
||||||
|
for _, f := range struc.fields {
|
||||||
|
if f.typ.Kind() == reflect.Struct {
|
||||||
|
fcprefix := canonicalPrefix + f.canonicalAlias + "."
|
||||||
|
for _, fspath := range f.paths(searchPrefix) {
|
||||||
|
fm, ferrs := d.findRequiredFields(f.typ, fcprefix, fspath+".")
|
||||||
|
for key, fields := range fm {
|
||||||
|
m[key] = append(m[key], fields...)
|
||||||
|
}
|
||||||
|
errs.merge(ferrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if f.isRequired {
|
||||||
|
key := canonicalPrefix + f.canonicalAlias
|
||||||
|
m[key] = append(m[key], fieldWithPrefix{
|
||||||
|
fieldInfo: f,
|
||||||
|
prefix: searchPrefix,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
type fieldWithPrefix struct {
|
||||||
|
*fieldInfo
|
||||||
|
prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
// isEmptyFields returns true if all of specified fields are empty.
|
||||||
|
func isEmptyFields(fields []fieldWithPrefix, src map[string][]string) bool {
|
||||||
|
for _, f := range fields {
|
||||||
|
for _, path := range f.paths(f.prefix) {
|
||||||
|
v, ok := src[path]
|
||||||
|
if ok && !isEmpty(f.typ, v) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for key := range src {
|
||||||
|
// issue references:
|
||||||
|
// https://github.com/gofiber/fiber/issues/1414
|
||||||
|
// https://github.com/gorilla/schema/issues/176
|
||||||
|
nested := strings.IndexByte(key, '.') != -1
|
||||||
|
|
||||||
|
// for non required nested structs
|
||||||
|
c1 := strings.HasSuffix(f.prefix, ".") && key == path
|
||||||
|
|
||||||
|
// for required nested structs
|
||||||
|
c2 := f.prefix == "" && nested && strings.HasPrefix(key, path)
|
||||||
|
|
||||||
|
// for non nested fields
|
||||||
|
c3 := f.prefix == "" && !nested && key == path
|
||||||
|
if !isEmpty(f.typ, src[key]) && (c1 || c2 || c3) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// isEmpty returns true if value is empty for specific type
|
||||||
|
func isEmpty(t reflect.Type, value []string) bool {
|
||||||
|
if len(value) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
switch t.Kind() {
|
||||||
|
case boolType, float32Type, float64Type, intType, int8Type, int32Type, int64Type, stringType, uint8Type, uint16Type, uint32Type, uint64Type:
|
||||||
|
return len(value[0]) == 0
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode fills a struct field using a parsed path.
|
||||||
|
func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values []string) error {
|
||||||
|
// Get the field walking the struct fields by index.
|
||||||
|
for _, name := range parts[0].path {
|
||||||
|
if v.Type().Kind() == reflect.Ptr {
|
||||||
|
if v.IsNil() {
|
||||||
|
v.Set(reflect.New(v.Type().Elem()))
|
||||||
|
}
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// alloc embedded structs
|
||||||
|
if v.Type().Kind() == reflect.Struct {
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
field := v.Field(i)
|
||||||
|
if field.Type().Kind() == reflect.Ptr && field.IsNil() && v.Type().Field(i).Anonymous {
|
||||||
|
field.Set(reflect.New(field.Type().Elem()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v = v.FieldByName(name)
|
||||||
|
}
|
||||||
|
// Don't even bother for unexported fields.
|
||||||
|
if !v.CanSet() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dereference if needed.
|
||||||
|
t := v.Type()
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
if v.IsNil() {
|
||||||
|
v.Set(reflect.New(t))
|
||||||
|
}
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slice of structs. Let's go recursive.
|
||||||
|
if len(parts) > 1 {
|
||||||
|
idx := parts[0].index
|
||||||
|
if v.IsNil() || v.Len() < idx+1 {
|
||||||
|
value := reflect.MakeSlice(t, idx+1, idx+1)
|
||||||
|
if v.Len() < idx+1 {
|
||||||
|
// Resize it.
|
||||||
|
reflect.Copy(value, v)
|
||||||
|
}
|
||||||
|
v.Set(value)
|
||||||
|
}
|
||||||
|
return d.decode(v.Index(idx), path, parts[1:], values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the converter early in case there is one for a slice type.
|
||||||
|
conv := d.cache.converter(t)
|
||||||
|
m := isTextUnmarshaler(v)
|
||||||
|
if conv == nil && t.Kind() == reflect.Slice && m.IsSliceElement {
|
||||||
|
var items []reflect.Value
|
||||||
|
elemT := t.Elem()
|
||||||
|
isPtrElem := elemT.Kind() == reflect.Ptr
|
||||||
|
if isPtrElem {
|
||||||
|
elemT = elemT.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get a converter for the element type.
|
||||||
|
conv := d.cache.converter(elemT)
|
||||||
|
if conv == nil {
|
||||||
|
conv = builtinConverters[elemT.Kind()]
|
||||||
|
if conv == nil {
|
||||||
|
// As we are not dealing with slice of structs here, we don't need to check if the type
|
||||||
|
// implements TextUnmarshaler interface
|
||||||
|
return fmt.Errorf("schema: converter not found for %v", elemT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range values {
|
||||||
|
if value == "" {
|
||||||
|
if d.zeroEmpty {
|
||||||
|
items = append(items, reflect.Zero(elemT))
|
||||||
|
}
|
||||||
|
} else if m.IsValid {
|
||||||
|
u := reflect.New(elemT)
|
||||||
|
if m.IsSliceElementPtr {
|
||||||
|
u = reflect.New(reflect.PtrTo(elemT).Elem())
|
||||||
|
}
|
||||||
|
if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)); err != nil {
|
||||||
|
return ConversionError{
|
||||||
|
Key: path,
|
||||||
|
Type: t,
|
||||||
|
Index: key,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.IsSliceElementPtr {
|
||||||
|
items = append(items, u.Elem().Addr())
|
||||||
|
} else if u.Kind() == reflect.Ptr {
|
||||||
|
items = append(items, u.Elem())
|
||||||
|
} else {
|
||||||
|
items = append(items, u)
|
||||||
|
}
|
||||||
|
} else if item := conv(value); item.IsValid() {
|
||||||
|
if isPtrElem {
|
||||||
|
ptr := reflect.New(elemT)
|
||||||
|
ptr.Elem().Set(item)
|
||||||
|
item = ptr
|
||||||
|
}
|
||||||
|
if item.Type() != elemT && !isPtrElem {
|
||||||
|
item = item.Convert(elemT)
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
} else {
|
||||||
|
if strings.Contains(value, ",") {
|
||||||
|
values := strings.Split(value, ",")
|
||||||
|
for _, value := range values {
|
||||||
|
if value == "" {
|
||||||
|
if d.zeroEmpty {
|
||||||
|
items = append(items, reflect.Zero(elemT))
|
||||||
|
}
|
||||||
|
} else if item := conv(value); item.IsValid() {
|
||||||
|
if isPtrElem {
|
||||||
|
ptr := reflect.New(elemT)
|
||||||
|
ptr.Elem().Set(item)
|
||||||
|
item = ptr
|
||||||
|
}
|
||||||
|
if item.Type() != elemT && !isPtrElem {
|
||||||
|
item = item.Convert(elemT)
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
} else {
|
||||||
|
return ConversionError{
|
||||||
|
Key: path,
|
||||||
|
Type: elemT,
|
||||||
|
Index: key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ConversionError{
|
||||||
|
Key: path,
|
||||||
|
Type: elemT,
|
||||||
|
Index: key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value := reflect.Append(reflect.MakeSlice(t, 0, 0), items...)
|
||||||
|
v.Set(value)
|
||||||
|
} else {
|
||||||
|
val := ""
|
||||||
|
// Use the last value provided if any values were provided
|
||||||
|
if len(values) > 0 {
|
||||||
|
val = values[len(values)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if conv != nil {
|
||||||
|
if value := conv(val); value.IsValid() {
|
||||||
|
v.Set(value.Convert(t))
|
||||||
|
} else {
|
||||||
|
return ConversionError{
|
||||||
|
Key: path,
|
||||||
|
Type: t,
|
||||||
|
Index: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if m.IsValid {
|
||||||
|
if m.IsPtr {
|
||||||
|
u := reflect.New(v.Type())
|
||||||
|
if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(val)); err != nil {
|
||||||
|
return ConversionError{
|
||||||
|
Key: path,
|
||||||
|
Type: t,
|
||||||
|
Index: -1,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.Set(reflect.Indirect(u))
|
||||||
|
} else {
|
||||||
|
// If the value implements the encoding.TextUnmarshaler interface
|
||||||
|
// apply UnmarshalText as the converter
|
||||||
|
if err := m.Unmarshaler.UnmarshalText([]byte(val)); err != nil {
|
||||||
|
return ConversionError{
|
||||||
|
Key: path,
|
||||||
|
Type: t,
|
||||||
|
Index: -1,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if val == "" {
|
||||||
|
if d.zeroEmpty {
|
||||||
|
v.Set(reflect.Zero(t))
|
||||||
|
}
|
||||||
|
} else if conv := builtinConverters[t.Kind()]; conv != nil {
|
||||||
|
if value := conv(val); value.IsValid() {
|
||||||
|
v.Set(value.Convert(t))
|
||||||
|
} else {
|
||||||
|
return ConversionError{
|
||||||
|
Key: path,
|
||||||
|
Type: t,
|
||||||
|
Index: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("schema: converter not found for %v", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTextUnmarshaler(v reflect.Value) unmarshaler {
|
||||||
|
// Create a new unmarshaller instance
|
||||||
|
m := unmarshaler{}
|
||||||
|
if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
// As the UnmarshalText function should be applied to the pointer of the
|
||||||
|
// type, we check that type to see if it implements the necessary
|
||||||
|
// method.
|
||||||
|
if m.Unmarshaler, m.IsValid = reflect.New(v.Type()).Interface().(encoding.TextUnmarshaler); m.IsValid {
|
||||||
|
m.IsPtr = true
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// if v is []T or *[]T create new T
|
||||||
|
t := v.Type()
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
if t.Kind() == reflect.Slice {
|
||||||
|
// Check if the slice implements encoding.TextUnmarshaller
|
||||||
|
if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
// If t is a pointer slice, check if its elements implement
|
||||||
|
// encoding.TextUnmarshaler
|
||||||
|
m.IsSliceElement = true
|
||||||
|
if t = t.Elem(); t.Kind() == reflect.Ptr {
|
||||||
|
t = reflect.PtrTo(t.Elem())
|
||||||
|
v = reflect.Zero(t)
|
||||||
|
m.IsSliceElementPtr = true
|
||||||
|
m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v = reflect.New(t)
|
||||||
|
m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextUnmarshaler helpers ----------------------------------------------------
|
||||||
|
// unmarshaller contains information about a TextUnmarshaler type
|
||||||
|
type unmarshaler struct {
|
||||||
|
Unmarshaler encoding.TextUnmarshaler
|
||||||
|
// IsValid indicates whether the resolved type indicated by the other
|
||||||
|
// flags implements the encoding.TextUnmarshaler interface.
|
||||||
|
IsValid bool
|
||||||
|
// IsPtr indicates that the resolved type is the pointer of the original
|
||||||
|
// type.
|
||||||
|
IsPtr bool
|
||||||
|
// IsSliceElement indicates that the resolved type is a slice element of
|
||||||
|
// the original type.
|
||||||
|
IsSliceElement bool
|
||||||
|
// IsSliceElementPtr indicates that the resolved type is a pointer to a
|
||||||
|
// slice element of the original type.
|
||||||
|
IsSliceElementPtr bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ConversionError stores information about a failed conversion.
|
||||||
|
type ConversionError struct {
|
||||||
|
Key string // key from the source map.
|
||||||
|
Type reflect.Type // expected type of elem
|
||||||
|
Index int // index for multi-value fields; -1 for single-value fields.
|
||||||
|
Err error // low-level error (when it exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ConversionError) Error() string {
|
||||||
|
var output string
|
||||||
|
|
||||||
|
if e.Index < 0 {
|
||||||
|
output = fmt.Sprintf("schema: error converting value for %q", e.Key)
|
||||||
|
} else {
|
||||||
|
output = fmt.Sprintf("schema: error converting value for index %d of %q",
|
||||||
|
e.Index, e.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Err != nil {
|
||||||
|
output = fmt.Sprintf("%s. Details: %s", output, e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnknownKeyError stores information about an unknown key in the source map.
|
||||||
|
type UnknownKeyError struct {
|
||||||
|
Key string // key from the source map.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e UnknownKeyError) Error() string {
|
||||||
|
return fmt.Sprintf("schema: invalid path %q", e.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmptyFieldError stores information about an empty required field.
|
||||||
|
type EmptyFieldError struct {
|
||||||
|
Key string // required key in the source map.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e EmptyFieldError) Error() string {
|
||||||
|
return fmt.Sprintf("%v is empty", e.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiError stores multiple decoding errors.
|
||||||
|
//
|
||||||
|
// Borrowed from the App Engine SDK.
|
||||||
|
type MultiError map[string]error
|
||||||
|
|
||||||
|
func (e MultiError) Error() string {
|
||||||
|
s := ""
|
||||||
|
for _, err := range e {
|
||||||
|
s = err.Error()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch len(e) {
|
||||||
|
case 0:
|
||||||
|
return "(0 errors)"
|
||||||
|
case 1:
|
||||||
|
return s
|
||||||
|
case 2:
|
||||||
|
return s + " (and 1 other error)"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s (and %d other errors)", s, len(e)-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e MultiError) merge(errors MultiError) {
|
||||||
|
for key, err := range errors {
|
||||||
|
if e[key] == nil {
|
||||||
|
e[key] = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
148
internal/schema/doc.go
Normal file
148
internal/schema/doc.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package gorilla/schema fills a struct with form values.
|
||||||
|
|
||||||
|
The basic usage is really simple. Given this struct:
|
||||||
|
|
||||||
|
type Person struct {
|
||||||
|
Name string
|
||||||
|
Phone string
|
||||||
|
}
|
||||||
|
|
||||||
|
...we can fill it passing a map to the Decode() function:
|
||||||
|
|
||||||
|
values := map[string][]string{
|
||||||
|
"Name": {"John"},
|
||||||
|
"Phone": {"999-999-999"},
|
||||||
|
}
|
||||||
|
person := new(Person)
|
||||||
|
decoder := schema.NewDecoder()
|
||||||
|
decoder.Decode(person, values)
|
||||||
|
|
||||||
|
This is just a simple example and it doesn't make a lot of sense to create
|
||||||
|
the map manually. Typically it will come from a http.Request object and
|
||||||
|
will be of type url.Values, http.Request.Form, or http.Request.MultipartForm:
|
||||||
|
|
||||||
|
func MyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := r.ParseForm()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := schema.NewDecoder()
|
||||||
|
// r.PostForm is a map of our POST form values
|
||||||
|
err := decoder.Decode(person, r.PostForm)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do something with person.Name or person.Phone
|
||||||
|
}
|
||||||
|
|
||||||
|
Note: it is a good idea to set a Decoder instance as a package global,
|
||||||
|
because it caches meta-data about structs, and an instance can be shared safely:
|
||||||
|
|
||||||
|
var decoder = schema.NewDecoder()
|
||||||
|
|
||||||
|
To define custom names for fields, use a struct tag "schema". To not populate
|
||||||
|
certain fields, use a dash for the name and it will be ignored:
|
||||||
|
|
||||||
|
type Person struct {
|
||||||
|
Name string `schema:"name"` // custom name
|
||||||
|
Phone string `schema:"phone"` // custom name
|
||||||
|
Admin bool `schema:"-"` // this field is never set
|
||||||
|
}
|
||||||
|
|
||||||
|
The supported field types in the destination struct are:
|
||||||
|
|
||||||
|
- bool
|
||||||
|
- float variants (float32, float64)
|
||||||
|
- int variants (int, int8, int16, int32, int64)
|
||||||
|
- string
|
||||||
|
- uint variants (uint, uint8, uint16, uint32, uint64)
|
||||||
|
- struct
|
||||||
|
- a pointer to one of the above types
|
||||||
|
- a slice or a pointer to a slice of one of the above types
|
||||||
|
|
||||||
|
Non-supported types are simply ignored, however custom types can be registered
|
||||||
|
to be converted.
|
||||||
|
|
||||||
|
To fill nested structs, keys must use a dotted notation as the "path" for the
|
||||||
|
field. So for example, to fill the struct Person below:
|
||||||
|
|
||||||
|
type Phone struct {
|
||||||
|
Label string
|
||||||
|
Number string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Person struct {
|
||||||
|
Name string
|
||||||
|
Phone Phone
|
||||||
|
}
|
||||||
|
|
||||||
|
...the source map must have the keys "Name", "Phone.Label" and "Phone.Number".
|
||||||
|
This means that an HTML form to fill a Person struct must look like this:
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<input type="text" name="Name">
|
||||||
|
<input type="text" name="Phone.Label">
|
||||||
|
<input type="text" name="Phone.Number">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
Single values are filled using the first value for a key from the source map.
|
||||||
|
Slices are filled using all values for a key from the source map. So to fill
|
||||||
|
a Person with multiple Phone values, like:
|
||||||
|
|
||||||
|
type Person struct {
|
||||||
|
Name string
|
||||||
|
Phones []Phone
|
||||||
|
}
|
||||||
|
|
||||||
|
...an HTML form that accepts three Phone values would look like this:
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<input type="text" name="Name">
|
||||||
|
<input type="text" name="Phones.0.Label">
|
||||||
|
<input type="text" name="Phones.0.Number">
|
||||||
|
<input type="text" name="Phones.1.Label">
|
||||||
|
<input type="text" name="Phones.1.Number">
|
||||||
|
<input type="text" name="Phones.2.Label">
|
||||||
|
<input type="text" name="Phones.2.Number">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
Notice that only for slices of structs the slice index is required.
|
||||||
|
This is needed for disambiguation: if the nested struct also had a slice
|
||||||
|
field, we could not translate multiple values to it if we did not use an
|
||||||
|
index for the parent struct.
|
||||||
|
|
||||||
|
There's also the possibility to create a custom type that implements the
|
||||||
|
TextUnmarshaler interface, and in this case there's no need to register
|
||||||
|
a converter, like:
|
||||||
|
|
||||||
|
type Person struct {
|
||||||
|
Emails []Email
|
||||||
|
}
|
||||||
|
|
||||||
|
type Email struct {
|
||||||
|
*mail.Address
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Email) UnmarshalText(text []byte) (err error) {
|
||||||
|
e.Address, err = mail.ParseAddress(string(text))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
...an HTML form that accepts three Email values would look like this:
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<input type="email" name="Emails.0">
|
||||||
|
<input type="email" name="Emails.1">
|
||||||
|
<input type="email" name="Emails.2">
|
||||||
|
</form>
|
||||||
|
*/
|
||||||
|
package schema
|
202
internal/schema/encoder.go
Normal file
202
internal/schema/encoder.go
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type encoderFunc func(reflect.Value) string
|
||||||
|
|
||||||
|
// Encoder encodes values from a struct into url.Values.
|
||||||
|
type Encoder struct {
|
||||||
|
cache *cache
|
||||||
|
regenc map[reflect.Type]encoderFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEncoder returns a new Encoder with defaults.
|
||||||
|
func NewEncoder() *Encoder {
|
||||||
|
return &Encoder{cache: newCache(), regenc: make(map[reflect.Type]encoderFunc)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode encodes a struct into map[string][]string.
|
||||||
|
//
|
||||||
|
// Intended for use with url.Values.
|
||||||
|
func (e *Encoder) Encode(src interface{}, dst map[string][]string) error {
|
||||||
|
v := reflect.ValueOf(src)
|
||||||
|
|
||||||
|
return e.encode(v, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterEncoder registers a converter for encoding a custom type.
|
||||||
|
func (e *Encoder) RegisterEncoder(value interface{}, encoder func(reflect.Value) string) {
|
||||||
|
e.regenc[reflect.TypeOf(value)] = encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAliasTag changes the tag used to locate custom field aliases.
|
||||||
|
// The default tag is "schema".
|
||||||
|
func (e *Encoder) SetAliasTag(tag string) {
|
||||||
|
e.cache.tag = tag
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidStructPointer test if input value is a valid struct pointer.
|
||||||
|
func isValidStructPointer(v reflect.Value) bool {
|
||||||
|
return v.Type().Kind() == reflect.Ptr && v.Elem().IsValid() && v.Elem().Type().Kind() == reflect.Struct
|
||||||
|
}
|
||||||
|
|
||||||
|
func isZero(v reflect.Value) bool {
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Func:
|
||||||
|
case reflect.Map, reflect.Slice:
|
||||||
|
return v.IsNil() || v.Len() == 0
|
||||||
|
case reflect.Array:
|
||||||
|
z := true
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
z = z && isZero(v.Index(i))
|
||||||
|
}
|
||||||
|
return z
|
||||||
|
case reflect.Struct:
|
||||||
|
type zero interface {
|
||||||
|
IsZero() bool
|
||||||
|
}
|
||||||
|
if v.Type().Implements(reflect.TypeOf((*zero)(nil)).Elem()) {
|
||||||
|
iz := v.MethodByName("IsZero").Call([]reflect.Value{})[0]
|
||||||
|
return iz.Interface().(bool)
|
||||||
|
}
|
||||||
|
z := true
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
z = z && isZero(v.Field(i))
|
||||||
|
}
|
||||||
|
return z
|
||||||
|
}
|
||||||
|
// Compare other types directly:
|
||||||
|
z := reflect.Zero(v.Type())
|
||||||
|
return v.Interface() == z.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error {
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
if v.Kind() != reflect.Struct {
|
||||||
|
return errors.New("schema: interface must be a struct")
|
||||||
|
}
|
||||||
|
t := v.Type()
|
||||||
|
|
||||||
|
errors := MultiError{}
|
||||||
|
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
name, opts := fieldAlias(t.Field(i), e.cache.tag)
|
||||||
|
if name == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode struct pointer types if the field is a valid pointer and a struct.
|
||||||
|
if isValidStructPointer(v.Field(i)) {
|
||||||
|
_ = e.encode(v.Field(i).Elem(), dst)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
encFunc := typeEncoder(v.Field(i).Type(), e.regenc)
|
||||||
|
|
||||||
|
// Encode non-slice types and custom implementations immediately.
|
||||||
|
if encFunc != nil {
|
||||||
|
value := encFunc(v.Field(i))
|
||||||
|
if opts.Contains("omitempty") && isZero(v.Field(i)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dst[name] = append(dst[name], value)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Field(i).Type().Kind() == reflect.Struct {
|
||||||
|
_ = e.encode(v.Field(i), dst)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Field(i).Type().Kind() == reflect.Slice {
|
||||||
|
encFunc = typeEncoder(v.Field(i).Type().Elem(), e.regenc)
|
||||||
|
}
|
||||||
|
|
||||||
|
if encFunc == nil {
|
||||||
|
errors[v.Field(i).Type().String()] = fmt.Errorf("schema: encoder not found for %v", v.Field(i))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode a slice.
|
||||||
|
if v.Field(i).Len() == 0 && opts.Contains("omitempty") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dst[name] = []string{}
|
||||||
|
for j := 0; j < v.Field(i).Len(); j++ {
|
||||||
|
dst[name] = append(dst[name], encFunc(v.Field(i).Index(j)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func typeEncoder(t reflect.Type, reg map[reflect.Type]encoderFunc) encoderFunc {
|
||||||
|
if f, ok := reg[t]; ok {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
return encodeBool
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return encodeInt
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
return encodeUint
|
||||||
|
case reflect.Float32:
|
||||||
|
return encodeFloat32
|
||||||
|
case reflect.Float64:
|
||||||
|
return encodeFloat64
|
||||||
|
case reflect.Ptr:
|
||||||
|
f := typeEncoder(t.Elem(), reg)
|
||||||
|
return func(v reflect.Value) string {
|
||||||
|
if v.IsNil() {
|
||||||
|
return "null"
|
||||||
|
}
|
||||||
|
return f(v.Elem())
|
||||||
|
}
|
||||||
|
case reflect.String:
|
||||||
|
return encodeString
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeBool(v reflect.Value) string {
|
||||||
|
return strconv.FormatBool(v.Bool())
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeInt(v reflect.Value) string {
|
||||||
|
return strconv.FormatInt(int64(v.Int()), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeUint(v reflect.Value) string {
|
||||||
|
return strconv.FormatUint(uint64(v.Uint()), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeFloat(v reflect.Value, bits int) string {
|
||||||
|
return strconv.FormatFloat(v.Float(), 'f', 6, bits)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeFloat32(v reflect.Value) string {
|
||||||
|
return encodeFloat(v, 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeFloat64(v reflect.Value) string {
|
||||||
|
return encodeFloat(v, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeString(v reflect.Value) string {
|
||||||
|
return v.String()
|
||||||
|
}
|
106
internal/sse/sse-encoder.go
Normal file
106
internal/sse/sse-encoder.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package sse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Server-Sent Events
|
||||||
|
// W3C Working Draft 29 October 2009
|
||||||
|
// http://www.w3.org/TR/2009/WD-eventsource-20091029/
|
||||||
|
|
||||||
|
const ContentType = "text/event-stream"
|
||||||
|
|
||||||
|
var contentType = []string{ContentType}
|
||||||
|
var noCache = []string{"no-cache"}
|
||||||
|
|
||||||
|
var fieldReplacer = strings.NewReplacer(
|
||||||
|
"\n", "\\n",
|
||||||
|
"\r", "\\r")
|
||||||
|
|
||||||
|
var dataReplacer = strings.NewReplacer(
|
||||||
|
"\n", "\ndata:",
|
||||||
|
"\r", "\\r")
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Event string
|
||||||
|
Id string
|
||||||
|
Retry uint
|
||||||
|
Data interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Encode(writer io.Writer, event Event) error {
|
||||||
|
w := checkWriter(writer)
|
||||||
|
writeId(w, event.Id)
|
||||||
|
writeEvent(w, event.Event)
|
||||||
|
writeRetry(w, event.Retry)
|
||||||
|
return writeData(w, event.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeId(w stringWriter, id string) {
|
||||||
|
if len(id) > 0 {
|
||||||
|
w.WriteString("id:")
|
||||||
|
fieldReplacer.WriteString(w, id)
|
||||||
|
w.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeEvent(w stringWriter, event string) {
|
||||||
|
if len(event) > 0 {
|
||||||
|
w.WriteString("event:")
|
||||||
|
fieldReplacer.WriteString(w, event)
|
||||||
|
w.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeRetry(w stringWriter, retry uint) {
|
||||||
|
if retry > 0 {
|
||||||
|
w.WriteString("retry:")
|
||||||
|
w.WriteString(strconv.FormatUint(uint64(retry), 10))
|
||||||
|
w.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeData(w stringWriter, data interface{}) error {
|
||||||
|
w.WriteString("data:")
|
||||||
|
switch kindOfData(data) {
|
||||||
|
case reflect.Struct, reflect.Slice, reflect.Map:
|
||||||
|
err := json.NewEncoder(w).Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.WriteString("\n")
|
||||||
|
default:
|
||||||
|
dataReplacer.WriteString(w, fmt.Sprint(data))
|
||||||
|
w.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Event) Render(w http.ResponseWriter) error {
|
||||||
|
r.WriteContentType(w)
|
||||||
|
return Encode(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Event) WriteContentType(w http.ResponseWriter) {
|
||||||
|
header := w.Header()
|
||||||
|
header["Content-Type"] = contentType
|
||||||
|
|
||||||
|
if _, exist := header["Cache-Control"]; !exist {
|
||||||
|
header["Cache-Control"] = noCache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func kindOfData(data interface{}) reflect.Kind {
|
||||||
|
value := reflect.ValueOf(data)
|
||||||
|
valueType := value.Kind()
|
||||||
|
if valueType == reflect.Ptr {
|
||||||
|
valueType = value.Elem().Kind()
|
||||||
|
}
|
||||||
|
return valueType
|
||||||
|
}
|
24
internal/sse/writer.go
Normal file
24
internal/sse/writer.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package sse
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
type stringWriter interface {
|
||||||
|
io.Writer
|
||||||
|
WriteString(string) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type stringWrapper struct {
|
||||||
|
io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w stringWrapper) WriteString(str string) (int, error) {
|
||||||
|
return w.Writer.Write([]byte(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkWriter(writer io.Writer) stringWriter {
|
||||||
|
if w, ok := writer.(stringWriter); ok {
|
||||||
|
return w
|
||||||
|
} else {
|
||||||
|
return stringWrapper{writer}
|
||||||
|
}
|
||||||
|
}
|
57
middleware.go
Normal file
57
middleware.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package nf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime/debug"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/loveuer/nf/nft/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRecover(enableStackTrace bool) HandlerFunc {
|
||||||
|
return func(c *Ctx) error {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
if enableStackTrace {
|
||||||
|
os.Stderr.WriteString(fmt.Sprintf("recovered from panic: %v\n%s\n", r, debug.Stack()))
|
||||||
|
} else {
|
||||||
|
os.Stderr.WriteString(fmt.Sprintf("recovered from panic: %v\n", r))
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveError(c, 500, []byte(fmt.Sprint(r)))
|
||||||
|
_ = c.Status(500).SendString(fmt.Sprint(r))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogger() HandlerFunc {
|
||||||
|
return func(c *Ctx) error {
|
||||||
|
var (
|
||||||
|
now = time.Now()
|
||||||
|
logFn func(msg string, data ...any)
|
||||||
|
ip = c.IP()
|
||||||
|
)
|
||||||
|
|
||||||
|
err := c.Next()
|
||||||
|
duration := time.Since(now)
|
||||||
|
|
||||||
|
msg := fmt.Sprintf("NF | %v | %15s | %3d | %s | %6s | %s", c.Context().Value(TraceKey), ip, c.StatusCode, HumanDuration(duration.Nanoseconds()), c.Method(), c.Path())
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case c.StatusCode >= 500:
|
||||||
|
logFn = log.Error
|
||||||
|
case c.StatusCode >= 400:
|
||||||
|
logFn = log.Warn
|
||||||
|
default:
|
||||||
|
logFn = log.Info
|
||||||
|
}
|
||||||
|
|
||||||
|
logFn(msg)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
86
nf.go
86
nf.go
@ -1,19 +1,99 @@
|
|||||||
package nf
|
package nf
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
banner = " _ _ _ ___ _ \n | \\| |___| |_ | __|__ _ _ _ _ __| |\n | .` / _ \\ _| | _/ _ \\ || | ' \\/ _` |\n |_|\\_\\___/\\__| |_|\\___/\\_,_|_||_\\__,_|\n "
|
banner = " _ _ _ ___ _ \n | \\| |___| |_ | __|__ _ _ _ _ __| |\n | .` / _ \\ _| | _/ _ \\ || | ' \\/ _` |\n |_|\\_\\___/\\__| |_|\\___/\\_,_|_||_\\__,_|\n "
|
||||||
|
_404 = "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1\"><meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"><title>Not Found</title><style>body{background:#333;margin:0;color:#ccc;display:flex;align-items:center;max-height:100vh;height:100vh;justify-content:center}textarea{min-height:5rem;min-width:20rem;text-align:center;border:none;background:0 0;color:#ccc;resize:none;user-input:none;user-select:none;cursor:default;-webkit-user-select:none;-webkit-touch-callout:none;-moz-user-select:none;-ms-user-select:none;outline:0}</style></head><body><textarea id=\"banner\" readonly=\"readonly\"></textarea><script type=\"text/javascript\">let htmlCodes = [\n ' _ _ _ ___ _ ',\n '| \\\\| |___| |_ | __|__ _ _ _ _ __| |',\n '| .` / _ \\\\ _| | _/ _ \\\\ || | \\' \\\\/ _` |',\n '|_|\\\\_\\\\___/\\\\__| |_|\\\\___/\\\\_,_|_||_\\\\__,_|'\n].join('\\n');\ndocument.querySelector('#banner').value = htmlCodes</script></body></html>"
|
||||||
|
_405 = `405 Method Not Allowed`
|
||||||
|
_500 = `500 Internal Server Error`
|
||||||
|
TraceKey = "X-Trace-Id"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Map map[string]interface{}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
DisableMessagePrint bool `json:"-"`
|
||||||
|
// Default: 4 * 1024 * 1024
|
||||||
|
BodyLimit int64 `json:"-"`
|
||||||
|
|
||||||
|
// if report http.ErrServerClosed as run err
|
||||||
|
ErrServeClose bool `json:"-"`
|
||||||
|
|
||||||
|
DisableBanner bool `json:"-"`
|
||||||
|
DisableLogger bool `json:"-"`
|
||||||
|
DisableRecover bool `json:"-"`
|
||||||
|
DisableHttpErrorLog bool `json:"-"`
|
||||||
|
|
||||||
|
// EnableNotImplementHandler bool `json:"-"`
|
||||||
|
NotFoundHandler HandlerFunc `json:"-"`
|
||||||
|
MethodNotAllowedHandler HandlerFunc `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultConfig = &Config{
|
||||||
|
BodyLimit: 4 * 1024 * 1024,
|
||||||
|
NotFoundHandler: func(c *Ctx) error {
|
||||||
|
c.Set("Content-Type", MIMETextHTML)
|
||||||
|
_, err := c.Status(404).Write([]byte(_404))
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
MethodNotAllowedHandler: func(c *Ctx) error {
|
||||||
|
c.Set("Content-Type", MIMETextPlain)
|
||||||
|
_, err := c.Status(405).Write([]byte(_405))
|
||||||
|
return err
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(config ...Config) *App {
|
func New(config ...Config) *App {
|
||||||
app := &App{
|
app := &App{
|
||||||
router: newRouter(),
|
RouterGroup: RouterGroup{
|
||||||
|
Handlers: nil,
|
||||||
|
basePath: "/",
|
||||||
|
root: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
pool: &sync.Pool{},
|
||||||
|
|
||||||
|
redirectTrailingSlash: true, // true
|
||||||
|
redirectFixedPath: false, // false
|
||||||
|
handleMethodNotAllowed: true, // false
|
||||||
|
useRawPath: false, // false
|
||||||
|
unescapePathValues: true, // true
|
||||||
|
removeExtraSlash: false, // false
|
||||||
}
|
}
|
||||||
|
|
||||||
app.RouterGroup = &RouterGroup{app: app}
|
if len(config) > 0 {
|
||||||
app.groups = []*RouterGroup{app.RouterGroup}
|
app.config = &config[0]
|
||||||
|
|
||||||
|
if app.config.BodyLimit == 0 {
|
||||||
|
app.config.BodyLimit = defaultConfig.BodyLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.config.NotFoundHandler == nil {
|
||||||
|
app.config.NotFoundHandler = defaultConfig.NotFoundHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.config.MethodNotAllowedHandler == nil {
|
||||||
|
app.config.MethodNotAllowedHandler = defaultConfig.MethodNotAllowedHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
app.config = defaultConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
app.RouterGroup.app = app
|
||||||
|
|
||||||
|
if !app.config.DisableLogger {
|
||||||
|
app.Use(NewLogger())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !app.config.DisableRecover {
|
||||||
|
app.Use(NewRecover(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
app.pool.New = func() any {
|
||||||
|
return app.allocateContext()
|
||||||
|
}
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
123
nft/loading/loading.go
Normal file
123
nft/loading/loading.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
package loading
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Type int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeProcessing Type = iota
|
||||||
|
TypeInfo
|
||||||
|
TypeSuccess
|
||||||
|
TypeWarning
|
||||||
|
TypeError
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t Type) Symbol() string {
|
||||||
|
switch t {
|
||||||
|
case TypeSuccess:
|
||||||
|
return "✔️ "
|
||||||
|
case TypeWarning:
|
||||||
|
return "❗ "
|
||||||
|
case TypeError:
|
||||||
|
return "❌ "
|
||||||
|
case TypeInfo:
|
||||||
|
return "❕ "
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type _msg struct {
|
||||||
|
msg string
|
||||||
|
t Type
|
||||||
|
}
|
||||||
|
|
||||||
|
var frames = []string{"|", "/", "-", "\\"}
|
||||||
|
|
||||||
|
func Do(ctx context.Context, fn func(ctx context.Context, print func(msg string, types ...Type)) error) (err error) {
|
||||||
|
start := time.Now()
|
||||||
|
ch := make(chan *_msg)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
fmt.Printf("\r\033[K")
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var (
|
||||||
|
m *_msg
|
||||||
|
ok bool
|
||||||
|
processing string
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
for _, frame := range frames {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case m, ok = <-ch:
|
||||||
|
if !ok || m == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch m.t {
|
||||||
|
case TypeProcessing:
|
||||||
|
if m.msg != "" {
|
||||||
|
processing = m.msg
|
||||||
|
}
|
||||||
|
case TypeInfo,
|
||||||
|
TypeSuccess,
|
||||||
|
TypeWarning,
|
||||||
|
TypeError:
|
||||||
|
// Clear the loading animation
|
||||||
|
fmt.Printf("\r\033[K")
|
||||||
|
fmt.Printf("%s%s\n", m.t.Symbol(), m.msg)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
elapsed := time.Since(start).Seconds()
|
||||||
|
if processing != "" {
|
||||||
|
fmt.Printf("\r\033[K%s %s (%.2fs)", frame, processing, elapsed)
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
printFn := func(msg string, types ...Type) {
|
||||||
|
if msg == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m := &_msg{
|
||||||
|
msg: msg,
|
||||||
|
t: TypeProcessing,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(types) > 0 {
|
||||||
|
m.t = types[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
ch <- m
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
if err = fn(ctx, printFn); err != nil {
|
||||||
|
ch <- &_msg{msg: err.Error(), t: TypeError}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(ch)
|
||||||
|
done <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
case <-done:
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
25
nft/loading/loading_test.go
Normal file
25
nft/loading/loading_test.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package loading
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadingPrint(t *testing.T) {
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
Do(ctx, func(ctx context.Context, print func(msg string, types ...Type)) error {
|
||||||
|
print("start task 1...")
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
print("warning...1", TypeWarning)
|
||||||
|
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
67
nft/log/default.go
Normal file
67
nft/log/default.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
nilLogger = func(prefix, timestamp, msg string, data ...any) {}
|
||||||
|
normalLogger = func(prefix, timestamp, msg string, data ...any) {
|
||||||
|
fmt.Printf(prefix+"| "+timestamp+" | "+msg+"\n", data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
panicLogger = func(prefix, timestamp, msg string, data ...any) {
|
||||||
|
panic(fmt.Sprintf(prefix+"| "+timestamp+" | "+msg+"\n", data...))
|
||||||
|
}
|
||||||
|
|
||||||
|
fatalLogger = func(prefix, timestamp, msg string, data ...any) {
|
||||||
|
fmt.Printf(prefix+"| "+timestamp+" | "+msg+"\n", data...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultLogger = &logger{
|
||||||
|
Mutex: sync.Mutex{},
|
||||||
|
timeFormat: "2006-01-02T15:04:05",
|
||||||
|
writer: os.Stdout,
|
||||||
|
level: LogLevelInfo,
|
||||||
|
debug: nilLogger,
|
||||||
|
info: normalLogger,
|
||||||
|
warn: normalLogger,
|
||||||
|
error: normalLogger,
|
||||||
|
panic: panicLogger,
|
||||||
|
fatal: fatalLogger,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetTimeFormat(format string) {
|
||||||
|
DefaultLogger.SetTimeFormat(format)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetLogLevel(level LogLevel) {
|
||||||
|
DefaultLogger.SetLogLevel(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debug(msg string, data ...any) {
|
||||||
|
DefaultLogger.Debug(msg, data...)
|
||||||
|
}
|
||||||
|
func Info(msg string, data ...any) {
|
||||||
|
DefaultLogger.Info(msg, data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Warn(msg string, data ...any) {
|
||||||
|
DefaultLogger.Warn(msg, data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(msg string, data ...any) {
|
||||||
|
DefaultLogger.Error(msg, data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Panic(msg string, data ...any) {
|
||||||
|
DefaultLogger.Panic(msg, data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fatal(msg string, data ...any) {
|
||||||
|
DefaultLogger.Fatal(msg, data...)
|
||||||
|
}
|
115
nft/log/log.go
Normal file
115
nft/log/log.go
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LogLevel uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
LogLevelDebug = iota
|
||||||
|
LogLevelInfo
|
||||||
|
LogLevelWarn
|
||||||
|
LogLevelError
|
||||||
|
LogLevelPanic
|
||||||
|
LogLevelFatal
|
||||||
|
)
|
||||||
|
|
||||||
|
type logger struct {
|
||||||
|
sync.Mutex
|
||||||
|
timeFormat string
|
||||||
|
writer io.Writer
|
||||||
|
level LogLevel
|
||||||
|
debug func(prefix, timestamp, msg string, data ...any)
|
||||||
|
info func(prefix, timestamp, msg string, data ...any)
|
||||||
|
warn func(prefix, timestamp, msg string, data ...any)
|
||||||
|
error func(prefix, timestamp, msg string, data ...any)
|
||||||
|
panic func(prefix, timestamp, msg string, data ...any)
|
||||||
|
fatal func(prefix, timestamp, msg string, data ...any)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
red = color.New(color.FgRed)
|
||||||
|
hired = color.New(color.FgHiRed)
|
||||||
|
green = color.New(color.FgGreen)
|
||||||
|
yellow = color.New(color.FgYellow)
|
||||||
|
white = color.New(color.FgWhite)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l *logger) SetTimeFormat(format string) {
|
||||||
|
l.Lock()
|
||||||
|
defer l.Unlock()
|
||||||
|
l.timeFormat = format
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) SetLogLevel(level LogLevel) {
|
||||||
|
l.Lock()
|
||||||
|
defer l.Unlock()
|
||||||
|
|
||||||
|
if level > LogLevelDebug {
|
||||||
|
l.debug = nilLogger
|
||||||
|
} else {
|
||||||
|
l.debug = normalLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
if level > LogLevelInfo {
|
||||||
|
l.info = nilLogger
|
||||||
|
} else {
|
||||||
|
l.info = normalLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
if level > LogLevelWarn {
|
||||||
|
l.warn = nilLogger
|
||||||
|
} else {
|
||||||
|
l.warn = normalLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
if level > LogLevelError {
|
||||||
|
l.error = nilLogger
|
||||||
|
} else {
|
||||||
|
l.error = normalLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
if level > LogLevelPanic {
|
||||||
|
l.panic = nilLogger
|
||||||
|
} else {
|
||||||
|
l.panic = panicLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
if level > LogLevelFatal {
|
||||||
|
l.fatal = nilLogger
|
||||||
|
} else {
|
||||||
|
l.fatal = fatalLogger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Debug(msg string, data ...any) {
|
||||||
|
l.debug(white.Sprint("Debug "), time.Now().Format(l.timeFormat), msg, data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Info(msg string, data ...any) {
|
||||||
|
l.info(green.Sprint("Info "), time.Now().Format(l.timeFormat), msg, data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Warn(msg string, data ...any) {
|
||||||
|
l.warn(yellow.Sprint("Warn "), time.Now().Format(l.timeFormat), msg, data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Error(msg string, data ...any) {
|
||||||
|
l.error(red.Sprint("Error "), time.Now().Format(l.timeFormat), msg, data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Panic(msg string, data ...any) {
|
||||||
|
l.panic(hired.Sprint("Panic "), time.Now().Format(l.timeFormat), msg, data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Fatal(msg string, data ...any) {
|
||||||
|
l.fatal(hired.Sprint("Fatal "), time.Now().Format(l.timeFormat), msg, data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WroteLogger interface {
|
||||||
|
Info(msg string, data ...any)
|
||||||
|
}
|
21
nft/log/new.go
Normal file
21
nft/log/new.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New() *logger {
|
||||||
|
return &logger{
|
||||||
|
Mutex: sync.Mutex{},
|
||||||
|
timeFormat: "2006-01-02T15:04:05",
|
||||||
|
writer: os.Stdout,
|
||||||
|
level: LogLevelInfo,
|
||||||
|
debug: nilLogger,
|
||||||
|
info: normalLogger,
|
||||||
|
warn: normalLogger,
|
||||||
|
error: normalLogger,
|
||||||
|
panic: panicLogger,
|
||||||
|
fatal: fatalLogger,
|
||||||
|
}
|
||||||
|
}
|
149
nft/nfctl/internal/cmd/cmd.new.go
Normal file
149
nft/nfctl/internal/cmd/cmd.new.go
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/loveuer/nf/nft/loading"
|
||||||
|
"github.com/loveuer/nf/nft/log"
|
||||||
|
"github.com/loveuer/nf/nft/nfctl/internal/opt"
|
||||||
|
"github.com/loveuer/nf/nft/tool"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var newCmd = &cobra.Command{
|
||||||
|
Use: "new",
|
||||||
|
Short: "new a nf project",
|
||||||
|
Example: "nfctl new <project> -t ultone [options]",
|
||||||
|
RunE: doNew,
|
||||||
|
SilenceErrors: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func initNew() *cobra.Command {
|
||||||
|
newCmd.Flags().StringVarP(&opt.Cfg.New.Template, "template", "t", "ultone", "template name/url[example:ultone, https://gitea.loveuer.com/loveuer/ultone.git]")
|
||||||
|
newCmd.Flags().BoolVar(&opt.Cfg.New.DisableInitScript, "disable-init-script", false, "disable init script(.nfctl)")
|
||||||
|
return newCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func doNew(cmd *cobra.Command, args []string) (err error) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return errors.New("必须提供 project 名称")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(args[0], "/") {
|
||||||
|
return errors.New("project 名称不能以 / 结尾")
|
||||||
|
}
|
||||||
|
|
||||||
|
base := path.Base(args[0])
|
||||||
|
if strings.HasPrefix(base, ".") {
|
||||||
|
return errors.New("project 名称不能以 . 开头")
|
||||||
|
}
|
||||||
|
|
||||||
|
return loading.Do(cmd.Context(), func(ctx context.Context, print func(msg string, types ...loading.Type)) error {
|
||||||
|
print("开始新建项目: "+args[0], loading.TypeInfo)
|
||||||
|
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
moduleName := args[0]
|
||||||
|
pwd = path.Join(filepath.ToSlash(pwd), base)
|
||||||
|
|
||||||
|
log.Debug("cmd.new: new project, pwd = %s, name = %s, template = %s", pwd, moduleName, opt.Cfg.New.Template)
|
||||||
|
|
||||||
|
print("开始下载模板: "+opt.Cfg.New.Template, loading.TypeProcessing)
|
||||||
|
|
||||||
|
repo := opt.Cfg.New.Template
|
||||||
|
if v, ok := opt.TemplateMap[repo]; ok {
|
||||||
|
repo = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tool.Clone(pwd, repo); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
print("下载模板完成: "+opt.Cfg.New.Template, loading.TypeSuccess)
|
||||||
|
|
||||||
|
if err = os.RemoveAll(path.Join(pwd, ".git")); err != nil {
|
||||||
|
print(err.Error(), loading.TypeWarning)
|
||||||
|
}
|
||||||
|
|
||||||
|
print("开始初始化项目: "+args[0], loading.TypeProcessing)
|
||||||
|
|
||||||
|
if err = filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "go.mod") {
|
||||||
|
var content []byte
|
||||||
|
if content, err = os.ReadFile(path); err != nil {
|
||||||
|
print("初始化文件失败: "+err.Error(), loading.TypeWarning)
|
||||||
|
print("开始初始化项目: "+args[0], loading.TypeProcessing)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(bytes.NewReader(content))
|
||||||
|
replaced := make([]string, 0, 16)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
// 操作 go.mod 文件时, 忽略 toolchain 行, 以更好的兼容 go1.20
|
||||||
|
if strings.HasSuffix(path, "go.mod") && strings.HasPrefix(line, "toolchain") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
replaced = append(replaced, strings.ReplaceAll(line, opt.Cfg.New.Template, moduleName))
|
||||||
|
}
|
||||||
|
if err = os.WriteFile(path, []byte(strings.Join(replaced, "\n")), 0o644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
render *template.Template
|
||||||
|
rf *os.File
|
||||||
|
)
|
||||||
|
|
||||||
|
if render, err = template.New(base).Parse(opt.README); err != nil {
|
||||||
|
log.Debug("cmd.new: new text template err, err = %s", err.Error())
|
||||||
|
print("生成 readme 失败", loading.TypeWarning)
|
||||||
|
goto END
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, err = os.OpenFile(path.Join(pwd, "readme.md"), os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0o644); err != nil {
|
||||||
|
log.Debug("cmd.new: new readme file err, err = %s", err.Error())
|
||||||
|
print("生成 readme 失败", loading.TypeWarning)
|
||||||
|
goto END
|
||||||
|
}
|
||||||
|
defer rf.Close()
|
||||||
|
|
||||||
|
if err = render.Execute(rf, map[string]any{
|
||||||
|
"project_name": base,
|
||||||
|
}); err != nil {
|
||||||
|
log.Debug("cmd.new: template execute err, err = %s", err.Error())
|
||||||
|
print("生成 readme 失败", loading.TypeWarning)
|
||||||
|
}
|
||||||
|
|
||||||
|
END:
|
||||||
|
print(fmt.Sprintf("项目: %s 初始化成功", args[0]), loading.TypeSuccess)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
41
nft/nfctl/internal/cmd/cmd.root.go
Normal file
41
nft/nfctl/internal/cmd/cmd.root.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/loveuer/nf/nft/log"
|
||||||
|
"github.com/loveuer/nf/nft/nfctl/internal/opt"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "nfctl",
|
||||||
|
Short: "nfctl is a tool for quick start a nf projects",
|
||||||
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if opt.Cfg.Debug {
|
||||||
|
log.SetLogLevel(log.LogLevelDebug)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opt.Cfg.Version {
|
||||||
|
doVersion(cmd, args)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !opt.Cfg.DisableUpdate {
|
||||||
|
doUpdate(cmd.Context())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
DisableSuggestions: true,
|
||||||
|
SilenceUsage: true,
|
||||||
|
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {},
|
||||||
|
}
|
||||||
|
|
||||||
|
func initRoot(cmds ...*cobra.Command) {
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&opt.Cfg.Debug, "debug", false, "debug mode")
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&opt.Cfg.DisableUpdate, "disable-update", false, "disable self update")
|
||||||
|
rootCmd.PersistentFlags().BoolVarP(&opt.Cfg.Version, "version", "v", false, "print nfctl version")
|
||||||
|
rootCmd.AddCommand(cmds...)
|
||||||
|
}
|
83
nft/nfctl/internal/cmd/cmd.update.go
Normal file
83
nft/nfctl/internal/cmd/cmd.update.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
resty "github.com/go-resty/resty/v2"
|
||||||
|
"github.com/loveuer/nf/nft/loading"
|
||||||
|
"github.com/loveuer/nf/nft/log"
|
||||||
|
"github.com/loveuer/nf/nft/nfctl/internal/opt"
|
||||||
|
"github.com/loveuer/nf/nft/tool"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var updateCmd = &cobra.Command{
|
||||||
|
Use: "update",
|
||||||
|
Short: "update nfctl self",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func initUpdate() *cobra.Command {
|
||||||
|
return updateCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func doUpdate(ctx context.Context) (err error) {
|
||||||
|
return loading.Do(tool.TimeoutCtx(ctx, 30), func(ctx context.Context, print func(msg string, types ...loading.Type)) error {
|
||||||
|
print("正在检查更新...")
|
||||||
|
tip := "❗ 请尝试手动更新: go install github.com/loveuer/nf/nft/nfctl@master"
|
||||||
|
version := ""
|
||||||
|
|
||||||
|
var rr *resty.Response
|
||||||
|
if rr, err = resty.New().SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).R().
|
||||||
|
SetContext(ctx).
|
||||||
|
Get(opt.VersionURL); err != nil {
|
||||||
|
err = fmt.Errorf("检查更新失败: %s\n%s", err.Error(), tip)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("cmd.update: url = %s, raw_response = %s", opt.VersionURL, rr.String())
|
||||||
|
|
||||||
|
if rr.StatusCode() != 200 {
|
||||||
|
err = fmt.Errorf("检查更新失败: %s\n%s", rr.Status(), tip)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reg := regexp.MustCompile(`const Version = "v\d{2}\.\d{2}\.\d{2}-r\d{1,2}"`)
|
||||||
|
for _, line := range strings.Split(rr.String(), "\n") {
|
||||||
|
if reg.MatchString(line) {
|
||||||
|
version = strings.TrimSpace(strings.TrimPrefix(line, "const Version = "))
|
||||||
|
version = version[1 : len(version)-1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if version == "" {
|
||||||
|
err = fmt.Errorf("检查更新失败: 未找到版本信息\n%s", tip)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("cmd.update: find version = %s, now_version = %s", version, opt.Version)
|
||||||
|
|
||||||
|
if version <= opt.Version {
|
||||||
|
print(fmt.Sprintf("已是最新版本: %s", opt.Version), loading.TypeSuccess)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
print(fmt.Sprintf("发现新版本: %s", version), loading.TypeInfo)
|
||||||
|
|
||||||
|
print(fmt.Sprintf("正在更新到 %s ...", version))
|
||||||
|
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
print("暂时无法自动更新, 请尝试手动更新: go install github.com/loveuer/nf/nft/nfctl@master", loading.TypeWarning)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
19
nft/nfctl/internal/cmd/cmd.version.go
Normal file
19
nft/nfctl/internal/cmd/cmd.version.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/loveuer/nf/nft/nfctl/internal/opt"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initVersion() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Run: doVersion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func doVersion(cmd *cobra.Command, args []string) {
|
||||||
|
fmt.Printf("%s\nnfctl: %s\n\n", opt.Banner, opt.Version)
|
||||||
|
}
|
26
nft/nfctl/internal/cmd/init.go
Normal file
26
nft/nfctl/internal/cmd/init.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
initRoot(
|
||||||
|
initVersion(),
|
||||||
|
initUpdate(),
|
||||||
|
initNew(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run(ctx context.Context) {
|
||||||
|
if err := rootCmd.ExecuteContext(ctx); err != nil {
|
||||||
|
fmt.Printf("❌ %s\n", err.Error())
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
}
|
21
nft/nfctl/internal/opt/opt.go
Normal file
21
nft/nfctl/internal/opt/opt.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package opt
|
||||||
|
|
||||||
|
type _new struct {
|
||||||
|
Template string
|
||||||
|
DisableInitScript bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Debug bool
|
||||||
|
DisableUpdate bool
|
||||||
|
Version bool
|
||||||
|
New _new
|
||||||
|
}
|
||||||
|
|
||||||
|
var Cfg = &config{}
|
||||||
|
|
||||||
|
var TemplateMap = map[string]string{
|
||||||
|
"ultone": "https://gitea.loveuer.com/loveuer/ultone.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
const README = "# {{ .project_name }}\n\n### 启动\n- `go run . --help`\n- `go run .`\n\n### 构建\n- `go build -ldflags '-s -w' -o dist/{{ .project_name}}_app .`\n- `docker build -t <image> -f Dockerfile .`"
|
13
nft/nfctl/internal/opt/version.go
Normal file
13
nft/nfctl/internal/opt/version.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package opt
|
||||||
|
|
||||||
|
const Version = "v24.12.27-r03"
|
||||||
|
|
||||||
|
// const VersionURL = "https://github.com/loveuer/nf/nft/nfctl/internal/opt/version.go"
|
||||||
|
|
||||||
|
const VersionURL = "https://gitea.loveuer.com/loveuer/nf/raw/branch/master/nft/nfctl/internal/opt/version.go"
|
||||||
|
|
||||||
|
const Banner = ` ___ __ __
|
||||||
|
___ / _/___/ /_/ /
|
||||||
|
/ _ \/ _/ __/ __/ /
|
||||||
|
/_//_/_/ \__/\__/_/
|
||||||
|
`
|
10
nft/nfctl/internal/opt/version_test.go
Normal file
10
nft/nfctl/internal/opt/version_test.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package opt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBanner(t *testing.T) {
|
||||||
|
fmt.Printf("%s\nnfctl: %s\n\n", Banner, Version)
|
||||||
|
}
|
22
nft/nfctl/main.go
Normal file
22
nft/nfctl/main.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/loveuer/nf/nft/nfctl/internal/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
time.Local = time.FixedZone("CST", 8*3600)
|
||||||
|
cmd.Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd.Run(ctx)
|
||||||
|
}
|
69
nft/resp/error.go
Normal file
69
nft/resp/error.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package resp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/loveuer/nf"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
status uint32
|
||||||
|
msg string
|
||||||
|
err error
|
||||||
|
data any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Error) Error() string {
|
||||||
|
if e.msg != "" {
|
||||||
|
return e.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e.status {
|
||||||
|
case 200:
|
||||||
|
return MSG200
|
||||||
|
case 202:
|
||||||
|
return MSG202
|
||||||
|
case 400:
|
||||||
|
return MSG400
|
||||||
|
case 401:
|
||||||
|
return MSG401
|
||||||
|
case 403:
|
||||||
|
return MSG403
|
||||||
|
case 404:
|
||||||
|
return MSG404
|
||||||
|
case 429:
|
||||||
|
return MSG429
|
||||||
|
case 500:
|
||||||
|
return MSG500
|
||||||
|
case 501:
|
||||||
|
return MSG501
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.err.Error()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewError(statusCode uint32, msg string, rawErr error, data any) Error {
|
||||||
|
return Error{
|
||||||
|
status: statusCode,
|
||||||
|
msg: msg,
|
||||||
|
err: rawErr,
|
||||||
|
data: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RespError(c *nf.Ctx, err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return Resp(c, 500, MSG500, "response with nil error", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var re = &Error{}
|
||||||
|
if errors.As(err, re) {
|
||||||
|
if re.err == nil {
|
||||||
|
return Resp(c, re.status, re.msg, re.msg, re.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Resp(c, re.status, re.msg, re.err.Error(), re.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Resp(c, 500, MSG500, err.Error(), nil)
|
||||||
|
}
|
140
nft/resp/resp.go
Normal file
140
nft/resp/resp.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package resp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/loveuer/nf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleEmptyMsg(status uint32, msg string) string {
|
||||||
|
if msg == "" {
|
||||||
|
switch status {
|
||||||
|
case 200:
|
||||||
|
msg = MSG200
|
||||||
|
case 202:
|
||||||
|
msg = MSG202
|
||||||
|
case 400:
|
||||||
|
msg = MSG400
|
||||||
|
case 401:
|
||||||
|
msg = MSG401
|
||||||
|
case 403:
|
||||||
|
msg = MSG403
|
||||||
|
case 404:
|
||||||
|
msg = MSG404
|
||||||
|
case 429:
|
||||||
|
msg = MSG429
|
||||||
|
case 500:
|
||||||
|
msg = MSG500
|
||||||
|
case 501:
|
||||||
|
msg = MSG501
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func Resp(c *nf.Ctx, status uint32, msg string, err string, data any) error {
|
||||||
|
msg = handleEmptyMsg(status, msg)
|
||||||
|
|
||||||
|
c.Set(RealStatusHeader, strconv.Itoa(int(status)))
|
||||||
|
|
||||||
|
if data == nil {
|
||||||
|
return c.JSON(nf.Map{"status": status, "msg": msg, "err": err})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(nf.Map{"status": status, "msg": msg, "err": err, "data": data})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Resp200(c *nf.Ctx, data any, msgs ...string) error {
|
||||||
|
msg := MSG200
|
||||||
|
|
||||||
|
if len(msgs) > 0 && msgs[0] != "" {
|
||||||
|
msg = fmt.Sprintf("%s: %s", msg, strings.Join(msgs, "; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Resp(c, 200, msg, "", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Resp202(c *nf.Ctx, data any, msgs ...string) error {
|
||||||
|
msg := MSG202
|
||||||
|
|
||||||
|
if len(msgs) > 0 && msgs[0] != "" {
|
||||||
|
msg = fmt.Sprintf("%s: %s", msg, strings.Join(msgs, "; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Resp(c, 202, msg, "", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Resp400(c *nf.Ctx, data any, msgs ...string) error {
|
||||||
|
msg := MSG400
|
||||||
|
err := ""
|
||||||
|
|
||||||
|
if len(msgs) > 0 && msgs[0] != "" {
|
||||||
|
msg = fmt.Sprintf("%s: %s", msg, strings.Join(msgs, "; "))
|
||||||
|
err = msg
|
||||||
|
}
|
||||||
|
|
||||||
|
return Resp(c, 400, msg, err, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Resp401(c *nf.Ctx, data any, msgs ...string) error {
|
||||||
|
msg := MSG401
|
||||||
|
err := ""
|
||||||
|
|
||||||
|
if len(msgs) > 0 && msgs[0] != "" {
|
||||||
|
msg = fmt.Sprintf("%s: %s", msg, strings.Join(msgs, "; "))
|
||||||
|
err = msg
|
||||||
|
}
|
||||||
|
|
||||||
|
return Resp(c, 401, msg, err, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Resp403(c *nf.Ctx, data any, msgs ...string) error {
|
||||||
|
msg := MSG403
|
||||||
|
err := ""
|
||||||
|
|
||||||
|
if len(msgs) > 0 && msgs[0] != "" {
|
||||||
|
msg = fmt.Sprintf("%s: %s", msg, strings.Join(msgs, "; "))
|
||||||
|
err = msg
|
||||||
|
}
|
||||||
|
|
||||||
|
return Resp(c, 403, msg, err, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Resp418(c *nf.Ctx, data any, msgs ...string) error {
|
||||||
|
msg := MSG418
|
||||||
|
err := ""
|
||||||
|
|
||||||
|
if len(msgs) > 0 && msgs[0] != "" {
|
||||||
|
msg = fmt.Sprintf("%s: %s", msg, strings.Join(msgs, "; "))
|
||||||
|
err = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return Resp(c, 418, msg, err, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Resp429(c *nf.Ctx, data any, msgs ...string) error {
|
||||||
|
msg := MSG429
|
||||||
|
err := ""
|
||||||
|
|
||||||
|
if len(msgs) > 0 && msgs[0] != "" {
|
||||||
|
msg = fmt.Sprintf("%s: %s", msg, strings.Join(msgs, "; "))
|
||||||
|
err = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return Resp(c, 429, msg, err, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Resp500(c *nf.Ctx, data any, msgs ...string) error {
|
||||||
|
msg := MSG500
|
||||||
|
err := ""
|
||||||
|
|
||||||
|
if len(msgs) > 0 && msgs[0] != "" {
|
||||||
|
msg = fmt.Sprintf("%s: %s", msg, strings.Join(msgs, "; "))
|
||||||
|
err = msg
|
||||||
|
}
|
||||||
|
|
||||||
|
return Resp(c, 500, msg, err, data)
|
||||||
|
}
|
18
nft/resp/var.go
Normal file
18
nft/resp/var.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package resp
|
||||||
|
|
||||||
|
const (
|
||||||
|
MSG200 = "请求成功"
|
||||||
|
MSG202 = "请求成功, 请稍后..."
|
||||||
|
MSG400 = "请求参数错误"
|
||||||
|
MSG401 = "登录已过期, 请重新登录"
|
||||||
|
MSG403 = "请求权限不足"
|
||||||
|
MSG404 = "请求资源未找到"
|
||||||
|
MSG418 = "请求条件不满足, 请稍后再试"
|
||||||
|
MSG429 = "请求过于频繁, 请稍后再试"
|
||||||
|
MSG500 = "服务器开小差了, 请稍后再试"
|
||||||
|
MSG501 = "功能开发中, 尽情期待"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RealStatusHeader = "NF-STATUS"
|
||||||
|
)
|
40
nft/tool/clone.go
Normal file
40
nft/tool/clone.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package tool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Clone(projectDir string, repoURL string) error {
|
||||||
|
ins, err := url.Parse(repoURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
uri := fmt.Sprintf("%s://%s%s", ins.Scheme, ins.Host, ins.Path)
|
||||||
|
|
||||||
|
opt := &git.CloneOptions{
|
||||||
|
URL: uri,
|
||||||
|
Depth: 1,
|
||||||
|
InsecureSkipTLS: true,
|
||||||
|
SingleBranch: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ins.User != nil {
|
||||||
|
password, _ := ins.User.Password()
|
||||||
|
opt.Auth = &http.BasicAuth{
|
||||||
|
Username: ins.User.Username(),
|
||||||
|
Password: password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = git.PlainClone(projectDir, false, opt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
34
nft/tool/ctx.go
Normal file
34
nft/tool/ctx.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package tool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Timeout(seconds ...int) (ctx context.Context) {
|
||||||
|
var duration time.Duration
|
||||||
|
|
||||||
|
if len(seconds) > 0 && seconds[0] > 0 {
|
||||||
|
duration = time.Duration(seconds[0]) * time.Second
|
||||||
|
} else {
|
||||||
|
duration = time.Duration(30) * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, _ = context.WithTimeout(context.Background(), duration)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TimeoutCtx(ctx context.Context, seconds ...int) context.Context {
|
||||||
|
var duration time.Duration
|
||||||
|
|
||||||
|
if len(seconds) > 0 && seconds[0] > 0 {
|
||||||
|
duration = time.Duration(seconds[0]) * time.Second
|
||||||
|
} else {
|
||||||
|
duration = time.Duration(30) * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
nctx, _ := context.WithTimeout(ctx, duration)
|
||||||
|
|
||||||
|
return nctx
|
||||||
|
}
|
102
readme.md
Normal file
102
readme.md
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
# NF Web Framework
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
##### basic usage
|
||||||
|
|
||||||
|
- get param
|
||||||
|
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
app := nf.New()
|
||||||
|
|
||||||
|
app.Get("/hello/:name", func(c *nf.Ctx) error {
|
||||||
|
name := c.Param("name")
|
||||||
|
return c.JSON(nf.Map{"status": 200, "data": "hello, " + name})
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Fatal(app.Run("0.0.0.0:80"))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- parse request query
|
||||||
|
|
||||||
|
```go
|
||||||
|
func handleQuery(c *nf.Ctx) error {
|
||||||
|
type Req struct {
|
||||||
|
Name string `query:"name"`
|
||||||
|
Addr []string `query:"addr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
req = Req{}
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = c.QueryParser(&req); err != nil {
|
||||||
|
return nf.NewNFError(400, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(nf.Map{"query": req})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- parse application/json body
|
||||||
|
|
||||||
|
```go
|
||||||
|
func handlePost(c *nf.Ctx) error {
|
||||||
|
type Req struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Addr []string `json:"addr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
req = Req{}
|
||||||
|
reqMap = make(map[string]interface{})
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = c.BodyParser(&req); err != nil {
|
||||||
|
return nf.NewNFError(400, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// can parse body multi times
|
||||||
|
if err = c.BodyParser(&reqMap); err != nil {
|
||||||
|
return nf.NewNFError(400, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(nf.Map{"struct": req, "map": reqMap})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- pass local value
|
||||||
|
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
Id int
|
||||||
|
Username string
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := nf.New()
|
||||||
|
app.Use(auth())
|
||||||
|
|
||||||
|
app.Get("/item/list", list)
|
||||||
|
}
|
||||||
|
|
||||||
|
func auth() nf.HandlerFunc {
|
||||||
|
return func(c *nf.Ctx) error {
|
||||||
|
c.Locals("user", &User{Id: 1, Username:"user"})
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func list(c *nf.Ctx) error {
|
||||||
|
user, ok := c.Locals("user").(*User)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(401).SendString("login required")
|
||||||
|
}
|
||||||
|
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
45
req.go
45
req.go
@ -1,45 +0,0 @@
|
|||||||
package nf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *Ctx) Form(key string) string {
|
|
||||||
return c.Request.FormValue(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ctx) Query(key string) string {
|
|
||||||
return c.Request.URL.Query().Get(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ctx) BodyParser(out interface{}) error {
|
|
||||||
|
|
||||||
ctype := strings.ToLower(c.Request.Header.Get("Content-Type"))
|
|
||||||
|
|
||||||
log.Printf("BodyParser: Content-Type=%s", ctype)
|
|
||||||
|
|
||||||
ctype = parseVendorSpecificContentType(ctype)
|
|
||||||
|
|
||||||
ctypeEnd := strings.IndexByte(ctype, ';')
|
|
||||||
if ctypeEnd != -1 {
|
|
||||||
ctype = ctype[:ctypeEnd]
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasSuffix(ctype, "json") {
|
|
||||||
bs, err := io.ReadAll(c.Request.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Request.Body = io.NopCloser(bytes.NewReader(bs))
|
|
||||||
|
|
||||||
return json.Unmarshal(bs, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New("422 Unprocessable Content")
|
|
||||||
}
|
|
49
resp.go
49
resp.go
@ -1,49 +0,0 @@
|
|||||||
package nf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *Ctx) Status(code int) *Ctx {
|
|
||||||
c.StatusCode = code
|
|
||||||
c.Writer.WriteHeader(code)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ctx) SetHeader(key string, value string) {
|
|
||||||
c.Writer.Header().Set(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ctx) SendString(data string) error {
|
|
||||||
c.SetHeader("Content-Type", "text/plain")
|
|
||||||
_, err := c.Write([]byte(data))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ctx) Writef(format string, values ...interface{}) (int, error) {
|
|
||||||
c.SetHeader("Content-Type", "text/plain")
|
|
||||||
return c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ctx) JSON(data interface{}) error {
|
|
||||||
c.SetHeader("Content-Type", "application/json")
|
|
||||||
|
|
||||||
encoder := json.NewEncoder(c.Writer)
|
|
||||||
|
|
||||||
if err := encoder.Encode(data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ctx) Write(data []byte) (int, error) {
|
|
||||||
return c.Writer.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ctx) HTML(html string) error {
|
|
||||||
c.SetHeader("Content-Type", "text/html")
|
|
||||||
_, err := c.Writer.Write([]byte(html))
|
|
||||||
return err
|
|
||||||
}
|
|
134
response_writer.go
Normal file
134
response_writer.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
package nf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
noWritten = -1
|
||||||
|
defaultStatus = http.StatusOK
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResponseWriter ...
|
||||||
|
type ResponseWriter interface {
|
||||||
|
http.ResponseWriter
|
||||||
|
http.Hijacker
|
||||||
|
http.Flusher
|
||||||
|
http.CloseNotifier
|
||||||
|
|
||||||
|
// Status returns the HTTP response status code of the current request.
|
||||||
|
Status() int
|
||||||
|
|
||||||
|
// Size returns the number of bytes already written into the response http body.
|
||||||
|
// See Written()
|
||||||
|
Size() int
|
||||||
|
|
||||||
|
// WriteString writes the string into the response body.
|
||||||
|
WriteString(string) (int, error)
|
||||||
|
|
||||||
|
// Written returns true if the response body was already written.
|
||||||
|
Written() bool
|
||||||
|
|
||||||
|
// WriteHeaderNow forces to write the http header (status code + headers).
|
||||||
|
WriteHeaderNow()
|
||||||
|
|
||||||
|
// Pusher get the http.Pusher for server push
|
||||||
|
Pusher() http.Pusher
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
written bool
|
||||||
|
size int
|
||||||
|
status int
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ResponseWriter = (*responseWriter)(nil)
|
||||||
|
|
||||||
|
func (w *responseWriter) Unwrap() http.ResponseWriter {
|
||||||
|
return w.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) reset(writer http.ResponseWriter) {
|
||||||
|
w.ResponseWriter = writer
|
||||||
|
w.size = noWritten
|
||||||
|
w.status = defaultStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) WriteHeader(code int) {
|
||||||
|
if code > 0 && w.status != code {
|
||||||
|
if w.Written() {
|
||||||
|
log.Printf("[NF] WARNING: Headers were already written. Wanted to override status code %d with %d", w.status, code)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.status = code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) WriteHeaderNow() {
|
||||||
|
if !w.Written() {
|
||||||
|
w.size = 0
|
||||||
|
|
||||||
|
if w.status == 0 {
|
||||||
|
w.status = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
w.ResponseWriter.WriteHeader(w.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) Write(data []byte) (n int, err error) {
|
||||||
|
w.WriteHeaderNow()
|
||||||
|
n, err = w.ResponseWriter.Write(data)
|
||||||
|
w.size += n
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) WriteString(s string) (n int, err error) {
|
||||||
|
w.WriteHeaderNow()
|
||||||
|
n, err = io.WriteString(w.ResponseWriter, s)
|
||||||
|
w.size += n
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) Status() int {
|
||||||
|
return w.status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) Size() int {
|
||||||
|
return w.size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) Written() bool {
|
||||||
|
return w.size != noWritten || w.written
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hijack implements the http.Hijacker interface.
|
||||||
|
func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
if w.size < 0 {
|
||||||
|
w.size = 0
|
||||||
|
}
|
||||||
|
return w.ResponseWriter.(http.Hijacker).Hijack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseNotify implements the http.CloseNotifier interface.
|
||||||
|
func (w *responseWriter) CloseNotify() <-chan bool {
|
||||||
|
return w.ResponseWriter.(http.CloseNotifier).CloseNotify()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush implements the http.Flusher interface.
|
||||||
|
func (w *responseWriter) Flush() {
|
||||||
|
w.WriteHeaderNow()
|
||||||
|
w.ResponseWriter.(http.Flusher).Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) Pusher() (pusher http.Pusher) {
|
||||||
|
if pusher, ok := w.ResponseWriter.(http.Pusher); ok {
|
||||||
|
return pusher
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
95
router.go
95
router.go
@ -1,95 +0,0 @@
|
|||||||
package nf
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
type router struct {
|
|
||||||
roots map[string]*_node
|
|
||||||
handlers map[string][]HandlerFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRouter() *router {
|
|
||||||
return &router{
|
|
||||||
roots: make(map[string]*_node),
|
|
||||||
handlers: make(map[string][]HandlerFunc),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only one * is allowed
|
|
||||||
func parsePattern(pattern string) []string {
|
|
||||||
vs := strings.Split(pattern, "/")
|
|
||||||
|
|
||||||
parts := make([]string, 0)
|
|
||||||
for _, item := range vs {
|
|
||||||
if item != "" {
|
|
||||||
parts = append(parts, item)
|
|
||||||
if item[0] == '*' {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parts
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *router) addRoute(method string, pattern string, handlers ...HandlerFunc) {
|
|
||||||
parts := parsePattern(pattern)
|
|
||||||
|
|
||||||
key := method + "-" + pattern
|
|
||||||
_, ok := r.roots[method]
|
|
||||||
if !ok {
|
|
||||||
r.roots[method] = &_node{}
|
|
||||||
}
|
|
||||||
r.roots[method].insert(pattern, parts, 0)
|
|
||||||
r.handlers[key] = handlers
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *router) getRoute(method string, path string) (*_node, map[string]string) {
|
|
||||||
searchParts := parsePattern(path)
|
|
||||||
params := make(map[string]string)
|
|
||||||
root, ok := r.roots[method]
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
n := root.search(searchParts, 0)
|
|
||||||
|
|
||||||
if n != nil {
|
|
||||||
parts := parsePattern(n.pattern)
|
|
||||||
for index, part := range parts {
|
|
||||||
if part[0] == ':' {
|
|
||||||
params[part[1:]] = searchParts[index]
|
|
||||||
}
|
|
||||||
if part[0] == '*' && len(part) > 1 {
|
|
||||||
params[part[1:]] = strings.Join(searchParts[index:], "/")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n, params
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *router) getRoutes(method string) []*_node {
|
|
||||||
root, ok := r.roots[method]
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
nodes := make([]*_node, 0)
|
|
||||||
root.travel(&nodes)
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *router) handle(c *Ctx) error {
|
|
||||||
node, params := r.getRoute(c.Method, c.path)
|
|
||||||
if node != nil {
|
|
||||||
c.params = params
|
|
||||||
key := c.Method + "-" + node.pattern
|
|
||||||
c.handlers = append(c.handlers, r.handlers[key]...)
|
|
||||||
} else {
|
|
||||||
_, err := c.Writef("404 NOT FOUND: %s\n", c.path)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Next()
|
|
||||||
}
|
|
155
routergroup.go
Normal file
155
routergroup.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package nf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// regEnLetter matches english letters for http method name
|
||||||
|
regEnLetter = regexp.MustCompile("^[A-Z]+$")
|
||||||
|
|
||||||
|
// anyMethods for RouterGroup Any method
|
||||||
|
anyMethods = []string{
|
||||||
|
http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch,
|
||||||
|
http.MethodHead, http.MethodOptions, http.MethodDelete, http.MethodConnect,
|
||||||
|
http.MethodTrace,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// IRouter defines all router handle interface includes single and group router.
|
||||||
|
type IRouter interface {
|
||||||
|
IRoutes
|
||||||
|
Group(string, ...HandlerFunc) *RouterGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
// IRoutes defines all router handle interface.
|
||||||
|
type IRoutes interface {
|
||||||
|
Use(...HandlerFunc) IRoutes
|
||||||
|
|
||||||
|
Handle(string, string, ...HandlerFunc) IRoutes
|
||||||
|
Any(string, ...HandlerFunc) IRoutes
|
||||||
|
Get(string, ...HandlerFunc) IRoutes
|
||||||
|
Post(string, ...HandlerFunc) IRoutes
|
||||||
|
Delete(string, ...HandlerFunc) IRoutes
|
||||||
|
Patch(string, ...HandlerFunc) IRoutes
|
||||||
|
Put(string, ...HandlerFunc) IRoutes
|
||||||
|
Options(string, ...HandlerFunc) IRoutes
|
||||||
|
Head(string, ...HandlerFunc) IRoutes
|
||||||
|
Match([]string, string, ...HandlerFunc) IRoutes
|
||||||
|
|
||||||
|
//StaticFile(string, string) IRoutes
|
||||||
|
//StaticFileFS(string, string, http.FileSystem) IRoutes
|
||||||
|
//Static(string, string) IRoutes
|
||||||
|
//StaticFS(string, http.FileSystem) IRoutes
|
||||||
|
}
|
||||||
|
|
||||||
|
type RouterGroup struct {
|
||||||
|
Handlers []HandlerFunc
|
||||||
|
basePath string
|
||||||
|
app *App
|
||||||
|
root bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ IRouter = (*RouterGroup)(nil)
|
||||||
|
|
||||||
|
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
|
||||||
|
group.Handlers = append(group.Handlers, middleware...)
|
||||||
|
return group.returnObj()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
|
||||||
|
return &RouterGroup{
|
||||||
|
Handlers: group.combineHandlers(handlers...),
|
||||||
|
basePath: group.calculateAbsolutePath(relativePath),
|
||||||
|
app: group.app,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (group *RouterGroup) BasePath() string {
|
||||||
|
return group.basePath
|
||||||
|
}
|
||||||
|
|
||||||
|
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||||
|
absolutePath := group.calculateAbsolutePath(relativePath)
|
||||||
|
handlers = group.combineHandlers(handlers...)
|
||||||
|
group.app.addRoute(httpMethod, absolutePath, handlers...)
|
||||||
|
return group.returnObj()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||||
|
if matched := regEnLetter.MatchString(httpMethod); !matched {
|
||||||
|
panic("http method " + httpMethod + " is not valid")
|
||||||
|
}
|
||||||
|
return group.handle(httpMethod, relativePath, handlers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (group *RouterGroup) Post(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||||
|
return group.handle(http.MethodPost, relativePath, handlers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (group *RouterGroup) Get(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||||
|
return group.handle(http.MethodGet, relativePath, handlers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (group *RouterGroup) Delete(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||||
|
return group.handle(http.MethodDelete, relativePath, handlers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (group *RouterGroup) Patch(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||||
|
return group.handle(http.MethodPatch, relativePath, handlers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (group *RouterGroup) Put(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||||
|
return group.handle(http.MethodPut, relativePath, handlers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (group *RouterGroup) Options(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||||
|
return group.handle(http.MethodOptions, relativePath, handlers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (group *RouterGroup) Head(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||||
|
return group.handle(http.MethodHead, relativePath, handlers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any registers a route that matches all the HTTP methods.
|
||||||
|
// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.
|
||||||
|
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||||
|
for _, method := range anyMethods {
|
||||||
|
group.handle(method, relativePath, handlers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return group.returnObj()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (group *RouterGroup) Match(methods []string, relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||||
|
for _, method := range methods {
|
||||||
|
group.handle(method, relativePath, handlers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return group.returnObj()
|
||||||
|
}
|
||||||
|
|
||||||
|
const abortIndex int8 = math.MaxInt8 >> 1
|
||||||
|
|
||||||
|
func (group *RouterGroup) combineHandlers(handlers ...HandlerFunc) []HandlerFunc {
|
||||||
|
finalSize := len(group.Handlers) + len(handlers)
|
||||||
|
elsePanic(finalSize < int(abortIndex), "too many handlers")
|
||||||
|
mergedHandlers := make([]HandlerFunc, finalSize)
|
||||||
|
copy(mergedHandlers, group.Handlers)
|
||||||
|
copy(mergedHandlers[len(group.Handlers):], handlers)
|
||||||
|
return mergedHandlers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
|
||||||
|
return path.Join(group.basePath, relativePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (group *RouterGroup) returnObj() IRoutes {
|
||||||
|
if group.root {
|
||||||
|
return group.app
|
||||||
|
}
|
||||||
|
return group
|
||||||
|
}
|
903
tree.go
903
tree.go
@ -1,76 +1,891 @@
|
|||||||
package nf
|
package nf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/loveuer/nf/internal/bytesconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
type _node struct {
|
var (
|
||||||
pattern string
|
strColon = []byte(":")
|
||||||
part string
|
strStar = []byte("*")
|
||||||
children []*_node
|
strSlash = []byte("/")
|
||||||
isWild bool
|
)
|
||||||
|
|
||||||
|
// Param is a single URL parameter, consisting of a key and a value.
|
||||||
|
type Param struct {
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *_node) insert(pattern string, parts []string, height int) {
|
// Params is a Param-slice, as returned by the router.
|
||||||
if len(parts) == height {
|
// The slice is ordered, the first URL parameter is also the first slice value.
|
||||||
n.pattern = pattern
|
// It is therefore safe to read values by the index.
|
||||||
|
type Params []Param
|
||||||
|
|
||||||
|
// Get returns the value of the first Param which key matches the given name and a boolean true.
|
||||||
|
// If no matching Param is found, an empty string is returned and a boolean false .
|
||||||
|
func (ps Params) Get(name string) (string, bool) {
|
||||||
|
for _, entry := range ps {
|
||||||
|
if entry.Key == name {
|
||||||
|
return entry.Value, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByName returns the value of the first Param which key matches the given name.
|
||||||
|
// If no matching Param is found, an empty string is returned.
|
||||||
|
func (ps Params) ByName(name string) (va string) {
|
||||||
|
va, _ = ps.Get(name)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
part := parts[height]
|
type methodTree struct {
|
||||||
child := n.matchChild(part)
|
method string
|
||||||
if child == nil {
|
root *node
|
||||||
child = &_node{part: part, isWild: part[0] == ':' || part[0] == '*'}
|
|
||||||
n.children = append(n.children, child)
|
|
||||||
}
|
|
||||||
child.insert(pattern, parts, height+1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *_node) search(parts []string, height int) *_node {
|
type methodTrees []methodTree
|
||||||
if len(parts) == height || strings.HasPrefix(n.part, "*") {
|
|
||||||
if n.pattern == "" {
|
func (trees methodTrees) get(method string) *node {
|
||||||
|
for _, tree := range trees {
|
||||||
|
if tree.method == method {
|
||||||
|
return tree.root
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a <= b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func longestCommonPrefix(a, b string) int {
|
||||||
|
i := 0
|
||||||
|
max := min(len(a), len(b))
|
||||||
|
for i < max && a[i] == b[i] {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
// addChild will add a child node, keeping wildcardChild at the end
|
||||||
|
func (n *node) addChild(child *node) {
|
||||||
|
if n.wildChild && len(n.children) > 0 {
|
||||||
|
wildcardChild := n.children[len(n.children)-1]
|
||||||
|
n.children = append(n.children[:len(n.children)-1], child, wildcardChild)
|
||||||
|
} else {
|
||||||
|
n.children = append(n.children, child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func countParams(path string) uint16 {
|
||||||
|
var n uint16
|
||||||
|
s := bytesconv.StringToBytes(path)
|
||||||
|
n += uint16(bytes.Count(s, strColon))
|
||||||
|
n += uint16(bytes.Count(s, strStar))
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
part := parts[height]
|
func countSections(path string) uint16 {
|
||||||
children := n.matchChildren(part)
|
s := bytesconv.StringToBytes(path)
|
||||||
|
return uint16(bytes.Count(s, strSlash))
|
||||||
|
}
|
||||||
|
|
||||||
for _, child := range children {
|
type nodeType uint8
|
||||||
result := child.search(parts, height+1)
|
|
||||||
if result != nil {
|
const (
|
||||||
return result
|
static nodeType = iota
|
||||||
|
root
|
||||||
|
param
|
||||||
|
catchAll
|
||||||
|
)
|
||||||
|
|
||||||
|
type node struct {
|
||||||
|
path string
|
||||||
|
indices string
|
||||||
|
wildChild bool
|
||||||
|
nType nodeType
|
||||||
|
priority uint32
|
||||||
|
children []*node // child nodes, at most 1 :param style node at the end of the array
|
||||||
|
handlers []HandlerFunc
|
||||||
|
fullPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increments priority of the given child and reorders if necessary
|
||||||
|
func (n *node) incrementChildPrio(pos int) int {
|
||||||
|
cs := n.children
|
||||||
|
cs[pos].priority++
|
||||||
|
prio := cs[pos].priority
|
||||||
|
|
||||||
|
// Adjust position (move to front)
|
||||||
|
newPos := pos
|
||||||
|
for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
|
||||||
|
// Swap node positions
|
||||||
|
cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build new index char string
|
||||||
|
if newPos != pos {
|
||||||
|
n.indices = n.indices[:newPos] + // Unchanged prefix, might be empty
|
||||||
|
n.indices[pos:pos+1] + // The index char we move
|
||||||
|
n.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos'
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPos
|
||||||
|
}
|
||||||
|
|
||||||
|
// addRoute adds a node with the given handle to the path.
|
||||||
|
// Not concurrency-safe!
|
||||||
|
func (n *node) addRoute(path string, handlers ...HandlerFunc) {
|
||||||
|
fullPath := path
|
||||||
|
n.priority++
|
||||||
|
|
||||||
|
// Empty tree
|
||||||
|
if len(n.path) == 0 && len(n.children) == 0 {
|
||||||
|
n.insertChild(path, fullPath, handlers...)
|
||||||
|
n.nType = root
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parentFullPathIndex := 0
|
||||||
|
|
||||||
|
walk:
|
||||||
|
for {
|
||||||
|
// Find the longest common prefix.
|
||||||
|
// This also implies that the common prefix contains no ':' or '*'
|
||||||
|
// since the existing key can't contain those chars.
|
||||||
|
i := longestCommonPrefix(path, n.path)
|
||||||
|
|
||||||
|
// Split edge
|
||||||
|
if i < len(n.path) {
|
||||||
|
child := node{
|
||||||
|
path: n.path[i:],
|
||||||
|
wildChild: n.wildChild,
|
||||||
|
nType: static,
|
||||||
|
indices: n.indices,
|
||||||
|
children: n.children,
|
||||||
|
handlers: n.handlers,
|
||||||
|
priority: n.priority - 1,
|
||||||
|
fullPath: n.fullPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
n.children = []*node{&child}
|
||||||
|
// []byte for proper unicode char conversion, see #65
|
||||||
|
n.indices = bytesconv.BytesToString([]byte{n.path[i]})
|
||||||
|
n.path = path[:i]
|
||||||
|
n.handlers = nil
|
||||||
|
n.wildChild = false
|
||||||
|
n.fullPath = fullPath[:parentFullPathIndex+i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make new node a child of this node
|
||||||
|
if i < len(path) {
|
||||||
|
path = path[i:]
|
||||||
|
c := path[0]
|
||||||
|
|
||||||
|
// '/' after param
|
||||||
|
if n.nType == param && c == '/' && len(n.children) == 1 {
|
||||||
|
parentFullPathIndex += len(n.path)
|
||||||
|
n = n.children[0]
|
||||||
|
n.priority++
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a child with the next path byte exists
|
||||||
|
for i, max := 0, len(n.indices); i < max; i++ {
|
||||||
|
if c == n.indices[i] {
|
||||||
|
parentFullPathIndex += len(n.path)
|
||||||
|
i = n.incrementChildPrio(i)
|
||||||
|
n = n.children[i]
|
||||||
|
continue walk
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Otherwise insert it
|
||||||
|
if c != ':' && c != '*' && n.nType != catchAll {
|
||||||
|
// []byte for proper unicode char conversion, see #65
|
||||||
|
n.indices += bytesconv.BytesToString([]byte{c})
|
||||||
|
child := &node{
|
||||||
|
fullPath: fullPath,
|
||||||
|
}
|
||||||
|
n.addChild(child)
|
||||||
|
n.incrementChildPrio(len(n.indices) - 1)
|
||||||
|
n = child
|
||||||
|
} else if n.wildChild {
|
||||||
|
// inserting a wildcard node, need to check if it conflicts with the existing wildcard
|
||||||
|
n = n.children[len(n.children)-1]
|
||||||
|
n.priority++
|
||||||
|
|
||||||
|
// Check if the wildcard matches
|
||||||
|
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
|
||||||
|
// Adding a child to a catchAll is not possible
|
||||||
|
n.nType != catchAll &&
|
||||||
|
// Check for longer wildcard, e.g. :name and :names
|
||||||
|
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wildcard conflict
|
||||||
|
pathSeg := path
|
||||||
|
if n.nType != catchAll {
|
||||||
|
pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
|
||||||
|
}
|
||||||
|
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
|
||||||
|
panic("'" + pathSeg +
|
||||||
|
"' in new path '" + fullPath +
|
||||||
|
"' conflicts with existing wildcard '" + n.path +
|
||||||
|
"' in existing prefix '" + prefix +
|
||||||
|
"'")
|
||||||
|
}
|
||||||
|
|
||||||
|
n.insertChild(path, fullPath, handlers...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise add handle to current node
|
||||||
|
if n.handlers != nil {
|
||||||
|
panic("handlers are already registered for path '" + fullPath + "'")
|
||||||
|
}
|
||||||
|
n.handlers = handlers
|
||||||
|
n.fullPath = fullPath
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for a wildcard segment and check the name for invalid characters.
|
||||||
|
// Returns -1 as index, if no wildcard was found.
|
||||||
|
func findWildcard(path string) (wildcard string, i int, valid bool) {
|
||||||
|
// Find start
|
||||||
|
for start, c := range []byte(path) {
|
||||||
|
// A wildcard starts with ':' (param) or '*' (catch-all)
|
||||||
|
if c != ':' && c != '*' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find end and check for invalid characters
|
||||||
|
valid = true
|
||||||
|
for end, c := range []byte(path[start+1:]) {
|
||||||
|
switch c {
|
||||||
|
case '/':
|
||||||
|
return path[start : start+1+end], start, valid
|
||||||
|
case ':', '*':
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path[start:], start, valid
|
||||||
|
}
|
||||||
|
return "", -1, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) insertChild(path string, fullPath string, handlers ...HandlerFunc) {
|
||||||
|
for {
|
||||||
|
// Find prefix until first wildcard
|
||||||
|
wildcard, i, valid := findWildcard(path)
|
||||||
|
if i < 0 { // No wildcard found
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// The wildcard name must only contain one ':' or '*' character
|
||||||
|
if !valid {
|
||||||
|
panic("only one wildcard per path segment is allowed, has: '" +
|
||||||
|
wildcard + "' in path '" + fullPath + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the wildcard has a name
|
||||||
|
if len(wildcard) < 2 {
|
||||||
|
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if wildcard[0] == ':' { // param
|
||||||
|
if i > 0 {
|
||||||
|
// Insert prefix before the current wildcard
|
||||||
|
n.path = path[:i]
|
||||||
|
path = path[i:]
|
||||||
|
}
|
||||||
|
|
||||||
|
child := &node{
|
||||||
|
nType: param,
|
||||||
|
path: wildcard,
|
||||||
|
fullPath: fullPath,
|
||||||
|
}
|
||||||
|
n.addChild(child)
|
||||||
|
n.wildChild = true
|
||||||
|
n = child
|
||||||
|
n.priority++
|
||||||
|
|
||||||
|
// if the path doesn't end with the wildcard, then there
|
||||||
|
// will be another subpath starting with '/'
|
||||||
|
if len(wildcard) < len(path) {
|
||||||
|
path = path[len(wildcard):]
|
||||||
|
|
||||||
|
child := &node{
|
||||||
|
priority: 1,
|
||||||
|
fullPath: fullPath,
|
||||||
|
}
|
||||||
|
n.addChild(child)
|
||||||
|
n = child
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise we're done. Insert the handle in the new leaf
|
||||||
|
n.handlers = handlers
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// catchAll
|
||||||
|
if i+len(wildcard) != len(path) {
|
||||||
|
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
|
||||||
|
pathSeg := ""
|
||||||
|
if len(n.children) != 0 {
|
||||||
|
pathSeg = strings.SplitN(n.children[0].path, "/", 2)[0]
|
||||||
|
}
|
||||||
|
panic("catch-all wildcard '" + path +
|
||||||
|
"' in new path '" + fullPath +
|
||||||
|
"' conflicts with existing path segment '" + pathSeg +
|
||||||
|
"' in existing prefix '" + n.path + pathSeg +
|
||||||
|
"'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// currently fixed width 1 for '/'
|
||||||
|
i--
|
||||||
|
if path[i] != '/' {
|
||||||
|
panic("no / before catch-all in path '" + fullPath + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
n.path = path[:i]
|
||||||
|
|
||||||
|
// First node: catchAll node with empty path
|
||||||
|
child := &node{
|
||||||
|
wildChild: true,
|
||||||
|
nType: catchAll,
|
||||||
|
fullPath: fullPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
n.addChild(child)
|
||||||
|
n.indices = string('/')
|
||||||
|
n = child
|
||||||
|
n.priority++
|
||||||
|
|
||||||
|
// second node: node holding the variable
|
||||||
|
child = &node{
|
||||||
|
path: path[i:],
|
||||||
|
nType: catchAll,
|
||||||
|
handlers: handlers,
|
||||||
|
priority: 1,
|
||||||
|
fullPath: fullPath,
|
||||||
|
}
|
||||||
|
n.children = []*node{child}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no wildcard was found, simply insert the path and handle
|
||||||
|
n.path = path
|
||||||
|
n.handlers = handlers
|
||||||
|
n.fullPath = fullPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// nodeValue holds return values of (*Node).getValue method
|
||||||
|
type nodeValue struct {
|
||||||
|
handlers []HandlerFunc
|
||||||
|
params *Params
|
||||||
|
tsr bool
|
||||||
|
fullPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
type skippedNode struct {
|
||||||
|
path string
|
||||||
|
node *node
|
||||||
|
paramsCount int16
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the handle registered with the given path (key). The values of
|
||||||
|
// wildcards are saved to a map.
|
||||||
|
// If no handle can be found, a TSR (trailing slash redirect) recommendation is
|
||||||
|
// made if a handle exists with an extra (without the) trailing slash for the
|
||||||
|
// given path.
|
||||||
|
func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode, unescape bool) (value nodeValue) {
|
||||||
|
var globalParamsCount int16
|
||||||
|
|
||||||
|
walk: // Outer loop for walking the tree
|
||||||
|
for {
|
||||||
|
prefix := n.path
|
||||||
|
if len(path) > len(prefix) {
|
||||||
|
if path[:len(prefix)] == prefix {
|
||||||
|
path = path[len(prefix):]
|
||||||
|
|
||||||
|
// Try all the non-wildcard children first by matching the indices
|
||||||
|
idxc := path[0]
|
||||||
|
for i, c := range []byte(n.indices) {
|
||||||
|
if c == idxc {
|
||||||
|
// strings.HasPrefix(n.children[len(n.children)-1].path, ":") == n.wildChild
|
||||||
|
if n.wildChild {
|
||||||
|
index := len(*skippedNodes)
|
||||||
|
*skippedNodes = (*skippedNodes)[:index+1]
|
||||||
|
(*skippedNodes)[index] = skippedNode{
|
||||||
|
path: prefix + path,
|
||||||
|
node: &node{
|
||||||
|
path: n.path,
|
||||||
|
wildChild: n.wildChild,
|
||||||
|
nType: n.nType,
|
||||||
|
priority: n.priority,
|
||||||
|
children: n.children,
|
||||||
|
handlers: n.handlers,
|
||||||
|
fullPath: n.fullPath,
|
||||||
|
},
|
||||||
|
paramsCount: globalParamsCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n = n.children[i]
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !n.wildChild {
|
||||||
|
// If the path at the end of the loop is not equal to '/' and the current node has no child nodes
|
||||||
|
// the current node needs to roll back to last valid skippedNode
|
||||||
|
if path != "/" {
|
||||||
|
for length := len(*skippedNodes); length > 0; length-- {
|
||||||
|
skippedNode := (*skippedNodes)[length-1]
|
||||||
|
*skippedNodes = (*skippedNodes)[:length-1]
|
||||||
|
if strings.HasSuffix(skippedNode.path, path) {
|
||||||
|
path = skippedNode.path
|
||||||
|
n = skippedNode.node
|
||||||
|
if value.params != nil {
|
||||||
|
*value.params = (*value.params)[:skippedNode.paramsCount]
|
||||||
|
}
|
||||||
|
globalParamsCount = skippedNode.paramsCount
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing found.
|
||||||
|
// We can recommend to redirect to the same URL without a
|
||||||
|
// trailing slash if a leaf exists for that path.
|
||||||
|
value.tsr = path == "/" && n.handlers != nil
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle wildcard child, which is always at the end of the array
|
||||||
|
n = n.children[len(n.children)-1]
|
||||||
|
globalParamsCount++
|
||||||
|
|
||||||
|
switch n.nType {
|
||||||
|
case param:
|
||||||
|
// fix truncate the parameter
|
||||||
|
// tree_test.go line: 204
|
||||||
|
|
||||||
|
// Find param end (either '/' or path end)
|
||||||
|
end := 0
|
||||||
|
for end < len(path) && path[end] != '/' {
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save param value
|
||||||
|
if params != nil {
|
||||||
|
// Preallocate capacity if necessary
|
||||||
|
if cap(*params) < int(globalParamsCount) {
|
||||||
|
newParams := make(Params, len(*params), globalParamsCount)
|
||||||
|
copy(newParams, *params)
|
||||||
|
*params = newParams
|
||||||
|
}
|
||||||
|
|
||||||
|
if value.params == nil {
|
||||||
|
value.params = params
|
||||||
|
}
|
||||||
|
// Expand slice within preallocated capacity
|
||||||
|
i := len(*value.params)
|
||||||
|
*value.params = (*value.params)[:i+1]
|
||||||
|
val := path[:end]
|
||||||
|
if unescape {
|
||||||
|
if v, err := url.QueryUnescape(val); err == nil {
|
||||||
|
val = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(*value.params)[i] = Param{
|
||||||
|
Key: n.path[1:],
|
||||||
|
Value: val,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need to go deeper!
|
||||||
|
if end < len(path) {
|
||||||
|
if len(n.children) > 0 {
|
||||||
|
path = path[end:]
|
||||||
|
n = n.children[0]
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... but we can't
|
||||||
|
value.tsr = len(path) == end+1
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
if value.handlers = n.handlers; value.handlers != nil {
|
||||||
|
value.fullPath = n.fullPath
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
if len(n.children) == 1 {
|
||||||
|
// No handle found. Check if a handle for this path + a
|
||||||
|
// trailing slash exists for TSR recommendation
|
||||||
|
n = n.children[0]
|
||||||
|
value.tsr = (n.path == "/" && n.handlers != nil) || (n.path == "" && n.indices == "/")
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
|
||||||
|
case catchAll:
|
||||||
|
// Save param value
|
||||||
|
if params != nil {
|
||||||
|
// Preallocate capacity if necessary
|
||||||
|
if cap(*params) < int(globalParamsCount) {
|
||||||
|
newParams := make(Params, len(*params), globalParamsCount)
|
||||||
|
copy(newParams, *params)
|
||||||
|
*params = newParams
|
||||||
|
}
|
||||||
|
|
||||||
|
if value.params == nil {
|
||||||
|
value.params = params
|
||||||
|
}
|
||||||
|
// Expand slice within preallocated capacity
|
||||||
|
i := len(*value.params)
|
||||||
|
*value.params = (*value.params)[:i+1]
|
||||||
|
val := path
|
||||||
|
if unescape {
|
||||||
|
if v, err := url.QueryUnescape(path); err == nil {
|
||||||
|
val = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(*value.params)[i] = Param{
|
||||||
|
Key: n.path[2:],
|
||||||
|
Value: val,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value.handlers = n.handlers
|
||||||
|
value.fullPath = n.fullPath
|
||||||
|
return value
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("invalid node type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if path == prefix {
|
||||||
|
// If the current path does not equal '/' and the node does not have a registered handle and the most recently matched node has a child node
|
||||||
|
// the current node needs to roll back to last valid skippedNode
|
||||||
|
if n.handlers == nil && path != "/" {
|
||||||
|
for length := len(*skippedNodes); length > 0; length-- {
|
||||||
|
skippedNode := (*skippedNodes)[length-1]
|
||||||
|
*skippedNodes = (*skippedNodes)[:length-1]
|
||||||
|
if strings.HasSuffix(skippedNode.path, path) {
|
||||||
|
path = skippedNode.path
|
||||||
|
n = skippedNode.node
|
||||||
|
if value.params != nil {
|
||||||
|
*value.params = (*value.params)[:skippedNode.paramsCount]
|
||||||
|
}
|
||||||
|
globalParamsCount = skippedNode.paramsCount
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// n = latestNode.children[len(latestNode.children)-1]
|
||||||
|
}
|
||||||
|
// We should have reached the node containing the handle.
|
||||||
|
// Check if this node has a handle registered.
|
||||||
|
if value.handlers = n.handlers; value.handlers != nil {
|
||||||
|
value.fullPath = n.fullPath
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no handle for this route, but this route has a
|
||||||
|
// wildcard child, there must be a handle for this path with an
|
||||||
|
// additional trailing slash
|
||||||
|
if path == "/" && n.wildChild && n.nType != root {
|
||||||
|
value.tsr = true
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
if path == "/" && n.nType == static {
|
||||||
|
value.tsr = true
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// No handle found. Check if a handle for this path + a
|
||||||
|
// trailing slash exists for trailing slash recommendation
|
||||||
|
for i, c := range []byte(n.indices) {
|
||||||
|
if c == '/' {
|
||||||
|
n = n.children[i]
|
||||||
|
value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
|
||||||
|
(n.nType == catchAll && n.children[0].handlers != nil)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing found. We can recommend to redirect to the same URL with an
|
||||||
|
// extra trailing slash if a leaf exists for that path
|
||||||
|
value.tsr = path == "/" ||
|
||||||
|
(len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
|
||||||
|
path == prefix[:len(prefix)-1] && n.handlers != nil)
|
||||||
|
|
||||||
|
// roll back to last valid skippedNode
|
||||||
|
if !value.tsr && path != "/" {
|
||||||
|
for length := len(*skippedNodes); length > 0; length-- {
|
||||||
|
skippedNode := (*skippedNodes)[length-1]
|
||||||
|
*skippedNodes = (*skippedNodes)[:length-1]
|
||||||
|
if strings.HasSuffix(skippedNode.path, path) {
|
||||||
|
path = skippedNode.path
|
||||||
|
n = skippedNode.node
|
||||||
|
if value.params != nil {
|
||||||
|
*value.params = (*value.params)[:skippedNode.paramsCount]
|
||||||
|
}
|
||||||
|
globalParamsCount = skippedNode.paramsCount
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Makes a case-insensitive lookup of the given path and tries to find a handler.
|
||||||
|
// It can optionally also fix trailing slashes.
|
||||||
|
// It returns the case-corrected path and a bool indicating whether the lookup
|
||||||
|
// was successful.
|
||||||
|
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) {
|
||||||
|
const stackBufSize = 128
|
||||||
|
|
||||||
|
// Use a static sized buffer on the stack in the common case.
|
||||||
|
// If the path is too long, allocate a buffer on the heap instead.
|
||||||
|
buf := make([]byte, 0, stackBufSize)
|
||||||
|
if length := len(path) + 1; length > stackBufSize {
|
||||||
|
buf = make([]byte, 0, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
ciPath := n.findCaseInsensitivePathRec(
|
||||||
|
path,
|
||||||
|
buf, // Preallocate enough memory for new path
|
||||||
|
[4]byte{}, // Empty rune buffer
|
||||||
|
fixTrailingSlash,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ciPath, ciPath != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift bytes in array by n bytes left
|
||||||
|
func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
|
||||||
|
switch n {
|
||||||
|
case 0:
|
||||||
|
return rb
|
||||||
|
case 1:
|
||||||
|
return [4]byte{rb[1], rb[2], rb[3], 0}
|
||||||
|
case 2:
|
||||||
|
return [4]byte{rb[2], rb[3]}
|
||||||
|
case 3:
|
||||||
|
return [4]byte{rb[3]}
|
||||||
|
default:
|
||||||
|
return [4]byte{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursive case-insensitive lookup function used by n.findCaseInsensitivePath
|
||||||
|
func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) []byte {
|
||||||
|
npLen := len(n.path)
|
||||||
|
|
||||||
|
walk: // Outer loop for walking the tree
|
||||||
|
for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) {
|
||||||
|
// Add common prefix to result
|
||||||
|
oldPath := path
|
||||||
|
path = path[npLen:]
|
||||||
|
ciPath = append(ciPath, n.path...)
|
||||||
|
|
||||||
|
if len(path) == 0 {
|
||||||
|
// We should have reached the node containing the handle.
|
||||||
|
// Check if this node has a handle registered.
|
||||||
|
if n.handlers != nil {
|
||||||
|
return ciPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// No handle found.
|
||||||
|
// Try to fix the path by adding a trailing slash
|
||||||
|
if fixTrailingSlash {
|
||||||
|
for i, c := range []byte(n.indices) {
|
||||||
|
if c == '/' {
|
||||||
|
n = n.children[i]
|
||||||
|
if (len(n.path) == 1 && n.handlers != nil) ||
|
||||||
|
(n.nType == catchAll && n.children[0].handlers != nil) {
|
||||||
|
return append(ciPath, '/')
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *_node) travel(list *([]*_node)) {
|
|
||||||
if n.pattern != "" {
|
|
||||||
*list = append(*list, n)
|
|
||||||
}
|
|
||||||
for _, child := range n.children {
|
|
||||||
child.travel(list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *_node) matchChild(part string) *_node {
|
|
||||||
for _, child := range n.children {
|
|
||||||
if child.part == part || child.isWild {
|
|
||||||
return child
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *_node) matchChildren(part string) []*_node {
|
// If this node does not have a wildcard (param or catchAll) child,
|
||||||
nodes := make([]*_node, 0)
|
// we can just look up the next child node and continue to walk down
|
||||||
for _, child := range n.children {
|
// the tree
|
||||||
if child.part == part || child.isWild {
|
if !n.wildChild {
|
||||||
nodes = append(nodes, child)
|
// Skip rune bytes already processed
|
||||||
|
rb = shiftNRuneBytes(rb, npLen)
|
||||||
|
|
||||||
|
if rb[0] != 0 {
|
||||||
|
// Old rune not finished
|
||||||
|
idxc := rb[0]
|
||||||
|
for i, c := range []byte(n.indices) {
|
||||||
|
if c == idxc {
|
||||||
|
// continue with child node
|
||||||
|
n = n.children[i]
|
||||||
|
npLen = len(n.path)
|
||||||
|
continue walk
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nodes
|
} else {
|
||||||
|
// Process a new rune
|
||||||
|
var rv rune
|
||||||
|
|
||||||
|
// Find rune start.
|
||||||
|
// Runes are up to 4 byte long,
|
||||||
|
// -4 would definitely be another rune.
|
||||||
|
var off int
|
||||||
|
for max := min(npLen, 3); off < max; off++ {
|
||||||
|
if i := npLen - off; utf8.RuneStart(oldPath[i]) {
|
||||||
|
// read rune from cached path
|
||||||
|
rv, _ = utf8.DecodeRuneInString(oldPath[i:])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate lowercase bytes of current rune
|
||||||
|
lo := unicode.ToLower(rv)
|
||||||
|
utf8.EncodeRune(rb[:], lo)
|
||||||
|
|
||||||
|
// Skip already processed bytes
|
||||||
|
rb = shiftNRuneBytes(rb, off)
|
||||||
|
|
||||||
|
idxc := rb[0]
|
||||||
|
for i, c := range []byte(n.indices) {
|
||||||
|
// Lowercase matches
|
||||||
|
if c == idxc {
|
||||||
|
// must use a recursive approach since both the
|
||||||
|
// uppercase byte and the lowercase byte might exist
|
||||||
|
// as an index
|
||||||
|
if out := n.children[i].findCaseInsensitivePathRec(
|
||||||
|
path, ciPath, rb, fixTrailingSlash,
|
||||||
|
); out != nil {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found no match, the same for the uppercase rune,
|
||||||
|
// if it differs
|
||||||
|
if up := unicode.ToUpper(rv); up != lo {
|
||||||
|
utf8.EncodeRune(rb[:], up)
|
||||||
|
rb = shiftNRuneBytes(rb, off)
|
||||||
|
|
||||||
|
idxc := rb[0]
|
||||||
|
for i, c := range []byte(n.indices) {
|
||||||
|
// Uppercase matches
|
||||||
|
if c == idxc {
|
||||||
|
// Continue with child node
|
||||||
|
n = n.children[i]
|
||||||
|
npLen = len(n.path)
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing found. We can recommend to redirect to the same URL
|
||||||
|
// without a trailing slash if a leaf exists for that path
|
||||||
|
if fixTrailingSlash && path == "/" && n.handlers != nil {
|
||||||
|
return ciPath
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
n = n.children[0]
|
||||||
|
switch n.nType {
|
||||||
|
case param:
|
||||||
|
// Find param end (either '/' or path end)
|
||||||
|
end := 0
|
||||||
|
for end < len(path) && path[end] != '/' {
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add param value to case insensitive path
|
||||||
|
ciPath = append(ciPath, path[:end]...)
|
||||||
|
|
||||||
|
// We need to go deeper!
|
||||||
|
if end < len(path) {
|
||||||
|
if len(n.children) > 0 {
|
||||||
|
// Continue with child node
|
||||||
|
n = n.children[0]
|
||||||
|
npLen = len(n.path)
|
||||||
|
path = path[end:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... but we can't
|
||||||
|
if fixTrailingSlash && len(path) == end+1 {
|
||||||
|
return ciPath
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.handlers != nil {
|
||||||
|
return ciPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if fixTrailingSlash && len(n.children) == 1 {
|
||||||
|
// No handle found. Check if a handle for this path + a
|
||||||
|
// trailing slash exists
|
||||||
|
n = n.children[0]
|
||||||
|
if n.path == "/" && n.handlers != nil {
|
||||||
|
return append(ciPath, '/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case catchAll:
|
||||||
|
return append(ciPath, path...)
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("invalid node type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing found.
|
||||||
|
// Try to fix the path by adding / removing a trailing slash
|
||||||
|
if fixTrailingSlash {
|
||||||
|
if path == "/" {
|
||||||
|
return ciPath
|
||||||
|
}
|
||||||
|
if len(path)+1 == npLen && n.path[len(path)] == '/' &&
|
||||||
|
strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handlers != nil {
|
||||||
|
return append(ciPath, n.path...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
199
util.go
199
util.go
@ -2,20 +2,30 @@ package nf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/loveuer/nf/internal/schema"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func verifyHandlers(path string, handlers ...HandlerFunc) {
|
const (
|
||||||
if len(handlers) == 0 {
|
MIMETextXML = "text/xml"
|
||||||
panic(fmt.Sprintf("missing handler in route: %s", path))
|
MIMETextHTML = "text/html"
|
||||||
}
|
MIMETextPlain = "text/plain"
|
||||||
|
MIMETextJavaScript = "text/javascript"
|
||||||
|
MIMEApplicationXML = "application/xml"
|
||||||
|
MIMEApplicationJSON = "application/json"
|
||||||
|
MIMEApplicationForm = "application/x-www-form-urlencoded"
|
||||||
|
MIMEOctetStream = "application/octet-stream"
|
||||||
|
MIMEMultipartForm = "multipart/form-data"
|
||||||
|
|
||||||
for _, handler := range handlers {
|
MIMETextXMLCharsetUTF8 = "text/xml; charset=utf-8"
|
||||||
if handler == nil {
|
MIMETextHTMLCharsetUTF8 = "text/html; charset=utf-8"
|
||||||
panic(fmt.Sprintf("nil handler found in route: %s", path))
|
MIMETextPlainCharsetUTF8 = "text/plain; charset=utf-8"
|
||||||
}
|
MIMETextJavaScriptCharsetUTF8 = "text/javascript; charset=utf-8"
|
||||||
}
|
MIMEApplicationXMLCharsetUTF8 = "application/xml; charset=utf-8"
|
||||||
}
|
MIMEApplicationJSONCharsetUTF8 = "application/json; charset=utf-8"
|
||||||
|
// Deprecated: use MIMETextJavaScriptCharsetUTF8 instead
|
||||||
|
MIMEApplicationJavaScriptCharsetUTF8 = "application/javascript; charset=utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
// parseVendorSpecificContentType check if content type is vendor specific and
|
// parseVendorSpecificContentType check if content type is vendor specific and
|
||||||
// if it is parsable to any known types. If it's not vendor specific then returns
|
// if it is parsable to any known types. If it's not vendor specific then returns
|
||||||
@ -44,3 +54,172 @@ func parseVendorSpecificContentType(cType string) string {
|
|||||||
|
|
||||||
return cType[0:slashIndex+1] + parsableType
|
return cType[0:slashIndex+1] + parsableType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseToStruct(aliasTag string, out interface{}, data map[string][]string) error {
|
||||||
|
schemaDecoder := schema.NewDecoder()
|
||||||
|
schemaDecoder.SetAliasTag(aliasTag)
|
||||||
|
|
||||||
|
if err := schemaDecoder.Decode(out, data); err != nil {
|
||||||
|
return fmt.Errorf("failed to decode: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func elsePanic(guard bool, text string) {
|
||||||
|
if !guard {
|
||||||
|
panic(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanPath(p string) string {
|
||||||
|
const stackBufSize = 128
|
||||||
|
// Turn empty string into "/"
|
||||||
|
if p == "" {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reasonably sized buffer on stack to avoid allocations in the common case.
|
||||||
|
// If a larger buffer is required, it gets allocated dynamically.
|
||||||
|
buf := make([]byte, 0, stackBufSize)
|
||||||
|
|
||||||
|
n := len(p)
|
||||||
|
|
||||||
|
// Invariants:
|
||||||
|
// reading from path; r is index of next byte to process.
|
||||||
|
// writing to buf; w is index of next byte to write.
|
||||||
|
|
||||||
|
// path must start with '/'
|
||||||
|
r := 1
|
||||||
|
w := 1
|
||||||
|
|
||||||
|
if p[0] != '/' {
|
||||||
|
r = 0
|
||||||
|
|
||||||
|
if n+1 > stackBufSize {
|
||||||
|
buf = make([]byte, n+1)
|
||||||
|
} else {
|
||||||
|
buf = buf[:n+1]
|
||||||
|
}
|
||||||
|
buf[0] = '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
trailing := n > 1 && p[n-1] == '/'
|
||||||
|
|
||||||
|
// A bit more clunky without a 'lazybuf' like the path package, but the loop
|
||||||
|
// gets completely inlined (bufApp calls).
|
||||||
|
// loop has no expensive function calls (except 1x make) // So in contrast to the path package this loop has no expensive function
|
||||||
|
// calls (except make, if needed).
|
||||||
|
|
||||||
|
for r < n {
|
||||||
|
switch {
|
||||||
|
case p[r] == '/':
|
||||||
|
// empty path element, trailing slash is added after the end
|
||||||
|
r++
|
||||||
|
|
||||||
|
case p[r] == '.' && r+1 == n:
|
||||||
|
trailing = true
|
||||||
|
r++
|
||||||
|
|
||||||
|
case p[r] == '.' && p[r+1] == '/':
|
||||||
|
// . element
|
||||||
|
r += 2
|
||||||
|
|
||||||
|
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
|
||||||
|
// .. element: remove to last /
|
||||||
|
r += 3
|
||||||
|
|
||||||
|
if w > 1 {
|
||||||
|
// can backtrack
|
||||||
|
w--
|
||||||
|
|
||||||
|
if len(buf) == 0 {
|
||||||
|
for w > 1 && p[w] != '/' {
|
||||||
|
w--
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for w > 1 && buf[w] != '/' {
|
||||||
|
w--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Real path element.
|
||||||
|
// Add slash if needed
|
||||||
|
if w > 1 {
|
||||||
|
bufApp(&buf, p, w, '/')
|
||||||
|
w++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy element
|
||||||
|
for r < n && p[r] != '/' {
|
||||||
|
bufApp(&buf, p, w, p[r])
|
||||||
|
w++
|
||||||
|
r++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-append trailing slash
|
||||||
|
if trailing && w > 1 {
|
||||||
|
bufApp(&buf, p, w, '/')
|
||||||
|
w++
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the original string was not modified (or only shortened at the end),
|
||||||
|
// return the respective substring of the original string.
|
||||||
|
// Otherwise return a new string from the buffer.
|
||||||
|
if len(buf) == 0 {
|
||||||
|
return p[:w]
|
||||||
|
}
|
||||||
|
return string(buf[:w])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal helper to lazily create a buffer if necessary.
|
||||||
|
// Calls to this function get inlined.
|
||||||
|
func bufApp(buf *[]byte, s string, w int, c byte) {
|
||||||
|
b := *buf
|
||||||
|
if len(b) == 0 {
|
||||||
|
// No modification of the original string so far.
|
||||||
|
// If the next character is the same as in the original string, we do
|
||||||
|
// not yet have to allocate a buffer.
|
||||||
|
if s[w] == c {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise use either the stack buffer, if it is large enough, or
|
||||||
|
// allocate a new buffer on the heap, and copy all previous characters.
|
||||||
|
length := len(s)
|
||||||
|
if length > cap(b) {
|
||||||
|
*buf = make([]byte, length)
|
||||||
|
} else {
|
||||||
|
*buf = (*buf)[:length]
|
||||||
|
}
|
||||||
|
b = *buf
|
||||||
|
|
||||||
|
copy(b, s[:w])
|
||||||
|
}
|
||||||
|
b[w] = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func HumanDuration(nano int64) string {
|
||||||
|
duration := float64(nano)
|
||||||
|
unit := "ns"
|
||||||
|
if duration >= 1000 {
|
||||||
|
duration /= 1000
|
||||||
|
unit = "us"
|
||||||
|
}
|
||||||
|
|
||||||
|
if duration >= 1000 {
|
||||||
|
duration /= 1000
|
||||||
|
unit = "ms"
|
||||||
|
}
|
||||||
|
|
||||||
|
if duration >= 1000 {
|
||||||
|
duration /= 1000
|
||||||
|
unit = " s"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%6.2f%s", duration, unit)
|
||||||
|
}
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
go ft()
|
|
||||||
go gint()
|
|
||||||
|
|
||||||
select {}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ft() {
|
|
||||||
app := fiber.New()
|
|
||||||
app.Use(logger.New())
|
|
||||||
|
|
||||||
app.Get("/hello", func(ctx *fiber.Ctx) error {
|
|
||||||
return ctx.BodyParser(nil)
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Fatal(app.Listen(":9989"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func gint() {
|
|
||||||
app := gin.Default()
|
|
||||||
|
|
||||||
app.POST("/data", func(c *gin.Context) {
|
|
||||||
type Req struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Age int `json:"age"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
req = new(Req)
|
|
||||||
rm = make(map[string]any)
|
|
||||||
)
|
|
||||||
|
|
||||||
if err = c.Bind(req); err != nil {
|
|
||||||
log.Print("err 1:", err)
|
|
||||||
c.String(500, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = c.Bind(&rm); err != nil {
|
|
||||||
log.Print("err 2:", err)
|
|
||||||
c.String(500, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(200, gin.H{"rm": rm, "req": req})
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Fatal(app.Run(":9998"))
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user