Skip to content

Commit ceeea28

Browse files
authored
Add support for overriding values
1 parent 43c631b commit ceeea28

15 files changed

+854
-40
lines changed

components/context.jsonld

+9
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,15 @@
7777
"undefined": {
7878
"@id": "oo:isUndefined"
7979
},
80+
"Override": {
81+
"@id": "oo:Override"
82+
},
83+
"overrideInstance": {
84+
"@id": "oo:overrideInstance"
85+
},
86+
"overrideParameters": {
87+
"@id": "oo:overrideParameters"
88+
},
8089
"ParameterRange": {
8190
"@id": "oo:ParameterRange"
8291
},

lib/construction/ConfigConstructorPool.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export class ConfigConstructorPool<Instance> implements IConfigConstructorPool<I
2121
private readonly configConstructor: ConfigConstructor<Instance>;
2222
private readonly constructionStrategy: IConstructionStrategy<Instance>;
2323

24-
private readonly instances: Record<string, Promise<any>> = {};
24+
private instances: Record<string, Promise<any>> = {};
2525

2626
public constructor(options: IInstancePoolOptions<Instance>) {
2727
this.configPreprocessors = options.configPreprocessors;
@@ -89,9 +89,12 @@ export class ConfigConstructorPool<Instance> implements IConfigConstructorPool<I
8989
for (const rawConfigFactory of this.configPreprocessors) {
9090
const handleResponse = rawConfigFactory.canHandle(config);
9191
if (handleResponse) {
92-
const rawConfig = rawConfigFactory.transform(config, handleResponse);
93-
this.validateRawConfig(rawConfig);
94-
return rawConfig;
92+
const { rawConfig, finishTransformation } = rawConfigFactory.transform(config, handleResponse);
93+
if (finishTransformation) {
94+
this.validateRawConfig(rawConfig);
95+
return rawConfig;
96+
}
97+
config = rawConfig;
9598
}
9699
}
97100

@@ -137,6 +140,16 @@ export class ConfigConstructorPool<Instance> implements IConfigConstructorPool<I
137140
public getInstanceRegistry(): Record<string, Promise<any>> {
138141
return this.instances;
139142
}
143+
144+
/**
145+
* Resets all preprocessors and clears the cached instances.
146+
*/
147+
public reset(): void {
148+
this.instances = {};
149+
for (const preprocessor of this.configPreprocessors) {
150+
preprocessor.reset();
151+
}
152+
}
140153
}
141154

