Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for list overrides #134

Merged
merged 1 commit into from
Jan 9, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 34 additions & 0 deletions components/context.jsonld
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,46 @@
"Override": {
"@id": "oo:Override"
},
"OverrideParameters": {
"@id": "oo:OverrideParameters"
},
"OverrideMapEntry": {
"@id": "oo:OverrideMapEntry"
},
"OverrideListInsertBefore": {
"@id": "oo:OverrideListInsertBefore"
},
"OverrideListInsertAfter": {
"@id": "oo:OverrideListInsertAfter"
},
"OverrideListInsertAt": {
"@id": "oo:OverrideListInsertAt"
},
"OverrideListRemove": {
"@id": "oo:OverrideListRemove"
},
"overrideInstance": {
"@id": "oo:overrideInstance"
},
"overrideParameters": {
"@id": "oo:overrideParameters"
},
"overrideSteps": {
"@id": "oo:overrideSteps",
"@container": "@list"
},
"overrideParameter": {
"@id": "oo:overrideParameter",
"@container": "@list"
},
"overrideTarget": {
"@id": "oo:overrideTarget",
"@container": "@list"
},
"overrideValue": {
"@id": "oo:overrideValue",
"@container": "@list"
},
"ParameterRange": {
"@id": "oo:ParameterRange"
},
Expand Down
147 changes: 81 additions & 66 deletions lib/preprocess/ConfigPreprocessorOverride.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,80 @@
import type { Resource } from 'rdf-object';
import type { RdfObjectLoader } from 'rdf-object/lib/RdfObjectLoader';
import type { Logger } from 'winston';
import { IRIS_OO, IRIS_RDF } from '../rdf/Iris';
import { IRIS_OO, PREFIX_OO } from '../rdf/Iris';
import { uniqueTypes } from '../rdf/ResourceUtil';
import { ErrorResourcesContext } from '../util/ErrorResourcesContext';
import type { IConfigPreprocessor, IConfigPreprocessorTransform } from './IConfigPreprocessor';
import type { IOverrideStep } from './overridesteps/IOverrideStep';
import { OverrideListInsertAfter } from './overridesteps/OverrideListInsertAfter';
import { OverrideListInsertAt } from './overridesteps/OverrideListInsertAt';
import { OverrideListInsertBefore } from './overridesteps/OverrideListInsertBefore';
import { OverrideListRemove } from './overridesteps/OverrideListRemove';
import { OverrideMapEntry } from './overridesteps/OverrideMapEntry';
import { OverrideParameters } from './overridesteps/OverrideParameters';

