Compare commits
No commits in common. "master" and "dev" have entirely different histories.
62
.github/workflows/nfctl.yml
vendored
62
.github/workflows/nfctl.yml
vendored
@ -1,62 +0,0 @@
|
|||||||
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,4 +1,3 @@
|
|||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
.DS_Store
|
.DS_Store
|
||||||
xtest
|
|
313
app.go
313
app.go
@ -1,303 +1,62 @@
|
|||||||
package nf
|
package nf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"strings"
|
||||||
"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
|
||||||
config *Config
|
router *router
|
||||||
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) {
|
||||||
var (
|
c := newContext(writer, request)
|
||||||
err error
|
for _, group := range a.groups {
|
||||||
c = a.pool.Get().(*Ctx)
|
if strings.HasPrefix(request.URL.Path, group.prefix) {
|
||||||
nfe = new(Err)
|
c.handlers = append(c.handlers, group.middlewares...)
|
||||||
)
|
|
||||||
|
|
||||||
c.reset(writer, request)
|
|
||||||
|
|
||||||
if err = c.verify(); err != nil {
|
|
||||||
if errors.As(err, nfe) {
|
|
||||||
_ = c.Status(nfe.Status).SendString(nfe.Msg)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = c.Status(500).SendString(err.Error())
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a.handleHTTPRequest(c)
|
if err := a.router.handle(c); err != nil {
|
||||||
|
writer.WriteHeader(500)
|
||||||
a.pool.Put(c)
|
_, _ = writer.Write([]byte(err.Error()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) run(ln net.Listener) error {
|
func (a *App) Get(path string, handlers ...HandlerFunc) {
|
||||||
srv := &http.Server{Handler: a}
|
verifyHandlers(path, handlers...)
|
||||||
|
a.router.addRoute(http.MethodGet, path, handlers...)
|
||||||
|
}
|
||||||
|
|
||||||
if a.config.DisableHttpErrorLog {
|
func (a *App) Post(path string, handlers ...HandlerFunc) {
|
||||||
srv.ErrorLog = log.New(io.Discard, "", 0)
|
verifyHandlers(path, handlers...)
|
||||||
}
|
a.router.addRoute(http.MethodPost, path, handlers...)
|
||||||
|
}
|
||||||
|
|
||||||
a.server = srv
|
func (a *App) Delete(path string, handlers ...HandlerFunc) {
|
||||||
|
verifyHandlers(path, handlers...)
|
||||||
|
a.router.addRoute(http.MethodDelete, path, handlers...)
|
||||||
|
}
|
||||||
|
|
||||||
if !a.config.DisableBanner {
|
func (a *App) Put(path string, handlers ...HandlerFunc) {
|
||||||
fmt.Println(banner + "nf serve at: " + ln.Addr().String() + "\n")
|
verifyHandlers(path, handlers...)
|
||||||
}
|
a.router.addRoute(http.MethodPut, path, handlers...)
|
||||||
|
}
|
||||||
|
|
||||||
err := a.server.Serve(ln)
|
func (a *App) Patch(path string, handlers ...HandlerFunc) {
|
||||||
if !errors.Is(err, http.ErrServerClosed) || a.config.ErrServeClose {
|
verifyHandlers(path, handlers...)
|
||||||
return err
|
a.router.addRoute(http.MethodPatch, path, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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 {
|
||||||
ln, err := net.Listen("tcp", address)
|
fmt.Println(banner + "nf serve at: " + address + "\n")
|
||||||
if err != nil {
|
return http.ListenAndServe(address, a)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
349
ctx.go
349
ctx.go
@ -1,69 +1,40 @@
|
|||||||
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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var forwardHeaders = []string{"CF-Connecting-IP", "X-Forwarded-For", "X-Real-Ip"}
|
type Map map[string]interface{}
|
||||||
|
|
||||||
type Ctx struct {
|
type Ctx struct {
|
||||||
lock sync.Mutex
|
// origin objects
|
||||||
writermem responseWriter
|
Writer http.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
|
||||||
|
|
||||||
app *App
|
params map[string]string
|
||||||
params *Params
|
index int
|
||||||
index int
|
handlers []HandlerFunc
|
||||||
handlers []HandlerFunc
|
locals map[string]any
|
||||||
locals map[string]interface{}
|
|
||||||
skippedNodes *[]skippedNode
|
|
||||||
fullPath string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Ctx) reset(w http.ResponseWriter, r *http.Request) {
|
func newContext(writer http.ResponseWriter, request *http.Request) *Ctx {
|
||||||
traceId := r.Header.Get(TraceKey)
|
return &Ctx{
|
||||||
if traceId == "" {
|
Writer: writer,
|
||||||
traceId = uuid.Must(uuid.NewV7()).String()
|
Request: request,
|
||||||
|
path: request.URL.Path,
|
||||||
|
Method: request.Method,
|
||||||
|
index: -1,
|
||||||
|
locals: map[string]any{},
|
||||||
|
handlers: make([]HandlerFunc, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
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{} {
|
func (c *Ctx) Locals(key string, value ...any) any {
|
||||||
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]
|
||||||
@ -72,16 +43,6 @@ func (c *Ctx) Locals(key string, value ...interface{}) interface{} {
|
|||||||
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] != "" {
|
||||||
@ -91,274 +52,14 @@ 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)
|
||||||
if c.index >= len(c.handlers) {
|
for ; c.index < s; c.index++ {
|
||||||
return nil
|
if err := c.handlers[c.index](c); err != 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
16
error.go
@ -1,16 +0,0 @@
|
|||||||
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}
|
|
||||||
}
|
|
37
example/body/main.go
Normal file
37
example/body/main.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
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"))
|
||||||
|
}
|
32
example/simple/logger.go
Normal file
32
example/simple/logger.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
50
example/simple/main.go
Normal file
50
example/simple/main.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
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,39 +1,44 @@
|
|||||||
module github.com/loveuer/nf
|
module nf
|
||||||
|
|
||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/fatih/color v1.17.0
|
github.com/gin-gonic/gin v1.9.1
|
||||||
github.com/go-git/go-git/v5 v5.12.0
|
github.com/gofiber/fiber/v2 v2.52.0
|
||||||
github.com/google/uuid v1.6.0
|
|
||||||
github.com/spf13/cobra v1.8.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.0 // indirect
|
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
github.com/cloudflare/circl v1.3.7 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||||
github.com/go-resty/resty/v2 v2.16.2
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/google/uuid v1.5.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/klauspost/compress v1.17.0 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.4 // 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/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/skeema/knownhosts v1.2.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
golang.org/x/crypto v0.25.0 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
golang.org/x/mod v0.12.0 // indirect
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
golang.org/x/net v0.27.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||||
golang.org/x/tools v0.13.0 // indirect
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
golang.org/x/arch v0.3.0 // 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,151 +1,107 @@
|
|||||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||||
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
|
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||||
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
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/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||||
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
|
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
|
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||||
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
|
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
github.com/gofiber/fiber/v2 v2.52.0 h1:S+qXi7y+/Pgvqq4DrSmREGiFwtB7Bu6+QFLuIHYw/UE=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/gofiber/fiber/v2 v2.52.0/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||||
|
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/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||||
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
github.com/stretchr/testify v1.8.1/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.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/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.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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
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/check.v1 v1.0.0-20190902080502-41f04d3bba15/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-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
Normal file
60
group.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
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,9 +1,3 @@
|
|||||||
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()))
|
|
||||||
}
|
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
// 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))
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
// 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))
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
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.
|
|
@ -1,305 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
@ -1,145 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
@ -1,534 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,148 +0,0 @@
|
|||||||
// 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
|
|
@ -1,202 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
@ -1,106 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
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}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
88
nf.go
88
nf.go
@ -1,99 +1,19 @@
|
|||||||
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{
|
||||||
RouterGroup: RouterGroup{
|
router: newRouter(),
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(config) > 0 {
|
app.RouterGroup = &RouterGroup{app: app}
|
||||||
app.config = &config[0]
|
app.groups = []*RouterGroup{app.RouterGroup}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -1,123 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
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
115
nft/log/log.go
@ -1,115 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,149 +0,0 @@
|
|||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
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...)
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
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 .`"
|
|
@ -1,13 +0,0 @@
|
|||||||
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 = ` ___ __ __
|
|
||||||
___ / _/___/ /_/ /
|
|
||||||
/ _ \/ _/ __/ __/ /
|
|
||||||
/_//_/_/ \__/\__/_/
|
|
||||||
`
|
|
@ -1,10 +0,0 @@
|
|||||||
package opt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBanner(t *testing.T) {
|
|
||||||
fmt.Printf("%s\nnfctl: %s\n\n", Banner, Version)
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
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
140
nft/resp/resp.go
@ -1,140 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
package resp
|
|
||||||
|
|
||||||
const (
|
|
||||||
MSG200 = "请求成功"
|
|
||||||
MSG202 = "请求成功, 请稍后..."
|
|
||||||
MSG400 = "请求参数错误"
|
|
||||||
MSG401 = "登录已过期, 请重新登录"
|
|
||||||
MSG403 = "请求权限不足"
|
|
||||||
MSG404 = "请求资源未找到"
|
|
||||||
MSG418 = "请求条件不满足, 请稍后再试"
|
|
||||||
MSG429 = "请求过于频繁, 请稍后再试"
|
|
||||||
MSG500 = "服务器开小差了, 请稍后再试"
|
|
||||||
MSG501 = "功能开发中, 尽情期待"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
RealStatusHeader = "NF-STATUS"
|
|
||||||
)
|
|
@ -1,40 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
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
102
readme.md
@ -1,102 +0,0 @@
|
|||||||
# 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
Normal file
45
req.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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
Normal file
49
resp.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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
|
||||||
|
}
|
@ -1,134 +0,0 @@
|
|||||||
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
Normal file
95
router.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
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
155
routergroup.go
@ -1,155 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
917
tree.go
917
tree.go
@ -1,891 +1,76 @@
|
|||||||
package nf
|
package nf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"github.com/loveuer/nf/internal/bytesconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
type _node struct {
|
||||||
strColon = []byte(":")
|
pattern string
|
||||||
strStar = []byte("*")
|
part string
|
||||||
strSlash = []byte("/")
|
children []*_node
|
||||||
)
|
isWild bool
|
||||||
|
|
||||||
// Param is a single URL parameter, consisting of a key and a value.
|
|
||||||
type Param struct {
|
|
||||||
Key string
|
|
||||||
Value string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Params is a Param-slice, as returned by the router.
|
func (n *_node) insert(pattern string, parts []string, height int) {
|
||||||
// The slice is ordered, the first URL parameter is also the first slice value.
|
if len(parts) == height {
|
||||||
// It is therefore safe to read values by the index.
|
n.pattern = pattern
|
||||||
type Params []Param
|
return
|
||||||
|
|
||||||
// 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.
|
part := parts[height]
|
||||||
// If no matching Param is found, an empty string is returned.
|
child := n.matchChild(part)
|
||||||
func (ps Params) ByName(name string) (va string) {
|
if child == nil {
|
||||||
va, _ = ps.Get(name)
|
child = &_node{part: part, isWild: part[0] == ':' || part[0] == '*'}
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type methodTree struct {
|
|
||||||
method string
|
|
||||||
root *node
|
|
||||||
}
|
|
||||||
|
|
||||||
type methodTrees []methodTree
|
|
||||||
|
|
||||||
func (trees methodTrees) get(method string) *node {
|
|
||||||
for _, tree := range trees {
|
|
||||||
if tree.method == method {
|
|
||||||
return tree.root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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)
|
n.children = append(n.children, child)
|
||||||
}
|
}
|
||||||
|
child.insert(pattern, parts, height+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func countParams(path string) uint16 {
|
func (n *_node) search(parts []string, height int) *_node {
|
||||||
var n uint16
|
if len(parts) == height || strings.HasPrefix(n.part, "*") {
|
||||||
s := bytesconv.StringToBytes(path)
|
if n.pattern == "" {
|
||||||
n += uint16(bytes.Count(s, strColon))
|
|
||||||
n += uint16(bytes.Count(s, strStar))
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func countSections(path string) uint16 {
|
|
||||||
s := bytesconv.StringToBytes(path)
|
|
||||||
return uint16(bytes.Count(s, strSlash))
|
|
||||||
}
|
|
||||||
|
|
||||||
type nodeType uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
// If this node does not have a wildcard (param or catchAll) child,
|
part := parts[height]
|
||||||
// we can just look up the next child node and continue to walk down
|
children := n.matchChildren(part)
|
||||||
// the tree
|
|
||||||
if !n.wildChild {
|
|
||||||
// Skip rune bytes already processed
|
|
||||||
rb = shiftNRuneBytes(rb, npLen)
|
|
||||||
|
|
||||||
if rb[0] != 0 {
|
for _, child := range children {
|
||||||
// Old rune not finished
|
result := child.search(parts, height+1)
|
||||||
idxc := rb[0]
|
if result != nil {
|
||||||
for i, c := range []byte(n.indices) {
|
return result
|
||||||
if c == idxc {
|
|
||||||
// continue with child node
|
|
||||||
n = n.children[i]
|
|
||||||
npLen = len(n.path)
|
|
||||||
continue walk
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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.
|
return nil
|
||||||
// Try to fix the path by adding / removing a trailing slash
|
}
|
||||||
if fixTrailingSlash {
|
|
||||||
if path == "/" {
|
func (n *_node) travel(list *([]*_node)) {
|
||||||
return ciPath
|
if n.pattern != "" {
|
||||||
}
|
*list = append(*list, n)
|
||||||
if len(path)+1 == npLen && n.path[len(path)] == '/' &&
|
}
|
||||||
strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handlers != nil {
|
for _, child := range n.children {
|
||||||
return append(ciPath, n.path...)
|
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 {
|
||||||
|
nodes := make([]*_node, 0)
|
||||||
|
for _, child := range n.children {
|
||||||
|
if child.part == part || child.isWild {
|
||||||
|
nodes = append(nodes, child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
199
util.go
199
util.go
@ -2,30 +2,20 @@ package nf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/loveuer/nf/internal/schema"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
func verifyHandlers(path string, handlers ...HandlerFunc) {
|
||||||
MIMETextXML = "text/xml"
|
if len(handlers) == 0 {
|
||||||
MIMETextHTML = "text/html"
|
panic(fmt.Sprintf("missing handler in route: %s", path))
|
||||||
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"
|
|
||||||
|
|
||||||
MIMETextXMLCharsetUTF8 = "text/xml; charset=utf-8"
|
for _, handler := range handlers {
|
||||||
MIMETextHTMLCharsetUTF8 = "text/html; charset=utf-8"
|
if handler == nil {
|
||||||
MIMETextPlainCharsetUTF8 = "text/plain; charset=utf-8"
|
panic(fmt.Sprintf("nil handler found in route: %s", path))
|
||||||
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
|
||||||
@ -54,172 +44,3 @@ 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)
|
|
||||||
}
|
|
||||||
|
59
xtest/main.go
Normal file
59
xtest/main.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
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