Skip to content

Commit 9623e44

Browse files
authored
Add support for list overrides
Closes #129
1 parent bc679db commit 9623e44

20 files changed

+1433
-217
lines changed

components/context.jsonld

+34
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,46 @@
8080
"Override": {
8181
"@id": "oo:Override"
8282
},
83+
"OverrideParameters": {
84+
"@id": "oo:OverrideParameters"
85+
},
86+
"OverrideMapEntry": {
87+
"@id": "oo:OverrideMapEntry"
88+
},
89+
"OverrideListInsertBefore": {
90+
"@id": "oo:OverrideListInsertBefore"
91+
},
92+
"OverrideListInsertAfter": {
93+
"@id": "oo:OverrideListInsertAfter"
94+
},
95+
"OverrideListInsertAt": {
96+
"@id": "oo:OverrideListInsertAt"
97+
},
98+
"OverrideListRemove": {
99+
"@id": "oo:OverrideListRemove"
100+
},
83101
"overrideInstance": {
84102
"@id": "oo:overrideInstance"
85103
},
86104
"overrideParameters": {
87105
"@id": "oo:overrideParameters"
88106
},
107+
"overrideSteps": {
108+
"@id": "oo:overrideSteps",
109+
"@container": "@list"
110+
},
111+
"overrideParameter": {
112+
"@id": "oo:overrideParameter",
113+
"@container": "@list"
114+
},
115+
"overrideTarget": {
116+
"@id": "oo:overrideTarget",
117+
"@container": "@list"
118+
},
119+
"overrideValue": {
120+
"@id": "oo:overrideValue",
121+
"@container": "@list"
122+
},
89123
"ParameterRange": {
90124
"@id": "oo:ParameterRange"
91125
},

lib/preprocess/ConfigPreprocessorOverride.ts

+81-66
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,80 @@
11
import type { Resource } from 'rdf-object';
22
import type { RdfObjectLoader } from 'rdf-object/lib/RdfObjectLoader';
33
import type { Logger } from 'winston';
4-
import { IRIS_OO, IRIS_RDF } from '../rdf/Iris';
4+
import { IRIS_OO, PREFIX_OO } from '../rdf/Iris';
55
import { uniqueTypes } from '../rdf/ResourceUtil';
66
import { ErrorResourcesContext } from '../util/ErrorResourcesContext';
77
import type { IConfigPreprocessor, IConfigPreprocessorTransform } from './IConfigPreprocessor';
8+
import type { IOverrideStep } from './overridesteps/IOverrideStep';
9+
import { OverrideListInsertAfter } from './overridesteps/OverrideListInsertAfter';
10+
import { OverrideListInsertAt } from './overridesteps/OverrideListInsertAt';
11+
import { OverrideListInsertBefore } from './overridesteps/OverrideListInsertBefore';
12+
import { OverrideListRemove } from './overridesteps/OverrideListRemove';
13+
import { OverrideMapEntry } from './overridesteps/OverrideMapEntry';
14+
import { OverrideParameters } from './overridesteps/OverrideParameters';
815

