Skip to content

Commit

Permalink
Handle generics in nested components
Browse files Browse the repository at this point in the history
  • Loading branch information
rubensworks committed Dec 7, 2021
1 parent d3358b7 commit d33d4c2
Show file tree
Hide file tree
Showing 10 changed files with 465 additions and 4 deletions.
4 changes: 4 additions & 0 deletions components/context.jsonld
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@
"@id": "oo:parameterRangeGenericType",
"@type": "@id"
},
"parameterRangeGenericBindings": {
"@id": "oo:parameterRangeGenericBindings",
"@type": "@id"
},

"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"comment": {
Expand Down
26 changes: 23 additions & 3 deletions lib/preprocess/ConfigPreprocessorComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,31 @@ export class ConfigPreprocessorComponent implements IConfigPreprocessor<ICompone
return configRaw;
}

protected createGenericsContext(handleResponse: IComponentConfigPreprocessorHandleResponse): GenericsContext {
return new GenericsContext(
protected createGenericsContext(
handleResponse: IComponentConfigPreprocessorHandleResponse,
config: Resource,
): GenericsContext {
const genericsContext = new GenericsContext(
this.objectLoader,
handleResponse.component.properties.genericTypeParameters,
);

// Populate with manually defined generic type bindings
const genericTypesInner = handleResponse.component.properties.genericTypeParameters;
if (genericTypesInner.length < config.properties.genericTypeInstances.length) {
throw new ErrorResourcesContext(`Invalid generic type instantiations: more generic types are passed than are defined on the component.`, {
config,
component: handleResponse.component,
});
}
for (const [ i, genericTypeInstance ] of config.properties.genericTypeInstances.entries()) {
// Remap generic type IRI to inner generic type IRI
const genericTypeIdInner = genericTypesInner[i].value;
genericsContext.bindings[genericTypeIdInner] = genericTypeInstance.properties.parameterRangeGenericBindings;
genericsContext.genericTypeIds[genericTypeIdInner] = true;
}

return genericsContext;
}

/**
Expand All @@ -125,7 +145,7 @@ export class ConfigPreprocessorComponent implements IConfigPreprocessor<ICompone
handleResponse: IComponentConfigPreprocessorHandleResponse,
): Resource {
const entries: Resource[] = [];
const genericsContext = this.createGenericsContext(handleResponse);
const genericsContext = this.createGenericsContext(handleResponse, config);
for (const fieldData of handleResponse.component.properties.parameters) {
const field = this.objectLoader.createCompactedResource({});
field.property.key = this.objectLoader.createCompactedResource(`"${fieldData.term.value}"`);
Expand Down
2 changes: 1 addition & 1 deletion lib/preprocess/ConfigPreprocessorComponentMapped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class ConfigPreprocessorComponentMapped extends ConfigPreprocessorCompone
handleResponse: IComponentConfigPreprocessorHandleResponse,
): Resource {
const constructorArgs = handleResponse.component.property.constructorArguments;
const genericsContext = this.createGenericsContext(handleResponse);
const genericsContext = this.createGenericsContext(handleResponse, config);
return this.applyConstructorArgumentsParameters(config, constructorArgs, config, genericsContext);
}

Expand Down
24 changes: 24 additions & 0 deletions lib/preprocess/parameterproperty/ParameterPropertyHandlerRange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,26 @@ export class ParameterPropertyHandlerRange implements IParameterPropertyHandler
);
}

// Check if the range refers to a component with a generic type
if (paramRange.isA('ParameterRangeGenericComponent')) {
if (value) {
if (value.property.genericTypeInstances) {
// Once we support manual generics setting, we'll need to check here if we can merge with it.
throw new ErrorResourcesContext(`Simultaneous manual generic type passing and generic type inference are not supported yet.`, { parameter: param, value });
}

// For the defined generic type instances, apply them into the instance so they can be checked later
value.properties.genericTypeInstances = paramRange.properties.genericTypeInstances
.map(genericTypeInstance => this.objectLoader.createCompactedResource({
type: 'ParameterRangeGenericTypeReference',
parameterRangeGenericType: genericTypeInstance.property.parameterRangeGenericType.value,
parameterRangeGenericBindings: genericsContext
.bindings[genericTypeInstance.property.parameterRangeGenericType.value],
}));
}
return this.hasParamValueValidType(value, param, paramRange.property.component, genericsContext);
}

// Check if this param defines a field with sub-params
if (paramRange.isA('ParameterRangeCollectEntries')) {
// TODO: Add support for type-checking nested fields with collectEntries
Expand Down Expand Up @@ -265,6 +285,10 @@ export class ParameterPropertyHandlerRange implements IParameterPropertyHandler
const valid = paramRange.property.parameterRangeGenericType.value in genericsContext.genericTypeIds;
return `<${valid ? '' : 'UNKNOWN GENERIC: '}${paramRange.property.parameterRangeGenericType.value}>`;
}
if (paramRange.isA('ParameterRangeGenericComponent')) {
return `(${this.rangeToDisplayString(paramRange.property.component, genericsContext)})${paramRange.properties.genericTypeInstances
.map(genericTypeInstance => this.rangeToDisplayString(genericTypeInstance, genericsContext)).join('')}`;
}
return paramRange.value;
}
}
18 changes: 18 additions & 0 deletions test/assets/config-paramranges-generics-nested-invalid.jsonld
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"@context": {
"@vocab": "https://linkedsoftwaredependencies.org/vocabularies/object-oriented#",
"ex": "http://example.org/",
"hello": "http://example.org/hello/"
},
"@graph": [
{
"@id": "ex:myconfig1",
"@type": "ex:HelloWorldModule#SayHelloComponent",
"hello:hello": 123,
"hello:inner": {
"@type": "ex:HelloWorldModule#SayHelloComponentInner",
"hello:inner2": "abc",
},
}
]
}
18 changes: 18 additions & 0 deletions test/assets/config-paramranges-generics-nested.jsonld
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"@context": {
"@vocab": "https://linkedsoftwaredependencies.org/vocabularies/object-oriented#",
"ex": "http://example.org/",
"hello": "http://example.org/hello/"
},
"@graph": [
{
"@id": "ex:myconfig2",
"@type": "ex:HelloWorldModule#SayHelloComponent",
"hello:hello": 123,
"hello:inner": {
"@type": "ex:HelloWorldModule#SayHelloComponentInner",
"hello:inner2": 456,
},
}
]
}
97 changes: 97 additions & 0 deletions test/assets/module-paramranges-generics-nested.jsonld
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
{
"@context": [
"https://linkedsoftwaredependencies.org/bundles/npm/componentsjs/^4.0.0/components/context.jsonld",
{
"hello": "http://example.org/hello/",
"ex": "http://example.org/"
}
],
"@graph": [
{
"@id": "ex:HelloWorldModule",
"@type": "Module",
"requireName": "helloworld",
"components": [
{
"@id": "ex:HelloWorldModule#SayHelloComponent",
"@type": "Class",
"requireElement": "Hello",
"genericTypeParameters": [
{
"@id": "ex:HelloWorldModule#SayHelloComponent__generic_T",
"range": "xsd:number",
},
],
"parameters": [
{
"@id": "hello:hello",
"range": {
"@type": "ParameterRangeGenericTypeReference",
"parameterRangeGenericType": "ex:HelloWorldModule#SayHelloComponent__generic_T"
},
},
{
"@id": "hello:inner",
"range": {
"@type": "ParameterRangeGenericComponent",
"component": "ex:HelloWorldModule#SayHelloComponentInner",
"genericTypeInstances": [
{
"@type": "ParameterRangeGenericTypeReference",
"parameterRangeGenericType": "ex:HelloWorldModule#SayHelloComponent__generic_T"
}
]
},
}
],
"constructorArguments": [
{
"@id": "ex:HelloWorldModule#SayHelloComponent_constructorArgumentsObject",
"fields": [
{
"keyRaw": "hello",
"value": "hello:hello"
},
{
"keyRaw": "inner",
"value": "hello:inner"
}
]
}
]
},
{
"@id": "ex:HelloWorldModule#SayHelloComponentInner",
"@type": "Class",
"requireElement": "Hello",
"genericTypeParameters": [
{
"@id": "ex:HelloWorldModule#SayHelloComponentInner__generic_T",
"range": "xsd:number",
},
],
"parameters": [
{
"@id": "hello:inner2",
"range": {
"@type": "ParameterRangeGenericTypeReference",
"parameterRangeGenericType": "ex:HelloWorldModule#SayHelloComponentInner__generic_T"
},
}
],
"constructorArguments": [
{
"@id": "ex:HelloWorldModule#SayHelloComponentInner_constructorArgumentsObject",
"fields": [
{
"keyRaw": "inner",
"value": "hello:inner2"
}
]
}
]
}
]
}
]
}
41 changes: 41 additions & 0 deletions test/integration/instantiateFile-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,47 @@ describe('construction with component configs as files', () => {
});
});

describe(`for a component with generically typed params with links to nested components`, () => {
beforeEach(async() => {
manager = await ComponentsManager.build({
mainModulePath: __dirname,
moduleState,
async moduleLoader(registry) {
await registry.registerModule(Path.join(__dirname, '../assets/module-paramranges-generics-nested.jsonld'));
},
});
});

it('should throw on invalid param values', async() => {
await manager.configRegistry
.register(Path.join(__dirname, '../assets/config-paramranges-generics-nested-invalid.jsonld'));
manager.logger.error = jest.fn();

await expect(manager.instantiate('http://example.org/myconfig1')).rejects
.toThrow(`The value "abc" for parameter "http://example.org/hello/inner2" is not of required range type "<http://example.org/HelloWorldModule#SayHelloComponentInner__generic_T>"`);
expect(fs.existsSync('componentsjs-error-state.json')).toBeTruthy();
fs.unlinkSync('componentsjs-error-state.json');
});

it('should handle valid param values', async() => {
await manager.configRegistry
.register(Path.join(__dirname, '../assets/config-paramranges-generics-nested.jsonld'));

const run1 = await manager.instantiate('http://example.org/myconfig2');
expect(run1).toBeInstanceOf(Hello);
expect(run1._params).toEqual([{
hello: 123,
inner: {
_params: [
{
inner: 456,
},
],
},
}]);
});
});

describe('for a component with constructor args with nested entry collection', () => {
beforeEach(async() => {
manager = await ComponentsManager.build({
Expand Down
78 changes: 78 additions & 0 deletions test/unit/preprocess/ConfigPreprocessorComponent-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,84 @@ describe('ConfigPreprocessorComponent', () => {
});
expectTransformOutput(config, expectedArgs);
});

it('should handle one parameter with one value and generic type instance', () => {
const config = objectLoader.createCompactedResource({
'@id': 'ex:myComponentInstance',
types: 'ex:ComponentThis',
'ex:myComponentInstance#param1': '"A"',
genericTypeInstances: [
{
parameterRangeGenericType: 'ex:ComponentThis__generic_T',
parameterRangeGenericBindings: 'xsd:number',
},
],
});
componentResources['ex:ComponentThis'] = objectLoader.createCompactedResource({
'@id': 'ex:ComponentThis',
module: 'ex:Module',
parameters: [
{
'@id': 'ex:myComponentInstance#param1',
},
],
genericTypeParameters: [
{
'@id': 'ex:ComponentThis__generic_T',
},
],
});
const expectedArgs = objectLoader.createCompactedResource({
list: [
{
fields: {
list: [
{
key: '"ex:myComponentInstance#param1"',
value: '"A"',
},
],
},
},
],
});
expectTransformOutput(config, expectedArgs);
});

it('should not handle with incompatible generic type instances', () => {
const config = objectLoader.createCompactedResource({
'@id': 'ex:myComponentInstance',
types: 'ex:ComponentThis',
'ex:myComponentInstance#param1': '"A"',
genericTypeInstances: [
{
parameterRangeGenericType: 'ex:ComponentThis__generic_T',
parameterRangeGenericBindings: 'xsd:number',
},
{
parameterRangeGenericType: 'ex:ComponentThis__generic_T',
parameterRangeGenericBindings: 'xsd:number',
},
],
});
componentResources['ex:ComponentThis'] = objectLoader.createCompactedResource({
'@id': 'ex:ComponentThis',
module: 'ex:Module',
parameters: [
{
'@id': 'ex:myComponentInstance#param1',
},
],
genericTypeParameters: [
{
'@id': 'ex:ComponentThis__generic_T',
},
],
});
const expectedArgs = objectLoader.createCompactedResource({});
expect(() => expectTransformOutput(config, expectedArgs))
.toThrowError(`Invalid generic type instantiations: more generic types are passed than are defined on the component.`);
});
});

describe('transform', () => {
Expand Down

0 comments on commit d33d4c2

Please sign in to comment.