/**
* An {@link IConfigPreprocessor} that handles the overriding of parameters.
* Values in the given {@link Resource}s will be replaced if any overriding object is found,
* targeting this resource.
*/
export class ConfigPreprocessorOverride implements IConfigPreprocessor<Record<string, Resource>> {
export class ConfigPreprocessorOverride implements IConfigPreprocessor<Resource[]> {
public readonly objectLoader: RdfObjectLoader;
public readonly componentResources: Record<string, Resource>;
public readonly logger: Logger;

private overrides: Record<string, Record<string, Resource>> | undefined;
private readonly stepHandlers: IOverrideStep[];
private overrides: Record<string, Resource[]> | undefined;

public constructor(options: IComponentConfigPreprocessorOverrideOptions) {
this.objectLoader = options.objectLoader;
this.componentResources = options.componentResources;
this.logger = options.logger;

this.stepHandlers = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of adding the handlers here, could we pass them through the options from ComponentsManagerBuilder?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No strong opinion either way, just that this was based on how this is done in some other classes:

private readonly mappingHandlers: IConstructorArgumentsElementMappingHandler[] = [
new ConstructorArgumentsElementMappingHandlerKeyValue(),
new ConstructorArgumentsElementMappingHandlerCollectEntries(this.parameterHandler),
new ConstructorArgumentsElementMappingHandlerFields(),
new ConstructorArgumentsElementMappingHandlerElements(),
new ConstructorArgumentsElementMappingHandlerList(),
];
and
this.parameterPropertyHandlers = [
new ParameterPropertyHandlerDefaultScoped(this.objectLoader),
new ParameterPropertyHandlerDefault(this.objectLoader),
new ParameterPropertyHandlerFixed(this.objectLoader),
this.parameterPropertyHandlerRange = new ParameterPropertyHandlerRange(this.objectLoader, options.typeChecking),
new ParameterPropertyHandlerLazy(),
];

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see. In that case, you can ignore my comment.

new OverrideParameters(),
new OverrideListInsertBefore(),
new OverrideListInsertAfter(),
new OverrideListInsertAt(),
new OverrideListRemove(),
new OverrideMapEntry(),
];
}

/**
* Checks if there are any overrides targeting the given resource.
* @param config - Resource to find overrides for.
*
* @returns A key/value object with keys being the properties that have an override.
* @returns A list of override steps to apply to the target, in order.
*/
public canHandle(config: Resource): Record<string, Resource> | undefined {
public canHandle(config: Resource): Resource[] | undefined {
if (!this.overrides) {
this.overrides = this.createOverrideObjects();
this.overrides = this.createOverrideSteps();
}
return this.overrides[config.value];
}

/**
* Override the resource with the stored values.
* Override the resource with the stored override steps.
* @param config - The resource to override.
* @param handleResponse - Override values that were found for this resource.
* @param handleResponse - Override steps that were found for this resource.
*/
public transform(config: Resource, handleResponse: Record<string, Resource>): IConfigPreprocessorTransform {
// We know this has exactly 1 result due to the canHandle call
const configType = uniqueTypes(config, this.componentResources)[0];
const overrideType = handleResponse[IRIS_RDF.type]?.value;
// In case the type changes we have to delete all the original properties as those correspond to the old type
if (overrideType && configType.value !== overrideType) {
for (const id of Object.keys(config.properties)) {
delete config.properties[id];
public transform(config: Resource, handleResponse: Resource[]): IConfigPreprocessorTransform {
// Apply all override steps sequentially
for (const step of handleResponse) {
let handler: IOverrideStep | undefined;
for (const stepHandler of this.stepHandlers) {
if (stepHandler.canHandle(config, step)) {
handler = stepHandler;
break;
}
}
}
for (const property of Object.keys(handleResponse)) {
config.properties[property] = [ handleResponse[property] ];
if (!handler) {
throw new ErrorResourcesContext(`Found no handler supporting an override step of type ${step.property.type.value}`, {
step,
});
}
handler.handle(config, step);
}

return { rawConfig: config, finishTransformation: false };
Expand All @@ -71,18 +92,18 @@ export class ConfigPreprocessorOverride implements IConfigPreprocessor<Record<st
* Keys of the object are the identifiers of the resources that need to be modified,
* values are key/value maps listing all parameters with their new values.
*/
public createOverrideObjects(): Record<string, Record<string, Resource>> {
public createOverrideSteps(): Record<string, Resource[]> {
const overrides = [ ...this.findOverrideTargets() ];
const chains = this.createOverrideChains(overrides);
this.validateChains(chains);
const overrideObjects: Record<string, Record<string, Resource>> = {};
const overrideSteps: Record<string, Resource[]> = {};
for (const chain of chains) {
const { target, values } = this.chainToOverrideObject(chain);
if (Object.keys(values).length > 0) {
overrideObjects[target] = values;
const { target, steps } = this.chainToOverrideSteps(chain);
if (Object.keys(steps).length > 0) {
overrideSteps[target.value] = steps;
}
}
return overrideObjects;
return overrideSteps;
}

/**
Expand Down Expand Up @@ -110,6 +131,7 @@ export class ConfigPreprocessorOverride implements IConfigPreprocessor<Record<st
* Chains all Overrides together if they reference each other.
* E.g., if the input is a list of Overrides A -> B, B -> C, D -> E,
* the result wil be [[ A, B, C ], [ D, E ]].
* The last element in the array will always be the non-Override resource being targeted.
*
* @param overrides - All Overrides that have to be combined.
*/
Expand Down Expand Up @@ -169,30 +191,39 @@ export class ConfigPreprocessorOverride implements IConfigPreprocessor<Record<st
}

/**
* Merges all Overrides in a chain to create a single override object
* containing replacement values for all relevant parameters of the final entry in the chain.
* Merges all Overrides in a chain to create a single list of override steps.
* The order of the steps is the order in which they should be applied,
* with the first entry being the first step of the override closest to the target resource.
*
* @param chain - The chain of Overrides, with a normal resource as the last entry in the array.
*/
protected chainToOverrideObject(chain: Resource[]): { target: string; values: Record<string, Resource> } {
protected chainToOverrideSteps(chain: Resource[]): { target: Resource; steps: Resource[] } {
const target = this.getChainTarget(chain);

// Apply all overrides sequentially, starting from the one closest to the target.
// This ensures the most recent override has priority.
let mergedOverride: Record<string, Resource> = {};
const steps: Resource[] = [];
for (let i = chain.length - 2; i >= 0; --i) {
const validatedObject = this.extractOverrideParameters(chain[i], target);
// In case an Override has a different type, the properties of the target don't matter any more,
// as the object is being replaced completely.
const mergedType = mergedOverride[IRIS_RDF.type]?.value;
const overrideType = validatedObject[IRIS_RDF.type]?.value;
if (overrideType && overrideType !== mergedType) {
mergedOverride = validatedObject;
} else {
Object.assign(mergedOverride, validatedObject);
const subStepProperties = chain[i].properties[IRIS_OO.overrideSteps];

if (subStepProperties.length > 1) {
throw new ErrorResourcesContext(`Detected multiple values for overrideSteps in Override ${chain[i].value}. RDF lists should be used for defining multiple values.`, {
override: chain[i],
});
}

let subSteps = subStepProperties[0]?.list ?? subStepProperties;

// Translate simplified format to override step
if (chain[i].properties[IRIS_OO.overrideParameters].length > 0) {
subSteps = [ this.simplifiedOverrideToStep(chain[i]) ];
}

if (subSteps.length === 0) {
this.logger.warn(`No steps found for Override ${chain[i].value}. This Override will be ignored.`);
continue;
}

steps.push(...subSteps);
}
return { target: target.value, values: mergedOverride };
return { target, steps };
}

/**
Expand All @@ -218,37 +249,21 @@ export class ConfigPreprocessorOverride implements IConfigPreprocessor<Record<st
}

/**
* Extracts all parameters of an Override with their corresponding value.
* @param override - The Override to apply.
* @param target - The target resource to apply the Override to. Only used for error messages.
*
* @param override
* @protected
*/
protected extractOverrideParameters(override: Resource, target: Resource): Record<string, Resource> {
protected simplifiedOverrideToStep(override: Resource): Resource {
const overrideObjects = override.properties[IRIS_OO.overrideParameters];
if (!overrideObjects || overrideObjects.length === 0) {
this.logger.warn(`No overrideParameters found for ${override.value}.`);
return {};
}
if (overrideObjects.length > 1) {
throw new ErrorResourcesContext(`Detected multiple values for overrideParameters in Override ${override.value}`, {
override,
});
}
const overrideObject = overrideObjects[0];

// Only keep the parameters that are known to the type of the target object
const validatedObject: Record<string, Resource> = {};
for (const parameter of Object.keys(overrideObject.properties)) {
const overrideValues = overrideObject.properties[parameter];
if (overrideValues.length > 1) {
throw new ErrorResourcesContext(`Detected multiple values for override parameter ${parameter} in Override ${override.value}. RDF lists should be used for defining multiple values.`, {
arguments: overrideValues,
target,
override,
});
}
validatedObject[parameter] = overrideValues[0];
}
return validatedObject;
return this.objectLoader.createCompactedResource({
types: PREFIX_OO('OverrideParameters'),
overrideValue: overrideObjects[0],
});
}
}

Expand Down
26 changes: 26 additions & 0 deletions lib/preprocess/overridesteps/IOverrideStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Resource } from 'rdf-object';

/**
* Transforms a resource based on the contents of an override step.
*/
export interface IOverrideStep {
/**
* Determines if this handler can apply the given override step to the resource.
*
* @param config - The resource to override.
* @param step - The override step to apply.
*
* @returns true if this handler should be used.
*/
canHandle: (config: Resource, step: Resource) => boolean;

/**
* Applies the changes described in the given override step to the resource.
*
* @param config - The resource to override.
* @param step - The override step to apply.
*
* @returns The modified resource.
*/
handle: (config: Resource, step: Resource) => Resource;
}
31 changes: 31 additions & 0 deletions lib/preprocess/overridesteps/OverrideListInsertAfter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Resource } from 'rdf-object';
import { PREFIX_OO } from '../../rdf/Iris';
import type { IOverrideStep } from './IOverrideStep';
import { extractOverrideStepFields, findResourceIndex, getPropertyResourceList } from './OverrideUtil';

/**
* Override step that inserts elements in a list after a specific element.
*
* Uses the following override step fields:
* - `overrideParameter`: Parameter of the original object that contains the list.
* - `overrideTarget`: Element already in the list that is used as reference. This can be a named node or a literal.
* - `overrideValue`: Element(s) to insert immediately after the target element.
*/
export class OverrideListInsertAfter implements IOverrideStep {
public canHandle(config: Resource, step: Resource): boolean {
return step.property.type.value === PREFIX_OO('OverrideListInsertAfter');
}

public handle(config: Resource, step: Resource): Resource {
const { parameters, targets, values } = extractOverrideStepFields(step, { parameters: 1, targets: 1 });

const list = getPropertyResourceList(config, parameters[0]);

const index = findResourceIndex(list, targets[0]);

// +1 so we start after the selected element
list.splice(index + 1, 0, ...values);

return config;
}
}
45 changes: 45 additions & 0 deletions lib/preprocess/overridesteps/OverrideListInsertAt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { Resource } from 'rdf-object';
import { PREFIX_OO } from '../../rdf/Iris';
import { ErrorResourcesContext } from '../../util/ErrorResourcesContext';
import type { IOverrideStep } from './IOverrideStep';
import { extractOverrideStepFields, getPropertyResourceList } from './OverrideUtil';

/**
* Override step that inserts elements in a list at a specific index.
* A negative index can be used to count from the back of the list.
* An index of `-0` can be used to insert at the end of the list.
*
* Uses the following override step fields:
* - `overrideParameter`: Parameter of the original object that contains the list.
* - `overrideTarget`: A literal containing the index.
* - `overrideValue`: Element(s) to insert at the chosen index.
*/
export class OverrideListInsertAt implements IOverrideStep {
public canHandle(config: Resource, step: Resource): boolean {
return step.property.type.value === PREFIX_OO('OverrideListInsertAt');
}

public handle(config: Resource, step: Resource): Resource {
const { parameters, targets, values } = extractOverrideStepFields(step, { parameters: 1, targets: 1 });

const list = getPropertyResourceList(config, parameters[0]);

const val = targets[0].value;
if (!/^-?\d+$/u.test(val)) {
throw new ErrorResourcesContext(`Invalid index in Override step OverrideListInsertAt for parameter ${parameters[0].value}: ${val}`, {
config,
step,
});
}

// Support adding elements at the end using -0
if (val === '-0') {
list.push(...values);
} else {
const index = Number.parseInt(val, 10);
list.splice(index, 0, ...values);
}

return config;
}
}