基本逻辑完成

This commit is contained in:
2024-11-01 17:40:34 +08:00
commit f9b9beea4b
40 changed files with 1869 additions and 0 deletions

134
leaf/context.go Normal file
View File

@@ -0,0 +1,134 @@
package leaf
import (
"context"
"github.com/eclipse/paho.golang/paho"
"math"
)
// abortIndex represents a typical value used in abort functions.
const abortIndex int8 = math.MaxInt8 >> 1
type endKeyType = string
const EndKey endKeyType = "end"
type EndType int
const (
EndTimer EndType = iota + 1
EndStop
)
type KeyValue struct {
parent *KeyValue
key any
value any
}
var rootKeyType = &KeyValue{}
type Context struct {
context.Context
*paho.Publish
engine *Engine
index int8
handlers HandlersChain
value *KeyValue
}
func WithLeafContext(c context.Context, p *paho.Publish, engine *Engine, handlers HandlersChain) *Context {
return &Context{
Context: c,
Publish: p,
engine: engine,
index: -1,
handlers: handlers,
value: rootKeyType,
}
}
// HandlerName returns the main handler's name. For example if the handler is "handleGetUsers()",
// this function will return "main.handleGetUsers".
func (c *Context) HandlerName() string {
return nameOfFunction(c.handlers.Last())
}
// HandlerNames returns a list of all registered handlers for this context in descending order,
// following the semantics of HandlerName()
func (c *Context) HandlerNames() []string {
hn := make([]string, 0, len(c.handlers))
for _, val := range c.handlers {
if val == nil {
continue
}
hn = append(hn, nameOfFunction(val))
}
return hn
}
// Handler returns the main handler.
func (c *Context) Handler() HandlerFunc {
return c.handlers.Last()
}
/************************************/
/*********** FLOW CONTROL ***********/
/************************************/
// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in GitHub.
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
if c.handlers[c.index] == nil {
continue
}
c.handlers[c.index](c)
c.index++
}
}
// IsAborted returns true if the current context was aborted.
func (c *Context) IsAborted() bool {
return c.index >= abortIndex
}
// Abort prevents pending handlers from being called. Note that this will not stop the current handler.
// Let's say you have an authorization middleware that validates that the current request is authorized.
// If the authorization fails (ex: the password does not match), call Abort to ensure the remaining handlers
// for this request are not called.
func (c *Context) Abort() {
c.index = abortIndex
}
func WithValue[T any](ctx *Context, k any, v T) {
ctx.value = &KeyValue{
parent: ctx.value,
key: k,
value: v,
}
}
func Value[T any](ctx *Context, k any) (v T) {
vo := ctx.value
for {
if vo.key == k {
v, _ = vo.value.(T)
break
} else if vo.parent == rootKeyType {
break
}
vo = vo.parent
}
return
}
func WithCancel(ctx *Context) context.CancelFunc {
c, cancel := context.WithCancel(ctx.Context)
ctx.Context = c
return cancel
}

144
leaf/leaf.go Normal file
View File

@@ -0,0 +1,144 @@
package leaf
import (
"context"
"github.com/eclipse/paho.golang/packets"
"github.com/eclipse/paho.golang/paho"
"github.com/eclipse/paho.golang/paho/log"
"sync"
)
type Router interface {
RegisterHandler(string, ...HandlerFunc)
UnregisterHandler(string)
Route(*packets.Publish)
SetDebugLogger(log.Logger)
Use(...HandlerFunc)
}
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
// OptionFunc defines the function to change the default configuration
type OptionFunc func(*Engine)
// HandlersChain defines a HandlerFunc slice.
type HandlersChain []HandlerFunc
// Last returns the last handler in the chain. i.e. the last handler is the main one.
func (c HandlersChain) Last() HandlerFunc {
if length := len(c); length > 0 {
return c[length-1]
}
return nil
}
type Engine struct {
mu sync.RWMutex
ctx context.Context
Handlers HandlersChain
defaultHandler HandlersChain
subscriptions map[string]HandlersChain
aliases map[uint16]string
debug log.Logger
}
func New(ctx context.Context) *Engine {
return &Engine{
ctx: ctx,
Handlers: make(HandlersChain, 0),
subscriptions: make(map[string]HandlersChain),
aliases: make(map[uint16]string),
debug: log.NOOPLogger{},
}
}
func Default(ctx context.Context) *Engine {
engine := &Engine{
ctx: ctx,
Handlers: make(HandlersChain, 0),
subscriptions: make(map[string]HandlersChain),
aliases: make(map[uint16]string),
debug: log.NOOPLogger{},
}
engine.Use(Recovery())
return engine
}
func (e *Engine) RegisterHandler(topic string, handlers ...HandlerFunc) {
e.debug.Println("registering handler for:", topic)
e.mu.Lock()
defer e.mu.Unlock()
e.subscriptions[topic] = e.combineHandlers(handlers)
}
func (e *Engine) UnregisterHandler(topic string) {
e.debug.Println("unregistering handler for:", topic)
e.mu.Lock()
defer e.mu.Unlock()
delete(e.subscriptions, topic)
}
func (e *Engine) Route(pb *packets.Publish) {
e.debug.Println("routing message for:", pb.Topic)
e.mu.Lock()
defer e.mu.Unlock()
m := paho.PublishFromPacketPublish(pb)
var topic string
if pb.Properties.TopicAlias != nil {
e.debug.Println("message is using topic aliasing")
if pb.Topic != "" {
// Register new alias
e.debug.Printf("registering new topic alias '%d' for topic '%s'", *pb.Properties.TopicAlias, m.Topic)
e.aliases[*pb.Properties.TopicAlias] = pb.Topic
}
if t, ok := e.aliases[*pb.Properties.TopicAlias]; ok {
e.debug.Printf("aliased topic '%d' translates to '%s'", *pb.Properties.TopicAlias, m.Topic)
topic = t
}
} else {
topic = m.Topic
}
handlerCalled := false
for route, handlers := range e.subscriptions {
if match(route, topic) {
e.debug.Println("found handler for:", route)
go WithLeafContext(e.ctx, m, e, handlers).Next()
handlerCalled = true
}
}
if !handlerCalled && e.defaultHandler != nil {
go WithLeafContext(e.ctx, m, e, e.defaultHandler).Next()
}
}
func (e *Engine) SetDebugLogger(l log.Logger) {
e.debug = l
}
func (e *Engine) Use(middleware ...HandlerFunc) {
e.Handlers = append(e.Handlers, middleware...)
}
func (e *Engine) DefaultHandler(h HandlerFunc) {
e.debug.Println("registering default handler")
e.mu.Lock()
defer e.mu.Unlock()
e.defaultHandler = e.combineHandlers(HandlersChain{h})
}
func (e *Engine) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(e.Handlers) + len(handlers)
assert1(finalSize < int(abortIndex), "too many handlers")
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, e.Handlers)
copy(mergedHandlers[len(e.Handlers):], handlers)
return mergedHandlers
}

