Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/registry/news/+utilities-exact-match.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed `getUtilities` and `getUtility` so a dependency set matches exactly: a lookup for `{ widget: 'url' }` no longer returns utilities registered for `{ widget: 'url_list' }`, and dependency keys can be passed in any order. @sneridagh
40 changes: 20 additions & 20 deletions packages/registry/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ type GetComponentResult = {
component: React.ComponentType<any> | undefined;
};

/**
* Builds the part of a utility key that encodes its dependencies:
* `|<key:value pairs sorted by key, joined by '+'>|`, or an empty string when
* there are none. The trailing separator keeps the dependencies apart from the
* utility name, so a dependency set only ever matches itself.
*/
function utilityDepsPrefix(dependencies: Record<string, string>): string {
const depsString = Object.keys(dependencies)
.sort()
.map((key) => `${key}:${dependencies[key]}`)
.join('+');
return depsString ? `|${depsString}|` : '';
}

type UtilityMethodFor<Type extends string> = Type extends keyof UtilityTypeMap
? UtilityTypeMap[Type]
: (...args: any[]) => any;
Expand Down Expand Up @@ -472,16 +486,10 @@ class Config {
method: UtilityMethodFor<Type>;
}) {
const { name, type, method, dependencies = {} } = options;
let depsString: string = '';
if (!method) {
throw new Error('No method provided');
} else {
depsString = Object.keys(dependencies)
.sort()
.map((key) => `${key}:${dependencies[key]}`)
.join('+');
}
const utilityName = `${depsString ? `|${depsString}` : ''}${name}`;
const utilityName = `${utilityDepsPrefix(dependencies)}${name}`;

let utilityType = this._data.utilities[type] as Utility<Type> | undefined;
if (!utilityType) {
Expand All @@ -500,12 +508,7 @@ class Config {

if (!name || !type) return {};

let depsString: string = '';
depsString = Object.keys(dependencies)
.map((key) => `${key}:${dependencies[key]}`)
.join('+');

const utilityName = `${depsString ? `|${depsString}` : ''}${name}`;
const utilityName = `${utilityDepsPrefix(dependencies)}${name}`;
const utilitiesForType = this._data.utilities[type] as
Utility<Type> | undefined;

Expand All @@ -520,18 +523,15 @@ class Config {

if (!type) return [];

let depsString: string = '';
depsString = Object.keys(dependencies)
.map((key) => `${key}:${dependencies[key]}`)
.join('+');

const utilitiesForType = this._data.utilities[type] as
Utility<Type> | undefined;
if (!utilitiesForType) return [];

const utilityName = `${depsString ? `|${depsString}` : ''}`;
// Without dependencies, every utility of the type matches. With them, the
// prefix ends in the separator, so only that exact dependency set matches.
const depsPrefix = utilityDepsPrefix(dependencies);
const utilitiesKeys = Object.keys(utilitiesForType).filter((key) =>
key.startsWith(utilityName),
key.startsWith(depsPrefix),
);
const utilities = utilitiesKeys.map((key) => utilitiesForType[key]);

Expand Down
103 changes: 103 additions & 0 deletions packages/registry/src/registry.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,109 @@ describe('Utilities registry', () => {
}),
).toEqual([]);
});

it('getUtilities - does not match dependency values that share a prefix', () => {
config.registerUtility({
name: 'url',
type: 'validator',
dependencies: { widget: 'url' },
method: () => 'url validator',
});
config.registerUtility({
name: 'urlList',
type: 'validator',
dependencies: { widget: 'url_list' },
method: () => 'url_list validator',
});

const utilities = config.getUtilities({
type: 'validator',
dependencies: { widget: 'url' },
});
expect(utilities.map((utility) => utility.method())).toEqual([
'url validator',
]);
});

it('getUtilities - matches multiple dependencies regardless of key order', () => {
config.registerUtility({
name: 'required',
type: 'validator',
dependencies: { fieldName: 'title', behaviorName: 'dublincore' },
method: () => 'required validator',
});

const sorted = config.getUtilities({
type: 'validator',
dependencies: { behaviorName: 'dublincore', fieldName: 'title' },
});
const unsorted = config.getUtilities({
type: 'validator',
dependencies: { fieldName: 'title', behaviorName: 'dublincore' },
});
expect(sorted.map((utility) => utility.method())).toEqual([
'required validator',
]);
expect(unsorted.map((utility) => utility.method())).toEqual([
'required validator',
]);
});

it('getUtilities - without dependencies returns every utility of the type', () => {
config.registerUtility({
name: 'plain',
type: 'validator',
method: () => 'plain',
});
config.registerUtility({
name: 'withDeps',
type: 'validator',
dependencies: { widget: 'url' },
method: () => 'withDeps',
});

expect(
config
.getUtilities({ type: 'validator' })
.map((utility) => utility.method()),
).toEqual(['plain', 'withDeps']);
});

it('getUtility - finds the utility with unsorted dependency keys', () => {
config.registerUtility({
name: 'required',
type: 'validator',
dependencies: { fieldName: 'title', behaviorName: 'dublincore' },
method: () => 'required validator',
});

expect(
config
.getUtility({
name: 'required',
type: 'validator',
dependencies: { fieldName: 'title', behaviorName: 'dublincore' },
})
.method(),
).toEqual('required validator');
});

it('getUtility - does not mix the dependencies and the name', () => {
config.registerUtility({
name: 'url',
type: 'validator',
dependencies: { widget: 'url' },
method: () => 'url validator',
});

expect(
config.getUtility({
name: 'rl',
type: 'validator',
dependencies: { widget: 'urlu' },
}),
).toEqual({});
});
});

describe('Routes registry', () => {
Expand Down
Loading