import path from 'node:path' import fs from 'node:fs/promises' import { GeneratorDirectiveInterface } from '../model/index.js' import { LoggerInterface, LoggerMemory } from '../logger/index.js' import { DataGeneratorGenerateRequest, DataGeneratorInterface } from './DataGeneratorInterface.js' import { DataGeneratorRegistry } from './DataGeneratorRegistry.js' /** * Represents the structure of the persistent store for a data generator. */ export interface DataGeneratorStore { /** Array of values that must remain unique */ uniqueSet: any[] // eslint-disable-line @typescript-eslint/no-explicit-any /** Array of instance-specific generated data */ instanceData: any[] // eslint-disable-line @typescript-eslint/no-explicit-any } /** * Options for configuring a Data Generator. */ export interface DataGeneratorOptions { /** The registry that holds all available generators */ generatorRegistry: DataGeneratorRegistry /** The unique name assigned when this generator is registered in the service registry */ name: string /** The logger instance; if not provided, LoggerMemory will be used */ logger?: LoggerInterface /** * Indicates whether the data generator should produce unique values. * The definition of uniqueness is determined by the generator itself. */ unique?: boolean /** The maximum number of attempts to generate a unique value before throwing an error */ maxUniqueTries?: number /** Directory path for storing unique data persistently */ varDir?: string /** Determines if the generator should use persistent storage */ useStore?: boolean /** The name for the data store associated with this generator */ storeName?: string } /** * Base implementation of the Data Generator interface. * * This class provides default functionalities including: * - Loading and saving generated data from/to a persistent store. * - Managing unique data sets and instance-specific data using an instance identifier. * * When the generator is invoked with the same instance ID, it is expected to return the previously generated data. */ export class DataGeneratorBase implements DataGeneratorInterface { /** The registry containing all available data generators */ generatorRegistry: DataGeneratorRegistry /** The unique name assigned to this generator */ name: string /** Logger instance used for logging; defaults to LoggerMemory if not provided */ logger: LoggerInterface /** * Flag indicating whether the generator should produce unique values. * The definition of uniqueness depends on the generator's implementation. */ unique: boolean /** Maximum number of attempts for generating a unique value before throwing an error */ maxUniqueTries: number /** Directory path used for storing persistent generator data */ varDir: string /** Indicates whether the generator should use persistent storage */ useStore: boolean /** Name for the persistent data store; defaults to the generator name if not explicitly set */ storeName: string /** Set to track unique values generated by this generator */ uniqueSet = new Set() // eslint-disable-line @typescript-eslint/no-explicit-any /** Map to store generated data associated with each instance ID */ instanceData = new Map() // eslint-disable-line @typescript-eslint/no-explicit-any /** Internal store object for persisting uniqueSet and instanceData */ store: DataGeneratorStore = { uniqueSet: [], instanceData: [] } /** * Constructs a new DataGeneratorBase instance. * * @param opts - Configuration options for the data generator. * The service registry may be left empty if the generator does not require access to other generators. */ constructor(opts: DataGeneratorOptions) { this.generatorRegistry = opts.generatorRegistry this.name = opts.name this.logger = opts.logger ? opts.logger : new LoggerMemory() this.unique = opts.unique !== undefined ? opts.unique : false this.maxUniqueTries = opts.maxUniqueTries ? opts.maxUniqueTries : 20 this.varDir = opts.varDir ? opts.varDir : 'var' this.useStore = opts.useStore !== undefined ? opts.useStore : false this.storeName = opts.storeName ? opts.storeName : opts.name } /** * Returns the full file path for the persistent store file. */ get storeFileName(): string { return path.join(this.varDir, `${this.storeName}.json`) } /** * Retrieves a registered data generator by its name. * * @param generatorName - The unique name of the registered data generator. * @returns The corresponding data generator. * @throws Error if the generator with the given name is not registered. */ public getGenerator(generatorName: string) { const gen = this.generatorRegistry.getGenerator(generatorName) if (gen === undefined) { throw new Error( `The generator with the name '${generatorName}' was not registered in the registry` ) } return gen } /** * Clears the current generator context. * * This method resets the unique value set and instance-specific data map, * effectively clearing any previously generated data. */ public clearContext() { this.uniqueSet = new Set() // eslint-disable-line @typescript-eslint/no-explicit-any this.instanceData = new Map() // eslint-disable-line @typescript-eslint/no-explicit-any } /** * Loads persisted generator data from the storage file. * * If persistent storage is enabled (`useStore` is true), this method: * - Ensures the storage directory exists. * - Checks for an existing store file and loads data from it. * - Resets the current context and populates it with stored values. */ public async loadStore() { if (this.useStore) { await fs.mkdir(this.varDir, { recursive: true }) try { await fs.access(this.storeFileName, fs.constants.F_OK) // File exists; load the stored data. this.store = JSON.parse(await fs.readFile(this.storeFileName, 'utf8')) // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // No existing store file; initialize with empty data. this.store = { uniqueSet: [], instanceData: [] } } // Populate uniqueSet and instanceData from the loaded store. this.clearContext() this.uniqueSet = new Set(this.store.uniqueSet) this.instanceData = new Map(this.store.instanceData) // Reset the internal store. this.store = { uniqueSet: [], instanceData: [] } } } /** * Saves the current generator data to the persistent store. * * If persistent storage is enabled (`useStore` is true), this method: * - Converts the current unique set and instance data to arrays. * - Ensures the storage directory exists. * - Writes the data to the designated store file in JSON format. */ public async saveStore() { if (this.useStore) { // Convert the current context to an object for storage. this.store.uniqueSet = Array.from(this.uniqueSet) this.store.instanceData = Array.from(this.instanceData) await fs.mkdir(this.varDir, { recursive: true }) if (Object.keys(this.store).length > 0) { const storeDataRaw = JSON.stringify(this.store, null, 2) await fs.writeFile(this.storeFileName, storeDataRaw) } } } /** * Returns the data structure that would be written to the persistent store. * * @returns An object containing arrays representing the unique set and instance data. */ public getStoreData() { return { uniqueSet: Array.from(this.uniqueSet), instanceData: Array.from(this.instanceData) } } /** * Generates data based on the provided request and caches it per instance. * * If data for the given instance ID already exists, it returns the cached data. * Otherwise, it calls the generator-specific `doGenerate()` method to produce new data. * * @param request - The generation request parameters, as defined by DataGeneratorGenerateRequest. * @returns The generated data, or `undefined` if generation could not be completed. * The generator should return `undefined` if required referenced data is not yet available. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any public async generate(request: DataGeneratorGenerateRequest): Promise { const { instanceId, testcaseData, generatorDirective } = request if (instanceId && this.instanceData.has(instanceId)) { return this.instanceData.get(instanceId) } try { const genData = await this.doGenerate(request) if (genData !== undefined && instanceId) { this.instanceData.set(instanceId, genData) } return genData } catch (err) { const testcaseName = testcaseData ? testcaseData.name : undefined const tableName = generatorDirective ? generatorDirective.testcaseMeta.tableName : 'unknown' const fieldName = generatorDirective ? generatorDirective.fieldName : 'unknown' let message: string = `Error in generating data` let stack: string | undefined if (err instanceof Error) { message = err.message stack = err.stack } this.logger.error({ message, function: 'generate', testcaseName, tableName, fieldName, generatorName: this.name, stack }) } } /** * Creates post-processing directives for the generated data. * * Some generators may require additional processing after the main data generation, * for example, to resolve dependencies on data produced by other generators. * * @param request - The generation request parameters, as defined by DataGeneratorGenerateRequest. * @returns A list of post-process directives, or `undefined` if no directives are generated. */ // eslint-disable-next-line require-await public async createPostProcessDirectives( request: DataGeneratorGenerateRequest // eslint-disable-line @typescript-eslint/no-unused-vars ): Promise { return } /** * Performs post-processing after all generators have completed their data generation. * * This method is invoked after the `generate()` method of all generators has been called. * It does not return any new data but may directly update existing data if necessary. * * @param request - The generation request parameters, as defined by DataGeneratorGenerateRequest. * @returns A list of post-process directives, or `undefined` if no directives are generated. */ // eslint-disable-next-line require-await public async postProcess( request: DataGeneratorGenerateRequest // eslint-disable-line @typescript-eslint/no-unused-vars ): Promise { return } /** * Generates data based on the provided request. * * This protected method should be overridden by subclasses to implement specific data generation logic. * It must not modify the test case data directly. * * @param request - The generation request parameters, as defined by DataGeneratorGenerateRequest. * @returns The generated data. */ protected async doGenerate( request: DataGeneratorGenerateRequest // eslint-disable-line @typescript-eslint/no-unused-vars ): Promise {} // eslint-disable-line @typescript-eslint/no-explicit-any }