diff --git a/projects/solace-message-client/src/lib/solace-message-client.module.ts b/projects/solace-message-client/src/lib/solace-message-client.module.ts index d132d46..450f6a9 100644 --- a/projects/solace-message-client/src/lib/solace-message-client.module.ts +++ b/projects/solace-message-client/src/lib/solace-message-client.module.ts @@ -1,7 +1,6 @@ import {Inject, Injectable, InjectionToken, ModuleWithProviders, NgModule, Optional, SkipSelf} from '@angular/core'; import {NullSolaceMessageClient, SolaceMessageClient} from './solace-message-client'; import {ɵSolaceMessageClient} from './ɵsolace-message-client'; -import {TopicMatcher} from './topic-matcher'; import {SolaceSessionProvider, ɵSolaceSessionProvider} from './solace-session-provider'; import {SOLACE_MESSAGE_CLIENT_CONFIG, SolaceMessageClientConfig} from './solace-message-client.config'; import {provideLogger} from './logger'; @@ -116,7 +115,6 @@ export class SolaceMessageClientModule { {provide: SOLACE_MESSAGE_CLIENT_CONFIG, useValue: config}, {provide: SolaceMessageClient, useClass: ɵSolaceMessageClient}, {provide: SolaceSessionProvider, useClass: ɵSolaceSessionProvider}, - TopicMatcher, provideLogger(), { provide: FORROOT_GUARD, diff --git a/projects/solace-message-client/src/lib/topic-matcher.spec.ts b/projects/solace-message-client/src/lib/topic-matcher.spec.ts index a5d26e1..02de848 100644 --- a/projects/solace-message-client/src/lib/topic-matcher.spec.ts +++ b/projects/solace-message-client/src/lib/topic-matcher.spec.ts @@ -1,89 +1,107 @@ -import {TestBed} from '@angular/core/testing'; import {TopicMatcher} from './topic-matcher'; describe('SolaceMessageClient', () => { - let testee: TopicMatcher; - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - TopicMatcher, - ], - }); - testee = TestBed.inject(TopicMatcher); - }); - - it('should not match a `null` topic', () => { - expect(testee.matchesSubscriptionTopic(null, 'a/b/c')).toBeFalse(); + it('should not match a `undefined` topic', () => { + expect(new TopicMatcher('a/b/c').matches(undefined)).toBeFalse(); }); it('should not match an empty topic', () => { - expect(testee.matchesSubscriptionTopic('', 'a/b/c')).toBeFalse(); + expect(new TopicMatcher('a/b/c').matches('')).toBeFalse(); }); it('should match when publishing a message to the exact topic \'a\'', () => { - expect(testee.matchesSubscriptionTopic('a', 'a')).toBeTrue(); + expect(new TopicMatcher('a').matches('a')).toBeTrue(); + }); + + it('should match when publishing a message to the topic with #share and #noexport', () => { + expect(new TopicMatcher('#share/sharename/a').matches('a')).toBeTrue(); + expect(new TopicMatcher('#share/sharename/a').matches('b')).toBeFalse(); + + expect(new TopicMatcher('#noexport/a').matches('a')).toBeTrue(); + expect(new TopicMatcher('#noexport/a').matches('b')).toBeFalse(); + + expect(new TopicMatcher('#noexport/#share/sharename/a').matches('a')).toBeTrue(); + expect(new TopicMatcher('#noexport/#share/sharename/a').matches('b')).toBeFalse(); + + expect(new TopicMatcher('#share/sharename/a/*/b').matches('a/x/b')).toBeTrue(); + expect(new TopicMatcher('#share/sharename/a/*/b').matches('a/x/c')).toBeFalse(); + + expect(new TopicMatcher('#noexport/a/*/b').matches('a/x/b')).toBeTrue(); + expect(new TopicMatcher('#noexport/a/*/b').matches('a/x/c')).toBeFalse(); + + expect(new TopicMatcher('#noexport/#share/sharename/a/*/b').matches('a/x/b')).toBeTrue(); + expect(new TopicMatcher('#noexport/#share/sharename/a/*/b').matches('a/x/c')).toBeFalse(); + + expect(new TopicMatcher('#share/sharename/a/>').matches('a/x/b/z')).toBeTrue(); + expect(new TopicMatcher('#share/sharename/a/>').matches('b/x/b/z')).toBeFalse(); + + expect(new TopicMatcher('#noexport/a/>').matches('a/x/b/z')).toBeTrue(); + expect(new TopicMatcher('#noexport/a/>').matches('b/x/b/z')).toBeFalse(); + + expect(new TopicMatcher('#noexport/#share/sharename/a/>').matches('a/x/b/z')).toBeTrue(); + expect(new TopicMatcher('#noexport/#share/sharename/a/b').matches('a/c')).toBeFalse(); }); it('should match when publishing a message to the exact topic \'a/b\'', () => { - expect(testee.matchesSubscriptionTopic('a/b', 'a/b')).toBeTrue(); + expect(new TopicMatcher('a/b').matches('a/b')).toBeTrue(); }); it('should match when publishing a message to the exact topic \'a/b/c\'', () => { - expect(testee.matchesSubscriptionTopic('a/b/c', 'a/b/c')).toBeTrue(); + expect(new TopicMatcher('a/b/c').matches('a/b/c')).toBeTrue(); }); it('should not match the subscription topic \'a/b/c\' when published to the topic \'a\'', () => { - expect(testee.matchesSubscriptionTopic('a', 'a/b/c')).toBeFalse(); + expect(new TopicMatcher('a/b/c').matches('a')).toBeFalse(); }); it('should not match the subscription topic \'a/b/c\' when published to the topic \'a/b\'', () => { - expect(testee.matchesSubscriptionTopic('a/b', 'a/b/c')).toBeFalse(); + expect(new TopicMatcher('a/b/c').matches('a/b')).toBeFalse(); }); it('should not match the subscription topic \'a/b/c\' when published to the topic \'a/b/c/d\'', () => { - expect(testee.matchesSubscriptionTopic('a/b/c/d', 'a/b/c')).toBeFalse(); + expect(new TopicMatcher('a/b/c').matches('a/b/c/d')).toBeFalse(); }); it('should fulfill examples at https://docs.solace.com/PubSub-Basics/Wildcard-Charaters-Topic-Subs.htm', () => { - expect(testee.matchesSubscriptionTopic('animals/domestic/cats', 'animals/domestic/*')).toBeTrue(); - expect(testee.matchesSubscriptionTopic('animals/domestic/dogs', 'animals/domestic/*')).toBeTrue(); - expect(testee.matchesSubscriptionTopic('animals/domestic/dogs/beagles', 'animals/domestic/*')).toBeFalse(); - - expect(testee.matchesSubscriptionTopic('animals/domestic/cats/persian', 'animals/*/cats/*')).toBeTrue(); - expect(testee.matchesSubscriptionTopic('animals/wild/cats/leopard', 'animals/*/cats/*')).toBeTrue(); - expect(testee.matchesSubscriptionTopic('animals/domestic/cats/persian/grey', 'animals/*/cats/*')).toBeFalse(); - expect(testee.matchesSubscriptionTopic('animals/domestic/dogs/beagles', 'animals/*/cats/*')).toBeFalse(); - - expect(testee.matchesSubscriptionTopic('animals/domestic/dog', 'animals/domestic/dog*')).toBeTrue(); - expect(testee.matchesSubscriptionTopic('animals/domestic/doggy', 'animals/domestic/dog*')).toBeTrue(); - expect(testee.matchesSubscriptionTopic('animals/domestic/dog/beagle', 'animals/domestic/dog*')).toBeFalse(); - expect(testee.matchesSubscriptionTopic('animals/domestic/cat', 'animals/domestic/dog*')).toBeFalse(); - - expect(testee.matchesSubscriptionTopic('animals/domestic/cats', 'animals/domestic/>')).toBeTrue(); - expect(testee.matchesSubscriptionTopic('animals/domestic/dogs/beagles', 'animals/domestic/>')).toBeTrue(); - expect(testee.matchesSubscriptionTopic('animals', 'animals/domestic/>')).toBeFalse(); - expect(testee.matchesSubscriptionTopic('animals', 'animals/domestic')).toBeFalse(); - expect(testee.matchesSubscriptionTopic('animals', 'animals/Domestic')).toBeFalse(); - - expect(testee.matchesSubscriptionTopic('animals/domestic/cats/tabby/grey', 'animals/*/cats/>')).toBeTrue(); - expect(testee.matchesSubscriptionTopic('animals/wild/cats/leopard', 'animals/*/cats/>')).toBeTrue(); - expect(testee.matchesSubscriptionTopic('animals/domestic/dogs/beagles', 'animals/*/cats/>')).toBeFalse(); - - expect(testee.matchesSubscriptionTopic('my/test/topic', 'my/test/*')).toBeTrue(); - expect(testee.matchesSubscriptionTopic('My/Test/Topic', 'my/test/*')).toBeFalse(); - expect(testee.matchesSubscriptionTopic('my/test', 'my/test/*')).toBeFalse(); - - expect(testee.matchesSubscriptionTopic('animals/red/wild', 'animals/red*/wild')).toBeTrue(); - expect(testee.matchesSubscriptionTopic('animals/reddish/wild', 'animals/red*/wild')).toBeTrue(); - - expect(testee.matchesSubscriptionTopic('animals/*bro', 'animals/*bro')).toBeTrue(); - expect(testee.matchesSubscriptionTopic('animals/bro', 'animals/*bro')).toBeFalse(); - expect(testee.matchesSubscriptionTopic('animals/xbro', 'animals/*bro')).toBeFalse(); - - expect(testee.matchesSubscriptionTopic('animals/br*wn', 'animals/br*wn')).toBeTrue(); - expect(testee.matchesSubscriptionTopic('animals/brown', 'animals/br*wn')).toBeFalse(); - expect(testee.matchesSubscriptionTopic('animals/brwn', 'animals/br*wn')).toBeFalse(); - expect(testee.matchesSubscriptionTopic('animals/brOOwn', 'animals/br*wn')).toBeFalse(); + expect(new TopicMatcher('animals/domestic/*').matches('animals/domestic/cats')).toBeTrue(); + expect(new TopicMatcher('animals/domestic/*').matches('animals/domestic/dogs')).toBeTrue(); + expect(new TopicMatcher('animals/domestic/*').matches('animals/domestic/dogs/beagles')).toBeFalse(); + + expect(new TopicMatcher('animals/*/cats/*').matches('animals/domestic/cats/persian')).toBeTrue(); + expect(new TopicMatcher('animals/*/cats/*').matches('animals/wild/cats/leopard')).toBeTrue(); + expect(new TopicMatcher('animals/*/cats/*').matches('animals/domestic/cats/persian/grey')).toBeFalse(); + expect(new TopicMatcher('animals/*/cats/*').matches('animals/domestic/dogs/beagles')).toBeFalse(); + + expect(new TopicMatcher('animals/domestic/dog*').matches('animals/domestic/dog')).toBeTrue(); + expect(new TopicMatcher('animals/domestic/dog*').matches('animals/domestic/doggy')).toBeTrue(); + expect(new TopicMatcher('animals/domestic/dog*').matches('animals/domestic/dog/beagle')).toBeFalse(); + expect(new TopicMatcher('animals/domestic/dog*').matches('animals/domestic/cat')).toBeFalse(); + + expect(new TopicMatcher('animals/domestic/>').matches('animals/domestic/cats')).toBeTrue(); + expect(new TopicMatcher('animals/domestic/>').matches('animals/domestic/dogs/beagles')).toBeTrue(); + expect(new TopicMatcher('animals/domestic/>').matches('animals')).toBeFalse(); + expect(new TopicMatcher('animals/domestic').matches('animals')).toBeFalse(); + expect(new TopicMatcher('animals/Domestic').matches('animals')).toBeFalse(); + + expect(new TopicMatcher('animals/*/cats/>').matches('animals/domestic/cats/tabby/grey')).toBeTrue(); + expect(new TopicMatcher('animals/*/cats/>').matches('animals/wild/cats/leopard')).toBeTrue(); + expect(new TopicMatcher('animals/*/cats/>').matches('animals/domestic/dogs/beagles')).toBeFalse(); + + expect(new TopicMatcher('my/test/*').matches('my/test/topic')).toBeTrue(); + expect(new TopicMatcher('my/test/*').matches('My/Test/Topic')).toBeFalse(); + expect(new TopicMatcher('my/test/*').matches('my/test')).toBeFalse(); + + expect(new TopicMatcher('animals/red*/wild').matches('animals/red/wild')).toBeTrue(); + expect(new TopicMatcher('animals/red*/wild').matches('animals/reddish/wild')).toBeTrue(); + + expect(new TopicMatcher('animals/*bro').matches('animals/*bro')).toBeTrue(); + expect(new TopicMatcher('animals/*bro').matches('animals/bro')).toBeFalse(); + expect(new TopicMatcher('animals/*bro').matches('animals/xbro')).toBeFalse(); + + expect(new TopicMatcher('animals/br*wn').matches('animals/br*wn')).toBeTrue(); + expect(new TopicMatcher('animals/br*wn').matches('animals/brown')).toBeFalse(); + expect(new TopicMatcher('animals/br*wn').matches('animals/brwn')).toBeFalse(); + expect(new TopicMatcher('animals/br*wn').matches('animals/brOOwn')).toBeFalse(); }); }); diff --git a/projects/solace-message-client/src/lib/topic-matcher.ts b/projects/solace-message-client/src/lib/topic-matcher.ts index a5863b4..63db255 100644 --- a/projects/solace-message-client/src/lib/topic-matcher.ts +++ b/projects/solace-message-client/src/lib/topic-matcher.ts @@ -1,26 +1,28 @@ -import {Injectable} from '@angular/core'; -import {Destination} from 'solclientjs'; - /** * Matches exact topics as used when publishing messages against subscription topics. * * This class implements the rules for 'Wildcard Characters in SMF Topic Subscriptions', * as outlined here: https://docs.solace.com/PubSub-Basics/Wildcard-Charaters-Topic-Subs.htm. */ -@Injectable() export class TopicMatcher { - public matchesSubscriptionTopic(testeeTopic: string | Destination | null, subscriptionTopic: string | Destination): boolean { - if (!testeeTopic) { + private readonly _subscriptionTopic: string[]; + + constructor(subscriptionTopic: string) { + this._subscriptionTopic = parseSubscriptionTopic(subscriptionTopic); + } + + public matches(topic: string | undefined): boolean { + if (!topic) { return false; } - const testeeSegments = coerceTopicName(testeeTopic).split('/'); - const subscriptionTopicSegments = coerceTopicName(subscriptionTopic).split('/'); + const testeeSegments = topic.split('/'); + const subscriptionSegments = this._subscriptionTopic; - for (let i = 0; i < subscriptionTopicSegments.length; i++) { - const subscriptionTopicSegment = subscriptionTopicSegments[i]; + for (let i = 0; i < subscriptionSegments.length; i++) { + const subscriptionTopicSegment = subscriptionSegments[i]; const testee = testeeSegments[i]; - const isLastSubscriptionTopicSegment = (i === subscriptionTopicSegments.length - 1); + const isLastSubscriptionTopicSegment = (i === subscriptionSegments.length - 1); if (testee === undefined) { return false; @@ -42,13 +44,27 @@ export class TopicMatcher { return false; } } - return testeeSegments.length === subscriptionTopicSegments.length; + return testeeSegments.length === subscriptionSegments.length; } } -function coerceTopicName(topic: string | Destination): string { - if (typeof topic === 'string') { - return topic; +/** + * Parses the subscription topic, removing #noexport and #share segments, if any. + */ +function parseSubscriptionTopic(topic: string): string[] { + const segments = topic.split('/'); + + // Remove #noexport segment, if any. See https://docs.solace.com/Messaging/No-Export.htm + // Example: #noexport/#share/ShareName/topicFilter, #noexport/topicFilter + if (segments[0] === '#noexport') { + segments.shift(); + } + + // Remove #share segments, if any. See https://docs.solace.com/Messaging/Direct-Msg/Direct-Messages.htm + // Examples: #share// + if (segments[0] === '#share') { + segments.shift(); // removes #share segment + segments.shift(); // removes share name segment } - return topic.getName(); + return segments; } diff --git "a/projects/solace-message-client/src/lib/\311\265solace-message-client.ts" "b/projects/solace-message-client/src/lib/\311\265solace-message-client.ts" index 9e0e7d0..4b964e3 100644 --- "a/projects/solace-message-client/src/lib/\311\265solace-message-client.ts" +++ "b/projects/solace-message-client/src/lib/\311\265solace-message-client.ts" @@ -29,7 +29,6 @@ export class ɵSolaceMessageClient implements SolaceMessageClient, OnDestroy { public connected$: Observable; constructor(private _sessionProvider: SolaceSessionProvider, - private _topicMatcher: TopicMatcher, private _injector: Injector, private _logger: Logger, private _zone: NgZone) { @@ -206,6 +205,7 @@ export class ɵSolaceMessageClient implements SolaceMessageClient, OnDestroy { const unsubscribe$ = new Subject(); const topicDestination = createSubscriptionTopicDestination(topic); const observeOutsideAngular = options?.emitOutsideAngularZone ?? false; + const topicMatcher = new TopicMatcher(topicDestination.getName()); // Wait until initialized the session so that 'subscriptionExecutor' and 'subscriptionCounter' are initialized. this.session @@ -217,7 +217,7 @@ export class ɵSolaceMessageClient implements SolaceMessageClient, OnDestroy { merge(this._message$, subscribeError$) .pipe( assertNotInAngularZone(), - filter(message => this._topicMatcher.matchesSubscriptionTopic(message.getDestination(), topicDestination)), + filter(message => topicMatcher.matches(message.getDestination()?.getName())), mapToMessageEnvelope(topic), observeOutsideAngular ? identity : observeInside(continueFn => this._zone.run(continueFn)), takeUntil(merge(this._sessionDisposed$, unsubscribe$)),