Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
88788776d2 | |||
e143e2b547 | |||
a36f99e30b | |||
326ee53333 | |||
1244c5bb4d | |||
4ccc8a9c85 | |||
8a2e84d489 | |||
d29363b78d | |||
734f751055 | |||
55d8a9ee20 | |||
07c93042ba | |||
b9bbfdf159 | |||
fbad257acc | |||
1829febb6e | |||
7838fa62a8 | |||
332803d8de | |||
11c868d476 | |||
38d6e482d7 | |||
07d4085201 | |||
6af837fd25 |
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -3,6 +3,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- v3
|
||||
jobs:
|
||||
test:
|
||||
name: test
|
||||
|
21
.github/workflows/codeql-analysis.yml
vendored
21
.github/workflows/codeql-analysis.yml
vendored
@@ -9,7 +9,7 @@
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
name: "codeql"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
@@ -17,16 +17,16 @@ on:
|
||||
types:
|
||||
- completed
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ master, v3 ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
branches: [ master, v3 ]
|
||||
schedule:
|
||||
- cron: '34 1 * * 0'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
name: analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
@@ -42,11 +42,14 @@ jobs:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: setup
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
- name: init
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
@@ -57,7 +60,7 @@ jobs:
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
- name: autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
@@ -71,5 +74,5 @@ jobs:
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
- name: analyze
|
||||
uses: github/codeql-action/analyze@v1
|
||||
|
75
.github/workflows/dependabot-automerge.yml
vendored
75
.github/workflows/dependabot-automerge.yml
vendored
@@ -1,66 +1,31 @@
|
||||
name: "prautomerge"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["prbuild"]
|
||||
types:
|
||||
- completed
|
||||
pull_request_target:
|
||||
types: [assigned, opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
Dependabot-Automerge:
|
||||
dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
# Contains workaround to execute if dependabot updates the PR by checking for the base branch in the linked PR
|
||||
# The the github.event.workflow_run.event value is 'push' and not 'pull_request'
|
||||
# dont work with multiple workflows when last returns success
|
||||
if: >-
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
&& github.actor == 'dependabot[bot]'
|
||||
&& github.event.sender.login == 'dependabot[bot]'
|
||||
&& github.event.sender.type == 'Bot'
|
||||
&& (github.event.workflow_run.event == 'pull_request'
|
||||
|| (github.event.workflow_run.event == 'push' && github.event.workflow_run.pull_requests[0].base.ref == github.event.repository.default_branch ))
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
steps:
|
||||
- name: Approve Changes and Merge changes if label 'dependencies' is set
|
||||
uses: actions/github-script@v5
|
||||
- name: metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v1.1.1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
console.log(context.payload.workflow_run);
|
||||
|
||||
var labelNames = await github.paginate(
|
||||
github.issues.listLabelsOnIssue,
|
||||
{
|
||||
repo: context.repo.repo,
|
||||
owner: context.repo.owner,
|
||||
issue_number: context.payload.workflow_run.pull_requests[0].number,
|
||||
},
|
||||
(response) => response.data.map(
|
||||
(label) => label.name
|
||||
)
|
||||
);
|
||||
|
||||
console.log(labelNames);
|
||||
|
||||
if (labelNames.includes('dependencies')) {
|
||||
console.log('Found label');
|
||||
|
||||
await github.pulls.createReview({
|
||||
repo: context.repo.repo,
|
||||
owner: context.repo.owner,
|
||||
pull_number: context.payload.workflow_run.pull_requests[0].number,
|
||||
event: 'APPROVE'
|
||||
});
|
||||
console.log('Approved PR');
|
||||
|
||||
await github.pulls.merge({
|
||||
repo: context.repo.repo,
|
||||
owner: context.repo.owner,
|
||||
pull_number: context.payload.workflow_run.pull_requests[0].number,
|
||||
});
|
||||
|
||||
console.log('Merged PR');
|
||||
}
|
||||
github-token: "${{ secrets.TOKEN }}"
|
||||
- name: approve
|
||||
run: gh pr review --approve "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{github.event.pull_request.html_url}}
|
||||
GITHUB_TOKEN: ${{secrets.TOKEN}}
|
||||
- name: merge
|
||||
if: ${{contains(steps.metadata.outputs.dependency-names, 'go.unistack.org')}}
|
||||
run: gh pr merge --auto --merge "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{github.event.pull_request.html_url}}
|
||||
GITHUB_TOKEN: ${{secrets.TOKEN}}
|
||||
|
1
.github/workflows/pr.yml
vendored
1
.github/workflows/pr.yml
vendored
@@ -3,6 +3,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- v3
|
||||
jobs:
|
||||
test:
|
||||
name: test
|
||||
|
@@ -49,6 +49,7 @@ type Message interface {
|
||||
Topic() string
|
||||
Payload() interface{}
|
||||
ContentType() string
|
||||
Metadata() metadata.Metadata
|
||||
}
|
||||
|
||||
// Request is the interface for a synchronous request used by Call or Stream
|
||||
|
@@ -12,7 +12,7 @@ import (
|
||||
type LookupFunc func(context.Context, Request, CallOptions) ([]string, error)
|
||||
|
||||
// LookupRoute for a request using the router and then choose one using the selector
|
||||
func LookupRoute(ctx context.Context, req Request, opts CallOptions) ([]string, error) {
|
||||
func LookupRoute(_ context.Context, req Request, opts CallOptions) ([]string, error) {
|
||||
// check to see if an address was provided as a call option
|
||||
if len(opts.Address) > 0 {
|
||||
return opts.Address, nil
|
||||
|
@@ -139,6 +139,10 @@ func (n *noopMessage) ContentType() string {
|
||||
return n.opts.ContentType
|
||||
}
|
||||
|
||||
func (n *noopMessage) Metadata() metadata.Metadata {
|
||||
return n.opts.Metadata
|
||||
}
|
||||
|
||||
func (n *noopClient) newCodec(contentType string) (codec.Codec, error) {
|
||||
if cf, ok := n.opts.Codecs[contentType]; ok {
|
||||
return cf, nil
|
||||
|
@@ -8,6 +8,7 @@ import (
|
||||
"go.unistack.org/micro/v3/broker"
|
||||
"go.unistack.org/micro/v3/codec"
|
||||
"go.unistack.org/micro/v3/logger"
|
||||
"go.unistack.org/micro/v3/metadata"
|
||||
"go.unistack.org/micro/v3/meter"
|
||||
"go.unistack.org/micro/v3/network/transport"
|
||||
"go.unistack.org/micro/v3/register"
|
||||
@@ -128,7 +129,7 @@ type PublishOptions struct {
|
||||
|
||||
// NewMessageOptions creates message options struct
|
||||
func NewMessageOptions(opts ...MessageOption) MessageOptions {
|
||||
options := MessageOptions{}
|
||||
options := MessageOptions{Metadata: metadata.New(1)}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
@@ -137,7 +138,10 @@ func NewMessageOptions(opts ...MessageOption) MessageOptions {
|
||||
|
||||
// MessageOptions holds client message options
|
||||
type MessageOptions struct {
|
||||
// Metadata additional metadata
|
||||
Metadata metadata.Metadata
|
||||
// ContentType specify content-type of message
|
||||
// deprecated
|
||||
ContentType string
|
||||
}
|
||||
|
||||
@@ -517,6 +521,7 @@ func WithSelectOptions(sops ...selector.SelectOption) CallOption {
|
||||
// Deprecated
|
||||
func WithMessageContentType(ct string) MessageOption {
|
||||
return func(o *MessageOptions) {
|
||||
o.Metadata.Set(metadata.HeaderContentType, ct)
|
||||
o.ContentType = ct
|
||||
}
|
||||
}
|
||||
@@ -524,10 +529,18 @@ func WithMessageContentType(ct string) MessageOption {
|
||||
// MessageContentType sets the message content type
|
||||
func MessageContentType(ct string) MessageOption {
|
||||
return func(o *MessageOptions) {
|
||||
o.Metadata.Set(metadata.HeaderContentType, ct)
|
||||
o.ContentType = ct
|
||||
}
|
||||
}
|
||||
|
||||
// MessageMetadata sets the message metadata
|
||||
func MessageMetadata(k, v string) MessageOption {
|
||||
return func(o *MessageOptions) {
|
||||
o.Metadata.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// StreamingRequest specifies that request is streaming
|
||||
func StreamingRequest(b bool) RequestOption {
|
||||
return func(o *RequestOptions) {
|
||||
|
@@ -20,7 +20,7 @@ func RetryNever(ctx context.Context, req Request, retryCount int, err error) (bo
|
||||
}
|
||||
|
||||
// RetryOnError retries a request on a 500 or timeout error
|
||||
func RetryOnError(ctx context.Context, req Request, retryCount int, err error) (bool, error) {
|
||||
func RetryOnError(_ context.Context, _ Request, _ int, err error) (bool, error) {
|
||||
if err == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
@@ -5,29 +5,40 @@ type Frame struct {
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// NewFrame returns new frame with data
|
||||
func NewFrame(data []byte) *Frame {
|
||||
return &Frame{Data: data}
|
||||
}
|
||||
|
||||
// MarshalJSON returns frame data
|
||||
func (m *Frame) MarshalJSON() ([]byte, error) {
|
||||
return m.Data, nil
|
||||
return m.Marshal()
|
||||
}
|
||||
|
||||
// UnmarshalJSON set frame data
|
||||
func (m *Frame) UnmarshalJSON(data []byte) error {
|
||||
m.Data = data
|
||||
return nil
|
||||
return m.Unmarshal(data)
|
||||
}
|
||||
|
||||
// ProtoMessage noop func
|
||||
func (m *Frame) ProtoMessage() {}
|
||||
|
||||
// Reset resets frame
|
||||
func (m *Frame) Reset() {
|
||||
*m = Frame{}
|
||||
}
|
||||
|
||||
// String returns frame as string
|
||||
func (m *Frame) String() string {
|
||||
return string(m.Data)
|
||||
}
|
||||
|
||||
// Marshal returns frame data
|
||||
func (m *Frame) Marshal() ([]byte, error) {
|
||||
return m.Data, nil
|
||||
}
|
||||
|
||||
// Unmarshal set frame data
|
||||
func (m *Frame) Unmarshal(data []byte) error {
|
||||
m.Data = data
|
||||
return nil
|
||||
|
@@ -23,6 +23,8 @@ var (
|
||||
ErrInvalidStruct = errors.New("invalid struct specified")
|
||||
// ErrWatcherStopped is returned when source watcher has been stopped
|
||||
ErrWatcherStopped = errors.New("watcher stopped")
|
||||
// ErrWatcherNotImplemented returned when config does not implement watch
|
||||
ErrWatcherNotImplemented = errors.New("watcher not implemented")
|
||||
)
|
||||
|
||||
// Config is an interface abstraction for dynamic configuration
|
||||
|
@@ -32,3 +32,33 @@ func SetOption(k, v interface{}) Option {
|
||||
o.Context = context.WithValue(o.Context, k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// SetSaveOption returns a function to setup a context with given value
|
||||
func SetSaveOption(k, v interface{}) SaveOption {
|
||||
return func(o *SaveOptions) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// SetLoadOption returns a function to setup a context with given value
|
||||
func SetLoadOption(k, v interface{}) LoadOption {
|
||||
return func(o *LoadOptions) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// SetWatchOption returns a function to setup a context with given value
|
||||
func SetWatchOption(k, v interface{}) WatchOption {
|
||||
return func(o *WatchOptions) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, k, v)
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@ package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -271,7 +270,7 @@ func (c *defaultConfig) Name() string {
|
||||
}
|
||||
|
||||
func (c *defaultConfig) Watch(ctx context.Context, opts ...WatchOption) (Watcher, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
return nil, ErrWatcherNotImplemented
|
||||
}
|
||||
|
||||
// NewConfig returns new default config source
|
||||
|
@@ -66,6 +66,7 @@ type LoadOptions struct {
|
||||
Struct interface{}
|
||||
Override bool
|
||||
Append bool
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
func NewLoadOptions(opts ...LoadOption) LoadOptions {
|
||||
@@ -102,7 +103,8 @@ type SaveOption func(o *SaveOptions)
|
||||
|
||||
// SaveOptions struct
|
||||
type SaveOptions struct {
|
||||
Struct interface{}
|
||||
Struct interface{}
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
// SaveStruct override struct for save to config
|
||||
|
@@ -16,6 +16,7 @@ import (
|
||||
"go.unistack.org/micro/v3/network/transport"
|
||||
"go.unistack.org/micro/v3/register"
|
||||
"go.unistack.org/micro/v3/tracer"
|
||||
"go.unistack.org/micro/v3/util/id"
|
||||
)
|
||||
|
||||
// Option func
|
||||
@@ -106,7 +107,7 @@ func NewOptions(opts ...Option) Options {
|
||||
Address: DefaultAddress,
|
||||
Name: DefaultName,
|
||||
Version: DefaultVersion,
|
||||
ID: DefaultID,
|
||||
ID: id.Must(),
|
||||
Namespace: DefaultNamespace,
|
||||
}
|
||||
|
||||
|
@@ -8,7 +8,6 @@ import (
|
||||
"go.unistack.org/micro/v3/codec"
|
||||
"go.unistack.org/micro/v3/metadata"
|
||||
"go.unistack.org/micro/v3/register"
|
||||
"go.unistack.org/micro/v3/util/id"
|
||||
)
|
||||
|
||||
// DefaultServer default server
|
||||
@@ -21,8 +20,6 @@ var (
|
||||
DefaultName = "server"
|
||||
// DefaultVersion will be used if no version passed
|
||||
DefaultVersion = "latest"
|
||||
// DefaultID will be used if no id passed
|
||||
DefaultID = id.Must()
|
||||
// DefaultRegisterCheck holds func that run before register server
|
||||
DefaultRegisterCheck = func(context.Context) error { return nil }
|
||||
// DefaultRegisterInterval holds interval for register
|
||||
|
@@ -1,239 +1,707 @@
|
||||
package http
|
||||
|
||||
// Radix tree implementation below is a based on the original work by
|
||||
// Armon Dadgar in https://github.com/armon/go-radix/blob/master/radix.go
|
||||
// (MIT licensed). It's been heavily modified for use as a HTTP routing tree.
|
||||
// Copied from chi mux tree.go https://raw.githubusercontent.com/go-chi/chi/master/tree.go
|
||||
// Modified by Unistack LLC to support interface{} type handler and parameters in map[string]string
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// TrieOptions contains search options
|
||||
type TrieOptions struct {
|
||||
IgnoreCase bool
|
||||
}
|
||||
|
||||
// TrieOption func signature
|
||||
type TrieOption func(*TrieOptions)
|
||||
|
||||
// IgnoreCase says that search must be case insensitive
|
||||
func IgnoreCase(b bool) TrieOption {
|
||||
return func(o *TrieOptions) {
|
||||
o.IgnoreCase = b
|
||||
}
|
||||
}
|
||||
|
||||
// Tree is a trie tree.
|
||||
type Trie struct {
|
||||
node *node
|
||||
rcache map[string]*regexp.Regexp
|
||||
rmu sync.RWMutex
|
||||
}
|
||||
|
||||
// node is a node of tree
|
||||
type node struct {
|
||||
actions map[string]interface{} // key is method, val is handler interface
|
||||
children map[string]*node // key is label of next nodes
|
||||
label string
|
||||
}
|
||||
type methodTyp uint
|
||||
|
||||
const (
|
||||
pathRoot string = "/"
|
||||
pathDelimiter string = "/"
|
||||
paramDelimiter string = ":"
|
||||
leftPtnDelimiter string = "{"
|
||||
rightPtnDelimiter string = "}"
|
||||
ptnWildcard string = "(.+)"
|
||||
mSTUB methodTyp = 1 << iota
|
||||
mCONNECT
|
||||
mDELETE
|
||||
mGET
|
||||
mHEAD
|
||||
mOPTIONS
|
||||
mPATCH
|
||||
mPOST
|
||||
mPUT
|
||||
mTRACE
|
||||
)
|
||||
|
||||
// NewTree creates a new trie tree.
|
||||
func NewTrie() *Trie {
|
||||
return &Trie{
|
||||
node: &node{
|
||||
label: pathRoot,
|
||||
actions: make(map[string]interface{}),
|
||||
children: make(map[string]*node),
|
||||
},
|
||||
rcache: make(map[string]*regexp.Regexp),
|
||||
var mALL = mCONNECT | mDELETE | mGET | mHEAD |
|
||||
mOPTIONS | mPATCH | mPOST | mPUT | mTRACE
|
||||
|
||||
var methodMap = map[string]methodTyp{
|
||||
http.MethodConnect: mCONNECT,
|
||||
http.MethodDelete: mDELETE,
|
||||
http.MethodGet: mGET,
|
||||
http.MethodHead: mHEAD,
|
||||
http.MethodOptions: mOPTIONS,
|
||||
http.MethodPatch: mPATCH,
|
||||
http.MethodPost: mPOST,
|
||||
http.MethodPut: mPUT,
|
||||
http.MethodTrace: mTRACE,
|
||||
}
|
||||
|
||||
// RegisterMethod adds support for custom HTTP method handlers, available
|
||||
// via Router#Method and Router#MethodFunc
|
||||
func RegisterMethod(method string) error {
|
||||
if method == "" {
|
||||
return nil
|
||||
}
|
||||
method = strings.ToUpper(method)
|
||||
if _, ok := methodMap[method]; ok {
|
||||
return nil
|
||||
}
|
||||
n := len(methodMap)
|
||||
if n > strconv.IntSize-2 {
|
||||
return fmt.Errorf("max number of methods reached (%d)", strconv.IntSize)
|
||||
}
|
||||
mt := methodTyp(2 << n)
|
||||
methodMap[method] = mt
|
||||
mALL |= mt
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type nodeTyp uint8
|
||||
|
||||
const (
|
||||
ntStatic nodeTyp = iota // /home
|
||||
ntRegexp // /{id:[0-9]+}
|
||||
ntParam // /{user}
|
||||
ntCatchAll // /api/v1/*
|
||||
)
|
||||
|
||||
func NewTrie() *Node {
|
||||
return &Node{}
|
||||
}
|
||||
|
||||
type Trie = Node
|
||||
|
||||
type Node struct {
|
||||
// regexp matcher for regexp nodes
|
||||
rex *regexp.Regexp
|
||||
|
||||
// HTTP handler endpoints on the leaf node
|
||||
endpoints endpoints
|
||||
|
||||
// prefix is the common prefix we ignore
|
||||
prefix string
|
||||
|
||||
// child nodes should be stored in-order for iteration,
|
||||
// in groups of the node type.
|
||||
children [ntCatchAll + 1]nodes
|
||||
|
||||
// first byte of the child prefix
|
||||
tail byte
|
||||
|
||||
// node type: static, regexp, param, catchAll
|
||||
typ nodeTyp
|
||||
|
||||
// first byte of the prefix
|
||||
label byte
|
||||
}
|
||||
|
||||
// endpoints is a mapping of http method constants to handlers
|
||||
// for a given route.
|
||||
type endpoints map[methodTyp]*endpoint
|
||||
|
||||
type endpoint struct {
|
||||
// endpoint handler
|
||||
handler interface{}
|
||||
// pattern is the routing pattern for handler nodes
|
||||
pattern string
|
||||
// parameters keys recorded on handler nodes
|
||||
paramKeys []string
|
||||
}
|
||||
|
||||
func (s endpoints) Value(method methodTyp) *endpoint {
|
||||
mh, ok := s[method]
|
||||
if !ok {
|
||||
mh = &endpoint{}
|
||||
s[method] = mh
|
||||
}
|
||||
|
||||
return mh
|
||||
}
|
||||
|
||||
func (n *Node) Insert(methods []string, pattern string, handler interface{}) error {
|
||||
var err error
|
||||
for _, method := range methods {
|
||||
if err = n.insert(methodMap[method], pattern, handler); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Node) insert(method methodTyp, pattern string, handler interface{}) error {
|
||||
var parent *Node
|
||||
search := pattern
|
||||
|
||||
for {
|
||||
// Handle key exhaustion
|
||||
if len(search) == 0 {
|
||||
// Insert or update the node's leaf handler
|
||||
return n.setEndpoint(method, handler, pattern)
|
||||
}
|
||||
|
||||
// We're going to be searching for a wild node next,
|
||||
// in this case, we need to get the tail
|
||||
label := search[0]
|
||||
var segTail byte
|
||||
var segEndIdx int
|
||||
var segTyp nodeTyp
|
||||
var segRexpat string
|
||||
var err error
|
||||
if label == '{' || label == '*' {
|
||||
segTyp, _, segRexpat, segTail, _, segEndIdx, err = patNextSegment(search)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var prefix string
|
||||
if segTyp == ntRegexp {
|
||||
prefix = segRexpat
|
||||
}
|
||||
|
||||
// Look for the edge to attach to
|
||||
parent = n
|
||||
n = n.getEdge(segTyp, label, segTail, prefix)
|
||||
|
||||
// No edge, create one
|
||||
if n == nil {
|
||||
child := &Node{typ: ntStatic, label: label, tail: segTail, prefix: search}
|
||||
var hn *Node
|
||||
hn, err = parent.addChild(child, search)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return hn.setEndpoint(method, handler, pattern)
|
||||
}
|
||||
|
||||
// Found an edge to match the pattern
|
||||
|
||||
if n.typ > ntStatic {
|
||||
// We found a param node, trim the param from the search path and continue.
|
||||
// This param/wild pattern segment would already be on the tree from a previous
|
||||
// call to addChild when creating a new node.
|
||||
search = search[segEndIdx:]
|
||||
continue
|
||||
}
|
||||
|
||||
// Static nodes fall below here.
|
||||
// Determine longest prefix of the search key on match.
|
||||
commonPrefix := longestPrefix(search, n.prefix)
|
||||
if commonPrefix == len(n.prefix) {
|
||||
// the common prefix is as long as the current node's prefix we're attempting to insert.
|
||||
// keep the search going.
|
||||
search = search[commonPrefix:]
|
||||
continue
|
||||
}
|
||||
|
||||
// Split the node
|
||||
child := &Node{
|
||||
typ: ntStatic,
|
||||
prefix: search[:commonPrefix],
|
||||
}
|
||||
if err = parent.replaceChild(search[0], segTail, child); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Restore the existing node
|
||||
n.label = n.prefix[commonPrefix]
|
||||
n.prefix = n.prefix[commonPrefix:]
|
||||
if _, err = child.addChild(n, n.prefix); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the new key is a subset, set the method/handler on this node and finish.
|
||||
search = search[commonPrefix:]
|
||||
if len(search) == 0 {
|
||||
return child.setEndpoint(method, handler, pattern)
|
||||
}
|
||||
|
||||
// Create a new edge for the node
|
||||
subchild := &Node{
|
||||
typ: ntStatic,
|
||||
label: search[0],
|
||||
prefix: search,
|
||||
}
|
||||
var hn *Node
|
||||
hn, err = child.addChild(subchild, search)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return hn.setEndpoint(method, handler, pattern)
|
||||
}
|
||||
}
|
||||
|
||||
// Insert inserts a route definition to tree.
|
||||
func (t *Trie) Insert(methods []string, path string, handler interface{}) {
|
||||
curNode := t.node
|
||||
if path == pathRoot {
|
||||
curNode.label = path
|
||||
for _, method := range methods {
|
||||
curNode.actions[method] = handler
|
||||
}
|
||||
return
|
||||
// addChild appends the new `child` node to the tree using the `pattern` as the trie key.
|
||||
// For a URL router like chi's, we split the static, param, regexp and wildcard segments
|
||||
// into different nodes. In addition, addChild will recursively call itself until every
|
||||
// pattern segment is added to the url pattern tree as individual nodes, depending on type.
|
||||
func (n *Node) addChild(child *Node, prefix string) (*Node, error) {
|
||||
search := prefix
|
||||
|
||||
// handler leaf node added to the tree is the child.
|
||||
// this may be overridden later down the flow
|
||||
hn := child
|
||||
|
||||
// Parse next segment
|
||||
segTyp, _, segRexpat, segTail, segStartIdx, segEndIdx, err := patNextSegment(search)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ep := splitPath(path)
|
||||
for i, p := range ep {
|
||||
nextNode, ok := curNode.children[p]
|
||||
if ok {
|
||||
curNode = nextNode
|
||||
}
|
||||
// Create a new node.
|
||||
if !ok {
|
||||
curNode.children[p] = &node{
|
||||
label: p,
|
||||
actions: make(map[string]interface{}),
|
||||
children: make(map[string]*node),
|
||||
// Add child depending on next up segment
|
||||
switch segTyp {
|
||||
|
||||
case ntStatic:
|
||||
// Search prefix is all static (that is, has no params in path)
|
||||
// noop
|
||||
|
||||
default:
|
||||
// Search prefix contains a param, regexp or wildcard
|
||||
|
||||
if segTyp == ntRegexp {
|
||||
rex, err := regexp.Compile(segRexpat)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid regexp pattern '%s' in route param", segRexpat)
|
||||
}
|
||||
curNode = curNode.children[p]
|
||||
child.prefix = segRexpat
|
||||
child.rex = rex
|
||||
}
|
||||
// last loop.
|
||||
// If there is already registered data, overwrite it.
|
||||
if i == len(ep)-1 {
|
||||
curNode.label = p
|
||||
for _, method := range methods {
|
||||
curNode.actions[method] = handler
|
||||
|
||||
switch {
|
||||
case segStartIdx == 0:
|
||||
// Route starts with a param
|
||||
child.typ = segTyp
|
||||
|
||||
if segTyp == ntCatchAll {
|
||||
segStartIdx = -1
|
||||
} else {
|
||||
segStartIdx = segEndIdx
|
||||
}
|
||||
if segStartIdx < 0 {
|
||||
segStartIdx = len(search)
|
||||
}
|
||||
child.tail = segTail // for params, we set the tail
|
||||
|
||||
if segStartIdx != len(search) {
|
||||
// add static edge for the remaining part, split the end.
|
||||
// its not possible to have adjacent param nodes, so its certainly
|
||||
// going to be a static node next.
|
||||
|
||||
search = search[segStartIdx:] // advance search position
|
||||
|
||||
nn := &Node{
|
||||
typ: ntStatic,
|
||||
label: search[0],
|
||||
prefix: search,
|
||||
}
|
||||
var err error
|
||||
hn, err = child.addChild(nn, search)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
case segStartIdx > 0:
|
||||
// Route has some param
|
||||
|
||||
// starts with a static segment
|
||||
child.typ = ntStatic
|
||||
child.prefix = search[:segStartIdx]
|
||||
child.rex = nil
|
||||
|
||||
// add the param edge node
|
||||
search = search[segStartIdx:]
|
||||
|
||||
nn := &Node{
|
||||
typ: segTyp,
|
||||
label: search[0],
|
||||
tail: segTail,
|
||||
}
|
||||
var err error
|
||||
hn, err = child.addChild(nn, search)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
n.children[child.typ] = append(n.children[child.typ], child)
|
||||
n.children[child.typ].Sort()
|
||||
return hn, nil
|
||||
}
|
||||
|
||||
// Search searches a path from a tree.
|
||||
func (t *Trie) Search(method string, path string, opts ...TrieOption) (interface{}, map[string]string, bool) {
|
||||
params := make(map[string]string)
|
||||
|
||||
options := TrieOptions{}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
curNode := t.node
|
||||
|
||||
nodeLoop:
|
||||
for _, p := range splitPath(path) {
|
||||
nextNode, ok := curNode.children[p]
|
||||
if ok {
|
||||
curNode = nextNode
|
||||
continue nodeLoop
|
||||
}
|
||||
if options.IgnoreCase {
|
||||
// additional loop for case insensitive matching
|
||||
for k, v := range curNode.children {
|
||||
if literalEqual(k, p, true) {
|
||||
curNode = v
|
||||
continue nodeLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(curNode.children) == 0 {
|
||||
if !literalEqual(curNode.label, p, options.IgnoreCase) {
|
||||
// no matching path was found
|
||||
return nil, nil, false
|
||||
}
|
||||
break
|
||||
}
|
||||
isParamMatch := false
|
||||
for c := range curNode.children {
|
||||
if string([]rune(c)[0]) == leftPtnDelimiter {
|
||||
ptn := getPattern(c)
|
||||
t.rmu.RLock()
|
||||
reg, ok := t.rcache[ptn]
|
||||
t.rmu.RUnlock()
|
||||
if !ok {
|
||||
var err error
|
||||
reg, err = regexp.Compile(ptn)
|
||||
if err != nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
t.rmu.Lock()
|
||||
t.rcache[ptn] = reg
|
||||
t.rmu.Unlock()
|
||||
}
|
||||
if reg.Match([]byte(p)) {
|
||||
pn := getParamName(c)
|
||||
params[pn] = p
|
||||
curNode = curNode.children[c]
|
||||
isParamMatch = true
|
||||
break
|
||||
}
|
||||
// no matching param was found.
|
||||
return nil, nil, false
|
||||
}
|
||||
}
|
||||
if !isParamMatch {
|
||||
return nil, nil, false
|
||||
func (n *Node) replaceChild(label, tail byte, child *Node) error {
|
||||
for i := 0; i < len(n.children[child.typ]); i++ {
|
||||
if n.children[child.typ][i].label == label && n.children[child.typ][i].tail == tail {
|
||||
n.children[child.typ][i] = child
|
||||
n.children[child.typ][i].label = label
|
||||
n.children[child.typ][i].tail = tail
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if path == pathRoot {
|
||||
if len(curNode.actions) == 0 {
|
||||
return nil, nil, false
|
||||
return fmt.Errorf("replacing missing child")
|
||||
}
|
||||
|
||||
func (n *Node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *Node {
|
||||
nds := n.children[ntyp]
|
||||
for i := 0; i < len(nds); i++ {
|
||||
if nds[i].label == label && nds[i].tail == tail {
|
||||
if ntyp == ntRegexp && nds[i].prefix != prefix {
|
||||
continue
|
||||
}
|
||||
return nds[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
handler, ok := curNode.actions[method]
|
||||
if !ok || handler == nil {
|
||||
func (n *Node) setEndpoint(method methodTyp, handler interface{}, pattern string) error {
|
||||
// Set the handler for the method type on the node
|
||||
if n.endpoints == nil {
|
||||
n.endpoints = make(endpoints)
|
||||
}
|
||||
|
||||
paramKeys, err := patParamKeys(pattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if method&mSTUB == mSTUB {
|
||||
n.endpoints.Value(mSTUB).handler = handler
|
||||
}
|
||||
if method&mALL == mALL {
|
||||
h := n.endpoints.Value(mALL)
|
||||
h.handler = handler
|
||||
h.pattern = pattern
|
||||
h.paramKeys = paramKeys
|
||||
for _, m := range methodMap {
|
||||
h := n.endpoints.Value(m)
|
||||
h.handler = handler
|
||||
h.pattern = pattern
|
||||
h.paramKeys = paramKeys
|
||||
}
|
||||
} else {
|
||||
h := n.endpoints.Value(method)
|
||||
h.handler = handler
|
||||
h.pattern = pattern
|
||||
h.paramKeys = paramKeys
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Node) Search(method string, path string) (interface{}, map[string]string, bool) {
|
||||
params := &routeParams{}
|
||||
// Find the routing handlers for the path
|
||||
rn := n.findRoute(params, methodMap[method], path)
|
||||
if rn == nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
return handler, params, true
|
||||
}
|
||||
|
||||
// getPattern gets a pattern from a label
|
||||
// {id:[^\d+$]} -> ^\d+$
|
||||
// {id} -> (.+)
|
||||
func getPattern(label string) string {
|
||||
leftI := strings.Index(label, leftPtnDelimiter)
|
||||
rightI := strings.Index(label, paramDelimiter)
|
||||
// if label doesn't have any pattern, return wild card pattern as default.
|
||||
if leftI == -1 || rightI == -1 {
|
||||
return ptnWildcard
|
||||
ep, ok := rn.endpoints[methodMap[method]]
|
||||
if !ok {
|
||||
return nil, nil, false
|
||||
}
|
||||
return label[rightI+1 : len(label)-1]
|
||||
|
||||
eparams := make(map[string]string, len(params.keys))
|
||||
for idx, key := range params.keys {
|
||||
eparams[key] = params.vals[idx]
|
||||
}
|
||||
|
||||
return ep.handler, eparams, true
|
||||
}
|
||||
|
||||
// getParamName gets a parameter from a label
|
||||
// {id:[^\d+$]} -> id
|
||||
// {id} -> id
|
||||
func getParamName(label string) string {
|
||||
leftI := strings.Index(label, leftPtnDelimiter)
|
||||
rightI := func(l string) int {
|
||||
r := []rune(l)
|
||||
type routeParams struct {
|
||||
keys []string
|
||||
vals []string
|
||||
}
|
||||
|
||||
var n int
|
||||
// Recursive edge traversal by checking all nodeTyp groups along the way.
|
||||
// It's like searching through a multi-dimensional radix trie.
|
||||
func (n *Node) findRoute(params *routeParams, method methodTyp, path string) *Node {
|
||||
nn := n
|
||||
search := path
|
||||
|
||||
loop:
|
||||
for i := 0; i < len(r); i++ {
|
||||
n = i
|
||||
switch string(r[i]) {
|
||||
case paramDelimiter:
|
||||
n = i
|
||||
break loop
|
||||
case rightPtnDelimiter:
|
||||
n = i
|
||||
break loop
|
||||
for t, nds := range nn.children {
|
||||
ntyp := nodeTyp(t)
|
||||
if len(nds) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var xn *Node
|
||||
xsearch := search
|
||||
|
||||
var label byte
|
||||
if search != "" {
|
||||
label = search[0]
|
||||
}
|
||||
|
||||
switch ntyp {
|
||||
case ntStatic:
|
||||
xn = nds.findEdge(label)
|
||||
if xn == nil || !strings.HasPrefix(xsearch, xn.prefix) {
|
||||
continue
|
||||
}
|
||||
xsearch = xsearch[len(xn.prefix):]
|
||||
|
||||
case ntParam, ntRegexp:
|
||||
// short-circuit and return no matching route for empty param values
|
||||
if xsearch == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if i == len(r)-1 {
|
||||
n = i + 1
|
||||
break loop
|
||||
// serially loop through each node grouped by the tail delimiter
|
||||
for idx := 0; idx < len(nds); idx++ {
|
||||
xn = nds[idx]
|
||||
|
||||
// label for param nodes is the delimiter byte
|
||||
p := strings.IndexByte(xsearch, xn.tail)
|
||||
|
||||
if p < 0 {
|
||||
if xn.tail == '/' {
|
||||
p = len(xsearch)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
} else if ntyp == ntRegexp && p == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if ntyp == ntRegexp && xn.rex != nil {
|
||||
if !xn.rex.MatchString(xsearch[:p]) {
|
||||
continue
|
||||
}
|
||||
} else if strings.IndexByte(xsearch[:p], '/') != -1 {
|
||||
// avoid a match across path segments
|
||||
continue
|
||||
}
|
||||
|
||||
prevlen := len(params.vals)
|
||||
params.vals = append(params.vals, xsearch[:p])
|
||||
xsearch = xsearch[p:]
|
||||
|
||||
if len(xsearch) == 0 {
|
||||
if xn.isLeaf() {
|
||||
h := xn.endpoints[method]
|
||||
if h != nil && h.handler != nil {
|
||||
params.keys = append(params.keys, h.paramKeys...)
|
||||
return xn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// recursively find the next node on this branch
|
||||
fin := xn.findRoute(params, method, xsearch)
|
||||
if fin != nil {
|
||||
return fin
|
||||
}
|
||||
|
||||
// not found on this branch, reset vars
|
||||
params.vals = params.vals[:prevlen]
|
||||
xsearch = search
|
||||
}
|
||||
|
||||
params.vals = append(params.vals, "")
|
||||
|
||||
default:
|
||||
// catch-all nodes
|
||||
params.vals = append(params.vals, search)
|
||||
xn = nds[0]
|
||||
xsearch = ""
|
||||
}
|
||||
|
||||
if xn == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// did we find it yet?
|
||||
if len(xsearch) == 0 {
|
||||
if xn.isLeaf() {
|
||||
h := xn.endpoints[method]
|
||||
if h != nil && h.handler != nil {
|
||||
params.keys = append(params.keys, h.paramKeys...)
|
||||
return xn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return n
|
||||
}(label)
|
||||
// recursively find the next node..
|
||||
fin := xn.findRoute(params, method, xsearch)
|
||||
if fin != nil {
|
||||
return fin
|
||||
}
|
||||
|
||||
return label[leftI+1 : rightI]
|
||||
// Did not find final handler, let's remove the param here if it was set
|
||||
if xn.typ > ntStatic {
|
||||
if len(params.vals) > 0 {
|
||||
params.vals = params.vals[:len(params.vals)-1]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// splitPath removes an empty value in slice.
|
||||
func splitPath(path string) []string {
|
||||
s := strings.Split(path, pathDelimiter)
|
||||
var r []string
|
||||
for _, str := range s {
|
||||
if str != "" {
|
||||
r = append(r, str)
|
||||
func (n *Node) isLeaf() bool {
|
||||
return n.endpoints != nil
|
||||
}
|
||||
|
||||
// patNextSegment returns the next segment details from a pattern:
|
||||
// node type, param key, regexp string, param tail byte, param starting index, param ending index
|
||||
func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int, error) {
|
||||
ps := strings.Index(pattern, "{")
|
||||
ws := strings.Index(pattern, "*")
|
||||
|
||||
if ps < 0 && ws < 0 {
|
||||
return ntStatic, "", "", 0, 0, len(pattern), nil // we return the entire thing
|
||||
}
|
||||
|
||||
// Sanity check
|
||||
if ps >= 0 && ws >= 0 && ws < ps {
|
||||
return ntStatic, "", "", 0, 0, 0, fmt.Errorf("wildcard '*' must be the last pattern in a route, otherwise use a '{param}'")
|
||||
}
|
||||
|
||||
var tail byte = '/' // Default endpoint tail to / byte
|
||||
|
||||
if ps < 0 {
|
||||
// Wildcard pattern as finale
|
||||
if ws < len(pattern)-1 {
|
||||
return ntStatic, "", "", 0, 0, 0, fmt.Errorf("wildcard '*' must be the last value in a route. trim trailing text or use a '{param}' instead")
|
||||
}
|
||||
|
||||
return ntCatchAll, "*", "", 0, ws, len(pattern), nil
|
||||
}
|
||||
|
||||
// Param/Regexp pattern is next
|
||||
nt := ntParam
|
||||
|
||||
// Read to closing } taking into account opens and closes in curl count (cc)
|
||||
cc := 0
|
||||
pe := ps
|
||||
for i, c := range pattern[ps:] {
|
||||
if c == '{' {
|
||||
cc++
|
||||
} else if c == '}' {
|
||||
cc--
|
||||
if cc == 0 {
|
||||
pe = ps + i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return r
|
||||
|
||||
if pe == ps {
|
||||
return ntStatic, "", "", 0, 0, 0, fmt.Errorf("route param closing delimiter '}' is missing")
|
||||
}
|
||||
|
||||
key := pattern[ps+1 : pe]
|
||||
pe++ // set end to next position
|
||||
|
||||
if pe < len(pattern) {
|
||||
tail = pattern[pe]
|
||||
}
|
||||
|
||||
var rexpat string
|
||||
if idx := strings.Index(key, ":"); idx >= 0 {
|
||||
nt = ntRegexp
|
||||
rexpat = key[idx+1:]
|
||||
key = key[:idx]
|
||||
}
|
||||
|
||||
if len(rexpat) > 0 {
|
||||
if rexpat[0] != '^' {
|
||||
rexpat = "^" + rexpat
|
||||
}
|
||||
if rexpat[len(rexpat)-1] != '$' {
|
||||
rexpat += "$"
|
||||
}
|
||||
}
|
||||
|
||||
return nt, key, rexpat, tail, ps, pe, nil
|
||||
}
|
||||
|
||||
func literalEqual(component, literal string, ignoreCase bool) bool {
|
||||
if ignoreCase {
|
||||
return strings.EqualFold(component, literal)
|
||||
func patParamKeys(pattern string) ([]string, error) {
|
||||
pat := pattern
|
||||
paramKeys := []string{}
|
||||
for {
|
||||
ptyp, paramKey, _, _, _, e, err := patNextSegment(pat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ptyp == ntStatic {
|
||||
return paramKeys, nil
|
||||
}
|
||||
for i := 0; i < len(paramKeys); i++ {
|
||||
if paramKeys[i] == paramKey {
|
||||
return nil, fmt.Errorf("routing pattern '%s' contains duplicate param key, '%s'", pattern, paramKey)
|
||||
}
|
||||
}
|
||||
paramKeys = append(paramKeys, paramKey)
|
||||
pat = pat[e:]
|
||||
}
|
||||
return component == literal
|
||||
}
|
||||
|
||||
// longestPrefix finds the length of the shared prefix
|
||||
// of two strings
|
||||
func longestPrefix(k1, k2 string) int {
|
||||
max := len(k1)
|
||||
if l := len(k2); l < max {
|
||||
max = l
|
||||
}
|
||||
var i int
|
||||
for i = 0; i < max; i++ {
|
||||
if k1[i] != k2[i] {
|
||||
break
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
type nodes []*Node
|
||||
|
||||
// Sort the list of nodes by label
|
||||
func (ns nodes) Sort() { sort.Sort(ns); ns.tailSort() }
|
||||
func (ns nodes) Len() int { return len(ns) }
|
||||
func (ns nodes) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] }
|
||||
func (ns nodes) Less(i, j int) bool { return ns[i].label < ns[j].label }
|
||||
|
||||
// tailSort pushes nodes with '/' as the tail to the end of the list for param nodes.
|
||||
// The list order determines the traversal order.
|
||||
func (ns nodes) tailSort() {
|
||||
for i := len(ns) - 1; i >= 0; i-- {
|
||||
if ns[i].typ > ntStatic && ns[i].tail == '/' {
|
||||
ns.Swap(i, len(ns)-1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ns nodes) findEdge(label byte) *Node {
|
||||
num := len(ns)
|
||||
idx := 0
|
||||
i, j := 0, num-1
|
||||
for i <= j {
|
||||
idx = i + (j-i)/2
|
||||
if label > ns[idx].label {
|
||||
i = idx + 1
|
||||
} else if label < ns[idx].label {
|
||||
j = idx - 1
|
||||
} else {
|
||||
i = num // breaks cond
|
||||
}
|
||||
}
|
||||
|
||||
if ns[idx].label != label {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ns[idx]
|
||||
}
|
||||
|
@@ -5,14 +5,70 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTrieBackwards(t *testing.T) {
|
||||
_ = &Trie{}
|
||||
}
|
||||
|
||||
func TestTrieWildcardPathPrefix(t *testing.T) {
|
||||
var err error
|
||||
type handler struct {
|
||||
name string
|
||||
}
|
||||
tr := NewTrie()
|
||||
if err = tr.Insert([]string{http.MethodPost}, "/v1/update", &handler{name: "post_update"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = tr.Insert([]string{http.MethodPost}, "/v1/*", &handler{name: "post_create"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h, _, ok := tr.Search(http.MethodPost, "/v1/test/one")
|
||||
if !ok {
|
||||
t.Fatalf("unexpected error handler not found")
|
||||
}
|
||||
if h.(*handler).name != "post_create" {
|
||||
t.Fatalf("invalid handler %v", h)
|
||||
}
|
||||
h, _, ok = tr.Search(http.MethodPost, "/v1/update")
|
||||
if !ok {
|
||||
t.Fatalf("unexpected error")
|
||||
}
|
||||
if h.(*handler).name != "post_update" {
|
||||
t.Fatalf("invalid handler %v", h)
|
||||
}
|
||||
h, _, ok = tr.Search(http.MethodPost, "/v1/update/some/{x}")
|
||||
if !ok {
|
||||
t.Fatalf("unexpected error")
|
||||
}
|
||||
if h.(*handler).name != "post_create" {
|
||||
t.Fatalf("invalid handler %v", h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTriePathPrefix(t *testing.T) {
|
||||
type handler struct {
|
||||
name string
|
||||
}
|
||||
tr := NewTrie()
|
||||
_ = tr.Insert([]string{http.MethodPost}, "/v1/create/{id}", &handler{name: "post_create"})
|
||||
_ = tr.Insert([]string{http.MethodPost}, "/v1/update/{id}", &handler{name: "post_update"})
|
||||
_ = tr.Insert([]string{http.MethodPost}, "/", &handler{name: "post_wildcard"})
|
||||
h, _, ok := tr.Search(http.MethodPost, "/")
|
||||
if !ok {
|
||||
t.Fatalf("unexpected error")
|
||||
}
|
||||
if h.(*handler).name != "post_wildcard" {
|
||||
t.Fatalf("invalid handler %v", h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrieFixedPattern(t *testing.T) {
|
||||
type handler struct {
|
||||
name string
|
||||
}
|
||||
tr := NewTrie()
|
||||
tr.Insert([]string{http.MethodPut}, "/v1/create/{id}", &handler{name: "pattern"})
|
||||
tr.Insert([]string{http.MethodPut}, "/v1/create/12", &handler{name: "fixed"})
|
||||
h, _, ok := tr.Search(http.MethodPut, "/v1/create/12", IgnoreCase(false))
|
||||
_ = tr.Insert([]string{http.MethodPut}, "/v1/create/{id}", &handler{name: "pattern"})
|
||||
_ = tr.Insert([]string{http.MethodPut}, "/v1/create/12", &handler{name: "fixed"})
|
||||
h, _, ok := tr.Search(http.MethodPut, "/v1/create/12")
|
||||
if !ok {
|
||||
t.Fatalf("unexpected error")
|
||||
}
|
||||
@@ -21,43 +77,9 @@ func TestTrieFixedPattern(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrieIgnoreCase(t *testing.T) {
|
||||
type handler struct {
|
||||
name string
|
||||
}
|
||||
tr := NewTrie()
|
||||
tr.Insert([]string{http.MethodPut}, "/v1/create/{id}", &handler{name: "test"})
|
||||
|
||||
_, _, ok := tr.Search(http.MethodPut, "/v1/CREATE/12", IgnoreCase(true))
|
||||
if !ok {
|
||||
t.Fatalf("unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrieContentType(t *testing.T) {
|
||||
type handler struct {
|
||||
name string
|
||||
}
|
||||
tr := NewTrie()
|
||||
tr.Insert([]string{"application/json"}, "/v1/create/{id}", &handler{name: "test"})
|
||||
|
||||
h, _, ok := tr.Search("application/json", "/v1/create/12")
|
||||
if !ok {
|
||||
t.Fatalf("must be found error")
|
||||
}
|
||||
if h.(*handler).name != "test" {
|
||||
t.Fatalf("invalid handler %v", h)
|
||||
}
|
||||
_, _, ok = tr.Search("text/xml", "/v1/create/12")
|
||||
if ok {
|
||||
t.Fatalf("must be not found error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrieNoMatchMethod(t *testing.T) {
|
||||
tr := NewTrie()
|
||||
tr.Insert([]string{http.MethodPut}, "/v1/create/{id}", nil)
|
||||
|
||||
_ = tr.Insert([]string{http.MethodPut}, "/v1/create/{id}", nil)
|
||||
_, _, ok := tr.Search(http.MethodPost, "/v1/create")
|
||||
if ok {
|
||||
t.Fatalf("must be not found error")
|
||||
@@ -67,8 +89,7 @@ func TestTrieNoMatchMethod(t *testing.T) {
|
||||
func TestTrieMatchRegexp(t *testing.T) {
|
||||
type handler struct{}
|
||||
tr := NewTrie()
|
||||
tr.Insert([]string{http.MethodPut}, "/v1/create/{category}/{id:[0-9]+}", &handler{})
|
||||
|
||||
_ = tr.Insert([]string{http.MethodPut}, "/v1/create/{category}/{id:[0-9]+}", &handler{})
|
||||
_, params, ok := tr.Search(http.MethodPut, "/v1/create/test_cat/12345")
|
||||
switch {
|
||||
case !ok:
|
||||
@@ -83,8 +104,7 @@ func TestTrieMatchRegexp(t *testing.T) {
|
||||
func TestTrieMatchRegexpFail(t *testing.T) {
|
||||
type handler struct{}
|
||||
tr := NewTrie()
|
||||
tr.Insert([]string{http.MethodPut}, "/v1/create/{id:[a-z]+}", &handler{})
|
||||
|
||||
_ = tr.Insert([]string{http.MethodPut}, "/v1/create/{id:[a-z]+}", &handler{})
|
||||
_, _, ok := tr.Search(http.MethodPut, "/v1/create/12345")
|
||||
if ok {
|
||||
t.Fatalf("route must not be not found")
|
||||
@@ -96,8 +116,8 @@ func TestTrieMatchLongest(t *testing.T) {
|
||||
name string
|
||||
}
|
||||
tr := NewTrie()
|
||||
tr.Insert([]string{http.MethodPut}, "/v1/create", &handler{name: "first"})
|
||||
tr.Insert([]string{http.MethodPut}, "/v1/create/{id:[0-9]+}", &handler{name: "second"})
|
||||
_ = tr.Insert([]string{http.MethodPut}, "/v1/create", &handler{name: "first"})
|
||||
_ = tr.Insert([]string{http.MethodPut}, "/v1/create/{id:[0-9]+}", &handler{name: "second"})
|
||||
if h, _, ok := tr.Search(http.MethodPut, "/v1/create/12345"); !ok {
|
||||
t.Fatalf("route must be found")
|
||||
} else if h.(*handler).name != "second" {
|
||||
|
@@ -91,7 +91,10 @@ func Merge(dst interface{}, mp map[string]interface{}, opts ...Option) error {
|
||||
|
||||
val, ok := mp[fname]
|
||||
if !ok {
|
||||
continue
|
||||
val, ok = mp[dfld.Name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
sval = reflect.ValueOf(val)
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrInvalidParam specifies invalid url query params
|
||||
@@ -75,7 +76,7 @@ func ZeroFieldByPath(src interface{}, path string) error {
|
||||
val := reflect.ValueOf(src)
|
||||
|
||||
for _, p := range strings.Split(path, ".") {
|
||||
val, err = structValueByName(reflect.ValueOf(val), p)
|
||||
val, err = structValueByName(val, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -182,9 +183,6 @@ func StructFieldByName(src interface{}, tkey string) (interface{}, error) {
|
||||
continue
|
||||
}
|
||||
if fld.Name == tkey || strings.EqualFold(strings.ToLower(fld.Name), strings.ToLower(tkey)) {
|
||||
if val.Kind() != reflect.Ptr && val.CanAddr() {
|
||||
val = val.Addr()
|
||||
}
|
||||
return val.Interface(), nil
|
||||
}
|
||||
|
||||
@@ -236,10 +234,30 @@ func StructFields(src interface{}) ([]StructField, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch val.Interface().(type) {
|
||||
case time.Time, *time.Time:
|
||||
fields = append(fields, StructField{Field: fld, Value: val, Path: fld.Name})
|
||||
continue
|
||||
case time.Duration, *time.Duration:
|
||||
fields = append(fields, StructField{Field: fld, Value: val, Path: fld.Name})
|
||||
continue
|
||||
}
|
||||
|
||||
switch val.Kind() {
|
||||
// case timeKind:
|
||||
// fmt.Printf("GGG\n")
|
||||
// fields = append(fields, StructField{Field: fld, Value: val, Path: fld.Name})
|
||||
case reflect.Ptr:
|
||||
// if !val.IsValid()
|
||||
if reflect.Indirect(val).Kind() == reflect.Struct {
|
||||
infields, err := StructFields(val.Interface())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, infield := range infields {
|
||||
infield.Path = fmt.Sprintf("%s.%s", fld.Name, infield.Path)
|
||||
fields = append(fields, infield)
|
||||
}
|
||||
} else {
|
||||
fields = append(fields, StructField{Field: fld, Value: val, Path: fld.Name})
|
||||
}
|
||||
case reflect.Struct:
|
||||
infields, err := StructFields(val.Interface())
|
||||
if err != nil {
|
||||
|
@@ -4,24 +4,45 @@ import (
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
rutil "go.unistack.org/micro/v3/util/reflect"
|
||||
)
|
||||
|
||||
func TestStructfields(t *testing.T) {
|
||||
type Config struct {
|
||||
Wait time.Duration
|
||||
Time time.Time
|
||||
Metadata map[string]int
|
||||
Broker string
|
||||
Addr []string
|
||||
Verbose bool
|
||||
Nested *Config
|
||||
}
|
||||
cfg := &Config{Nested: &Config{}}
|
||||
fields, err := rutil.StructFields(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(fields) != 13 {
|
||||
t.Fatalf("invalid fields number: %v", fields)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetFieldByPath(t *testing.T) {
|
||||
type NestedStr struct {
|
||||
BBB string
|
||||
CCC int
|
||||
BBB string `json:"bbb"`
|
||||
CCC int `json:"ccc"`
|
||||
}
|
||||
type Str1 struct {
|
||||
Name []string `json:"name" codec:"flatten"`
|
||||
XXX string `json:"xxx"`
|
||||
Nested NestedStr
|
||||
Name []string `json:"name" codec:"flatten"`
|
||||
XXX string `json:"xxx"`
|
||||
Nested NestedStr `json:"nested"`
|
||||
}
|
||||
type Str2 struct {
|
||||
XXX string `json:"xxx"`
|
||||
Nested *NestedStr
|
||||
Name []string `json:"name" codec:"flatten"`
|
||||
XXX string `json:"xxx"`
|
||||
Nested *NestedStr `json:"nested"`
|
||||
Name []string `json:"name" codec:"flatten"`
|
||||
}
|
||||
var err error
|
||||
val1 := &Str1{Name: []string{"first", "second"}, XXX: "ttt", Nested: NestedStr{BBB: "ddd", CCC: 9}}
|
||||
@@ -44,27 +65,27 @@ func TestSetFieldByPath(t *testing.T) {
|
||||
|
||||
func TestZeroFieldByPath(t *testing.T) {
|
||||
type NestedStr struct {
|
||||
BBB string
|
||||
CCC int
|
||||
BBB string `json:"bbb"`
|
||||
CCC int `json:"ccc"`
|
||||
}
|
||||
type Str1 struct {
|
||||
Name []string `json:"name" codec:"flatten"`
|
||||
XXX string `json:"xxx"`
|
||||
Nested NestedStr
|
||||
Name []string `json:"name" codec:"flatten"`
|
||||
XXX string `json:"xxx"`
|
||||
Nested NestedStr `json:"nested"`
|
||||
}
|
||||
type Str2 struct {
|
||||
XXX string `json:"xxx"`
|
||||
Nested *NestedStr
|
||||
Name []string `json:"name" codec:"flatten"`
|
||||
XXX string `json:"xxx"`
|
||||
Nested *NestedStr `json:"nested"`
|
||||
Name []string `json:"name" codec:"flatten"`
|
||||
}
|
||||
var err error
|
||||
val1 := &Str1{Name: []string{"first", "second"}, XXX: "ttt", Nested: NestedStr{BBB: "ddd", CCC: 9}}
|
||||
|
||||
err = rutil.ZeroFieldByPath(val1, "name.nested.bbb")
|
||||
err = rutil.ZeroFieldByPath(val1, "Nested.BBB")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = rutil.ZeroFieldByPath(val1, "name.nested")
|
||||
err = rutil.ZeroFieldByPath(val1, "Nested")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -73,7 +94,7 @@ func TestZeroFieldByPath(t *testing.T) {
|
||||
}
|
||||
|
||||
val2 := &Str2{Name: []string{"first", "second"}, XXX: "ttt", Nested: &NestedStr{BBB: "ddd", CCC: 9}}
|
||||
err = rutil.ZeroFieldByPath(val2, "name.nested")
|
||||
err = rutil.ZeroFieldByPath(val2, "Nested")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -182,9 +203,9 @@ func TestStructByName(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if v, ok := iface.(*[]string); !ok {
|
||||
t.Fatalf("not *[]string %v", iface)
|
||||
} else if len(*v) != 2 {
|
||||
if v, ok := iface.([]string); !ok {
|
||||
t.Fatalf("not []string %v", iface)
|
||||
} else if len(v) != 2 {
|
||||
t.Fatalf("invalid number %v", iface)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user