Skip to content

Commit 724818d

Browse files
committed
feat: Added matcher request to match multiple requests together
1 parent 0af7980 commit 724818d

File tree

9 files changed

+203
-55
lines changed

9 files changed

+203
-55
lines changed

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codify-plugin-lib",
3-
"version": "1.0.142",
3+
"version": "1.0.145",
44
"description": "Library plugin library",
55
"main": "dist/index.js",
66
"typings": "dist/index.d.ts",
@@ -16,7 +16,7 @@
1616
"dependencies": {
1717
"ajv": "^8.12.0",
1818
"ajv-formats": "^2.1.1",
19-
"codify-schemas": "1.0.70",
19+
"codify-schemas": "1.0.73",
2020
"@npmcli/promise-spawn": "^7.0.1",
2121
"@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5",
2222
"uuid": "^10.0.0",

src/messages/handlers.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
IpcMessageSchema,
1414
IpcMessageV2,
1515
IpcMessageV2Schema,
16+
MatchRequestDataSchema,
17+
MatchResponseDataSchema,
1618
MessageStatus,
1719
PlanRequestDataSchema,
1820
PlanResponseDataSchema,
@@ -40,6 +42,11 @@ const SupportedRequests: Record<string, { handler: (plugin: Plugin, data: any) =
4042
requestValidator: GetResourceInfoRequestDataSchema,
4143
responseValidator: GetResourceInfoResponseDataSchema
4244
},
45+
'match': {
46+
handler: async (plugin: Plugin, data: any) => plugin.match(data),
47+
requestValidator: MatchRequestDataSchema,
48+
responseValidator: MatchResponseDataSchema
49+
},
4350
'import': {
4451
handler: async (plugin: Plugin, data: any) => plugin.import(data),
4552
requestValidator: ImportRequestDataSchema,
@@ -106,7 +113,7 @@ export class MessageHandler {
106113

107114
const responseValidator = this.responseValidators.get(message.cmd);
108115
if (responseValidator && !responseValidator(result)) {
109-
throw new Error(`Plugin: ${this.plugin}. Malformed response data: ${JSON.stringify(responseValidator.errors, null, 2)}`)
116+
throw new Error(`Plugin: ${this.plugin.name}. Malformed response data: ${JSON.stringify(responseValidator.errors, null, 2)}. Received ${JSON.stringify(result, null, 2)}`);
110117
}
111118

112119
process.send!({

src/plan/plan.ts

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ export class Plan<T extends StringIndexedObject> {
241241
desired: Partial<T> | null,
242242
currentArray: Partial<T>[] | null,
243243
state: Partial<T> | null,
244-
settings: ResourceSettings<T>,
244+
settings: ParsedResourceSettings<T>,
245245
isStateful: boolean,
246246
}): Partial<T> | null {
247247
const {
@@ -260,36 +260,15 @@ export class Plan<T extends StringIndexedObject> {
260260
return null;
261261
}
262262

263-
const matcher = typeof settings.allowMultiple === 'boolean' || !settings.allowMultiple.matcher
264-
? ((desired: Partial<T>, currentArr: Array<Partial<T>>) => {
265-
const requiredParameters = typeof settings.allowMultiple === 'object'
266-
? settings.allowMultiple?.identifyingParameters ?? (settings.schema?.required as string[]) ?? []
267-
: (settings.schema?.required as string[]) ?? []
268-
269-
const matched = currentArr.filter((c) => requiredParameters.every((key) => {
270-
const currentParameter = c[key];
271-
const desiredParameter = desired[key];
272-
273-
if (!currentParameter) {
274-
console.warn(`Unable to find required parameter for current ${currentParameter}`)
275-
return false;
276-
}
277-
278-
if (!desiredParameter) {
279-
console.warn(`Unable to find required parameter for current ${currentParameter}`)
280-
return false;
281-
}
282-
283-
return currentParameter === desiredParameter;
284-
}))
285-
286-
if (matched.length > 1) {
287-
console.warn(`Required parameters did not uniquely identify a resource: ${currentArray}. Defaulting to the first one`);
288-
}
263+
const { matcher: parameterMatcher, id } = settings;
264+
const matcher = (desired: Partial<T>, currentArray: Partial<T>[]): Partial<T> | undefined => {
265+
const matched = currentArray.filter((c) => parameterMatcher(desired, c))
266+
if (matched.length > 0) {
267+
console.log(`Resource: ${id} did not uniquely match resources when allow multiple is set to true`)
268+
}
289269

290-
return matched[0];
291-
})
292-
: settings.allowMultiple.matcher
270+
return matched[0];
271+
}
293272

294273
if (isStateful) {
295274
return state

src/plugin/plugin.test.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -343,27 +343,51 @@ describe('Plugin tests', () => {
343343
type: 'testResource',
344344
})
345345

346-
expect(resourceInfo.allowMultiple?.requiredParameters).toMatchObject([
347-
'path', 'paths'
348-
])
346+
expect(resourceInfo.allowMultiple).to.be.true;
349347
})
350348

351-
it('Returns an empty array by default for allowMultiple for getResourceInfo', async () => {
349+
it('Can match resources together', async () => {
352350
const resource = spy(new class extends TestResource {
353351
getSettings(): ResourceSettings<TestConfig> {
354352
return {
355353
...super.getSettings(),
356-
allowMultiple: true
354+
parameterSettings: {
355+
path: { type: 'directory' },
356+
paths: { type: 'array', itemType: 'directory' }
357+
},
358+
allowMultiple: {
359+
identifyingParameters: ['path', 'paths']
360+
}
357361
}
358362
}
359363
})
360364

361365
const testPlugin = Plugin.create('testPlugin', [resource as any]);
362366

363-
const resourceInfo = await testPlugin.getResourceInfo({
364-
type: 'testResource',
367+
const { match } = await testPlugin.match({
368+
resource: {
369+
core: { type: 'testResource' },
370+
parameters: { path: '/my/path', propA: 'abc' },
371+
},
372+
array: [
373+
{
374+
core: { type: 'testResource' },
375+
parameters: { path: '/my/other/path', propA: 'abc' },
376+
},
377+
{
378+
core: { type: 'testResource' },
379+
parameters: { paths: ['/my/path'], propA: 'def' },
380+
},
381+
{
382+
core: { type: 'testResource' },
383+
parameters: { path: '/my/path', propA: 'hig' },
384+
},
385+
]
386+
})
387+
expect(match).toMatchObject({
388+
core: { type: 'testResource' },
389+
parameters: { path: '/my/path', propA: 'hig' },
365390
})
366391

367-
expect(resourceInfo.allowMultiple?.requiredParameters).toMatchObject([])
368392
})
369393
});

src/plugin/plugin.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
ImportRequestData,
77
ImportResponseData,
88
InitializeResponseData,
9+
MatchRequestData,
10+
MatchResponseData,
911
PlanRequestData,
1012
PlanResponseData,
1113
ResourceConfig,
@@ -69,14 +71,11 @@ export class Plugin {
6971
const requiredPropertyNames = (
7072
resource.settings.importAndDestroy?.requiredParameters
7173
?? schema?.required
72-
?? null
73-
) as null | string[];
74+
?? undefined
75+
) as any;
7476

7577
const allowMultiple = resource.settings.allowMultiple !== undefined
76-
? (typeof resource.settings.allowMultiple === 'boolean'
77-
? { identifyingParameters: schema?.required ?? [] }
78-
: { identifyingParameters: resource.settings.allowMultiple.identifyingParameters ?? schema?.required ?? [] }
79-
) : undefined
78+
&& resource.settings.allowMultiple !== false;
8079

8180
return {
8281
plugin: this.name,
@@ -94,6 +93,34 @@ export class Plugin {
9493
}
9594
}
9695

96+
async match(data: MatchRequestData): Promise<MatchResponseData> {
97+
const { resource: resourceConfig, array } = data;
98+
99+
const resource = this.resourceControllers.get(resourceConfig.core.type);
100+
if (!resource) {
101+
throw new Error(`Resource of type ${resourceConfig.core.type} could not be found for match`);
102+
}
103+
104+
const parameterMatcher = resource?.parsedSettings.matcher;
105+
const match = array.find((r) => {
106+
if (resourceConfig.core.type !== r.core.type) {
107+
return false;
108+
}
109+
110+
// If the user specifies the same name for the resource and it's not auto-generated (a number) then it's the same resource
111+
if (resourceConfig.core.name === r.core.name
112+
&& resourceConfig.core.name
113+
&& Number.isInteger(Number.parseInt(resourceConfig.core.name, 10))
114+
) {
115+
return true;
116+
}
117+
118+
return parameterMatcher(resourceConfig.parameters, r.parameters);
119+
});
120+
121+
return { match }
122+
}
123+
97124
async import(data: ImportRequestData): Promise<ImportResponseData> {
98125
const { core, parameters } = data;
99126

src/resource/parsed-resource-settings.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
ParameterSetting,
1010
resolveElementEqualsFn,
1111
resolveEqualsFn,
12+
resolveMatcher,
1213
resolveParameterTransformFn,
1314
ResourceSettings,
1415
StatefulParameterSetting
@@ -38,7 +39,7 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
3839
id!: string;
3940
schema?: Partial<JSONSchemaType<T | any>>;
4041
allowMultiple?: {
41-
matcher?: (desired: Partial<T>, current: Partial<T>[]) => Partial<T>;
42+
matcher?: (desired: Partial<T>, current: Partial<T>) => boolean;
4243
requiredParameters?: string[]
4344
} | boolean;
4445

@@ -172,6 +173,10 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
172173
});
173174
}
174175

176+
get matcher(): (desired: Partial<T>, current: Partial<T>) => boolean {
177+
return resolveMatcher(this);
178+
}
179+
175180
private validateSettings(): void {
176181
// validate parameter settings
177182
if (this.settings.parameterSettings) {

src/resource/resource-settings.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,4 +1054,69 @@ describe('Resource parameter tests', () => {
10541054
operation: ResourceOperation.NOOP,
10551055
})
10561056
})
1057+
1058+
it('Supports matching using the identfying parameters', async () => {
1059+
const home = os.homedir()
1060+
const testPath = path.join(home, 'test/folder');
1061+
1062+
const resource = new class extends TestResource {
1063+
getSettings(): ResourceSettings<TestConfig> {
1064+
return {
1065+
id: 'resourceType',
1066+
parameterSettings: {
1067+
propA: { type: 'array', itemType: 'directory' }
1068+
},
1069+
allowMultiple: {
1070+
identifyingParameters: ['propA']
1071+
}
1072+
}
1073+
}
1074+
};
1075+
1076+
const controller = new ResourceController(resource);
1077+
expect(controller.parsedSettings.matcher({
1078+
propA: [testPath],
1079+
propB: 'random1',
1080+
}, {
1081+
propA: [testPath],
1082+
propB: 'random2',
1083+
})).to.be.true;
1084+
1085+
expect(controller.parsedSettings.matcher({
1086+
propA: [testPath],
1087+
propB: 'random1',
1088+
}, {
1089+
propA: [testPath, testPath],
1090+
propB: 'random2',
1091+
})).to.be.false;
1092+
})
1093+
1094+
it('Supports matching using custom matcher', async () => {
1095+
const home = os.homedir()
1096+
const testPath = path.join(home, 'test/folder');
1097+
1098+
const resource = new class extends TestResource {
1099+
getSettings(): ResourceSettings<TestConfig> {
1100+
return {
1101+
id: 'resourceType',
1102+
parameterSettings: {
1103+
propA: { type: 'array', itemType: 'directory' }
1104+
},
1105+
allowMultiple: {
1106+
identifyingParameters: ['propA'],
1107+
matcher: () => false,
1108+
}
1109+
}
1110+
}
1111+
};
1112+
1113+
const controller = new ResourceController(resource);
1114+
expect(controller.parsedSettings.matcher({
1115+
propA: [testPath],
1116+
propB: 'random1',
1117+
}, {
1118+
propA: [testPath],
1119+
propB: 'random2',
1120+
})).to.be.false;
1121+
})
10571122
})

0 commit comments

Comments
 (0)