From cab121d667b1eaea65aba6ae1da6d97025c09686 Mon Sep 17 00:00:00 2001 From: Odin Thomas Rochmann Date: Wed, 22 May 2024 08:43:38 +0200 Subject: [PATCH 1/4] feat(modules): allow async config builder overrides Allow `_createConfig` method in `BaseConfigBuilder` to return `ObservableInput` instead of `Observable`. This enables more flexibility in how the config is created, as the method can return a Promise or other observable-like type. --- .../allow-async-config-builder-overrides.md | 29 +++++++++++++++++++ .../modules/module/src/BaseConfigBuilder.ts | 9 +++--- 2 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 .changeset/allow-async-config-builder-overrides.md diff --git a/.changeset/allow-async-config-builder-overrides.md b/.changeset/allow-async-config-builder-overrides.md new file mode 100644 index 000000000..172d915df --- /dev/null +++ b/.changeset/allow-async-config-builder-overrides.md @@ -0,0 +1,29 @@ +--- +"@equinor/modules": patch +--- + +## @equinor/modules + +### Changes to `BaseConfigBuilder` + +The `_createConfig` method in `BaseConfigBuilder` has been updated to return an `ObservableInput` instead of an `Observable`. +This allows for more flexibility in how the config is created, as the method can now return a Promise or other observable-like type. + +Additionally, the `_createConfig` method now uses `from()` to convert the result of `_buildConfig` to an observable stream. + +Here's an example of how the updated `_createConfig` method can be used: + +```typescript +protected _createConfig( + init: ConfigBuilderCallbackArgs, + initial?: Partial +): ObservableInput { + return from(this._buildConfig(init, initial)).pipe( + mergeMap((config) => this._processConfig(config, init)) + ); +} +``` + +This change allows for asynchronous operations to be performed within the `_buildConfig` method, which can then be processed in the `_processConfig` method. + +Consumers of the `BaseConfigBuilder` class should not need to update their code, as the public API remains the same. \ No newline at end of file diff --git a/packages/modules/module/src/BaseConfigBuilder.ts b/packages/modules/module/src/BaseConfigBuilder.ts index 009dbd62a..11cf9bd48 100644 --- a/packages/modules/module/src/BaseConfigBuilder.ts +++ b/packages/modules/module/src/BaseConfigBuilder.ts @@ -105,7 +105,7 @@ export abstract class BaseConfigBuilder, ): Observable { - return this._createConfig(init, initial); + return from(this._createConfig(init, initial)); } /** @@ -137,9 +137,8 @@ export abstract class BaseConfigBuilder, - ): Observable { - return this._buildConfig(init, initial).pipe( - mergeMap((config) => this._processConfig(config, init)), + ): ObservableInput { + return from(this._buildConfig(init, initial)).pipe( ); } @@ -149,7 +148,7 @@ export abstract class BaseConfigBuilder, - ): Observable> { + ): ObservableInput> { return from(Object.entries(this.#configCallbacks)).pipe( mergeMap(async ([target, cb]) => { const value = await cb(init); From a91a173e080a8069b14cf072f126e3837ad22d2a Mon Sep 17 00:00:00 2001 From: Odin Thomas Rochmann Date: Wed, 22 May 2024 09:43:26 +0200 Subject: [PATCH 2/4] feat(BaseConfigBuilder): add methods to retrieve and check config callbacks - Add `_get` method to retrieve the configuration callback for a given target path - Add `_has` method to check if a target path exists in the configuration callbacks --- .../improve-extendability-config-builder.md | 50 +++++++++++++++++++ .../modules/module/src/BaseConfigBuilder.ts | 23 +++++++++ 2 files changed, 73 insertions(+) create mode 100644 .changeset/improve-extendability-config-builder.md diff --git a/.changeset/improve-extendability-config-builder.md b/.changeset/improve-extendability-config-builder.md new file mode 100644 index 000000000..31533c3c3 --- /dev/null +++ b/.changeset/improve-extendability-config-builder.md @@ -0,0 +1,50 @@ +--- +"@equinor/modules": patch +--- + +## @equinor/modules + +### Changes to `BaseConfigBuilder` + +The `BaseConfigBuilder` class has been updated to improve its extendability and provide better access to the internal configuration callbacks. + +#### Added `_get` and `_has` methods + +Two new protected methods have been added to the `BaseConfigBuilder` class: + +1. `_get>(target: TTarget)`: This method retrieves the configuration callback for the specified target path in the configuration. It returns the callback or `undefined` if no callback is registered for the given target. + +2. `_has>(target: TTarget)`: This method checks if the given target path exists in the configuration callbacks. It returns `true` if the target path exists, `false` otherwise. + +These methods allow subclasses of `BaseConfigBuilder` to easily access and check the existence of configuration callbacks for specific targets. + +#### Example usage + +Suppose you have a subclass of `BaseConfigBuilder` called `MyConfigBuilder`. You can use the new `_get` and `_has` methods like this: + +```typescript +class MyConfigBuilder extends BaseConfigBuilder { + // override the _buildConfig method + async _createConfig( + init: ConfigBuilderCallbackArgs, + initial?: Partial, + ): ObservableInput { + // Check if a callback is registered for the'my.custom.config' target + if (this._has('my.custom.config')) { + // register a fallback value for the'my.custom.config' target if no callback is registered + this._set('my.custom.config', async() => { return 42; }); + } else { + // if a callback is registered, call it and log the result + configCallback = this._get('my.custom.config'); + configValue$ = from(configCallback(init, initial)); + console.log(await lastValueFrom(configValue$)); + } + return lastValueFrom(from(super._createConfig(init, initial))); + } +} +``` + +> [!WARNING] +> the example code is not intended to be a working implementation of the `MyConfigBuilder` class. It is only intended to demonstrate how the new `_get` and `_has` methods can be used. + +This change allows for more flexibility and easier extensibility of the `BaseConfigBuilder` class. diff --git a/packages/modules/module/src/BaseConfigBuilder.ts b/packages/modules/module/src/BaseConfigBuilder.ts index 11cf9bd48..940f948c3 100644 --- a/packages/modules/module/src/BaseConfigBuilder.ts +++ b/packages/modules/module/src/BaseConfigBuilder.ts @@ -131,6 +131,29 @@ export abstract class BaseConfigBuilder>( + target: TTarget, + ): ConfigBuilderCallback> | undefined { + return this.#configCallbacks[target] as ConfigBuilderCallback< + DotPathType + >; + } + + /** + * Checks if the given target path exists in the configuration callbacks. + * @param target - The target path to check. + * @returns `true` if the target path exists in the configuration callbacks, `false` otherwise. + */ + protected _has>(target: TTarget): boolean { + return target in this.#configCallbacks; + } + /** * @private internal creation of config */ From c8d4c7a54960e2c366697ed2bb4a2b854bafc478 Mon Sep 17 00:00:00 2001 From: Odin Thomas Rochmann Date: Wed, 22 May 2024 12:09:10 +0200 Subject: [PATCH 3/4] refactor(BaseConfigBuilder): enhance configuration builder - Improve documentation and examples for BaseConfigBuilder - Update type annotations and JSDoc comments for better type safety and clarity --- .changeset/improve-docs-baseconfig-builder.md | 20 ++ .../modules/module/src/BaseConfigBuilder.ts | 184 +++++++++++++++--- 2 files changed, 182 insertions(+), 22 deletions(-) create mode 100644 .changeset/improve-docs-baseconfig-builder.md diff --git a/.changeset/improve-docs-baseconfig-builder.md b/.changeset/improve-docs-baseconfig-builder.md new file mode 100644 index 000000000..17a694b99 --- /dev/null +++ b/.changeset/improve-docs-baseconfig-builder.md @@ -0,0 +1,20 @@ +--- +"@equinor/fusion-framework": patch +--- + +## @equinor/fusion-framework + +### Improved documentation for `BaseConfigBuilder` + +The `BaseConfigBuilder` class has been updated with improved documentation to better explain its usage and capabilities. + +#### What changed? + +The `BaseConfigBuilder` class is an abstract class that provides a flexible and extensible way to build and configure modules. It allows you to define configuration callbacks for different parts of your module's configuration, and then combine and process these callbacks to generate the final configuration object. + +The documentation has been expanded to include: + +1. A detailed explanation of how the `BaseConfigBuilder` class is designed to be used, including an example of creating a configuration builder for a hypothetical `MyModule` module. +2. Descriptions of the key methods and properties provided by the `BaseConfigBuilder` class, such as `createConfig`, `createConfigAsync`, `_set`, `_buildConfig`, and `_processConfig`. +3. Guidance on how to override the `_processConfig` method to add additional logic or validation to the configuration object before it is returned. +4. Examples of how to use the `BaseConfigBuilder` class to handle common configuration scenarios, such as setting default values or validating configuration properties. diff --git a/packages/modules/module/src/BaseConfigBuilder.ts b/packages/modules/module/src/BaseConfigBuilder.ts index 940f948c3..eb3947c7e 100644 --- a/packages/modules/module/src/BaseConfigBuilder.ts +++ b/packages/modules/module/src/BaseConfigBuilder.ts @@ -1,28 +1,51 @@ import { from, lastValueFrom, of, type Observable, type ObservableInput } from 'rxjs'; -import { mergeMap, reduce } from 'rxjs/operators'; +import { mergeMap, reduce, switchMap } from 'rxjs/operators'; import { Modules, ModuleType } from './types'; import { type DotPath, type DotPathType } from './utils/dot-path'; -/** helper function for extracting multilevel attribute keys */ +/** + * Recursively assigns a configuration value to a nested object property. + * + * This helper function is used to set a value in a nested object structure, creating + * intermediate objects as needed. It supports dot-separated property paths to access + * deeply nested properties. + * + * @param obj - The object to assign the value to. + * @param prop - The property path, either as a string with dot-separated parts or an array of property names. + * @param value - The value to assign. + * @returns The modified object. + */ const assignConfigValue = ( obj: Record, prop: string | string[], value: unknown, ): T => { + // Split the property path into individual parts const props = typeof prop === 'string' ? prop.split('.') : prop; + + // Get the first property in the path const attr = props.shift(); + + // If there is a property to process if (attr) { + // Create the nested object if it doesn't exist obj[attr] ??= {}; + + // If there are more properties in the path, recurse props.length ? assignConfigValue(obj[attr] as Record, props, value) : Object.assign(obj, { [attr]: value }); } + + // Return the modified object return obj as T; }; /** - * callback arguments for config builder callback function - * @template TRef parent instance + * Defines the arguments passed to a configuration builder callback function. + * + * @template TConfig - The type of the configuration object. + * @template TRef - The type of the reference or parent module. */ export type ConfigBuilderCallbackArgs = { config: TConfig; @@ -58,38 +81,89 @@ export type ConfigBuilderCallbackArgs = { /** * config builder callback function blueprint * @template TReturn expected return type of callback - * @returns either a sync value or an observable input (async) + * @param args - An object containing arguments that can be used to configure the `ConfigBuilder`. + * @returns The configured value, or an observable that emits the configured value. */ export type ConfigBuilderCallback = ( args: ConfigBuilderCallbackArgs, ) => TReturn | ObservableInput; /** - * template class for building module config + * The `BaseConfigBuilder` class is an abstract class that provides a flexible and extensible way to build and configure modules. + * It allows you to define configuration callbacks for different parts of your module's configuration, + * and then combine and process these callbacks to generate the final configuration object. + * + * The config builder will be the interface consumers of the module will use to configure the module. + * + * The config builder is designed to be used in the following way: * * @example - * ```ts + * Imagine you have a module called `MyModule` that requires a configuration object with the following structure: + * + * ```typescript * type MyModuleConfig = { - * foo: string; - * bar?: number, - * nested?: { up: boolean } + * foo: string; + * bar?: number; + * nested?: { up: boolean }; * }; + * ``` + * + * You can create a configuration builder for this module by extending the `BaseConfigBuilder` class: + * + * ```typescript + * import { BaseConfigBuilder, ConfigBuilderCallback } from '@equinor/fusion-framework'; * * class MyModuleConfigurator extends BaseConfigBuilder { - * public setFoo(cb: ModuleConfigCallback) { - * this._set('foo', cb); + * public setFoo(cb: ConfigBuilderCallback) { + * this._set('foo', cb); * } * - * public setBar(cb: ModuleConfigCallback) { + * public setBar(cb: ConfigBuilderCallback) { * this._set('bar', cb); * } * - * public setUp(cb: ModuleConfigCallback) { + * public setUp(cb: ConfigBuilderCallback) { * this._set('nested.up', cb); * } * } * ``` - * @template TConfig expected config the builder will create + * + * In this example, we define three methods (`setFoo`, `setBar`, and `setUp`) that allow us to set configuration callbacks for different parts of the `MyModuleConfig` object. + * These methods use the `_set` method provided by the `BaseConfigBuilder` class to register the callbacks. + * + * To create the final configuration object, you can use the `createConfig` or `createConfigAsync` methods provided by the `BaseConfigBuilder` class: + * + * ```typescript + * import { configure } from './configure'; + * + * const configurator = new MyModuleConfigurator(); + * const config = await configurator.createConfigAsync(configure); + * ``` + * + * The `configure` function is where you define the actual configuration callbacks. For example: + * + * ```typescript + * import type { ModuleInitializerArgs } from '@equinor/fusion-framework'; + * + * export const configure: ModuleInitializerArgs = (configurator) => { + * configurator.setFoo(async () => 'https://foo.bar'); + * configurator.setBar(() => 69); + * configurator.setUp(() => true); + * }; + * ``` + * + * In this example, we define the configuration callbacks for the `foo`, `bar`, and `nested.up` properties of the `MyModuleConfig` object. + * + * The `BaseConfigBuilder` class provides several methods and properties to help you build and process the configuration object: + * + * - `createConfig`: Returns an observable that emits the final configuration object. + * - `createConfigAsync`: Returns a promise that resolves with the final configuration object. + * - `_set`: Registers a configuration callback for a specific target path in the configuration object. + * - `_buildConfig`: Builds the configuration object by executing all registered configuration callbacks and merging the results. + * - `_processConfig`: Allows you to perform post-processing on the built configuration object before returning it. + * + * You can override the `_processConfig` method to add additional logic or validation to the configuration object before it is returned. + * */ export abstract class BaseConfigBuilder> { /** internal hashmap of registered callback functions */ @@ -99,7 +173,7 @@ export abstract class BaseConfigBuilder>( target: TTarget, @@ -155,30 +236,69 @@ export abstract class BaseConfigBuilder { + * if(!args.hasModule('some_module')){ + * throw Error(`'some_module' is not configured`); + * } + * const someModule = await arg.requireInstance('some_module'); + * return someModule.doSomething(); + * }); + * } + * super._createConfig(init, initial); + * } + * ``` + * + * @param init - The configuration builder callback arguments, which include the module context and other relevant data. + * @param initial - An optional partial configuration object to use as the initial base for the configuration. + * @returns An observable that emits the processed configuration. */ protected _createConfig( init: ConfigBuilderCallbackArgs, initial?: Partial, ): ObservableInput { + // Build the initial configuration and then process it return from(this._buildConfig(init, initial)).pipe( + // Process the built configuration with the provided initialization arguments + switchMap((config) => this._processConfig(config, init)), ); } /** - * @private internal builder + * Builds the configuration object by executing all registered configuration callbacks and merging the results. + * + * @note overriding this method is not recommended, use {@link BaseConfigBuilder._createConfig} instead. + * - use {@link BaseConfigBuilder._createConfig} to add custom initialization logic before building the configuration. + * - use {@link BaseConfigBuilder._processConfig} to validate and post-process the configuration. + * + * + * @param init - The initialization arguments passed to the configuration callbacks. + * @param initial - An optional partial configuration object to use as the initial state. + * @returns An observable that emits the final configuration object. */ protected _buildConfig( init: ConfigBuilderCallbackArgs, initial?: Partial, ): ObservableInput> { return from(Object.entries(this.#configCallbacks)).pipe( + // Transform each config callback into a target-value pair mergeMap(async ([target, cb]) => { + // Execute callback with init and await result const value = await cb(init); + // Return target-value pair return { target, value }; }), + // Reduce the target-value pairs into a single configuration object reduce( + // Assign each value to the corresponding target in the accumulator (acc, { target, value }) => assignConfigValue(acc, target, value), + // Initialize accumulator with initial config or empty object initial ?? ({} as TConfig), ), ); @@ -190,6 +310,26 @@ export abstract class BaseConfigBuilder 100) { + * throw Error(`'foo' is too large`); + * } + * return config; + * } + * ``` + * + * @param config - The partial configuration object to process. + * @param _init - Additional configuration arguments (not used in this implementation). + * @returns An observable input that emits the processed configuration object. */ protected _processConfig( config: Partial, From 7f86a268f80138139d9ac0609524f00234bbeba3 Mon Sep 17 00:00:00 2001 From: Odin Thomas Rochmann Date: Wed, 22 May 2024 14:03:22 +0200 Subject: [PATCH 4/4] refactor(BaseConfigBuilder): add sealed and access modifiers - Add `@sealed` decorator to public methods in `BaseConfigBuilder` - Change access modifiers of some methods to `@protected` --- packages/modules/module/src/BaseConfigBuilder.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/modules/module/src/BaseConfigBuilder.ts b/packages/modules/module/src/BaseConfigBuilder.ts index eb3947c7e..137fab127 100644 --- a/packages/modules/module/src/BaseConfigBuilder.ts +++ b/packages/modules/module/src/BaseConfigBuilder.ts @@ -174,6 +174,7 @@ export abstract class BaseConfigBuilder>( target: TTarget, @@ -217,6 +221,8 @@ export abstract class BaseConfigBuilder>( target: TTarget, @@ -230,6 +236,8 @@ export abstract class BaseConfigBuilder>(target: TTarget): boolean { return target in this.#configCallbacks; @@ -258,6 +266,7 @@ export abstract class BaseConfigBuilder,