916
/**
1017
* An {@link IConfigPreprocessor} that handles the overriding of parameters.
1118
* Values in the given {@link Resource}s will be replaced if any overriding object is found,
1219
* targeting this resource.
1320
*/
14-
export class ConfigPreprocessorOverride implements IConfigPreprocessor<Record<string, Resource>> {
21+
export class ConfigPreprocessorOverride implements IConfigPreprocessor<Resource[]> {
1522
public readonly objectLoader: RdfObjectLoader;
1623
public readonly componentResources: Record<string, Resource>;
1724
public readonly logger: Logger;
1825

19-
private overrides: Record<string, Record<string, Resource>> | undefined;
26+
private readonly stepHandlers: IOverrideStep[];
27+
private overrides: Record<string, Resource[]> | undefined;
2028

2129
public constructor(options: IComponentConfigPreprocessorOverrideOptions) {
2230
this.objectLoader = options.objectLoader;
2331
this.componentResources = options.componentResources;
2432
this.logger = options.logger;
33+
34+
this.stepHandlers = [
35+
new OverrideParameters(),
36+
new OverrideListInsertBefore(),
37+
new OverrideListInsertAfter(),
38+
new OverrideListInsertAt(),
39+
new OverrideListRemove(),
40+
new OverrideMapEntry(),
41+
];
2542
}
2643

2744
/**
2845
* Checks if there are any overrides targeting the given resource.
2946
* @param config - Resource to find overrides for.
3047
*
31-
* @returns A key/value object with keys being the properties that have an override.
48+
* @returns A list of override steps to apply to the target, in order.
3249
*/
33-
public canHandle(config: Resource): Record<string, Resource> | undefined {
50+
public canHandle(config: Resource): Resource[] | undefined {
3451
if (!this.overrides) {
35-
this.overrides = this.createOverrideObjects();
52+
this.overrides = this.createOverrideSteps();
3653
}
3754
return this.overrides[config.value];
3855
}
3956

4057
/**
41-
* Override the resource with the stored values.
58+
* Override the resource with the stored override steps.
4259
* @param config - The resource to override.
43-
* @param handleResponse - Override values that were found for this resource.
60+
* @param handleResponse - Override steps that were found for this resource.
4461
*/
45-
public transform(config: Resource, handleResponse: Record<string, Resource>): IConfigPreprocessorTransform {
46-
// We know this has exactly 1 result due to the canHandle call
47-
const configType = uniqueTypes(config, this.componentResources)[0];
48-
const overrideType = handleResponse[IRIS_RDF.type]?.value;
49-
// In case the type changes we have to delete all the original properties as those correspond to the old type
50-
if (overrideType && configType.value !== overrideType) {
51-
for (const id of Object.keys(config.properties)) {
52-
delete config.properties[id];
62+
public transform(config: Resource, handleResponse: Resource[]): IConfigPreprocessorTransform {
63+
// Apply all override steps sequentially
64+
for (const step of handleResponse) {
65+
let handler: IOverrideStep | undefined;
66+
for (const stepHandler of this.stepHandlers) {
67+
if (stepHandler.canHandle(config, step)) {
68+
handler = stepHandler;
69+
break;
70+
}
5371
}
54-
}
55-
for (const property of Object.keys(handleResponse)) {
56-
config.properties[property] = [ handleResponse[property] ];
72+
if (!handler) {
73+
throw new ErrorResourcesContext(`Found no handler supporting an override step of type ${step.property.type.value}`, {
74+
step,
75+
});
76+
}
77+
handler.handle(config, step);
5778
}
5879

5980
return { rawConfig: config, finishTransformation: false };
@@ -71,18 +92,18 @@ export class ConfigPreprocessorOverride implements IConfigPreprocessor<Record<st
7192
* Keys of the object are the identifiers of the resources that need to be modified,
7293
* values are key/value maps listing all parameters with their new values.
7394
*/
74-
public createOverrideObjects(): Record<string, Record<string, Resource>> {
95+
public createOverrideSteps(): Record<string, Resource[]> {
7596
const overrides = [ ...this.findOverrideTargets() ];
7697
const chains = this.createOverrideChains(overrides);
7798
this.validateChains(chains);
78-
const overrideObjects: Record<string, Record<string, Resource>> = {};
99+
const overrideSteps: Record<string, Resource[]> = {};
79100
for (const chain of chains) {
80-
const { target, values } = this.chainToOverrideObject(chain);
81-
if (Object.keys(values).length > 0) {
82-
overrideObjects[target] = values;
101+
const { target, steps } = this.chainToOverrideSteps(chain);
102+
if (Object.keys(steps).length > 0) {
103+
overrideSteps[target.value] = steps;
83104
}
84105
}
85-
return overrideObjects;
106+
return overrideSteps;
86107
}
87108

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

171193
/**
172-
* Merges all Overrides in a chain to create a single override object
173-
* containing replacement values for all relevant parameters of the final entry in the chain.
194+
* Merges all Overrides in a chain to create a single list of override steps.
195+
* The order of the steps is the order in which they should be applied,
196+
* with the first entry being the first step of the override closest to the target resource.
174197
*
175198
* @param chain - The chain of Overrides, with a normal resource as the last entry in the array.
176199
*/
177-
protected chainToOverrideObject(chain: Resource[]): { target: string; values: Record<string, Resource> } {
200+
protected chainToOverrideSteps(chain: Resource[]): { target: Resource; steps: Resource[] } {
178201
const target = this.getChainTarget(chain);
179-
180-
// Apply all overrides sequentially, starting from the one closest to the target.
181-
// This ensures the most recent override has priority.
182-
let mergedOverride: Record<string, Resource> = {};
202+
const steps: Resource[] = [];
183203
for (let i = chain.length - 2; i >= 0; --i) {
184-
const validatedObject = this.extractOverrideParameters(chain[i], target);
185-
// In case an Override has a different type, the properties of the target don't matter any more,
186-
// as the object is being replaced completely.
187-
const mergedType = mergedOverride[IRIS_RDF.type]?.value;
188-
const overrideType = validatedObject[IRIS_RDF.type]?.value;
189-
if (overrideType && overrideType !== mergedType) {
190-
mergedOverride = validatedObject;
191-
} else {
192-
Object.assign(mergedOverride, validatedObject);
204+
const subStepProperties = chain[i].properties[IRIS_OO.overrideSteps];
205+
206+
if (subStepProperties.length > 1) {
207+
throw new ErrorResourcesContext(`Detected multiple values for overrideSteps in Override ${chain[i].value}. RDF lists should be used for defining multiple values.`, {
208+
override: chain[i],
209+
});
210+
}
211+
212+
let subSteps = subStepProperties[0]?.list ?? subStepProperties;
213+
214+
// Translate simplified format to override step
215+
if (chain[i].properties[IRIS_OO.overrideParameters].length > 0) {
216+
subSteps = [ this.simplifiedOverrideToStep(chain[i]) ];
217+
}
218+
219+
if (subSteps.length === 0) {
220+
this.logger.warn(`No steps found for Override ${chain[i].value}. This Override will be ignored.`);
221+
continue;
193222
}
223+
224+
steps.push(...subSteps);
194225
}
195-
return { target: target.value, values: mergedOverride };
226+
return { target, steps };
196227
}
197228

198229
/**
@@ -218,37 +249,21 @@ export class ConfigPreprocessorOverride implements IConfigPreprocessor<Record<st
218249
}
219250

220251
/**
221-
* Extracts all parameters of an Override with their corresponding value.
222-
* @param override - The Override to apply.
223-
* @param target - The target resource to apply the Override to. Only used for error messages.
252+
*
253+
* @param override
254+
* @protected
224255
*/
225-
protected extractOverrideParameters(override: Resource, target: Resource): Record<string, Resource> {
256+
protected simplifiedOverrideToStep(override: Resource): Resource {
226257
const overrideObjects = override.properties[IRIS_OO.overrideParameters];
227-
if (!overrideObjects || overrideObjects.length === 0) {
228-
this.logger.warn(`No overrideParameters found for ${override.value}.`);
229-
return {};
230-
}
231258
if (overrideObjects.length > 1) {
232259
throw new ErrorResourcesContext(`Detected multiple values for overrideParameters in Override ${override.value}`, {
233260
override,
234261
});
235262
}
236-
const overrideObject = overrideObjects[0];
237-
238-
// Only keep the parameters that are known to the type of the target object
239-
const validatedObject: Record<string, Resource> = {};
240-
for (const parameter of Object.keys(overrideObject.properties)) {
241-
const overrideValues = overrideObject.properties[parameter];
242-
if (overrideValues.length > 1) {
243-
throw new ErrorResourcesContext(`Detected multiple values for override parameter ${parameter} in Override ${override.value}. RDF lists should be used for defining multiple values.`, {
244-
arguments: overrideValues,
245-
target,
246-
override,
247-
});
248-
}
249-
validatedObject[parameter] = overrideValues[0];
250-
}
251-
return validatedObject;
263+
return this.objectLoader.createCompactedResource({
264+
types: PREFIX_OO('OverrideParameters'),
265+
overrideValue: overrideObjects[0],
266+
});
252267
}
253268
}
254269

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { Resource } from 'rdf-object';
2+
3+
/**
4+
* Transforms a resource based on the contents of an override step.
5+
*/
6+
export interface IOverrideStep {
7+
/**
8+
* Determines if this handler can apply the given override step to the resource.
9+
*
10+
* @param config - The resource to override.
11+
* @param step - The override step to apply.
12+
*
13+
* @returns true if this handler should be used.
14+
*/
15+
canHandle: (config: Resource, step: Resource) => boolean;
16+
17+
/**
18+
* Applies the changes described in the given override step to the resource.
19+
*
20+
* @param config - The resource to override.
21+
* @param step - The override step to apply.
22+
*
23+
* @returns The modified resource.
24+
*/
25+
handle: (config: Resource, step: Resource) => Resource;
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { Resource } from 'rdf-object';
2+
import { PREFIX_OO } from '../../rdf/Iris';
3+
import type { IOverrideStep } from './IOverrideStep';
4+
import { extractOverrideStepFields, findResourceIndex, getPropertyResourceList } from './OverrideUtil';
5+
6+
/**
7+
* Override step that inserts elements in a list after a specific element.
8+
*
9+
* Uses the following override step fields:
10+
* - `overrideParameter`: Parameter of the original object that contains the list.
11+
* - `overrideTarget`: Element already in the list that is used as reference. This can be a named node or a literal.
12+
* - `overrideValue`: Element(s) to insert immediately after the target element.
13+
*/
14+
export class OverrideListInsertAfter implements IOverrideStep {
15+
public canHandle(config: Resource, step: Resource): boolean {
16+
return step.property.type.value === PREFIX_OO('OverrideListInsertAfter');
17+
}
18+
19+
public handle(config: Resource, step: Resource): Resource {
20+
const { parameters, targets, values } = extractOverrideStepFields(step, { parameters: 1, targets: 1 });
21+
22+
const list = getPropertyResourceList(config, parameters[0]);
23+
24+
const index = findResourceIndex(list, targets[0]);
25+
26+
// +1 so we start after the selected element
27+
list.splice(index + 1, 0, ...values);
28+
29+
return config;
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { Resource } from 'rdf-object';
2+
import { PREFIX_OO } from '../../rdf/Iris';
3+
import { ErrorResourcesContext } from '../../util/ErrorResourcesContext';
4+
import type { IOverrideStep } from './IOverrideStep';
5+
import { extractOverrideStepFields, getPropertyResourceList } from './OverrideUtil';
6+
7+
/**
8+
* Override step that inserts elements in a list at a specific index.
9+
* A negative index can be used to count from the back of the list.
10+
* An index of `-0` can be used to insert at the end of the list.
11+
*
12+
* Uses the following override step fields:
13+
* - `overrideParameter`: Parameter of the original object that contains the list.
14+
* - `overrideTarget`: A literal containing the index.
15+
* - `overrideValue`: Element(s) to insert at the chosen index.
16+
*/
17+
export class OverrideListInsertAt implements IOverrideStep {
18+
public canHandle(config: Resource, step: Resource): boolean {
19+
return step.property.type.value === PREFIX_OO('OverrideListInsertAt');
20+
}
21+
22+
public handle(config: Resource, step: Resource): Resource {
23+
const { parameters, targets, values } = extractOverrideStepFields(step, { parameters: 1, targets: 1 });
24+
25+
const list = getPropertyResourceList(config, parameters[0]);
26+
27+
const val = targets[0].value;
28+
if (!/^-?\d+$/u.test(val)) {
29+
throw new ErrorResourcesContext(`Invalid index in Override step OverrideListInsertAt for parameter ${parameters[0].value}: ${val}`, {
30+
config,
31+
step,
32+
});
33+
}
34+
35+
// Support adding elements at the end using -0
36+
if (val === '-0') {
37+
list.push(...values);
38+
} else {
39+
const index = Number.parseInt(val, 10);
40+
list.splice(index, 0, ...values);
41+
}
42+
43+
return config;
44+
}
45+
}

0 commit comments

Comments
 (0)