Add namespace event filtering (SubscribeWithFilter)
All checks were successful
CI / build (pull_request) Successful in 19s
CI / build (push) Successful in 39s

Adds support for filtering events by type or actor pattern within namespace
subscriptions. Key changes:

- Add SubscriptionFilter type with EventTypes and ActorPattern fields
- Add SubscribeWithFilter to EventBroadcaster interface
- Implement filtering in EventBus with full wildcard pattern support preserved
- Implement filtering in NATSEventBus (server-side namespace, client-side filters)
- Add MatchActorPattern function for actor ID pattern matching
- Add comprehensive unit tests for all filtering scenarios

Filter Processing:
- EventTypes: Event must match at least one type in the list (OR within types)
- ActorPattern: Event's ActorID must match the pattern (supports * and > wildcards)
- Multiple filters are combined with AND logic

This implementation works alongside the existing wildcard subscription support:
- Namespace wildcards (* and >) work with event filters
- Filters are applied after namespace pattern matching
- Metrics are properly recorded for filtered subscriptions

Closes #21

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit was merged in pull request #54.
This commit is contained in:
2026-01-10 23:45:57 +01:00
parent e3dbe3d52d
commit ef73fb6bfd
5 changed files with 750 additions and 38 deletions

View File

@@ -81,3 +81,117 @@ func matchTokens(patternTokens, namespaceTokens []string) bool {
func IsWildcardPattern(pattern string) bool {
return strings.Contains(pattern, "*") || strings.Contains(pattern, ">")
}
// SubscriptionFilter defines optional filters for event subscriptions.
// All configured filters are combined with AND logic - an event must match
// all specified criteria to be delivered to the subscriber.
//
// Filter Processing:
// - EventTypes: Event must have an EventType matching at least one in the list (OR within types)
// - ActorPattern: Event's ActorID must match the pattern (supports * and > wildcards)
//
// Filtering is applied client-side in the EventBus. For NATSEventBus, namespace-level
// filtering uses NATS subject patterns, while EventTypes and ActorPattern filtering
// happens after message receipt.
type SubscriptionFilter struct {
// EventTypes filters events by type. Empty slice means all event types.
// If specified, only events with an EventType in this list are delivered.
// Example: []string{"OrderPlaced", "OrderShipped"} receives only those event types.
EventTypes []string
// ActorPattern filters events by actor ID pattern. Empty string means all actors.
// Supports NATS-style wildcards:
// - "*" matches a single token (e.g., "order-*" matches "order-123", "order-456")
// - ">" matches one or more tokens (e.g., "order.>" matches "order.us.123", "order.eu.456")
// Example: "order-*" receives events only for actors starting with "order-"
ActorPattern string
}
// IsEmpty returns true if no filters are configured.
func (f *SubscriptionFilter) IsEmpty() bool {
return len(f.EventTypes) == 0 && f.ActorPattern == ""
}
// Matches returns true if the event matches all configured filters.
// An empty filter matches all events.
func (f *SubscriptionFilter) Matches(event *Event) bool {
if event == nil {
return false
}
// Check event type filter
if len(f.EventTypes) > 0 {
typeMatch := false
for _, et := range f.EventTypes {
if event.EventType == et {
typeMatch = true
break
}
}
if !typeMatch {
return false
}
}
// Check actor pattern filter
if f.ActorPattern != "" {
if !MatchActorPattern(f.ActorPattern, event.ActorID) {
return false
}
}
return true
}
// MatchActorPattern checks if an actor ID matches a pattern.
// Uses the same matching logic as MatchNamespacePattern for consistency.
//
// Patterns:
// - "*" matches a single token (e.g., "order-*" matches "order-123")
// - ">" matches one or more tokens (e.g., "order.>" matches "order.us.east")
// - Exact strings match exactly (e.g., "order-123" matches only "order-123")
//
// Note: For simple prefix matching without dots (e.g., "order-*" matching "order-123"),
// this uses simplified matching where "*" matches any remaining characters in a token.
func MatchActorPattern(pattern, actorID string) bool {
// Empty pattern matches nothing
if pattern == "" {
return false
}
// Empty actor ID matches nothing except ">"
if actorID == "" {
return false
}
// If pattern contains dots, use token-based matching (same as namespace)
if strings.Contains(pattern, ".") || strings.Contains(actorID, ".") {
return MatchNamespacePattern(pattern, actorID)
}
// Simple matching for non-tokenized patterns
// ">" matches any non-empty actor ID
if pattern == ">" {
return true
}
// "*" matches any single-token actor ID (no dots)
if pattern == "*" {
return true
}
// Check for suffix wildcard (e.g., "order-*")
if strings.HasSuffix(pattern, "*") {
prefix := strings.TrimSuffix(pattern, "*")
return strings.HasPrefix(actorID, prefix)
}
// Check for suffix multi-match (e.g., "order->")
if strings.HasSuffix(pattern, ">") {
prefix := strings.TrimSuffix(pattern, ">")
return strings.HasPrefix(actorID, prefix)
}
// Exact match
return pattern == actorID
}