Files
wehub-resource-sync bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

185 lines
5.2 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package hook
import (
"context"
"sync"
"github.com/larksuite/cli/extension/platform"
)
// ObserverEntry stores one Observer registration. The full hook name
// (already namespaced with plugin prefix by the caller) lets diagnostic
// output point at the responsible plugin.
type ObserverEntry struct {
Name string
When platform.When
Selector platform.Selector
Fn platform.Observer
}
// WrapperEntry stores one Wrapper registration. Wrappers compose in
// registration order; the outermost (registered first) runs first.
type WrapperEntry struct {
Name string
Selector platform.Selector
Fn platform.Wrapper
}
// LifecycleEntry stores one lifecycle handler. Selector is unused
// (lifecycle events are global), but Name is preserved for diagnostics.
type LifecycleEntry struct {
Name string
Event platform.LifecycleEvent
Fn platform.LifecycleHandler
}
// Registry holds all registered hooks. The framework constructs one
// Registry per binary execution; concurrent reads after Install
// commits are safe because the maps are not mutated thereafter. Writes
// (during Install) are serialised by the internalplatform.
type Registry struct {
mu sync.RWMutex
observers []ObserverEntry
wrappers []WrapperEntry
lifecycles []LifecycleEntry
}
// NewRegistry returns an empty Registry.
func NewRegistry() *Registry { return &Registry{} }
// Observers returns a snapshot of all registered observers. Order is
// registration order. Diagnostic commands (config plugins show) call
// this to enumerate every hook attached to the binary.
func (r *Registry) Observers() []ObserverEntry {
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]ObserverEntry, len(r.observers))
copy(out, r.observers)
return out
}
// Wrappers returns a snapshot of all registered wrappers. Order is
// registration order (outermost first).
func (r *Registry) Wrappers() []WrapperEntry {
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]WrapperEntry, len(r.wrappers))
copy(out, r.wrappers)
return out
}
// Lifecycles returns a snapshot of all registered lifecycle handlers.
func (r *Registry) Lifecycles() []LifecycleEntry {
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]LifecycleEntry, len(r.lifecycles))
copy(out, r.lifecycles)
return out
}
// AddObserver registers an Observer. Caller is responsible for namespacing
// (the platformhost does this). Nil fn is silently skipped -- the staging
// Registrar should reject invalid registrations before this layer.
func (r *Registry) AddObserver(e ObserverEntry) {
if e.Fn == nil {
return
}
r.mu.Lock()
defer r.mu.Unlock()
r.observers = append(r.observers, e)
}
// AddWrapper registers a Wrapper.
func (r *Registry) AddWrapper(e WrapperEntry) {
if e.Fn == nil {
return
}
r.mu.Lock()
defer r.mu.Unlock()
r.wrappers = append(r.wrappers, e)
}
// AddLifecycle registers a LifecycleHandler.
func (r *Registry) AddLifecycle(e LifecycleEntry) {
if e.Fn == nil {
return
}
r.mu.Lock()
defer r.mu.Unlock()
r.lifecycles = append(r.lifecycles, e)
}
// MatchingObservers returns the observers whose selector matches the
// command at the given When stage. Result is a slice (not a generator)
// so callers can iterate without holding the registry lock.
func (r *Registry) MatchingObservers(cmd platform.CommandView, when platform.When) []ObserverEntry {
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]ObserverEntry, 0, len(r.observers))
for _, e := range r.observers {
if e.When == when && e.Selector != nil && e.Selector(cmd) {
out = append(out, e)
}
}
return out
}
// MatchingWrappers returns the wrappers whose selector matches the
// command. Order matches registration order.
func (r *Registry) MatchingWrappers(cmd platform.CommandView) []WrapperEntry {
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]WrapperEntry, 0, len(r.wrappers))
for _, e := range r.wrappers {
if e.Selector != nil && e.Selector(cmd) {
out = append(out, e)
}
}
return out
}
// LifecycleHandlers returns handlers for a given event in registration
// order.
func (r *Registry) LifecycleHandlers(event platform.LifecycleEvent) []LifecycleEntry {
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]LifecycleEntry, 0, len(r.lifecycles))
for _, e := range r.lifecycles {
if e.Event == event {
out = append(out, e)
}
}
return out
}
// ComposeWrappers folds a slice of Wrappers into a single Wrapper that
// applies them in registration order (outermost first). Empty slice
// returns the identity Wrapper (next as-is). Inspired by
// grpc.ChainUnaryInterceptor.
func ComposeWrappers(ws []platform.Wrapper) platform.Wrapper {
if len(ws) == 0 {
return identityWrapper
}
return func(next platform.Handler) platform.Handler {
// Build from the inside out so the first registered Wrapper
// ends up outermost.
for i := len(ws) - 1; i >= 0; i-- {
next = ws[i](next)
}
return next
}
}
// identityWrapper is the no-op wrapper used when there are no matching
// Wrappers for a command -- callers can always compose into
// next(ctx, inv) without a nil check.
func identityWrapper(next platform.Handler) platform.Handler {
return func(ctx context.Context, inv platform.Invocation) error {
return next(ctx, inv)
}
}