Hone logo
Hone
Problems

Advanced Typescript Metaprogramming: Dynamic Plugin System

This challenge focuses on building a foundational metaprogramming framework in Typescript to create a dynamic plugin system. The goal is to define types that allow you to register plugins with specific configurations at runtime, ensuring type safety while maintaining flexibility. This is useful for building extensible applications where functionality can be added or modified without recompilation.

Problem Description

You need to create a Typescript framework that allows you to define and register plugins. Each plugin should have a unique identifier, a configuration object of a specific type, and a function that performs some action. The framework should ensure that the configuration provided to a plugin matches the plugin's expected configuration type.

Key Requirements:

  1. Plugin<TConfig> Type: Define a generic type Plugin<TConfig> that represents a plugin. It should have the following properties:

    • id: A string representing the plugin's unique identifier.
    • config: A type alias TConfig representing the expected configuration for the plugin.
    • execute: A function that takes the configuration TConfig as input and returns a value of any type (unknown).
  2. PluginRegistry Type: Define a type PluginRegistry which is a record (object) where keys are plugin IDs (strings) and values are Plugin<TConfig> instances.

  3. registerPlugin<TConfig>(plugin: Plugin<TConfig>, id: string): PluginRegistry Function: Create a function registerPlugin that takes a Plugin<TConfig> and its ID as input and adds it to a PluginRegistry. The function should return the updated PluginRegistry. The function should throw an error if a plugin with the same ID already exists in the registry.

  4. getPlugin<TConfig>(registry: PluginRegistry, id: string): Plugin<TConfig> | undefined Function: Create a function getPlugin that takes a PluginRegistry and a plugin ID as input and returns the plugin associated with that ID, or undefined if no such plugin exists.

  5. Configuration Type Enforcement: The registerPlugin function must enforce that the configuration provided when executing a plugin matches the plugin's declared configuration type.

Expected Behavior:

  • Plugins should be registered with unique IDs.
  • The execute function of a plugin should be called with the correct configuration type.
  • Attempting to register a plugin with a duplicate ID should result in an error.
  • Retrieving a non-existent plugin should return undefined.

Edge Cases to Consider:

  • Empty plugin registry.
  • Plugin with an empty configuration type (TConfig is never).
  • Plugin ID is an empty string.
  • Plugin ID is null or undefined (should be handled gracefully, likely by throwing an error).

Examples

Example 1:

interface MyConfig {
  name: string;
  age: number;
}

const plugin1: Plugin<MyConfig> = {
  id: 'myPlugin',
  config: MyConfig,
  execute: (config) => `Hello, ${config.name}! You are ${config.age} years old.`
};

const registry: PluginRegistry = {};

const updatedRegistry = registerPlugin(plugin1, 'myPlugin');

const retrievedPlugin = getPlugin(updatedRegistry, 'myPlugin');

// Expected Output:
// retrievedPlugin?.execute({ name: "Alice", age: 30 }) === "Hello, Alice! You are 30 years old."

Example 2:

interface AnotherConfig {
  enabled: boolean;
}

const plugin2: Plugin<AnotherConfig> = {
  id: 'anotherPlugin',
  config: AnotherConfig,
  execute: (config) => `Plugin enabled: ${config.enabled}`
};

const registry2: PluginRegistry = {
  'myPlugin': plugin1
};

const updatedRegistry2 = registerPlugin(plugin2, 'anotherPlugin');

const retrievedPlugin2 = getPlugin(updatedRegistry2, 'anotherPlugin');

// Expected Output:
// retrievedPlugin2?.execute({ enabled: true }) === "Plugin enabled: true"

Example 3: (Error Handling)

interface Config1 {
  setting1: string;
}

const plugin3: Plugin<Config1> = {
  id: 'duplicatePlugin',
  config: Config1,
  execute: (config) => `Executing with ${config.setting1}`
};

const registry3: PluginRegistry = {
  'myPlugin': plugin1
};

try {
  const updatedRegistry3 = registerPlugin(plugin3, 'myPlugin'); // Should throw an error
} catch (error) {
  // Expected Output: Error message indicating duplicate plugin ID
  console.error(error.message);
}

Constraints

  • All code must be written in Typescript.
  • The solution should be well-structured and readable.
  • The PluginRegistry should be a standard Javascript object (record).
  • Error handling should be robust and informative.
  • The solution should be reasonably performant (avoid unnecessary iterations or complex operations).

Notes

  • Consider using conditional types and mapped types to make the framework more generic and reusable.
  • Think about how to handle potential errors during plugin execution (e.g., invalid configuration values).
  • This is a foundational framework; you can extend it with features like plugin loading, dependency injection, and lifecycle management.
  • Focus on type safety and ensuring that the configuration provided to each plugin matches its expected type. This is the core of the challenge.
Loading editor...
typescript