69
leaf/mode.go Normal file
View File

@@ -0,0 +1,69 @@
package leaf
import (
"flag"
"io"
"os"
"sync/atomic"
)
const EnvLeafMode = "LEAF_MODE"
const (
// DebugMode indicates gin mode is debug.
DebugMode = "debug"
// ReleaseMode indicates gin mode is release.
ReleaseMode = "release"
// TestMode indicates gin mode is test.
TestMode = "test"
)
const (
debugCode = iota
releaseCode
testCode
)
var DefaultWriter io.Writer = os.Stdout
var DefaultErrorWriter io.Writer = os.Stderr
var leafMode int32 = debugCode
var modeName atomic.Value
func init() {
mode := os.Getenv(EnvLeafMode)
SetMode(mode)
}
// SetMode sets gin mode according to input string.
func SetMode(value string) {
if value == "" {
if flag.Lookup("test.v") != nil {
value = TestMode
} else {
value = DebugMode
}
}
switch value {
case DebugMode, "":
atomic.StoreInt32(&leafMode, debugCode)
case ReleaseMode:
atomic.StoreInt32(&leafMode, releaseCode)
case TestMode:
atomic.StoreInt32(&leafMode, testCode)
default:
panic("leaf mode unknown: " + value + " (available mode: debug release test)")
}
modeName.Store(value)
}
func IsDebugging() bool {
return atomic.LoadInt32(&leafMode) == debugCode
}
// Mode returns current gin mode.
func Mode() string {
return modeName.Load().(string)
}

48
leaf/recovery.go Normal file
View File

@@ -0,0 +1,48 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package leaf
import (
"io"
"log"
)
// RecoveryFunc defines the function passable to CustomRecovery.
type RecoveryFunc func(c *Context, err any)
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
func Recovery() HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter)
}
// CustomRecovery returns a middleware that recovers from any panics and calls the provided handle func to handle it.
func CustomRecovery(handle RecoveryFunc) HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter, handle)
}
// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
func RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) HandlerFunc {
if len(recovery) > 0 {
return CustomRecoveryWithWriter(out, recovery[0])
}
return CustomRecoveryWithWriter(out, defaultHandleRecovery)
}
// CustomRecoveryWithWriter returns a middleware for a given writer that recovers from any panics and calls the provided handle func to handle it.
func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
log.Println("执行出错: ", err)
handle(c, err)
}
}()
c.Next()
}
}
func defaultHandleRecovery(c *Context, _ any) {
c.Abort()
}

64
leaf/utils.go Normal file
View File

@@ -0,0 +1,64 @@
package leaf
import (
"reflect"
"runtime"
"strings"
)
func assert1(guard bool, text string) {
if !guard {
panic(text)
}
}
func nameOfFunction(f any) string {
return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
}
func match(route, topic string) bool {
return route == topic || routeIncludesTopic(route, topic)
}
func matchDeep(route []string, topic []string) bool {
if len(route) == 0 {
return len(topic) == 0
}
if len(topic) == 0 {
return route[0] == "#"
}
if route[0] == "#" {
return true
}
if (route[0] == "+") || (route[0] == topic[0]) {
return matchDeep(route[1:], topic[1:])
}
return false
}
func routeIncludesTopic(route, topic string) bool {
return matchDeep(routeSplit(route), topicSplit(topic))
}
func routeSplit(route string) []string {
if len(route) == 0 {
return nil
}
var result []string
if strings.HasPrefix(route, "$share") {
result = strings.Split(route, "/")[2:]
} else {
result = strings.Split(route, "/")
}
return result
}
func topicSplit(topic string) []string {
if len(topic) == 0 {
return nil
}
return strings.Split(topic, "/")
}