Skip to content

Commit

Permalink
fix(solace-message-client): support subscriptions with #share and #no…
Browse files Browse the repository at this point in the history
  • Loading branch information
helios57 authored and danielwiehl committed May 7, 2024
1 parent 7449afb commit fa161bc
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 79 deletions.
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand Down
136 changes: 77 additions & 59 deletions projects/solace-message-client/src/lib/topic-matcher.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
48 changes: 32 additions & 16 deletions projects/solace-message-client/src/lib/topic-matcher.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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/<ShareName>/<topicFilter>
if (segments[0] === '#share') {
segments.shift(); // removes #share segment
segments.shift(); // removes share name segment
}
return topic.getName();
return segments;
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export class ɵSolaceMessageClient implements SolaceMessageClient, OnDestroy {
public connected$: Observable<boolean>;

constructor(private _sessionProvider: SolaceSessionProvider,
private _topicMatcher: TopicMatcher,
private _injector: Injector,
private _logger: Logger,
private _zone: NgZone) {
Expand Down Expand Up @@ -206,6 +205,7 @@ export class ɵSolaceMessageClient implements SolaceMessageClient, OnDestroy {
const unsubscribe$ = new Subject<void>();
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
Expand All @@ -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$)),
Expand Down

0 comments on commit fa161bc

Please sign in to comment.