-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathConfigPreprocessorOverride.ts
More file actions
274 lines (251 loc) · 10.4 KB
/
ConfigPreprocessorOverride.ts
File metadata and controls
274 lines (251 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
import type { Resource } from 'rdf-object';
import type { RdfObjectLoader } from 'rdf-object/lib/RdfObjectLoader';
import type { Logger } from 'winston';
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<Resource[]> {
public readonly objectLoader: RdfObjectLoader;
public readonly componentResources: Record<string, Resource>;
public readonly logger: Logger;
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 = [
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 list of override steps to apply to the target, in order.
*/
public canHandle(config: Resource): Resource[] | undefined {
if (!this.overrides) {
this.overrides = this.createOverrideSteps();
}
return this.overrides[config.value];
}
/**
* Override the resource with the stored override steps.
* @param config - The resource to override.
* @param handleResponse - Override steps that were found for this resource.
*/
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;
}
}
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 };
}
/**
* Clear all cached overrides, so they will be calculated again on the next call.
*/
public reset(): void {
this.overrides = undefined;
}
/**
* Generates a cache of all overrides found in the object loader.
* 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 createOverrideSteps(): Record<string, Resource[]> {
const overrides = [ ...this.findOverrideTargets() ];
const chains = this.createOverrideChains(overrides);
this.validateChains(chains);
const overrideSteps: Record<string, Resource[]> = {};
for (const chain of chains) {
const { target, steps } = this.chainToOverrideSteps(chain);
if (Object.keys(steps).length > 0) {
overrideSteps[target.value] = steps;
}
}
return overrideSteps;
}
/**
* Finds all Override resources in the object loader and links them to their target resource.
*/
protected * findOverrideTargets(): Iterable<{ override: Resource; target: Resource }> {
for (const [ id, resource ] of Object.entries(this.objectLoader.resources)) {
if (resource.isA(IRIS_OO.Override) && resource.value !== IRIS_OO.Override) {
const targets = resource.properties[IRIS_OO.overrideInstance];
if (!targets || targets.length === 0) {
this.logger.warn(`Missing overrideInstance for ${id}. This Override will be ignored.`);
continue;
}
if (targets.length > 1) {
throw new ErrorResourcesContext(`Detected multiple overrideInstance targets for ${id}`, {
override: resource,
});
}
yield { override: resource, target: targets[0] };
}
}
}
/**
* 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.
*/
protected createOverrideChains(overrides: { override: Resource; target: Resource }[]): Resource[][] {
// Start by creating small chains: from each override to its immediate target
const overrideChains = Object.fromEntries(
overrides.map(({ override, target }): [ string, Resource[]] =>
[ override.value, [ override, target ]]),
);
// Then keep combining those smaller chains into bigger chains until they are complete.
// If there is an override cycle (A -> B -> ... -> A) it will delete itself from the list of chains here.
let change = true;
while (change) {
change = false;
for (const [ id, chain ] of Object.entries(overrideChains)) {
let next = chain.at(-1)!;
// If the next part of the chain is found in `overrideChains` we can merge them and remove the tail entry
while (overrideChains[next.value]) {
change = true;
const nextChain = overrideChains[next.value];
// First element of nextChain will be equal to last element of this chain
overrideChains[id].push(...nextChain.slice(1));
// In case of a cycle there will be a point where next equals the first element,
// at which point it will delete itself.
delete overrideChains[next.value];
next = chain.at(-1)!;
}
// Reset the loop since we are modifying the object we are iterating over
if (change) {
break;
}
}
}
return Object.values(overrideChains);
}
/**
* Throws an error in case there are 2 chains targeting the same resource.
* @param chains - The override chains to check.
*/
protected validateChains(chains: Resource[][]): void {
const targets = chains.map((chain): string => chain.at(-1)!.value);
for (let i = 0; i < targets.length; ++i) {
const duplicateIdx = targets.findIndex((target, idx): boolean => idx > i && target === targets[i]);
if (duplicateIdx > 0) {
const target = chains[i][chains[i].length - 1];
const duplicate1 = chains[i][chains[i].length - 2];
const duplicate2 = chains[duplicateIdx][chains[duplicateIdx].length - 2];
throw new ErrorResourcesContext(`Found multiple Overrides targeting ${targets[i]}`, {
target,
overrides: [ duplicate1, duplicate2 ],
});
}
}
}
/**
* 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 chainToOverrideSteps(chain: Resource[]): { target: Resource; steps: Resource[] } {
const target = this.getChainTarget(chain);
const steps: Resource[] = [];
for (let i = chain.length - 2; i >= 0; --i) {
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, steps };
}
/**
* Finds the final target and validates its type value.
* @param chain - The chain to find the target of.
*/
protected getChainTarget(chain: Resource[]): Resource {
const target = chain.at(-1)!;
const types = uniqueTypes(target, this.componentResources);
if (!types || types.length === 0) {
throw new ErrorResourcesContext(`Missing type for override target ${target.value} of Override ${chain.at(-2)!.value}`, {
target,
override: chain.at(-2),
});
}
if (types.length > 1) {
throw new ErrorResourcesContext(`Found multiple types for override target ${target.value} of Override ${chain.at(-2)!.value}`, {
target,
override: chain.at(-2),
});
}
return target;
}
/**
*
* @param override
* @protected
*/
protected simplifiedOverrideToStep(override: Resource): Resource {
const overrideObjects = override.properties[IRIS_OO.overrideParameters];
if (overrideObjects.length > 1) {
throw new ErrorResourcesContext(`Detected multiple values for overrideParameters in Override ${override.value}`, {
override,
});
}
return this.objectLoader.createCompactedResource({
types: PREFIX_OO('OverrideParameters'),
overrideValue: overrideObjects[0],
});
}
}
export interface IComponentConfigPreprocessorOverrideOptions {
objectLoader: RdfObjectLoader;
componentResources: Record<string, Resource>;
logger: Logger;
}