Skip to content

Commit

Permalink
fix: match intent with wildcard qualifier key/value(s)
Browse files Browse the repository at this point in the history
When querying for capability providers, it was not possible to have wildcards in
the intent's qualifier entry key or value.
This prevented the platform to match intents like e.g. `{entity: '*'}` with more
specific capabilities like e.g. `{entity: 'user'}`.

fixes: #172
  • Loading branch information
mofogasy authored and danielwiehl committed Sep 10, 2019
1 parent 10c2b45 commit 5ea3981
Show file tree
Hide file tree
Showing 12 changed files with 274 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<app-qualifier-chip-list [type]="intent.type" [qualifier]="intent.qualifier"></app-qualifier-chip-list>
<div class="hints">
<span class="implicit" *ngIf="intent.metadata.implicit" title="This is an implicit intent because the application provides the capability itself.">IMPLICIT</span>
<span class="not-handled" *ngIf="!anyQualifier && unhandled$ | async" title="No application found to handle this intent.">NOT HANDLED</span>
<span class="not-handled" *ngIf="unhandled$ | async" title="No application found to handle this intent.">NOT HANDLED</span>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { map } from 'rxjs/operators';
})
export class IntentAccordionItemComponent implements OnChanges {

public anyQualifier: boolean;
public unhandled$: Observable<boolean>;

@Input()
Expand All @@ -31,8 +30,6 @@ export class IntentAccordionItemComponent implements OnChanges {
}

public ngOnChanges(changes: SimpleChanges): void {
this.anyQualifier = Object.keys(this.intent.qualifier || {}).includes('*');

this.unhandled$ = this._manifestRegistryService.capabilityProviders$(this.intent.metadata.id)
.pipe(map(providers => providers.length === 0));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
<section *ngIf="!anyQualifier">
<h2>Handled by following applications</h2>
<ul>
<li *ngFor="let provider of providers$ | async">
<a class="app-name" [wbRouterLink]="{entity: 'application', symbolicName: provider.symbolicName}"
[wbRouterLinkExtras]="{target: 'blank'}"
[class.self]="provider.symbolicName === intent.metadata.symbolicAppName">
{{provider.name}}
</a>
</li>
</ul>
</section>
<ng-container *ngIf="providers$ | async as providers">
<section *ngIf="providers.length">
<h2>Handled by following applications</h2>
<ul>
<li *ngFor="let provider of providers">
<a class="app-name" [wbRouterLink]="{entity: 'application', symbolicName: provider.symbolicName}"
[wbRouterLinkExtras]="{target: 'blank'}"
[class.self]="provider.symbolicName === intent.metadata.symbolicAppName">
{{provider.name}}
</a>
</li>
</ul>
</section>
</ng-container>
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { map } from 'rxjs/operators';
})
export class IntentAccordionPanelComponent implements OnChanges {

public anyQualifier: boolean;
public providers$: Observable<Application[]>;

@Input()
Expand All @@ -31,7 +30,6 @@ export class IntentAccordionPanelComponent implements OnChanges {
}

public ngOnChanges(changes: SimpleChanges): void {
this.anyQualifier = Object.keys(this.intent.qualifier || {}).includes('*');
this.providers$ = this._manifestRegistryService.capabilityProviders$(this.intent.metadata.id)
.pipe(map(providers => [...providers].sort((p1, p2) => p1.name.localeCompare(p2.name))));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,21 @@ export class Arrays {
}
return result;
}

/**
* Removes duplicate items from the array. The original array will not be modified.
*
* Use the parameter `identityFn` to provide a function for comparing objects.
*/
public static distinct<T>(items: T[], identityFn: (item: T) => any = (item: T): any => item): T[] {
const visitedItems = new Set<T>();
return items.filter(item => {
const identity = identityFn(item);
if (visitedItems.has(identity)) {
return false;
}
visitedItems.add(identity);
return true;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class ManifestRegistry {
}

/**
* Returns capabilities which have the given required type and qualifiers.
* Returns capabilities which have the given required type and qualifier.
*/
public getCapabilities<T extends Capability>(type: string, qualifier: Qualifier): T[] {
return this.getCapabilitiesByType(type)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2018-2019 Swiss Federal Railways
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/

import { NilQualifier, Qualifier } from '@scion/workbench-application-platform.api';

/**
* Returns a copy of the given intent qualifier with all its wildcard qualifier values
* replaced with values of the given capability qualifier, if any.
* Also, if the intent qualifier specifies a wildcard key, it is merged with the capability qualifier.
*
* @param intentQualifier
* qualifier for an intent as specified in the manifest, may contain wildcards as qualifier key (*)
* and/or qualifier value (* or ?).
* @param capabilityQualifier
* qualifier for a capability as specified in the manifest, may contain wildcards (* or ?) as qualifier value;
* if `null`, {NilQualifier} is used.
*/
export function patchQualifier(intentQualifier: Qualifier, capabilityQualifier: Qualifier): Qualifier {
if (!intentQualifier || !capabilityQualifier) {
return intentQualifier || NilQualifier;
}

// Create a working copy of the intent qualifier
const _intentQualifier: Qualifier = {...intentQualifier};
delete _intentQualifier['*'];

Object.keys(capabilityQualifier)
.forEach(key => {
if (intentQualifier[key] === '*' || intentQualifier[key] === '?') {
_intentQualifier[key] = capabilityQualifier[key];
}
else if (intentQualifier.hasOwnProperty('*') && !intentQualifier.hasOwnProperty(key)) {
_intentQualifier[key] = capabilityQualifier[key];
}
});

return _intentQualifier;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import { MessageBus } from '../core/message-bus.service';
import { ApplicationRegistry } from '../core/application-registry.service';
import { ManifestRegistry } from '../core/manifest-registry.service';
import { Logger } from '../core/logger.service';
import { matchesIntentQualifier } from '../core/qualifier-tester';
import { matchesCapabilityQualifier, matchesIntentQualifier } from '../core/qualifier-tester';
import { Arrays } from '../core/array.util';
import { patchQualifier } from '../core/qualifier-patcher';

/**
* Allows to query manifest registry.
Expand Down Expand Up @@ -87,10 +89,16 @@ export class ManifestRegistryIntentHandler implements IntentHandler {
const intentId = envelope.message.payload.intentId;
const intent: Intent = this._manifestRegistry.getIntent(intentId);

const providers: Application[] = this._manifestRegistry.getCapabilities(intent.type, intent.qualifier)
const providers: Application[] = this._manifestRegistry.getCapabilitiesByType(intent.type)
.filter(capability => this._manifestRegistry.isVisibleForApplication(capability, intent.metadata.symbolicAppName))
.filter(capability => {
const patchedQualifier: Qualifier = patchQualifier(intent.qualifier, capability.qualifier);
return matchesCapabilityQualifier(capability.qualifier, patchedQualifier);
})
.map(capability => this._applicationRegistry.getApplication(capability.metadata.symbolicAppName));
this._messageBus.publishReply(providers, envelope.sender, envelope.replyToUid);

const distinctProviders: Application[] = Arrays.distinct(providers, (app) => app.symbolicName);
this._messageBus.publishReply(distinctProviders, envelope.sender, envelope.replyToUid);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,38 @@ describe('Arrays', () => {
expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
});
});

describe('Arrays.distinct', () => {

it('should remove duplicate string items', () => {
const array = ['a', 'a', 'b', 'c', 'd', 'e', 'c'];
expect(Arrays.distinct(array)).toEqual(['a', 'b', 'c', 'd', 'e']);
});

it('should remove duplicate objects by reference', () => {
const apple = {id: 1, name: 'apple'};
const banana = {id: 2, name: 'banana'};
const cherry = {id: 3, name: 'cherry'};

const array = [apple, banana, cherry, banana, apple];
expect(Arrays.distinct(array)).toEqual([apple, banana, cherry]);
});

it('should remove duplicate objects by a given identity function', () => {
const apple = {id: 1, name: 'apple'};
const banana = {id: 2, name: 'banana'};
const cherry = {id: 3, name: 'cherry'};
const appleOtherInstance = {...apple};
const bananaOtherInstance = {...banana};

const array = [apple, banana, cherry, bananaOtherInstance, appleOtherInstance];
expect(Arrays.distinct(array, (fruit) => fruit.id)).toEqual([apple, banana, cherry]);
});

it('should not modify the original array', () => {
const array = ['a', 'a', 'b', 'c', 'd', 'e', 'c'];
expect(Arrays.distinct(array)).toEqual(['a', 'b', 'c', 'd', 'e']);
expect(array).toEqual(['a', 'a', 'b', 'c', 'd', 'e', 'c']);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

import { matchesCapabilityQualifier, matchesIntentQualifier } from '../core/qualifier-tester';
import { patchQualifier } from '../core/qualifier-patcher';

describe('Qualifier', () => {

Expand Down Expand Up @@ -41,6 +42,10 @@ describe('Qualifier', () => {
expect(matchesCapabilityQualifier({a: 'a', b: '*', c: 'c'}, {a: 'a', b: null, c: 'c'})).toBeFalsy();
expect(matchesCapabilityQualifier({a: 'a', b: '*', c: 'c'}, {a: 'a', c: 'c'})).toBeFalsy();
expect(matchesCapabilityQualifier({a: 'a', b: '*', c: 'c'}, {a: 'a', c: 'c', d: 'd'})).toBeFalsy();

expect(matchesCapabilityQualifier({entity: '*'}, undefined)).toBeFalsy();
expect(matchesCapabilityQualifier({entity: '*'}, null)).toBeFalsy();
expect(matchesCapabilityQualifier({entity: '*'}, {})).toBeFalsy();
});

it('supports value wildcards (?)', () => {
Expand All @@ -56,6 +61,10 @@ describe('Qualifier', () => {
expect(matchesCapabilityQualifier({a: 'a', b: '?', c: 'c'}, {a: 'a', b: null, c: 'c'})).toBeTruthy();
expect(matchesCapabilityQualifier({a: 'a', b: '?', c: 'c'}, {a: 'a', c: 'c'})).toBeTruthy();
expect(matchesCapabilityQualifier({a: 'a', b: '?', c: 'c'}, {a: 'a', c: 'c', d: 'd'})).toBeFalsy();

expect(matchesCapabilityQualifier({entity: '?'}, undefined)).toBeTruthy();
expect(matchesCapabilityQualifier({entity: '?'}, null)).toBeTruthy();
expect(matchesCapabilityQualifier({entity: '?'}, {})).toBeTruthy();
});

it('accepts only empty qualifiers', () => {
Expand Down Expand Up @@ -153,4 +162,95 @@ describe('Qualifier', () => {
expect(matchesIntentQualifier(null, {entity: 'person'})).toBeFalsy();
});
});

describe('function \'patchQualifier(...)\'', () => {

it('returns \'NilQualifier\' for an empty intent qualifier', () => {
expect(patchQualifier(null, null)).toEqual({});
expect(patchQualifier(undefined, null)).toEqual({});
expect(patchQualifier({}, null)).toEqual({});

expect(patchQualifier(null, undefined)).toEqual({});
expect(patchQualifier(undefined, undefined)).toEqual({});
expect(patchQualifier({}, undefined)).toEqual({});

expect(patchQualifier(null, {})).toEqual({});
expect(patchQualifier(undefined, {})).toEqual({});
expect(patchQualifier({}, {})).toEqual({});

expect(patchQualifier(null, {entity: 'user'})).toEqual({});
expect(patchQualifier(undefined, {entity: 'user'})).toEqual({});
expect(patchQualifier({}, {entity: 'user'})).toEqual({});
});

it('returns an exact copy of the intent qualifier for an empty capability qualifier', () => {
expect(patchQualifier({entity: 'train'}, undefined)).toEqual({entity: 'train'});
expect(patchQualifier({entity: 'user', id: '*'}, undefined)).toEqual({entity: 'user', id: '*'});
expect(patchQualifier({entity: 'user', id: '?'}, undefined)).toEqual({entity: 'user', id: '?'});

expect(patchQualifier({entity: 'train'}, null)).toEqual({entity: 'train'});
expect(patchQualifier({entity: 'user', id: '*'}, null)).toEqual({entity: 'user', id: '*'});
expect(patchQualifier({entity: 'user', id: '?'}, null)).toEqual({entity: 'user', id: '?'});

expect(patchQualifier({entity: 'train'}, {})).toEqual({entity: 'train'});
expect(patchQualifier({entity: 'user', id: '*'}, {})).toEqual({entity: 'user', id: '*'});
expect(patchQualifier({entity: 'user', id: '?'}, {})).toEqual({entity: 'user', id: '?'});
});

it('patches intent qualifier wildcard values by capability qualifier values', () => {
expect(patchQualifier({entity: 'train'}, {entity: 'user'})).toEqual({entity: 'train'});
expect(patchQualifier({entity: 'user', id: '*'}, {entity: 'user'})).toEqual({entity: 'user', id: '*'});
expect(patchQualifier({entity: 'user', id: '?'}, {entity: 'user'})).toEqual({entity: 'user', id: '?'});

expect(patchQualifier({entity: 'user'}, {entity: 'user'})).toEqual({entity: 'user'});
expect(patchQualifier({entity: '*'}, {entity: 'user'})).toEqual({entity: 'user'});
expect(patchQualifier({entity: '?'}, {entity: 'user'})).toEqual({entity: 'user'});
expect(patchQualifier({'*': '*'}, {entity: 'user'})).toEqual({entity: 'user'});

expect(patchQualifier({id: '42'}, {id: '42'})).toEqual({id: '42'});
expect(patchQualifier({id: '*'}, {id: '42'})).toEqual({id: '42'});
expect(patchQualifier({id: '?'}, {id: 42})).toEqual({id: 42});
expect(patchQualifier({'*': '*'}, {id: 42})).toEqual({id: 42});

expect(patchQualifier({flag: true}, {flag: true})).toEqual({flag: true});
expect(patchQualifier({flag: '*'}, {flag: false})).toEqual({flag: false});
expect(patchQualifier({flag: '?'}, {flag: false})).toEqual({flag: false});
});

it('patches intent qualifier wildcard values by capability qualifier wildcard (*) values', () => {
expect(patchQualifier(null, {entity: 'user', id: '*'})).toEqual({});
expect(patchQualifier(undefined, {entity: 'user', id: '*'})).toEqual({});
expect(patchQualifier({}, {entity: 'user', id: '*'})).toEqual({});

expect(patchQualifier({entity: 'train'}, {entity: 'user', id: '*'})).toEqual({entity: 'train'});
expect(patchQualifier({entity: 'user'}, {entity: 'user', id: '*'})).toEqual({entity: 'user'});
expect(patchQualifier({entity: 'user', id: '*', name: 'smith'}, {entity: 'user', id: '*'})).toEqual({entity: 'user', id: '*', name: 'smith'});
expect(patchQualifier({entity: 'user', id: '?', name: 'smith'}, {entity: 'user', id: '*'})).toEqual({entity: 'user', id: '*', name: 'smith'});
expect(patchQualifier({entity: 'person', '*': '*'}, {entity: 'user', id: '*'})).toEqual({entity: 'person', id: '*'});

expect(patchQualifier({entity: 'user', id: '*'}, {entity: 'user', id: '*'})).toEqual({entity: 'user', id: '*'});
expect(patchQualifier({entity: 'user', id: '?'}, {entity: 'user', id: '*'})).toEqual({entity: 'user', id: '*'});
expect(patchQualifier({entity: '*', id: '*'}, {entity: 'user', id: '*'})).toEqual({entity: 'user', id: '*'});
expect(patchQualifier({entity: '?', id: '?'}, {entity: 'user', id: '*'})).toEqual({entity: 'user', id: '*'});
expect(patchQualifier({'*': '*'}, {entity: 'user', id: '*'})).toEqual({entity: 'user', id: '*'});
});

it('patches intent qualifier wildcard values by capability qualifier wildcard (?) values', () => {
expect(patchQualifier(null, {entity: 'user', id: '?'})).toEqual({});
expect(patchQualifier(undefined, {entity: 'user', id: '?'})).toEqual({});
expect(patchQualifier({}, {entity: 'user', id: '?'})).toEqual({});

expect(patchQualifier({entity: 'train'}, {entity: 'user', id: '?'})).toEqual({entity: 'train'});
expect(patchQualifier({entity: 'user'}, {entity: 'user', id: '?'})).toEqual({entity: 'user'});
expect(patchQualifier({entity: 'user', id: '*', name: 'smith'}, {entity: 'user', id: '?'})).toEqual({entity: 'user', id: '?', name: 'smith'});
expect(patchQualifier({entity: 'user', id: '?', name: 'smith'}, {entity: 'user', id: '?'})).toEqual({entity: 'user', id: '?', name: 'smith'});
expect(patchQualifier({entity: 'person', '*': '*'}, {entity: 'user', id: '?'})).toEqual({entity: 'person', id: '?'});

expect(patchQualifier({entity: 'user', id: '*'}, {entity: 'user', id: '?'})).toEqual({entity: 'user', id: '?'});
expect(patchQualifier({entity: 'user', id: '?'}, {entity: 'user', id: '?'})).toEqual({entity: 'user', id: '?'});
expect(patchQualifier({entity: '*', id: '*'}, {entity: 'user', id: '?'})).toEqual({entity: 'user', id: '?'});
expect(patchQualifier({entity: '?', id: '?'}, {entity: 'user', id: '?'})).toEqual({entity: 'user', id: '?'});
expect(patchQualifier({'*': '*'}, {entity: 'user', id: '?'})).toEqual({entity: 'user', id: '?'});
});
});
});
34 changes: 34 additions & 0 deletions projects/scion/workbench/src/lib/array.util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,38 @@ describe('Arrays', () => {
expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
});
});

describe('Arrays.distinct', () => {

it('should remove duplicate string items', () => {
const array = ['a', 'a', 'b', 'c', 'd', 'e', 'c'];
expect(Arrays.distinct(array)).toEqual(['a', 'b', 'c', 'd', 'e']);
});

it('should remove duplicate objects by reference', () => {
const apple = {id: 1, name: 'apple'};
const banana = {id: 2, name: 'banana'};
const cherry = {id: 3, name: 'cherry'};

const array = [apple, banana, cherry, banana, apple];
expect(Arrays.distinct(array)).toEqual([apple, banana, cherry]);
});

it('should remove duplicate objects by a given identity function', () => {
const apple = {id: 1, name: 'apple'};
const banana = {id: 2, name: 'banana'};
const cherry = {id: 3, name: 'cherry'};
const appleOtherInstance = {...apple};
const bananaOtherInstance = {...banana};

const array = [apple, banana, cherry, bananaOtherInstance, appleOtherInstance];
expect(Arrays.distinct(array, (fruit) => fruit.id)).toEqual([apple, banana, cherry]);
});

it('should not modify the original array', () => {
const array = ['a', 'a', 'b', 'c', 'd', 'e', 'c'];
expect(Arrays.distinct(array)).toEqual(['a', 'b', 'c', 'd', 'e']);
expect(array).toEqual(['a', 'a', 'b', 'c', 'd', 'e', 'c']);
});
});
});
17 changes: 17 additions & 0 deletions projects/scion/workbench/src/lib/array.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,21 @@ export class Arrays {
}
return result;
}

/**
* Removes duplicate items from the array. The original array will not be modified.
*
* Use the parameter `identityFn` to provide a function for comparing objects.
*/
public static distinct<T>(items: T[], identityFn: (item: T) => any = (item: T): any => item): T[] {
const visitedItems = new Set<T>();
return items.filter(item => {
const identity = identityFn(item);
if (visitedItems.has(identity)) {
return false;
}
visitedItems.add(identity);
return true;
});
}
}

0 comments on commit 5ea3981

Please sign in to comment.