Add namespace event filtering (SubscribeWithFilter)
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:
114
pattern.go
114
pattern.go
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user