Skip to content

Commit 8d25ff4

Browse files
committed
Add hook system for plugins to interact
1 parent 9305664 commit 8d25ff4

File tree

7 files changed

+200
-19
lines changed

7 files changed

+200
-19
lines changed

packages/data-core/lib/context/plugin-config.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const PluginConfigContext = createContext<PluginConfigContextProps | null>(
1313

1414
/**
1515
* Global context provider for the configuration
16-
*
16+
*
1717
* @param props.config Configuration object
1818
* @param props.children Child components
1919
*/

packages/data-core/lib/plugin-utils/plugin.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,31 @@ export type PluginEditSchema =
77
| undefined
88
| symbol;
99

10+
export interface PluginHook {
11+
// Name of the target plugin.
12+
name: string;
13+
14+
// The `onAfterInit` hook is executed after the target plugin's `init`
15+
// function.
16+
onAfterInit?: (pluginInstance: Plugin, data: any) => void;
17+
18+
// The `onAfterEditSchema` hook is composed with the target plugin's
19+
// `editSchema` function.
20+
onAfterEditSchema?: (
21+
pluginInstance: Plugin,
22+
formData: any,
23+
schema: PluginEditSchema
24+
) => PluginEditSchema;
25+
}
26+
27+
const HIDDEN: unique symbol = Symbol('hidden');
28+
const HOOKS: unique symbol = Symbol('hooks');
29+
1030
export abstract class Plugin {
11-
static HIDDEN = Symbol('hidden');
31+
static readonly HIDDEN: typeof HIDDEN = HIDDEN;
32+
static readonly HOOKS: typeof HOOKS = HOOKS;
33+
34+
[HOOKS]: PluginHook[] = [];
1235

1336
name: string = 'Plugin';
1437

@@ -25,6 +48,17 @@ export abstract class Plugin {
2548
exitData(data: any): Record<string, any> {
2649
throw new Error(`Plugin [${this.name}] must implement exitData`);
2750
}
51+
52+
/**
53+
* Registers a hook to be applied on a given plugin.
54+
*
55+
* @param targetName - The name of the target plugin to which the hook will be
56+
* applied.
57+
* @param hooks - The hook details.
58+
*/
59+
registerHook(targetName: string, hooks: Omit<PluginHook, 'name'>) {
60+
this[Plugin.HOOKS].push({ name: targetName, ...hooks });
61+
}
2862
}
2963

3064
export type PluginConfigResolved = Plugin | Plugin[] | undefined | null;

packages/data-core/lib/plugin-utils/resolve.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
1+
import { cloneDeep } from 'lodash-es';
12
import { Plugin, PluginConfigItem } from './plugin';
23

4+
/**
5+
* Resolves the plugin config.
6+
*
7+
* @param plugins - An array of plugin configuration items which can be
8+
* instances of `Plugin` or functions that return a `Plugin`.
9+
* @param data - The data to be passed to plugin functions if they are not
10+
* instances of `Plugin`.
11+
* @returns An array of processed `Plugin` instances after applying hooks.
12+
*/
313
export const resolvePlugins = (plugins: PluginConfigItem[], data: any) => {
4-
return plugins
14+
const p = plugins
515
.flatMap((pl) => {
616
if (pl instanceof Plugin) {
717
return pl;
@@ -11,4 +21,84 @@ export const resolvePlugins = (plugins: PluginConfigItem[], data: any) => {
1121
return;
1222
})
1323
.filter((p) => p instanceof Plugin);
24+
25+
return applyHooks(p);
26+
};
27+
28+
/**
29+
* Applies hooks from the provided plugins to their respective targets.
30+
*
31+
* This function iterates over each plugin and applies hooks such as
32+
* `onAfterInit` and `onAfterEditSchema` to the corresponding target plugins.
33+
* The hooks are executed in the context of the source plugin.
34+
*
35+
* @param plugins - List of plugins. All the source and target plugins must be
36+
* on the list.
37+
*
38+
*
39+
* @remarks
40+
* - The `onAfterInit` hook is executed after the target plugin's `init`
41+
* function.
42+
* - The `onAfterEditSchema` hook is composed with the target plugin's
43+
* `editSchema` function.
44+
*
45+
* @example
46+
* ```typescript
47+
* class MyPlugin extends Plugin {
48+
* name = 'MyPlugin';
49+
*
50+
* [Plugin.HOOKS]: [
51+
* {
52+
* name: 'pluginA', // Target plugin
53+
* onAfterInit: async (target, data) => { }, // Executes after pluginA's init function.
54+
* onAfterEditSchema: (target, formData, origEditSchema) => { } // Composes with pluginA's editSchema function and returns a new one.
55+
* },
56+
* {
57+
* name: 'pluginB', // Target plugin
58+
* onAfterInit: async (target, data) => { }, // Executes after pluginB's init function.
59+
* }
60+
* ];
61+
* }
62+
*
63+
* applyHooks(plugins);
64+
* ```
65+
*/
66+
export const applyHooks = (plugins: Plugin[]) => {
67+
const pluginsCopy = cloneDeep(plugins);
68+
69+
for (const plSource of pluginsCopy) {
70+
for (const hook of plSource[Plugin.HOOKS]) {
71+
// Target where to apply the hook
72+
const plTarget = plugins.find((p) => p.name === hook.name);
73+
if (!plTarget) {
74+
continue;
75+
}
76+
77+
// The onAfterInit hook is made by executing one function after another.
78+
if (hook.onAfterInit) {
79+
const fn = hook.onAfterInit;
80+
const origInit = plTarget.init;
81+
plTarget.init = async (data: any) => {
82+
await origInit.call(plTarget, data);
83+
await fn.call(plSource, plTarget, data);
84+
};
85+
}
86+
87+
// The onAfterEditSchema hook is made by composing functions.
88+
if (hook.onAfterEditSchema) {
89+
const fn = hook.onAfterEditSchema;
90+
const origEditSchema = plTarget.editSchema;
91+
plTarget.editSchema = (formData?: any) => {
92+
return fn.call(
93+
plSource,
94+
plTarget,
95+
formData,
96+
origEditSchema.call(plTarget, formData)
97+
);
98+
};
99+
}
100+
}
101+
}
102+
103+
return pluginsCopy;
14104
};

packages/data-plugins/lib/collections/core.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,7 @@ export class PluginCore extends Plugin {
9999
allowOther: {
100100
type: 'string'
101101
},
102-
enum: [
103-
[
104-
'https://stac-extensions.github.io/item-assets/v1.0.0/schema.json',
105-
'Item Assets Defenition'
106-
],
107-
[
108-
'https://stac-extensions.github.io/render/v2.0.0/schema.json',
109-
'Render'
110-
]
111-
]
102+
enum: []
112103
}
113104
},
114105
spatial: {

packages/data-plugins/lib/collections/ext-item-assets.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
11
import { Plugin, PluginEditSchema } from '@stac-manager/data-core';
2-
import { array2Object, hasExtension, object2Array } from '../utils';
2+
import {
3+
addStacExtensionOption,
4+
array2Object,
5+
hasStacExtension,
6+
object2Array
7+
} from '../utils';
38

49
export class PluginItemAssets extends Plugin {
510
name = 'Item Assets Extension';
611

12+
constructor() {
13+
super();
14+
15+
addStacExtensionOption(
16+
this,
17+
'Item Assets Definition',
18+
'https://stac-extensions.github.io/item-assets/v1.0.0/schema.json'
19+
);
20+
}
21+
722
editSchema(data: any): PluginEditSchema {
8-
if (!hasExtension(data, 'item-assets')) {
23+
if (!hasStacExtension(data, 'item-assets')) {
924
return Plugin.HIDDEN;
1025
}
1126

packages/data-plugins/lib/collections/ext-render.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Plugin, PluginEditSchema } from '@stac-manager/data-core';
22
import {
3+
addStacExtensionOption,
34
array2Object,
4-
hasExtension,
5+
hasStacExtension,
56
object2Array,
67
object2Tuple,
78
tuple2Object
@@ -10,8 +11,18 @@ import {
1011
export class PluginRender extends Plugin {
1112
name = 'Render Extension';
1213

14+
constructor() {
15+
super();
16+
17+
addStacExtensionOption(
18+
this,
19+
'Render',
20+
'https://stac-extensions.github.io/render/v2.0.0/schema.json'
21+
);
22+
}
23+
1324
editSchema(data: any): PluginEditSchema {
14-
if (!hasExtension(data, 'render')) {
25+
if (!hasStacExtension(data, 'render')) {
1526
return Plugin.HIDDEN;
1627
}
1728

packages/data-plugins/lib/utils.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { SchemaField } from '@stac-manager/data-core';
1+
import {
2+
Plugin,
3+
PluginEditSchema,
4+
SchemaField,
5+
SchemaFieldArray,
6+
SchemaFieldString
7+
} from '@stac-manager/data-core';
8+
import { PluginCore } from './collections/core';
29

310
/**
411
*
@@ -182,7 +189,7 @@ export function tuple2Object(stack: string[][]) {
182189
* @returns A boolean indicating whether the specified extension (and version,
183190
* if provided) is present in the data.
184191
*/
185-
export function hasExtension(
192+
export function hasStacExtension(
186193
data: any,
187194
extension: string,
188195
version?: (v: string) => boolean
@@ -193,3 +200,36 @@ export function hasExtension(
193200
return match && (!version || version(match[1]));
194201
});
195202
}
203+
204+
/**
205+
* Adds a STAC extension option to the stac_extensions field of the
206+
* CollectionsCore plugin.
207+
*
208+
* @param $this - The plugin instance on which the hook will be registered. Must
209+
* be `this`.
210+
* @param label - The label for the new STAC extension option.
211+
* @param value - The value for the new STAC extension option.
212+
*/
213+
export function addStacExtensionOption(
214+
$this: Plugin,
215+
label: string,
216+
value: string
217+
) {
218+
// Quick way to get the name.
219+
const name = new PluginCore().name;
220+
$this.registerHook(name, {
221+
onAfterEditSchema: (pl, formData: any, schema: PluginEditSchema) => {
222+
if (!schema || typeof schema === 'symbol') {
223+
return schema;
224+
}
225+
226+
const stac_extensions = schema.properties
227+
.stac_extensions as SchemaFieldArray<SchemaFieldString>;
228+
229+
// Set the new extension value in the schema.
230+
stac_extensions.items.enum!.push([value, label]);
231+
232+
return schema;
233+
}
234+
});
235+
}

0 commit comments

Comments
 (0)