142155
export interface IInstancePoolOptions<Instance> {

lib/construction/IConfigConstructorPool.ts

+5
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,9 @@ export interface IConfigConstructorPool<Instance> {
2222
*/
2323
getInstanceRegistry: () => Record<string, Promise<Instance>>;
2424

25+
/**
26+
* Resets any internal state to what it originally was.
27+
* Used when new components are added inbetween 2 instantiations.
28+
*/
29+
reset: () => void;
2530
}

lib/loading/ComponentsManagerBuilder.ts

+6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ConstructionStrategyCommonJs } from '../construction/strategy/Construct
1010
import type { IConstructionStrategy } from '../construction/strategy/IConstructionStrategy';
1111
import { ConfigPreprocessorComponent } from '../preprocess/ConfigPreprocessorComponent';
1212
import { ConfigPreprocessorComponentMapped } from '../preprocess/ConfigPreprocessorComponentMapped';
13+
import { ConfigPreprocessorOverride } from '../preprocess/ConfigPreprocessorOverride';
1314
import { ParameterHandler } from '../preprocess/ParameterHandler';
1415
import type { LogLevel } from '../util/LogLevel';
1516
import { ComponentRegistry } from './ComponentRegistry';
@@ -125,6 +126,11 @@ export class ComponentsManagerBuilder<Instance = any> {
125126
const configConstructorPool: IConfigConstructorPool<Instance> = new ConfigConstructorPool({
126127
objectLoader,
127128
configPreprocessors: [
129+
new ConfigPreprocessorOverride({
130+
objectLoader,
131+
componentResources,
132+
logger: this.logger,
133+
}),
128134
new ConfigPreprocessorComponentMapped({
129135
objectLoader,
130136
runTypeConfigs,

lib/preprocess/ConfigPreprocessorComponent.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Logger } from 'winston';
33
import { IRIS_OWL } from '../rdf/Iris';
44
import { ErrorResourcesContext } from '../util/ErrorResourcesContext';
55
import { GenericsContext } from './GenericsContext';
6-
import type { IConfigPreprocessor } from './IConfigPreprocessor';
6+
import type { IConfigPreprocessorTransform, IConfigPreprocessor } from './IConfigPreprocessor';
77
import type { ParameterHandler } from './ParameterHandler';
88

99
/**
@@ -74,7 +74,8 @@ export class ConfigPreprocessorComponent implements IConfigPreprocessor<ICompone
7474
}
7575
}
7676

77-
public transform(config: Resource, handleResponse: IComponentConfigPreprocessorHandleResponse): Resource {
77+
public transform(config: Resource, handleResponse: IComponentConfigPreprocessorHandleResponse):
78+
IConfigPreprocessorTransform {
7879
// Inherit parameter values
7980
this.inheritParameterValues(config, handleResponse.component);
8081

@@ -105,7 +106,7 @@ export class ConfigPreprocessorComponent implements IConfigPreprocessor<ICompone
105106
// Validate the input config
106107
this.validateConfig(config, handleResponse);
107108

108-
return configRaw;
109+
return { rawConfig: configRaw, finishTransformation: true };
109110
}
110111

111112
protected createGenericsContext(
@@ -301,6 +302,10 @@ export class ConfigPreprocessorComponent implements IConfigPreprocessor<ICompone
301302
}
302303
}
303304
}
305+
306+
public reset(): void {
307+
// There is nothing to reset here
308+
}
304309
}
305310

306311
export interface IComponentConfigPreprocessorOptions {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import type { Resource } from 'rdf-object';
2+
import type { RdfObjectLoader } from 'rdf-object/lib/RdfObjectLoader';
3+
import type { Logger } from 'winston';
4+
import { ErrorResourcesContext } from '../util/ErrorResourcesContext';
5+
import type { IConfigPreprocessor, IConfigPreprocessorTransform } from './IConfigPreprocessor';
6+
7+
/**
8+
* An {@link IConfigPreprocessor} that handles the overriding of parameters.
9+
* Values in the given {@link Resource}s will be replaced if any overriding object is found,
10+
* targeting this resource.
11+
*/
12+
export class ConfigPreprocessorOverride implements IConfigPreprocessor<Record<string, Resource>> {
13+
public readonly objectLoader: RdfObjectLoader;
14+
public readonly componentResources: Record<string, Resource>;
15+
public readonly logger: Logger;
16+
17+
private overrides: Record<string, Record<string, Resource>> | undefined;
18+
19+
public constructor(options: IComponentConfigPreprocessorOverrideOptions) {
20+
this.objectLoader = options.objectLoader;
21+
this.componentResources = options.componentResources;
22+
this.logger = options.logger;
23+
}
24+
25+
/**
26+
* Checks if there are any overrides targeting the given resource.
27+
* @param config - Resource to find overrides for.
28+
*
29+
* @returns A key/value object with keys being the properties that have an override.
30+
*/
31+
public canHandle(config: Resource): Record<string, Resource> | undefined {
32+
if (!this.overrides) {
33+
this.overrides = this.createOverrideObjects();
34+
}
35+
return this.overrides[config.value];
36+
}
37+
38+
/**
39+
* Override the resource with the stored values.
40+
* @param config - The resource to override.
41+
* @param handleResponse - Override values that were found for this resource.
42+
*/
43+
public transform(config: Resource, handleResponse: Record<string, Resource>): IConfigPreprocessorTransform {
44+
for (const id of Object.keys(config.properties)) {
45+
const overrideValue = handleResponse[id];
46+
if (overrideValue) {
47+
config.properties[id] = [ overrideValue ];
48+
}
49+
}
50+
return { rawConfig: config, finishTransformation: false };
51+
}
52+
53+
/**
54+
* Clear all cached overrides so they will be calculated again on the next call.
55+
*/
56+
public reset(): void {
57+
this.overrides = undefined;
58+
}
59+
60+
/**
61+
* Generates a cache of all overrides found in the object loader.
62+
* Keys of the object are the identifiers of the resources that need to be modified,
63+
* values are key/value maps listing all parameters with their new values.
64+
*/
65+
public createOverrideObjects(): Record<string, Record<string, Resource>> {
66+
const overrides = [ ...this.findOverrideTargets() ];
67+
const chains = this.createOverrideChains(overrides);
68+
this.validateChains(chains);
69+
const overrideObjects: Record<string, Record<string, Resource>> = {};
70+
for (const chain of chains) {
71+
const { target, values } = this.chainToOverrideObject(chain);
72+
if (Object.keys(values).length > 0) {
73+
overrideObjects[target] = values;
74+
}
75+
}
76+
return overrideObjects;
77+
}
78+
79+
/**
80+
* Finds all Override resources in the object loader and links them to their target resource.
81+
*/
82+
protected * findOverrideTargets(): Iterable<{ override: Resource; target: Resource }> {
83+
const overrideUri = this.objectLoader.contextResolved.expandTerm('oo:Override')!;
84+
const overrideInstanceUri = this.objectLoader.contextResolved.expandTerm('oo:overrideInstance')!;
85+
for (const [ id, resource ] of Object.entries(this.objectLoader.resources)) {
86+
if (resource.isA(overrideUri) && resource.value !== overrideUri) {
87+
const targets = resource.properties[overrideInstanceUri];
88+
if (!targets || targets.length === 0) {
89+
this.logger.warn(`Missing overrideInstance for ${id}. This Override will be ignored.`);
90+
continue;
91+
}
92+
if (targets.length > 1) {
93+
throw new ErrorResourcesContext(`Detected multiple overrideInstance targets for ${id}`, {
94+
override: resource,
95+
});
96+
}
97+
yield { override: resource, target: targets[0] };
98+
}
99+
}
100+
}
101+
102+
/**
103+
* Chains all Overrides together if they reference each other.
104+
* E.g., if the input is a list of Overrides A -> B, B -> C, D -> E,
105+
* the result wil be [[ A, B, C ], [ D, E ]].
106+
*
107+
* @param overrides - All Overrides that have to be combined.
108+
*/
109+
protected createOverrideChains(overrides: { override: Resource; target: Resource }[]): Resource[][] {
110+
// Start by creating small chains: from each override to its immediate target
111+
const overrideChains = Object.fromEntries(
112+
overrides.map(({ override, target }): [ string, Resource[]] =>
113+
[ override.value, [ override, target ]]),
114+
);
115+
116+
// Then keep combining those smaller chains into bigger chains until they are complete.
117+
// If there is an override cycle (A -> B -> ... -> A) it will delete itself from the list of chains here.
118+
let change = true;
119+
while (change) {
120+
change = false;
121+
for (const [ id, chain ] of Object.entries(overrideChains)) {
122+
let next = chain[chain.length - 1];
123+
// If the next part of the chain is found in `overrideChains` we can merge them and remove the tail entry
124+
while (overrideChains[next.value]) {
125+
change = true;
126+
const nextChain = overrideChains[next.value];
127+
// First element of nextChain will be equal to last element of this chain
128+
overrideChains[id].push(...nextChain.slice(1));
129+
// In case of a cycle there will be a point where next equals the first element,
130+
// at which point it will delete itself.
131+
delete overrideChains[next.value];
132+
next = chain[chain.length - 1];
133+
}
134+
// Reset the loop since we are modifying the object we are iterating over
135+
if (change) {
136+
break;
137+
}
138+
}
139+
}
140+
141+
return Object.values(overrideChains);
142+
}
143+
144+
/**
145+
* Throws an error in case there are 2 chains targeting the same resource.
146+
* @param chains - The override chains to check.
147+
*/
148+
protected validateChains(chains: Resource[][]): void {
149+
const targets = chains.map((chain): string => chain[chain.length - 1].value);
150+
for (let i = 0; i < targets.length; ++i) {
151+
const duplicateIdx = targets.findIndex((target, idx): boolean => idx > i && target === targets[i]);
152+
if (duplicateIdx > 0) {
153+
const target = chains[i][chains[i].length - 1];
154+
const duplicate1 = chains[i][chains[i].length - 2];
155+
const duplicate2 = chains[duplicateIdx][chains[duplicateIdx].length - 2];
156+
throw new ErrorResourcesContext(`Found multiple Overrides targeting ${targets[i]}`, {
157+
target,
158+
overrides: [ duplicate1, duplicate2 ],
159+
});
160+
}
161+
}
162+
}
163+
164+
/**
165+
* Merges all Overrides in a chain to create a single override object
166+
* containing replacement values for all relevant parameters of the final entry in the chain.
167+
*
168+
* @param chain - The chain of Overrides, with a normal resource as the last entry in the array.
169+
*/
170+
protected chainToOverrideObject(chain: Resource[]): { target: string; values: Record<string, Resource> } {
171+
const { target, type } = this.getChainTarget(chain);
172+
173+
// Apply all overrides sequentially, starting from the one closest to the target.
174+
// This ensures the most recent override has priority.
175+
const parameters = this.componentResources[type.value].properties.parameters;
176+
const mergedOverride: Record<string, Resource> = {};
177+
for (let i = chain.length - 2; i >= 0; --i) {
178+
const filteredObject = this.filterOverrideObject(chain[i], target, parameters);
179+
Object.assign(mergedOverride, filteredObject);
180+
}
181+
return { target: target.value, values: mergedOverride };
182+
}
183+
184+
/**
185+
* Finds the final target and its type in an override chain.
186+
* @param chain - The chain to find the target of.
187+
*/
188+
protected getChainTarget(chain: Resource[]): { target: Resource; type: Resource } {
189+
const rdfTypeUri = this.objectLoader.contextResolved.expandTerm('rdf:type')!;
190+
const target = chain[chain.length - 1];
191+
const types = target.properties[rdfTypeUri];
192+
if (!types || types.length === 0) {
193+
throw new ErrorResourcesContext(`Missing type for override target ${target.value} of Override ${chain[chain.length - 2].value}`, {
194+
target,
195+
override: chain[chain.length - 2],
196+
});
197+
}
198+
if (types.length > 1) {
199+
throw new ErrorResourcesContext(`Found multiple types for override target ${target.value} of Override ${chain[chain.length - 2].value}`, {
200+
target,
201+
override: chain[chain.length - 2],
202+
});
203+
}
204+
return { target, type: types[0] };
205+
}
206+
207+
/**
208+
* Extracts all relevant parameters of an Override with their corresponding new value.
209+
* @param override - The Override to apply.
210+
* @param target - The target resource to apply the Override to.
211+
* @param parameters - The parameters that are relevant for the target.
212+
*/
213+
protected filterOverrideObject(override: Resource, target: Resource, parameters: Resource[]):
214+
Record<string, Resource> {
215+
const overrideParametersUri = this.objectLoader.contextResolved.expandTerm('oo:overrideParameters')!;
216+
const overrideObjects = override.properties[overrideParametersUri];
217+
if (!overrideObjects || overrideObjects.length === 0) {
218+
this.logger.warn(`No overrideParameters found for ${override.value}.`);
219+
return {};
220+
}
221+
if (overrideObjects.length > 1) {
222+
throw new ErrorResourcesContext(`Detected multiple values for overrideParameters in Override ${override.value}`, {
223+
override,
224+
});
225+
}
226+
const overrideObject = overrideObjects[0];
227+
228+
// Only keep the parameters that are known to the type of the target object
229+
const filteredObject: Record<string, Resource> = {};
230+
for (const parameter of parameters) {
231+
const overrideValues = overrideObject.properties[parameter.value];
232+
if (!overrideValues || overrideValues.length === 0) {
233+
continue;
234+
}
235+
if (overrideValues.length > 1) {
236+
throw new ErrorResourcesContext(`Detected multiple values for override parameter ${parameter.value} in Override ${override.value}. RDF lists should be used for defining multiple values.`, {
237+
arguments: overrideValues,
238+
target,
239+
override,
240+
});
241+
}
242+
filteredObject[parameter.value] = overrideValues[0];
243+
}
244+
return filteredObject;
245+
}
246+
}
247+
248+
export interface IComponentConfigPreprocessorOverrideOptions {
249+
objectLoader: RdfObjectLoader;
250+
componentResources: Record<string, Resource>;
251+
logger: Logger;
252+
}

0 commit comments

Comments
 (0)