Skip to content

Commit d33d4c2

Browse files
committed
Handle generics in nested components
1 parent d3358b7 commit d33d4c2

10 files changed

+465
-4
lines changed

components/context.jsonld

+4
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@
118118
"@id": "oo:parameterRangeGenericType",
119119
"@type": "@id"
120120
},
121+
"parameterRangeGenericBindings": {
122+
"@id": "oo:parameterRangeGenericBindings",
123+
"@type": "@id"
124+
},
121125

122126
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
123127
"comment": {

lib/preprocess/ConfigPreprocessorComponent.ts

+23-3
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,31 @@ export class ConfigPreprocessorComponent implements IConfigPreprocessor<ICompone
108108
return configRaw;
109109
}
110110

111-
protected createGenericsContext(handleResponse: IComponentConfigPreprocessorHandleResponse): GenericsContext {
112-
return new GenericsContext(
111+
protected createGenericsContext(
112+
handleResponse: IComponentConfigPreprocessorHandleResponse,
113+
config: Resource,
114+
): GenericsContext {
115+
const genericsContext = new GenericsContext(
113116
this.objectLoader,
114117
handleResponse.component.properties.genericTypeParameters,
115118
);
119+
120+
// Populate with manually defined generic type bindings
121+
const genericTypesInner = handleResponse.component.properties.genericTypeParameters;
122+
if (genericTypesInner.length < config.properties.genericTypeInstances.length) {
123+
throw new ErrorResourcesContext(`Invalid generic type instantiations: more generic types are passed than are defined on the component.`, {
124+
config,
125+
component: handleResponse.component,
126+
});
127+
}
128+
for (const [ i, genericTypeInstance ] of config.properties.genericTypeInstances.entries()) {
129+
// Remap generic type IRI to inner generic type IRI
130+
const genericTypeIdInner = genericTypesInner[i].value;
131+
genericsContext.bindings[genericTypeIdInner] = genericTypeInstance.properties.parameterRangeGenericBindings;
132+
genericsContext.genericTypeIds[genericTypeIdInner] = true;
133+
}
134+
135+
return genericsContext;
116136
}
117137

118138
/**
@@ -125,7 +145,7 @@ export class ConfigPreprocessorComponent implements IConfigPreprocessor<ICompone
125145
handleResponse: IComponentConfigPreprocessorHandleResponse,
126146
): Resource {
127147
const entries: Resource[] = [];
128-
const genericsContext = this.createGenericsContext(handleResponse);
148+
const genericsContext = this.createGenericsContext(handleResponse, config);
129149
for (const fieldData of handleResponse.component.properties.parameters) {
130150
const field = this.objectLoader.createCompactedResource({});
131151
field.property.key = this.objectLoader.createCompactedResource(`"${fieldData.term.value}"`);

lib/preprocess/ConfigPreprocessorComponentMapped.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export class ConfigPreprocessorComponentMapped extends ConfigPreprocessorCompone
5757
handleResponse: IComponentConfigPreprocessorHandleResponse,
5858
): Resource {
5959
const constructorArgs = handleResponse.component.property.constructorArguments;
60-
const genericsContext = this.createGenericsContext(handleResponse);
60+
const genericsContext = this.createGenericsContext(handleResponse, config);
6161
return this.applyConstructorArgumentsParameters(config, constructorArgs, config, genericsContext);
6262
}
6363

lib/preprocess/parameterproperty/ParameterPropertyHandlerRange.ts

+24
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,26 @@ export class ParameterPropertyHandlerRange implements IParameterPropertyHandler
199199
);
200200
}
201201

202+
// Check if the range refers to a component with a generic type
203+
if (paramRange.isA('ParameterRangeGenericComponent')) {
204+
if (value) {
205+
if (value.property.genericTypeInstances) {
206+
// Once we support manual generics setting, we'll need to check here if we can merge with it.
207+
throw new ErrorResourcesContext(`Simultaneous manual generic type passing and generic type inference are not supported yet.`, { parameter: param, value });
208+
}
209+
210+
// For the defined generic type instances, apply them into the instance so they can be checked later
211+
value.properties.genericTypeInstances = paramRange.properties.genericTypeInstances
212+
.map(genericTypeInstance => this.objectLoader.createCompactedResource({
213+
type: 'ParameterRangeGenericTypeReference',
214+
parameterRangeGenericType: genericTypeInstance.property.parameterRangeGenericType.value,
215+
parameterRangeGenericBindings: genericsContext
216+
.bindings[genericTypeInstance.property.parameterRangeGenericType.value],
217+
}));
218+
}
219+
return this.hasParamValueValidType(value, param, paramRange.property.component, genericsContext);
220+
}
221+
202222
// Check if this param defines a field with sub-params
203223
if (paramRange.isA('ParameterRangeCollectEntries')) {
204224
// TODO: Add support for type-checking nested fields with collectEntries
@@ -265,6 +285,10 @@ export class ParameterPropertyHandlerRange implements IParameterPropertyHandler
265285
const valid = paramRange.property.parameterRangeGenericType.value in genericsContext.genericTypeIds;
266286
return `<${valid ? '' : 'UNKNOWN GENERIC: '}${paramRange.property.parameterRangeGenericType.value}>`;
267287
}
288+
if (paramRange.isA('ParameterRangeGenericComponent')) {
289+
return `(${this.rangeToDisplayString(paramRange.property.component, genericsContext)})${paramRange.properties.genericTypeInstances
290+
.map(genericTypeInstance => this.rangeToDisplayString(genericTypeInstance, genericsContext)).join('')}`;
291+
}
268292
return paramRange.value;
269293
}
270294
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"@context": {
3+
"@vocab": "https://linkedsoftwaredependencies.org/vocabularies/object-oriented#",
4+
"ex": "http://example.org/",
5+
"hello": "http://example.org/hello/"
6+
},
7+
"@graph": [
8+
{
9+
"@id": "ex:myconfig1",
10+
"@type": "ex:HelloWorldModule#SayHelloComponent",
11+
"hello:hello": 123,
12+
"hello:inner": {
13+
"@type": "ex:HelloWorldModule#SayHelloComponentInner",
14+
"hello:inner2": "abc",
15+
},
16+
}
17+
]
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"@context": {
3+
"@vocab": "https://linkedsoftwaredependencies.org/vocabularies/object-oriented#",
4+
"ex": "http://example.org/",
5+
"hello": "http://example.org/hello/"
6+
},
7+
"@graph": [
8+
{
9+
"@id": "ex:myconfig2",
10+
"@type": "ex:HelloWorldModule#SayHelloComponent",
11+
"hello:hello": 123,
12+
"hello:inner": {
13+
"@type": "ex:HelloWorldModule#SayHelloComponentInner",
14+
"hello:inner2": 456,
15+
},
16+
}
17+
]
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
{
2+
"@context": [
3+
"https://linkedsoftwaredependencies.org/bundles/npm/componentsjs/^4.0.0/components/context.jsonld",
4+
{
5+
"hello": "http://example.org/hello/",
6+
"ex": "http://example.org/"
7+
}
8+
],
9+
"@graph": [
10+
{
11+
"@id": "ex:HelloWorldModule",
12+
"@type": "Module",
13+
"requireName": "helloworld",
14+
"components": [
15+
{
16+
"@id": "ex:HelloWorldModule#SayHelloComponent",
17+
"@type": "Class",
18+
"requireElement": "Hello",
19+
"genericTypeParameters": [
20+
{
21+
"@id": "ex:HelloWorldModule#SayHelloComponent__generic_T",
22+
"range": "xsd:number",
23+
},
24+
],
25+
"parameters": [
26+
{
27+
"@id": "hello:hello",
28+
"range": {
29+
"@type": "ParameterRangeGenericTypeReference",
30+
"parameterRangeGenericType": "ex:HelloWorldModule#SayHelloComponent__generic_T"
31+
},
32+
},
33+
{
34+
"@id": "hello:inner",
35+
"range": {
36+
"@type": "ParameterRangeGenericComponent",
37+
"component": "ex:HelloWorldModule#SayHelloComponentInner",
38+
"genericTypeInstances": [
39+
{
40+
"@type": "ParameterRangeGenericTypeReference",
41+
"parameterRangeGenericType": "ex:HelloWorldModule#SayHelloComponent__generic_T"
42+
}
43+
]
44+
},
45+
}
46+
],
47+
"constructorArguments": [
48+
{
49+
"@id": "ex:HelloWorldModule#SayHelloComponent_constructorArgumentsObject",
50+
"fields": [
51+
{
52+
"keyRaw": "hello",
53+
"value": "hello:hello"
54+
},
55+
{
56+
"keyRaw": "inner",
57+
"value": "hello:inner"
58+
}
59+
]
60+
}
61+
]
62+
},
63+
{
64+
"@id": "ex:HelloWorldModule#SayHelloComponentInner",
65+
"@type": "Class",
66+
"requireElement": "Hello",
67+
"genericTypeParameters": [
68+
{
69+
"@id": "ex:HelloWorldModule#SayHelloComponentInner__generic_T",
70+
"range": "xsd:number",
71+
},
72+
],
73+
"parameters": [
74+
{
75+
"@id": "hello:inner2",
76+
"range": {
77+
"@type": "ParameterRangeGenericTypeReference",
78+
"parameterRangeGenericType": "ex:HelloWorldModule#SayHelloComponentInner__generic_T"
79+
},
80+
}
81+
],
82+
"constructorArguments": [
83+
{
84+
"@id": "ex:HelloWorldModule#SayHelloComponentInner_constructorArgumentsObject",
85+
"fields": [
86+
{
87+
"keyRaw": "inner",
88+
"value": "hello:inner2"
89+
}
90+
]
91+
}
92+
]
93+
}
94+
]
95+
}
96+
]
97+
}

test/integration/instantiateFile-test.ts

+41
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,47 @@ describe('construction with component configs as files', () => {
636636
});
637637
});
638638

639+
describe(`for a component with generically typed params with links to nested components`, () => {
640+
beforeEach(async() => {
641+
manager = await ComponentsManager.build({
642+
mainModulePath: __dirname,
643+
moduleState,
644+
async moduleLoader(registry) {
645+
await registry.registerModule(Path.join(__dirname, '../assets/module-paramranges-generics-nested.jsonld'));
646+
},
647+
});
648+
});
649+
650+
it('should throw on invalid param values', async() => {
651+
await manager.configRegistry
652+
.register(Path.join(__dirname, '../assets/config-paramranges-generics-nested-invalid.jsonld'));
653+
manager.logger.error = jest.fn();
654+
655+
await expect(manager.instantiate('http://example.org/myconfig1')).rejects
656+
.toThrow(`The value "abc" for parameter "http://example.org/hello/inner2" is not of required range type "<http://example.org/HelloWorldModule#SayHelloComponentInner__generic_T>"`);
657+
expect(fs.existsSync('componentsjs-error-state.json')).toBeTruthy();
658+
fs.unlinkSync('componentsjs-error-state.json');
659+
});
660+
661+
it('should handle valid param values', async() => {
662+
await manager.configRegistry
663+
.register(Path.join(__dirname, '../assets/config-paramranges-generics-nested.jsonld'));
664+
665+
const run1 = await manager.instantiate('http://example.org/myconfig2');
666+
expect(run1).toBeInstanceOf(Hello);
667+
expect(run1._params).toEqual([{
668+
hello: 123,
669+
inner: {
670+
_params: [
671+
{
672+
inner: 456,
673+
},
674+
],
675+
},
676+
}]);
677+
});
678+
});
679+
639680
describe('for a component with constructor args with nested entry collection', () => {
640681
beforeEach(async() => {
641682
manager = await ComponentsManager.build({

test/unit/preprocess/ConfigPreprocessorComponent-test.ts

+78
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,84 @@ describe('ConfigPreprocessorComponent', () => {
361361
});
362362
expectTransformOutput(config, expectedArgs);
363363
});
364+
365+
it('should handle one parameter with one value and generic type instance', () => {
366+
const config = objectLoader.createCompactedResource({
367+
'@id': 'ex:myComponentInstance',
368+
types: 'ex:ComponentThis',
369+
'ex:myComponentInstance#param1': '"A"',
370+
genericTypeInstances: [
371+
{
372+
parameterRangeGenericType: 'ex:ComponentThis__generic_T',
373+
parameterRangeGenericBindings: 'xsd:number',
374+
},
375+
],
376+
});
377+
componentResources['ex:ComponentThis'] = objectLoader.createCompactedResource({
378+
'@id': 'ex:ComponentThis',
379+
module: 'ex:Module',
380+
parameters: [
381+
{
382+
'@id': 'ex:myComponentInstance#param1',
383+
},
384+
],
385+
genericTypeParameters: [
386+
{
387+
'@id': 'ex:ComponentThis__generic_T',
388+
},
389+
],
390+
});
391+
const expectedArgs = objectLoader.createCompactedResource({
392+
list: [
393+
{
394+
fields: {
395+
list: [
396+
{
397+
key: '"ex:myComponentInstance#param1"',
398+
value: '"A"',
399+
},
400+
],
401+
},
402+
},
403+
],
404+
});
405+
expectTransformOutput(config, expectedArgs);
406+
});
407+
408+
it('should not handle with incompatible generic type instances', () => {
409+
const config = objectLoader.createCompactedResource({
410+
'@id': 'ex:myComponentInstance',
411+
types: 'ex:ComponentThis',
412+
'ex:myComponentInstance#param1': '"A"',
413+
genericTypeInstances: [
414+
{
415+
parameterRangeGenericType: 'ex:ComponentThis__generic_T',
416+
parameterRangeGenericBindings: 'xsd:number',
417+
},
418+
{
419+
parameterRangeGenericType: 'ex:ComponentThis__generic_T',
420+
parameterRangeGenericBindings: 'xsd:number',
421+
},
422+
],
423+
});
424+
componentResources['ex:ComponentThis'] = objectLoader.createCompactedResource({
425+
'@id': 'ex:ComponentThis',
426+
module: 'ex:Module',
427+
parameters: [
428+
{
429+
'@id': 'ex:myComponentInstance#param1',
430+
},
431+
],
432+
genericTypeParameters: [
433+
{
434+
'@id': 'ex:ComponentThis__generic_T',
435+
},
436+
],
437+
});
438+
const expectedArgs = objectLoader.createCompactedResource({});
439+
expect(() => expectTransformOutput(config, expectedArgs))
440+
.toThrowError(`Invalid generic type instantiations: more generic types are passed than are defined on the component.`);
441+
});
364442
});
365443

366444
describe('transform', () => {

0 commit comments

Comments
 (0)