mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 07:26:36 +08:00
refactor(core): align architecture with cordis spatiotemporal composability
This commit is contained in:
@@ -40,6 +40,12 @@ func isNil(i any) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) remove(targetType reflect.Type) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
delete(c.services, targetType)
|
||||
}
|
||||
|
||||
// Provide registers a typed service implementation into the Context's IoC container.
|
||||
func Provide[T any](ctx *Context, service T) {
|
||||
if ctx == nil {
|
||||
@@ -50,7 +56,13 @@ func Provide[T any](ctx *Context, service T) {
|
||||
}
|
||||
|
||||
targetType := reflect.TypeFor[T]()
|
||||
ctx.Container().provide(targetType, service)
|
||||
targetContainer := ctx.Container()
|
||||
targetContainer.provide(targetType, service)
|
||||
|
||||
ctx.OnDispose(func() error {
|
||||
targetContainer.remove(targetType)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Container) provide(targetType reflect.Type, service any) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"Wavelet/core/contracts"
|
||||
"Wavelet/core/extpoints"
|
||||
)
|
||||
|
||||
@@ -214,6 +215,24 @@ func (c *Context) Setting() extpoints.SettingExtension {
|
||||
return c.settings
|
||||
}
|
||||
|
||||
// DB returns the contracts.DBService registered in the IoC container, or nil if not registered.
|
||||
func (c *Context) DB() contracts.DBService {
|
||||
svc, err := Inject[contracts.DBService](c)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return svc
|
||||
}
|
||||
|
||||
// Cache returns the contracts.CacheService registered in the IoC container, or nil if not registered.
|
||||
func (c *Context) Cache() contracts.CacheService {
|
||||
svc, err := Inject[contracts.CacheService](c)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return svc
|
||||
}
|
||||
|
||||
// OnDispose registers a cleanup callback function to be executed when this Context is disposed.
|
||||
// It accepts func() error, func(), or Disposer.
|
||||
func (c *Context) OnDispose(fn any) {
|
||||
|
||||
@@ -46,6 +46,33 @@ type OAuthUserInfoDTO struct {
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
}
|
||||
|
||||
// AuthSourceDTO represents an OAuth / OIDC authentication source.
|
||||
type AuthSourceDTO struct {
|
||||
ID uint64 `json:"id,string"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
DisplayName string `json:"display_name"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret,omitempty"`
|
||||
OpenIDDiscoveryURL string `json:"openid_discovery_url"`
|
||||
Scopes string `json:"scopes"`
|
||||
IconURL string `json:"icon_url"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// AuthSourceViewDTO is a sanitized view of an AuthSource for admin display.
|
||||
type AuthSourceViewDTO struct {
|
||||
ID uint64 `json:"id,string"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
DisplayName string `json:"display_name"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IconURL string `json:"icon_url"`
|
||||
ClientSecretConfigured bool `json:"client_secret_configured"`
|
||||
}
|
||||
|
||||
// OAuthProvider defines the pluggable OAuth provider contract.
|
||||
type OAuthProvider interface {
|
||||
Name() string
|
||||
@@ -79,6 +106,27 @@ type AuthService interface {
|
||||
// RevokeUserSessions revokes all active sessions and cached tokens for a user.
|
||||
RevokeUserSessions(ctx context.Context, userID uint64) error
|
||||
|
||||
// InvalidateCachedUser invalidates cached user profile data.
|
||||
InvalidateCachedUser(ctx context.Context, userID uint64)
|
||||
|
||||
// InvalidateCachedToken invalidates cached access token data.
|
||||
InvalidateCachedToken(ctx context.Context, tokenHash string)
|
||||
|
||||
// ListAuthSources lists all configured authentication sources.
|
||||
ListAuthSources(ctx context.Context) ([]AuthSourceViewDTO, error)
|
||||
|
||||
// CreateAuthSource creates a new authentication source.
|
||||
CreateAuthSource(ctx context.Context, source AuthSourceDTO) (*AuthSourceDTO, error)
|
||||
|
||||
// UpdateAuthSource updates an authentication source.
|
||||
UpdateAuthSource(ctx context.Context, id uint64, source AuthSourceDTO) (*AuthSourceDTO, error)
|
||||
|
||||
// DeleteAuthSource removes an authentication source.
|
||||
DeleteAuthSource(ctx context.Context, id uint64) error
|
||||
|
||||
// ToggleAuthSource toggles the active state of an authentication source.
|
||||
ToggleAuthSource(ctx context.Context, id uint64) (*AuthSourceDTO, error)
|
||||
|
||||
// DisallowTokenAuthMiddleware returns a middleware that rejects requests authenticated via access token.
|
||||
DisallowTokenAuthMiddleware() any
|
||||
}
|
||||
|
||||
@@ -26,6 +26,13 @@ const (
|
||||
|
||||
// EventTopicUserDeleted fires when a user account is deleted.
|
||||
EventTopicUserDeleted = "user:deleted"
|
||||
|
||||
// EventTopicUserStatusChanged fires when a user account active status changes.
|
||||
EventTopicUserStatusChanged = "user:status_changed"
|
||||
|
||||
// EventTopicTokenRevoked fires when an access token is revoked.
|
||||
// #nosec G101
|
||||
EventTopicTokenRevoked = "auth:token_revoked"
|
||||
)
|
||||
|
||||
// --- Admin & System Events ---
|
||||
@@ -107,3 +114,21 @@ type NotificationSentEvent struct {
|
||||
Success bool `json:"success"`
|
||||
ErrorInfo string `json:"error_info,omitempty"`
|
||||
}
|
||||
|
||||
// UserStatusChangedEvent fires when a user status is enabled/disabled.
|
||||
type UserStatusChangedEvent struct {
|
||||
UserID uint64 `json:"user_id,string"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// TokenRevokedEvent fires when an access token is revoked.
|
||||
type TokenRevokedEvent struct {
|
||||
UserID uint64 `json:"user_id,string"`
|
||||
TokenHash string `json:"token_hash"`
|
||||
}
|
||||
|
||||
// UserDeletedEvent fires when a user account is deleted.
|
||||
type UserDeletedEvent struct {
|
||||
CurrentUserID uint64 `json:"current_user_id,string"`
|
||||
TargetUserID uint64 `json:"target_user_id,string"`
|
||||
}
|
||||
|
||||
@@ -29,6 +29,34 @@ type UpdateUserProfileRequest struct {
|
||||
Location *string `json:"location,omitempty"`
|
||||
}
|
||||
|
||||
// AdminListUsersFilter contains query parameters for filtering users in admin panel.
|
||||
type AdminListUsersFilter struct {
|
||||
Page int
|
||||
PageSize int
|
||||
UserID *uint64
|
||||
Username string
|
||||
Email string
|
||||
}
|
||||
|
||||
// AdminCreateUserRequest contains fields for admin to create a user.
|
||||
type AdminCreateUserRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Nickname string `json:"nickname"`
|
||||
Email string `json:"email"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
}
|
||||
|
||||
// AdminUpdateUserRequest contains fields for admin to update a user.
|
||||
type AdminUpdateUserRequest struct {
|
||||
ID uint64 `json:"id,string"`
|
||||
Nickname string `json:"nickname"`
|
||||
Email string `json:"email"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
// UserService defines the contract for user account management and profile queries.
|
||||
type UserService interface {
|
||||
// GetUserByID retrieves a user by ID.
|
||||
@@ -81,4 +109,22 @@ type UserService interface {
|
||||
|
||||
// UniqueUsername generates a unique username candidate based on base.
|
||||
UniqueUsername(ctx context.Context, base string) (string, error)
|
||||
|
||||
// AdminListUsers returns a filtered paginated list of users for admin management.
|
||||
AdminListUsers(ctx context.Context, filter AdminListUsersFilter) (int64, []*UserDTO, error)
|
||||
|
||||
// AdminGetUser retrieves complete user details by ID for admin management.
|
||||
AdminGetUser(ctx context.Context, id uint64) (*UserDTO, error)
|
||||
|
||||
// AdminCreateUser creates a user with admin specified options.
|
||||
AdminCreateUser(ctx context.Context, req AdminCreateUserRequest) (*UserDTO, error)
|
||||
|
||||
// AdminUpdateUser updates user details, email, nickname, admin role, and optional password.
|
||||
AdminUpdateUser(ctx context.Context, currentUserID uint64, req AdminUpdateUserRequest) error
|
||||
|
||||
// AdminUpdateUserStatus updates a user's active status (with admin protection).
|
||||
AdminUpdateUserStatus(ctx context.Context, id uint64, active bool) error
|
||||
|
||||
// AdminDeleteUser deletes a user (with self and admin protection, cascading tokens and accounts).
|
||||
AdminDeleteUser(ctx context.Context, currentUserID, targetID uint64) error
|
||||
}
|
||||
|
||||
+210
-17
@@ -21,13 +21,16 @@ type eventListener struct {
|
||||
id uint64
|
||||
fnVal reflect.Value
|
||||
numIn int
|
||||
numOut int
|
||||
hasCtx bool
|
||||
hasPayload bool
|
||||
argType reflect.Type
|
||||
returnsErr bool
|
||||
returnsVal bool
|
||||
}
|
||||
|
||||
// EventBus is a thread-safe, strongly-typed in-process domain event bus.
|
||||
// EventBus is a thread-safe, strongly-typed in-process domain event bus supporting
|
||||
// Emit, Waterfall, Parallel, and Serial dispatch semantics.
|
||||
type EventBus struct {
|
||||
mu sync.RWMutex
|
||||
nextID atomic.Uint64
|
||||
@@ -44,6 +47,10 @@ func NewEventBus() *EventBus {
|
||||
// On registers an event handler for the given topic.
|
||||
//
|
||||
// Supported handler signatures:
|
||||
// - func(ctx context.Context, event T) (T, error)
|
||||
// - func(ctx context.Context, event T) T
|
||||
// - func(event T) (T, error)
|
||||
// - func(event T) T
|
||||
// - func(ctx context.Context, event T) error
|
||||
// - func(ctx context.Context, event T)
|
||||
// - func(event T) error
|
||||
@@ -71,17 +78,29 @@ func (b *EventBus) On(topic string, handler any) Disposer {
|
||||
panic(fmt.Sprintf("core/events: handler has %d parameters, maximum 2 supported (ctx, event)", numIn))
|
||||
}
|
||||
|
||||
const maxHandlerReturnValues = 2
|
||||
numOut := fnType.NumOut()
|
||||
if numOut > 1 {
|
||||
panic(fmt.Sprintf("core/events: handler has %d return values, maximum 1 supported (error)", numOut))
|
||||
if numOut > maxHandlerReturnValues {
|
||||
panic(fmt.Sprintf("core/events: handler has %d return values, maximum 2 supported (value, error)", numOut))
|
||||
}
|
||||
|
||||
returnsErr := false
|
||||
if numOut == 1 {
|
||||
outType := fnType.Out(0)
|
||||
if !outType.Implements(errInterfaceType) {
|
||||
panic(fmt.Sprintf("core/events: handler return type must be error, got %v", outType))
|
||||
returnsVal := false
|
||||
|
||||
switch numOut {
|
||||
case 1:
|
||||
out0 := fnType.Out(0)
|
||||
if out0.Implements(errInterfaceType) {
|
||||
returnsErr = true
|
||||
} else {
|
||||
returnsVal = true
|
||||
}
|
||||
case 2:
|
||||
out1 := fnType.Out(1)
|
||||
if !out1.Implements(errInterfaceType) {
|
||||
panic(fmt.Sprintf("core/events: second return value must be error, got %v", out1))
|
||||
}
|
||||
returnsVal = true
|
||||
returnsErr = true
|
||||
}
|
||||
|
||||
@@ -89,7 +108,9 @@ func (b *EventBus) On(topic string, handler any) Disposer {
|
||||
id: b.nextID.Add(1),
|
||||
fnVal: fnVal,
|
||||
numIn: numIn,
|
||||
numOut: numOut,
|
||||
returnsErr: returnsErr,
|
||||
returnsVal: returnsVal,
|
||||
}
|
||||
|
||||
switch numIn {
|
||||
@@ -162,17 +183,11 @@ func (b *EventBus) Emit(ctx context.Context, topic string, payload any) error {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
b.mu.RLock()
|
||||
rawListeners := b.handlers[topic]
|
||||
if len(rawListeners) == 0 {
|
||||
b.mu.RUnlock()
|
||||
listeners := b.getListeners(topic)
|
||||
if len(listeners) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
listeners := make([]eventListener, len(rawListeners))
|
||||
copy(listeners, rawListeners)
|
||||
b.mu.RUnlock()
|
||||
|
||||
var payloadVal reflect.Value
|
||||
if payload != nil {
|
||||
payloadVal = reflect.ValueOf(payload)
|
||||
@@ -190,8 +205,11 @@ func (b *EventBus) Emit(ctx context.Context, topic string, payload any) error {
|
||||
}()
|
||||
|
||||
results := l.fnVal.Call(args)
|
||||
if l.returnsErr && len(results) > 0 && !results[0].IsNil() {
|
||||
resErr = results[0].Interface().(error)
|
||||
if l.returnsErr {
|
||||
errIdx := l.numOut - 1
|
||||
if len(results) > errIdx && !results[errIdx].IsNil() {
|
||||
resErr = results[errIdx].Interface().(error)
|
||||
}
|
||||
}
|
||||
return resErr
|
||||
}()
|
||||
@@ -204,6 +222,181 @@ func (b *EventBus) Emit(ctx context.Context, topic string, payload any) error {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// Waterfall runs handlers sequentially as a transformation pipeline.
|
||||
// The returned value of each handler becomes the payload input for the next handler.
|
||||
// If any handler returns an error or panics, execution aborts immediately.
|
||||
//
|
||||
//nolint:contextcheck
|
||||
func (b *EventBus) Waterfall(ctx context.Context, topic string, initialPayload any) (any, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
listeners := b.getListeners(topic)
|
||||
if len(listeners) == 0 {
|
||||
return initialPayload, nil
|
||||
}
|
||||
|
||||
currentPayload := initialPayload
|
||||
|
||||
for _, l := range listeners {
|
||||
var payloadVal reflect.Value
|
||||
if currentPayload != nil {
|
||||
payloadVal = reflect.ValueOf(currentPayload)
|
||||
}
|
||||
|
||||
args := b.buildArgs(ctx, l, payloadVal)
|
||||
|
||||
var stepVal any
|
||||
var stepErr error
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
stepErr = fmt.Errorf("core/events: panic in waterfall handler for topic %q: %v", topic, r)
|
||||
}
|
||||
}()
|
||||
|
||||
results := l.fnVal.Call(args)
|
||||
if l.returnsErr {
|
||||
errIdx := l.numOut - 1
|
||||
if len(results) > errIdx && !results[errIdx].IsNil() {
|
||||
stepErr = results[errIdx].Interface().(error)
|
||||
}
|
||||
}
|
||||
if stepErr == nil && l.returnsVal && len(results) > 0 {
|
||||
stepVal = results[0].Interface()
|
||||
}
|
||||
}()
|
||||
|
||||
if stepErr != nil {
|
||||
return nil, stepErr
|
||||
}
|
||||
|
||||
if l.returnsVal {
|
||||
currentPayload = stepVal
|
||||
}
|
||||
}
|
||||
|
||||
return currentPayload, nil
|
||||
}
|
||||
|
||||
// Parallel executes all subscribers of the topic concurrently in separate goroutines.
|
||||
// It waits for all handlers to complete and collects any errors via errors.Join.
|
||||
//
|
||||
//nolint:contextcheck
|
||||
func (b *EventBus) Parallel(ctx context.Context, topic string, payload any) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
listeners := b.getListeners(topic)
|
||||
if len(listeners) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var payloadVal reflect.Value
|
||||
if payload != nil {
|
||||
payloadVal = reflect.ValueOf(payload)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errCh := make(chan error, len(listeners))
|
||||
|
||||
for _, l := range listeners {
|
||||
wg.Add(1)
|
||||
go func(listener eventListener) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
errCh <- fmt.Errorf("core/events: panic in parallel handler for topic %q: %v", topic, r)
|
||||
}
|
||||
}()
|
||||
|
||||
args := b.buildArgs(ctx, listener, payloadVal)
|
||||
results := listener.fnVal.Call(args)
|
||||
if listener.returnsErr {
|
||||
errIdx := listener.numOut - 1
|
||||
if len(results) > errIdx && !results[errIdx].IsNil() {
|
||||
errCh <- results[errIdx].Interface().(error)
|
||||
}
|
||||
}
|
||||
}(l)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
|
||||
var errs []error
|
||||
for err := range errCh {
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// Serial executes subscribers strictly in sequence.
|
||||
// If any subscriber returns an error or panics, execution stops immediately and returns that error.
|
||||
//
|
||||
//nolint:contextcheck
|
||||
func (b *EventBus) Serial(ctx context.Context, topic string, payload any) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
listeners := b.getListeners(topic)
|
||||
if len(listeners) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var payloadVal reflect.Value
|
||||
if payload != nil {
|
||||
payloadVal = reflect.ValueOf(payload)
|
||||
}
|
||||
|
||||
for _, l := range listeners {
|
||||
args := b.buildArgs(ctx, l, payloadVal)
|
||||
|
||||
var stepErr error
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
stepErr = fmt.Errorf("core/events: panic in serial handler for topic %q: %v", topic, r)
|
||||
}
|
||||
}()
|
||||
|
||||
results := l.fnVal.Call(args)
|
||||
if l.returnsErr {
|
||||
errIdx := l.numOut - 1
|
||||
if len(results) > errIdx && !results[errIdx].IsNil() {
|
||||
stepErr = results[errIdx].Interface().(error)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if stepErr != nil {
|
||||
return stepErr
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *EventBus) getListeners(topic string) []eventListener {
|
||||
b.mu.RLock()
|
||||
raw := b.handlers[topic]
|
||||
if len(raw) == 0 {
|
||||
b.mu.RUnlock()
|
||||
return nil
|
||||
}
|
||||
listeners := make([]eventListener, len(raw))
|
||||
copy(listeners, raw)
|
||||
b.mu.RUnlock()
|
||||
return listeners
|
||||
}
|
||||
|
||||
func (b *EventBus) buildArgs(ctx context.Context, l eventListener, payloadVal reflect.Value) []reflect.Value {
|
||||
if l.numIn == 0 {
|
||||
return nil
|
||||
|
||||
@@ -247,13 +247,13 @@ func TestEventBusInvalidHandlerPanics(t *testing.T) {
|
||||
})
|
||||
|
||||
assert.Panics(t, func() {
|
||||
// More than 1 return value
|
||||
bus.On("test:invalid", func() (int, error) { return 0, nil })
|
||||
// More than 2 return values
|
||||
bus.On("test:invalid", func() (int, string, error) { return 0, "", nil })
|
||||
})
|
||||
|
||||
assert.Panics(t, func() {
|
||||
// Return value is not error
|
||||
bus.On("test:invalid", func() int { return 0 })
|
||||
// 2 return values, but second is not error
|
||||
bus.On("test:invalid", func() (int, string) { return 0, "" })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -316,3 +316,81 @@ func TestEventBusConcurrentAccess(t *testing.T) {
|
||||
wg.Wait()
|
||||
assert.Greater(t, receivedCount.Load(), int64(0))
|
||||
}
|
||||
|
||||
func TestEventBusWaterfall(t *testing.T) {
|
||||
bus := core.NewEventBus()
|
||||
|
||||
// Handler 1: appends "-first"
|
||||
bus.On("pipeline:transform", func(ctx context.Context, s string) string {
|
||||
return s + "-first"
|
||||
})
|
||||
|
||||
// Handler 2: appends "-second" with error return
|
||||
bus.On("pipeline:transform", func(s string) (string, error) {
|
||||
return s + "-second", nil
|
||||
})
|
||||
|
||||
res, err := bus.Waterfall(context.Background(), "pipeline:transform", "init")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "init-first-second", res)
|
||||
|
||||
// Test short-circuit on error
|
||||
expectedErr := errors.New("waterfall step failed")
|
||||
bus.On("pipeline:error", func(s string) (string, error) {
|
||||
return s, expectedErr
|
||||
})
|
||||
bus.On("pipeline:error", func(s string) string {
|
||||
return s + "-should-not-run"
|
||||
})
|
||||
|
||||
res, err = bus.Waterfall(context.Background(), "pipeline:error", "start")
|
||||
assert.ErrorIs(t, err, expectedErr)
|
||||
assert.Nil(t, res)
|
||||
}
|
||||
|
||||
func TestEventBusParallel(t *testing.T) {
|
||||
bus := core.NewEventBus()
|
||||
|
||||
var counter atomic.Int64
|
||||
err1 := errors.New("parallel err 1")
|
||||
|
||||
bus.On("test:parallel", func(ctx context.Context, val int) error {
|
||||
counter.Add(int64(val))
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.On("test:parallel", func(val int) error {
|
||||
counter.Add(int64(val))
|
||||
return err1
|
||||
})
|
||||
|
||||
err := bus.Parallel(context.Background(), "test:parallel", 10)
|
||||
assert.ErrorIs(t, err, err1)
|
||||
assert.Equal(t, int64(20), counter.Load())
|
||||
}
|
||||
|
||||
func TestEventBusSerial(t *testing.T) {
|
||||
bus := core.NewEventBus()
|
||||
|
||||
var executed []int
|
||||
errStop := errors.New("serial stop")
|
||||
|
||||
bus.On("test:serial", func() error {
|
||||
executed = append(executed, 1)
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.On("test:serial", func() error {
|
||||
executed = append(executed, 2)
|
||||
return errStop
|
||||
})
|
||||
|
||||
bus.On("test:serial", func() error {
|
||||
executed = append(executed, 3)
|
||||
return nil
|
||||
})
|
||||
|
||||
err := bus.Serial(context.Background(), "test:serial", nil)
|
||||
assert.ErrorIs(t, err, errStop)
|
||||
assert.Equal(t, []int{1, 2}, executed)
|
||||
}
|
||||
|
||||
@@ -278,3 +278,44 @@ func TestContextExtensionPointsIntegration(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, eventReceived)
|
||||
}
|
||||
|
||||
func TestExtensionPointsUnregister(t *testing.T) {
|
||||
ctx := core.NewContext(context.Background())
|
||||
|
||||
// 1. Router unregister
|
||||
rd := ctx.Router().GET("/temp", "temp_handler")
|
||||
assert.Greater(t, rd.ID, uint64(0))
|
||||
assert.Len(t, ctx.Router().Routes(), 1)
|
||||
assert.True(t, ctx.Router().Unregister("GET", "/temp"))
|
||||
assert.Len(t, ctx.Router().Routes(), 0)
|
||||
|
||||
rd2 := ctx.Router().POST("/temp2", "temp2_handler")
|
||||
assert.Len(t, ctx.Router().Routes(), 1)
|
||||
assert.True(t, ctx.Router().UnregisterByID(rd2.ID))
|
||||
assert.Len(t, ctx.Router().Routes(), 0)
|
||||
|
||||
// 2. Task unregister
|
||||
ctx.Task().Register("temp:task", "handler")
|
||||
assert.Len(t, ctx.Task().Tasks(), 1)
|
||||
assert.True(t, ctx.Task().Unregister("temp:task"))
|
||||
assert.Len(t, ctx.Task().Tasks(), 0)
|
||||
|
||||
// 3. Schedule unregister
|
||||
ctx.Schedule().RegisterCron("@hourly", "temp:cron", nil)
|
||||
assert.Len(t, ctx.Schedule().Schedules(), 1)
|
||||
assert.True(t, ctx.Schedule().Unregister("temp:cron"))
|
||||
assert.Len(t, ctx.Schedule().Schedules(), 0)
|
||||
|
||||
// 4. Setting unregister
|
||||
ctx.Settings().Register(extpoints.SettingSchema{Key: "temp.key", Default: 1})
|
||||
assert.Len(t, ctx.Settings().Schemas(), 1)
|
||||
assert.True(t, ctx.Settings().Unregister("temp.key"))
|
||||
assert.Len(t, ctx.Settings().Schemas(), 0)
|
||||
|
||||
// 5. Migration unregister
|
||||
fsys := fstest.MapFS{"001.sql": &fstest.MapFile{Data: []byte("-- migration")}}
|
||||
ctx.Migrations().Register("temp_plugin", fsys)
|
||||
assert.Len(t, ctx.Migrations().Entries(), 1)
|
||||
assert.True(t, ctx.Migrations().Unregister("temp_plugin"))
|
||||
assert.Len(t, ctx.Migrations().Entries(), 0)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ type MigrationExtension interface {
|
||||
Register(pluginID string, fsys fs.FS, dir ...string)
|
||||
Entries() []MigrationEntry
|
||||
Get(pluginID string) (MigrationEntry, bool)
|
||||
Unregister(pluginID string) bool
|
||||
}
|
||||
|
||||
// MigrationRegistry collects and stores migration entries from plugins.
|
||||
@@ -69,6 +70,25 @@ func (m *MigrationRegistry) Register(pluginID string, fsys fs.FS, dir ...string)
|
||||
m.lookup[pluginID] = entry
|
||||
}
|
||||
|
||||
// Unregister removes a registered migration entry by plugin ID.
|
||||
func (m *MigrationRegistry) Unregister(pluginID string) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if _, exists := m.lookup[pluginID]; !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
delete(m.lookup, pluginID)
|
||||
for i, e := range m.entries {
|
||||
if e.PluginID == pluginID {
|
||||
m.entries = append(m.entries[:i], m.entries[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Entries returns a copy of all registered migration entries in registration order.
|
||||
func (m *MigrationRegistry) Entries() []MigrationEntry {
|
||||
m.mu.RLock()
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
// RouteDefinition holds the metadata and handler list for a single HTTP route.
|
||||
type RouteDefinition struct {
|
||||
ID uint64
|
||||
Method string
|
||||
Path string
|
||||
Handlers []any
|
||||
@@ -31,11 +32,14 @@ type RouterExtension interface {
|
||||
Any(path string, handlers ...any) []RouteDefinition
|
||||
Routes() []RouteDefinition
|
||||
Middlewares() []any
|
||||
Unregister(method, path string) bool
|
||||
UnregisterByID(id uint64) bool
|
||||
}
|
||||
|
||||
// RouterRegistry implements RouterExtension as the root route and middleware collector.
|
||||
type RouterRegistry struct {
|
||||
mu sync.RWMutex
|
||||
nextID uint64
|
||||
routes []RouteDefinition
|
||||
middlewares []any
|
||||
}
|
||||
@@ -75,7 +79,9 @@ func (r *RouterRegistry) Handle(method, path string, handlers ...any) RouteDefin
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
r.nextID++
|
||||
rd := RouteDefinition{
|
||||
ID: r.nextID,
|
||||
Method: strings.ToUpper(method),
|
||||
Path: cleanPath(path),
|
||||
Handlers: handlers,
|
||||
@@ -85,6 +91,37 @@ func (r *RouterRegistry) Handle(method, path string, handlers ...any) RouteDefin
|
||||
return rd
|
||||
}
|
||||
|
||||
// Unregister removes a route matching method and path from the registry.
|
||||
func (r *RouterRegistry) Unregister(method, path string) bool {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
targetMethod := strings.ToUpper(method)
|
||||
targetPath := cleanPath(path)
|
||||
|
||||
for i, rd := range r.routes {
|
||||
if rd.Method == targetMethod && rd.Path == targetPath {
|
||||
r.routes = append(r.routes[:i], r.routes[i+1:]...)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UnregisterByID removes a route by its unique ID.
|
||||
func (r *RouterRegistry) UnregisterByID(id uint64) bool {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
for i, rd := range r.routes {
|
||||
if rd.ID == id {
|
||||
r.routes = append(r.routes[:i], r.routes[i+1:]...)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GET registers a GET route.
|
||||
func (r *RouterRegistry) GET(path string, handlers ...any) RouteDefinition {
|
||||
return r.Handle("GET", path, handlers...)
|
||||
@@ -176,7 +213,9 @@ func (g *RouterGroup) Handle(method, path string, handlers ...any) RouteDefiniti
|
||||
allMiddlewares = append(allMiddlewares, g.registry.middlewares...)
|
||||
allMiddlewares = append(allMiddlewares, g.middlewares...)
|
||||
|
||||
g.registry.nextID++
|
||||
rd := RouteDefinition{
|
||||
ID: g.registry.nextID,
|
||||
Method: strings.ToUpper(method),
|
||||
Path: fullPath,
|
||||
Handlers: handlers,
|
||||
@@ -186,6 +225,17 @@ func (g *RouterGroup) Handle(method, path string, handlers ...any) RouteDefiniti
|
||||
return rd
|
||||
}
|
||||
|
||||
// Unregister removes a route under this group prefix matching method and path.
|
||||
func (g *RouterGroup) Unregister(method, path string) bool {
|
||||
fullPath := joinPaths(g.prefix, path)
|
||||
return g.registry.Unregister(method, fullPath)
|
||||
}
|
||||
|
||||
// UnregisterByID removes a route by its unique ID.
|
||||
func (g *RouterGroup) UnregisterByID(id uint64) bool {
|
||||
return g.registry.UnregisterByID(id)
|
||||
}
|
||||
|
||||
// GET registers a GET route in this group.
|
||||
func (g *RouterGroup) GET(path string, handlers ...any) RouteDefinition {
|
||||
return g.Handle("GET", path, handlers...)
|
||||
|
||||
@@ -32,6 +32,7 @@ type ScheduleExtension interface {
|
||||
RegisterCron(spec string, taskType string, payload any, opts ...ScheduleOption)
|
||||
Schedules() []ScheduleDefinition
|
||||
Get(taskType string) (ScheduleDefinition, bool)
|
||||
Unregister(taskType string) bool
|
||||
}
|
||||
|
||||
// ScheduleRegistry collects and manages schedule registrations.
|
||||
@@ -85,6 +86,25 @@ func (s *ScheduleRegistry) RegisterCron(spec string, taskType string, payload an
|
||||
s.Register(spec, taskType, payload, opts...)
|
||||
}
|
||||
|
||||
// Unregister removes a registered schedule definition by its task type.
|
||||
func (s *ScheduleRegistry) Unregister(taskType string) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, exists := s.lookup[taskType]; !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
delete(s.lookup, taskType)
|
||||
for i, item := range s.schedules {
|
||||
if item.TaskType == taskType {
|
||||
s.schedules = append(s.schedules[:i], s.schedules[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Schedules returns a copy of all registered ScheduleDefinitions.
|
||||
func (s *ScheduleRegistry) Schedules() []ScheduleDefinition {
|
||||
s.mu.RLock()
|
||||
|
||||
@@ -22,6 +22,7 @@ type SettingExtension interface {
|
||||
Register(schema SettingSchema)
|
||||
Schemas() []SettingSchema
|
||||
Get(key string) (SettingSchema, bool)
|
||||
Unregister(key string) bool
|
||||
}
|
||||
|
||||
// SettingRegistry collects and manages setting configuration schemas.
|
||||
@@ -62,6 +63,25 @@ func (s *SettingRegistry) Register(schema SettingSchema) {
|
||||
s.lookup[schema.Key] = schema
|
||||
}
|
||||
|
||||
// Unregister removes a registered SettingSchema by its key.
|
||||
func (s *SettingRegistry) Unregister(key string) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, exists := s.lookup[key]; !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
delete(s.lookup, key)
|
||||
for i, item := range s.schemas {
|
||||
if item.Key == key {
|
||||
s.schemas = append(s.schemas[:i], s.schemas[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Schemas returns a copy of all registered SettingSchemas.
|
||||
func (s *SettingRegistry) Schemas() []SettingSchema {
|
||||
s.mu.RLock()
|
||||
|
||||
@@ -57,6 +57,7 @@ type TaskExtension interface {
|
||||
Register(pattern string, handler any, opts ...TaskOption)
|
||||
Tasks() []TaskDefinition
|
||||
Get(pattern string) (TaskDefinition, bool)
|
||||
Unregister(pattern string) bool
|
||||
}
|
||||
|
||||
// TaskRegistry collects and manages task registrations.
|
||||
@@ -104,6 +105,25 @@ func (t *TaskRegistry) Register(pattern string, handler any, opts ...TaskOption)
|
||||
t.lookup[pattern] = td
|
||||
}
|
||||
|
||||
// Unregister removes a registered task definition by its pattern.
|
||||
func (t *TaskRegistry) Unregister(pattern string) bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if _, exists := t.lookup[pattern]; !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
delete(t.lookup, pattern)
|
||||
for i, item := range t.tasks {
|
||||
if item.Pattern == pattern {
|
||||
t.tasks = append(t.tasks[:i], t.tasks[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Tasks returns a copy of all registered TaskDefinitions.
|
||||
func (t *TaskRegistry) Tasks() []TaskDefinition {
|
||||
t.mu.RLock()
|
||||
|
||||
Reference in New Issue
Block a user