Skip to content

Commit fa161bc

Browse files
helios57danielwiehl
authored andcommitted
fix(solace-message-client): support subscriptions with #share and #noexport
For more information, refer to: - https://docs.solace.com/Messaging/Direct-Msg/Direct-Messages.htm - https://docs.solace.com/Messaging/No-Export.htm
1 parent 7449afb commit fa161bc

File tree

4 files changed

+111
-79
lines changed

4 files changed

+111
-79
lines changed

projects/solace-message-client/src/lib/solace-message-client.module.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {Inject, Injectable, InjectionToken, ModuleWithProviders, NgModule, Optional, SkipSelf} from '@angular/core';
22
import {NullSolaceMessageClient, SolaceMessageClient} from './solace-message-client';
33
import {ɵSolaceMessageClient} from './ɵsolace-message-client';
4-
import {TopicMatcher} from './topic-matcher';
54
import {SolaceSessionProvider, ɵSolaceSessionProvider} from './solace-session-provider';
65
import {SOLACE_MESSAGE_CLIENT_CONFIG, SolaceMessageClientConfig} from './solace-message-client.config';
76
import {provideLogger} from './logger';
@@ -116,7 +115,6 @@ export class SolaceMessageClientModule {
116115
{provide: SOLACE_MESSAGE_CLIENT_CONFIG, useValue: config},
117116
{provide: SolaceMessageClient, useClass: ɵSolaceMessageClient},
118117
{provide: SolaceSessionProvider, useClass: ɵSolaceSessionProvider},
119-
TopicMatcher,
120118
provideLogger(),
121119
{
122120
provide: FORROOT_GUARD,
Lines changed: 77 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,107 @@
1-
import {TestBed} from '@angular/core/testing';
21
import {TopicMatcher} from './topic-matcher';
32

43
describe('SolaceMessageClient', () => {
54

6-
let testee: TopicMatcher;
7-
beforeEach(() => {
8-
TestBed.configureTestingModule({
9-
providers: [
10-
TopicMatcher,
11-
],
12-
});
13-
testee = TestBed.inject(TopicMatcher);
14-
});
15-
16-
it('should not match a `null` topic', () => {
17-
expect(testee.matchesSubscriptionTopic(null, 'a/b/c')).toBeFalse();
5+
it('should not match a `undefined` topic', () => {
6+
expect(new TopicMatcher('a/b/c').matches(undefined)).toBeFalse();
187
});
198

209
it('should not match an empty topic', () => {
21-
expect(testee.matchesSubscriptionTopic('', 'a/b/c')).toBeFalse();
10+
expect(new TopicMatcher('a/b/c').matches('')).toBeFalse();
2211
});
2312

2413
it('should match when publishing a message to the exact topic \'a\'', () => {
25-
expect(testee.matchesSubscriptionTopic('a', 'a')).toBeTrue();
14+
expect(new TopicMatcher('a').matches('a')).toBeTrue();
15+
});
16+
17+
it('should match when publishing a message to the topic with #share and #noexport', () => {
18+
expect(new TopicMatcher('#share/sharename/a').matches('a')).toBeTrue();
19+
expect(new TopicMatcher('#share/sharename/a').matches('b')).toBeFalse();
20+
21+
expect(new TopicMatcher('#noexport/a').matches('a')).toBeTrue();
22+
expect(new TopicMatcher('#noexport/a').matches('b')).toBeFalse();
23+
24+
expect(new TopicMatcher('#noexport/#share/sharename/a').matches('a')).toBeTrue();
25+
expect(new TopicMatcher('#noexport/#share/sharename/a').matches('b')).toBeFalse();
26+
27+
expect(new TopicMatcher('#share/sharename/a/*/b').matches('a/x/b')).toBeTrue();
28+
expect(new TopicMatcher('#share/sharename/a/*/b').matches('a/x/c')).toBeFalse();
29+
30+
expect(new TopicMatcher('#noexport/a/*/b').matches('a/x/b')).toBeTrue();
31+
expect(new TopicMatcher('#noexport/a/*/b').matches('a/x/c')).toBeFalse();
32+
33+
expect(new TopicMatcher('#noexport/#share/sharename/a/*/b').matches('a/x/b')).toBeTrue();
34+
expect(new TopicMatcher('#noexport/#share/sharename/a/*/b').matches('a/x/c')).toBeFalse();
35+
36+
expect(new TopicMatcher('#share/sharename/a/>').matches('a/x/b/z')).toBeTrue();
37+
expect(new TopicMatcher('#share/sharename/a/>').matches('b/x/b/z')).toBeFalse();
38+
39+
expect(new TopicMatcher('#noexport/a/>').matches('a/x/b/z')).toBeTrue();
40+
expect(new TopicMatcher('#noexport/a/>').matches('b/x/b/z')).toBeFalse();
41+
42+
expect(new TopicMatcher('#noexport/#share/sharename/a/>').matches('a/x/b/z')).toBeTrue();
43+
expect(new TopicMatcher('#noexport/#share/sharename/a/b').matches('a/c')).toBeFalse();
2644
});
2745

2846
it('should match when publishing a message to the exact topic \'a/b\'', () => {
29-
expect(testee.matchesSubscriptionTopic('a/b', 'a/b')).toBeTrue();
47+
expect(new TopicMatcher('a/b').matches('a/b')).toBeTrue();
3048
});
3149

3250
it('should match when publishing a message to the exact topic \'a/b/c\'', () => {
33-
expect(testee.matchesSubscriptionTopic('a/b/c', 'a/b/c')).toBeTrue();
51+
expect(new TopicMatcher('a/b/c').matches('a/b/c')).toBeTrue();
3452
});
3553

3654
it('should not match the subscription topic \'a/b/c\' when published to the topic \'a\'', () => {
37-
expect(testee.matchesSubscriptionTopic('a', 'a/b/c')).toBeFalse();
55+
expect(new TopicMatcher('a/b/c').matches('a')).toBeFalse();
3856
});
3957

4058
it('should not match the subscription topic \'a/b/c\' when published to the topic \'a/b\'', () => {
41-
expect(testee.matchesSubscriptionTopic('a/b', 'a/b/c')).toBeFalse();
59+
expect(new TopicMatcher('a/b/c').matches('a/b')).toBeFalse();
4260
});
4361

4462
it('should not match the subscription topic \'a/b/c\' when published to the topic \'a/b/c/d\'', () => {
45-
expect(testee.matchesSubscriptionTopic('a/b/c/d', 'a/b/c')).toBeFalse();
63+
expect(new TopicMatcher('a/b/c').matches('a/b/c/d')).toBeFalse();
4664
});
4765

4866
it('should fulfill examples at https://docs.solace.com/PubSub-Basics/Wildcard-Charaters-Topic-Subs.htm', () => {
49-
expect(testee.matchesSubscriptionTopic('animals/domestic/cats', 'animals/domestic/*')).toBeTrue();
50-
expect(testee.matchesSubscriptionTopic('animals/domestic/dogs', 'animals/domestic/*')).toBeTrue();
51-
expect(testee.matchesSubscriptionTopic('animals/domestic/dogs/beagles', 'animals/domestic/*')).toBeFalse();
52-
53-
expect(testee.matchesSubscriptionTopic('animals/domestic/cats/persian', 'animals/*/cats/*')).toBeTrue();
54-
expect(testee.matchesSubscriptionTopic('animals/wild/cats/leopard', 'animals/*/cats/*')).toBeTrue();
55-
expect(testee.matchesSubscriptionTopic('animals/domestic/cats/persian/grey', 'animals/*/cats/*')).toBeFalse();
56-
expect(testee.matchesSubscriptionTopic('animals/domestic/dogs/beagles', 'animals/*/cats/*')).toBeFalse();
57-
58-
expect(testee.matchesSubscriptionTopic('animals/domestic/dog', 'animals/domestic/dog*')).toBeTrue();
59-
expect(testee.matchesSubscriptionTopic('animals/domestic/doggy', 'animals/domestic/dog*')).toBeTrue();
60-
expect(testee.matchesSubscriptionTopic('animals/domestic/dog/beagle', 'animals/domestic/dog*')).toBeFalse();
61-
expect(testee.matchesSubscriptionTopic('animals/domestic/cat', 'animals/domestic/dog*')).toBeFalse();
62-
63-
expect(testee.matchesSubscriptionTopic('animals/domestic/cats', 'animals/domestic/>')).toBeTrue();
64-
expect(testee.matchesSubscriptionTopic('animals/domestic/dogs/beagles', 'animals/domestic/>')).toBeTrue();
65-
expect(testee.matchesSubscriptionTopic('animals', 'animals/domestic/>')).toBeFalse();
66-
expect(testee.matchesSubscriptionTopic('animals', 'animals/domestic')).toBeFalse();
67-
expect(testee.matchesSubscriptionTopic('animals', 'animals/Domestic')).toBeFalse();
68-
69-
expect(testee.matchesSubscriptionTopic('animals/domestic/cats/tabby/grey', 'animals/*/cats/>')).toBeTrue();
70-
expect(testee.matchesSubscriptionTopic('animals/wild/cats/leopard', 'animals/*/cats/>')).toBeTrue();
71-
expect(testee.matchesSubscriptionTopic('animals/domestic/dogs/beagles', 'animals/*/cats/>')).toBeFalse();
72-
73-
expect(testee.matchesSubscriptionTopic('my/test/topic', 'my/test/*')).toBeTrue();
74-
expect(testee.matchesSubscriptionTopic('My/Test/Topic', 'my/test/*')).toBeFalse();
75-
expect(testee.matchesSubscriptionTopic('my/test', 'my/test/*')).toBeFalse();
76-
77-
expect(testee.matchesSubscriptionTopic('animals/red/wild', 'animals/red*/wild')).toBeTrue();
78-
expect(testee.matchesSubscriptionTopic('animals/reddish/wild', 'animals/red*/wild')).toBeTrue();
79-
80-
expect(testee.matchesSubscriptionTopic('animals/*bro', 'animals/*bro')).toBeTrue();
81-
expect(testee.matchesSubscriptionTopic('animals/bro', 'animals/*bro')).toBeFalse();
82-
expect(testee.matchesSubscriptionTopic('animals/xbro', 'animals/*bro')).toBeFalse();
83-
84-
expect(testee.matchesSubscriptionTopic('animals/br*wn', 'animals/br*wn')).toBeTrue();
85-
expect(testee.matchesSubscriptionTopic('animals/brown', 'animals/br*wn')).toBeFalse();
86-
expect(testee.matchesSubscriptionTopic('animals/brwn', 'animals/br*wn')).toBeFalse();
87-
expect(testee.matchesSubscriptionTopic('animals/brOOwn', 'animals/br*wn')).toBeFalse();
67+
expect(new TopicMatcher('animals/domestic/*').matches('animals/domestic/cats')).toBeTrue();
68+
expect(new TopicMatcher('animals/domestic/*').matches('animals/domestic/dogs')).toBeTrue();
69+
expect(new TopicMatcher('animals/domestic/*').matches('animals/domestic/dogs/beagles')).toBeFalse();
70+
71+
expect(new TopicMatcher('animals/*/cats/*').matches('animals/domestic/cats/persian')).toBeTrue();
72+
expect(new TopicMatcher('animals/*/cats/*').matches('animals/wild/cats/leopard')).toBeTrue();
73+
expect(new TopicMatcher('animals/*/cats/*').matches('animals/domestic/cats/persian/grey')).toBeFalse();
74+
expect(new TopicMatcher('animals/*/cats/*').matches('animals/domestic/dogs/beagles')).toBeFalse();
75+
76+
expect(new TopicMatcher('animals/domestic/dog*').matches('animals/domestic/dog')).toBeTrue();
77+
expect(new TopicMatcher('animals/domestic/dog*').matches('animals/domestic/doggy')).toBeTrue();
78+
expect(new TopicMatcher('animals/domestic/dog*').matches('animals/domestic/dog/beagle')).toBeFalse();
79+
expect(new TopicMatcher('animals/domestic/dog*').matches('animals/domestic/cat')).toBeFalse();
80+
81+
expect(new TopicMatcher('animals/domestic/>').matches('animals/domestic/cats')).toBeTrue();
82+
expect(new TopicMatcher('animals/domestic/>').matches('animals/domestic/dogs/beagles')).toBeTrue();
83+
expect(new TopicMatcher('animals/domestic/>').matches('animals')).toBeFalse();
84+
expect(new TopicMatcher('animals/domestic').matches('animals')).toBeFalse();
85+
expect(new TopicMatcher('animals/Domestic').matches('animals')).toBeFalse();
86+
87+
expect(new TopicMatcher('animals/*/cats/>').matches('animals/domestic/cats/tabby/grey')).toBeTrue();
88+
expect(new TopicMatcher('animals/*/cats/>').matches('animals/wild/cats/leopard')).toBeTrue();
89+
expect(new TopicMatcher('animals/*/cats/>').matches('animals/domestic/dogs/beagles')).toBeFalse();
90+
91+
expect(new TopicMatcher('my/test/*').matches('my/test/topic')).toBeTrue();
92+
expect(new TopicMatcher('my/test/*').matches('My/Test/Topic')).toBeFalse();
93+
expect(new TopicMatcher('my/test/*').matches('my/test')).toBeFalse();
94+
95+
expect(new TopicMatcher('animals/red*/wild').matches('animals/red/wild')).toBeTrue();
96+
expect(new TopicMatcher('animals/red*/wild').matches('animals/reddish/wild')).toBeTrue();
97+
98+
expect(new TopicMatcher('animals/*bro').matches('animals/*bro')).toBeTrue();
99+
expect(new TopicMatcher('animals/*bro').matches('animals/bro')).toBeFalse();
100+
expect(new TopicMatcher('animals/*bro').matches('animals/xbro')).toBeFalse();
101+
102+
expect(new TopicMatcher('animals/br*wn').matches('animals/br*wn')).toBeTrue();
103+
expect(new TopicMatcher('animals/br*wn').matches('animals/brown')).toBeFalse();
104+
expect(new TopicMatcher('animals/br*wn').matches('animals/brwn')).toBeFalse();
105+
expect(new TopicMatcher('animals/br*wn').matches('animals/brOOwn')).toBeFalse();
88106
});
89107
});
Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,28 @@
1-
import {Injectable} from '@angular/core';
2-
import {Destination} from 'solclientjs';
3-
41
/**
52
* Matches exact topics as used when publishing messages against subscription topics.
63
*
74
* This class implements the rules for 'Wildcard Characters in SMF Topic Subscriptions',
85
* as outlined here: https://docs.solace.com/PubSub-Basics/Wildcard-Charaters-Topic-Subs.htm.
96
*/
10-
@Injectable()
117
export class TopicMatcher {
128

13-
public matchesSubscriptionTopic(testeeTopic: string | Destination | null, subscriptionTopic: string | Destination): boolean {
14-
if (!testeeTopic) {
9+
private readonly _subscriptionTopic: string[];
10+
11+
constructor(subscriptionTopic: string) {
12+
this._subscriptionTopic = parseSubscriptionTopic(subscriptionTopic);
13+
}
14+
15+
public matches(topic: string | undefined): boolean {
16+
if (!topic) {
1517
return false;
1618
}
17-
const testeeSegments = coerceTopicName(testeeTopic).split('/');
18-
const subscriptionTopicSegments = coerceTopicName(subscriptionTopic).split('/');
19+
const testeeSegments = topic.split('/');
20+
const subscriptionSegments = this._subscriptionTopic;
1921

20-
for (let i = 0; i < subscriptionTopicSegments.length; i++) {
21-
const subscriptionTopicSegment = subscriptionTopicSegments[i];
22+
for (let i = 0; i < subscriptionSegments.length; i++) {
23+
const subscriptionTopicSegment = subscriptionSegments[i];
2224
const testee = testeeSegments[i];
23-
const isLastSubscriptionTopicSegment = (i === subscriptionTopicSegments.length - 1);
25+
const isLastSubscriptionTopicSegment = (i === subscriptionSegments.length - 1);
2426

2527
if (testee === undefined) {
2628
return false;
@@ -42,13 +44,27 @@ export class TopicMatcher {
4244
return false;
4345
}
4446
}
45-
return testeeSegments.length === subscriptionTopicSegments.length;
47+
return testeeSegments.length === subscriptionSegments.length;
4648
}
4749
}
4850

49-
function coerceTopicName(topic: string | Destination): string {
50-
if (typeof topic === 'string') {
51-
return topic;
51+
/**
52+
* Parses the subscription topic, removing #noexport and #share segments, if any.
53+
*/
54+
function parseSubscriptionTopic(topic: string): string[] {
55+
const segments = topic.split('/');
56+
57+
// Remove #noexport segment, if any. See https://docs.solace.com/Messaging/No-Export.htm
58+
// Example: #noexport/#share/ShareName/topicFilter, #noexport/topicFilter
59+
if (segments[0] === '#noexport') {
60+
segments.shift();
61+
}
62+
63+
// Remove #share segments, if any. See https://docs.solace.com/Messaging/Direct-Msg/Direct-Messages.htm
64+
// Examples: #share/<ShareName>/<topicFilter>
65+
if (segments[0] === '#share') {
66+
segments.shift(); // removes #share segment
67+
segments.shift(); // removes share name segment
5268
}
53-
return topic.getName();
69+
return segments;
5470
}

projects/solace-message-client/src/lib/ɵsolace-message-client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ export class ɵSolaceMessageClient implements SolaceMessageClient, OnDestroy {
2929
public connected$: Observable<boolean>;
3030

3131
constructor(private _sessionProvider: SolaceSessionProvider,
32-
private _topicMatcher: TopicMatcher,
3332
private _injector: Injector,
3433
private _logger: Logger,
3534
private _zone: NgZone) {
@@ -206,6 +205,7 @@ export class ɵSolaceMessageClient implements SolaceMessageClient, OnDestroy {
206205
const unsubscribe$ = new Subject<void>();
207206
const topicDestination = createSubscriptionTopicDestination(topic);
208207
const observeOutsideAngular = options?.emitOutsideAngularZone ?? false;
208+
const topicMatcher = new TopicMatcher(topicDestination.getName());
209209

210210
// Wait until initialized the session so that 'subscriptionExecutor' and 'subscriptionCounter' are initialized.
211211
this.session
@@ -217,7 +217,7 @@ export class ɵSolaceMessageClient implements SolaceMessageClient, OnDestroy {
217217
merge(this._message$, subscribeError$)
218218
.pipe(
219219
assertNotInAngularZone(),
220-
filter(message => this._topicMatcher.matchesSubscriptionTopic(message.getDestination(), topicDestination)),
220+
filter(message => topicMatcher.matches(message.getDestination()?.getName())),
221221
mapToMessageEnvelope(topic),
222222
observeOutsideAngular ? identity : observeInside(continueFn => this._zone.run(continueFn)),
223223
takeUntil(merge(this._sessionDisposed$, unsubscribe$)),

0 commit comments

Comments
 (0)