diff --git a/examples/access-protection/jitar.json b/examples/access-protection/jitar.json index 0686e62b..efbc65d9 100644 --- a/examples/access-protection/jitar.json +++ b/examples/access-protection/jitar.json @@ -1,4 +1,4 @@ { - "source": "./dist", - "target": "./build" + "source": "./build", + "target": "./dist" } \ No newline at end of file diff --git a/examples/access-protection/tsconfig.json b/examples/access-protection/tsconfig.json index db3953f7..64bab47f 100644 --- a/examples/access-protection/tsconfig.json +++ b/examples/access-protection/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "rootDir": "./src", - "outDir": "./dist" + "outDir": "./build" }, "include": ["./src"] } \ No newline at end of file diff --git a/examples/data-transportation/jitar.json b/examples/data-transportation/jitar.json index 0686e62b..efbc65d9 100644 --- a/examples/data-transportation/jitar.json +++ b/examples/data-transportation/jitar.json @@ -1,4 +1,4 @@ { - "source": "./dist", - "target": "./build" + "source": "./build", + "target": "./dist" } \ No newline at end of file diff --git a/examples/data-transportation/tsconfig.json b/examples/data-transportation/tsconfig.json index db3953f7..64bab47f 100644 --- a/examples/data-transportation/tsconfig.json +++ b/examples/data-transportation/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "rootDir": "./src", - "outDir": "./dist" + "outDir": "./build" }, "include": ["./src"] } \ No newline at end of file diff --git a/examples/error-handling/jitar.json b/examples/error-handling/jitar.json index 0686e62b..efbc65d9 100644 --- a/examples/error-handling/jitar.json +++ b/examples/error-handling/jitar.json @@ -1,4 +1,4 @@ { - "source": "./dist", - "target": "./build" + "source": "./build", + "target": "./dist" } \ No newline at end of file diff --git a/examples/error-handling/tsconfig.json b/examples/error-handling/tsconfig.json index db3953f7..64bab47f 100644 --- a/examples/error-handling/tsconfig.json +++ b/examples/error-handling/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "rootDir": "./src", - "outDir": "./dist" + "outDir": "./build" }, "include": ["./src"] } \ No newline at end of file diff --git a/examples/health-checks/jitar.json b/examples/health-checks/jitar.json index 0686e62b..efbc65d9 100644 --- a/examples/health-checks/jitar.json +++ b/examples/health-checks/jitar.json @@ -1,4 +1,4 @@ { - "source": "./dist", - "target": "./build" + "source": "./build", + "target": "./dist" } \ No newline at end of file diff --git a/examples/health-checks/tsconfig.json b/examples/health-checks/tsconfig.json index db3953f7..64bab47f 100644 --- a/examples/health-checks/tsconfig.json +++ b/examples/health-checks/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "rootDir": "./src", - "outDir": "./dist" + "outDir": "./build" }, "include": ["./src"] } \ No newline at end of file diff --git a/examples/hello-world/jitar.json b/examples/hello-world/jitar.json index 0686e62b..efbc65d9 100644 --- a/examples/hello-world/jitar.json +++ b/examples/hello-world/jitar.json @@ -1,4 +1,4 @@ { - "source": "./dist", - "target": "./build" + "source": "./build", + "target": "./dist" } \ No newline at end of file diff --git a/examples/hello-world/tsconfig.json b/examples/hello-world/tsconfig.json index db3953f7..64bab47f 100644 --- a/examples/hello-world/tsconfig.json +++ b/examples/hello-world/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "rootDir": "./src", - "outDir": "./dist" + "outDir": "./build" }, "include": ["./src"] } \ No newline at end of file diff --git a/examples/load-balancing/jitar.json b/examples/load-balancing/jitar.json index 0686e62b..efbc65d9 100644 --- a/examples/load-balancing/jitar.json +++ b/examples/load-balancing/jitar.json @@ -1,4 +1,4 @@ { - "source": "./dist", - "target": "./build" + "source": "./build", + "target": "./dist" } \ No newline at end of file diff --git a/examples/load-balancing/tsconfig.json b/examples/load-balancing/tsconfig.json index db3953f7..64bab47f 100644 --- a/examples/load-balancing/tsconfig.json +++ b/examples/load-balancing/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "rootDir": "./src", - "outDir": "./dist" + "outDir": "./build" }, "include": ["./src"] } \ No newline at end of file diff --git a/examples/middleware/jitar.json b/examples/middleware/jitar.json index 0686e62b..efbc65d9 100644 --- a/examples/middleware/jitar.json +++ b/examples/middleware/jitar.json @@ -1,4 +1,4 @@ { - "source": "./dist", - "target": "./build" + "source": "./build", + "target": "./dist" } \ No newline at end of file diff --git a/examples/middleware/tsconfig.json b/examples/middleware/tsconfig.json index db3953f7..64bab47f 100644 --- a/examples/middleware/tsconfig.json +++ b/examples/middleware/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "rootDir": "./src", - "outDir": "./dist" + "outDir": "./build" }, "include": ["./src"] } \ No newline at end of file diff --git a/examples/multi-version/jitar.json b/examples/multi-version/jitar.json index 0686e62b..efbc65d9 100644 --- a/examples/multi-version/jitar.json +++ b/examples/multi-version/jitar.json @@ -1,4 +1,4 @@ { - "source": "./dist", - "target": "./build" + "source": "./build", + "target": "./dist" } \ No newline at end of file diff --git a/examples/multi-version/tsconfig.json b/examples/multi-version/tsconfig.json index db3953f7..64bab47f 100644 --- a/examples/multi-version/tsconfig.json +++ b/examples/multi-version/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "rootDir": "./src", - "outDir": "./dist" + "outDir": "./build" }, "include": ["./src"] } \ No newline at end of file diff --git a/examples/resources/jitar.json b/examples/resources/jitar.json index 403a2140..dc40ac1f 100644 --- a/examples/resources/jitar.json +++ b/examples/resources/jitar.json @@ -1,6 +1,6 @@ { - "source": "./dist", - "target": "./build", + "source": "./build", + "target": "./dist", "segments": "./segments", "resources": "./resources" } \ No newline at end of file diff --git a/examples/resources/tsconfig.json b/examples/resources/tsconfig.json index db3953f7..64bab47f 100644 --- a/examples/resources/tsconfig.json +++ b/examples/resources/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "rootDir": "./src", - "outDir": "./dist" + "outDir": "./build" }, "include": ["./src"] } \ No newline at end of file diff --git a/examples/segmentation/jitar.json b/examples/segmentation/jitar.json index 1bb66f26..d213197f 100644 --- a/examples/segmentation/jitar.json +++ b/examples/segmentation/jitar.json @@ -1,5 +1,5 @@ { - "source": "./dist", - "target": "./build", + "source": "./build", + "target": "./dist", "segments": "./segments" } \ No newline at end of file diff --git a/examples/segmentation/tsconfig.json b/examples/segmentation/tsconfig.json index db3953f7..64bab47f 100644 --- a/examples/segmentation/tsconfig.json +++ b/examples/segmentation/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "rootDir": "./src", - "outDir": "./dist" + "outDir": "./build" }, "include": ["./src"] } \ No newline at end of file diff --git a/packages/analysis/README.md b/packages/analysis/README.md index 2fc7d39c..dbfcb627 100644 --- a/packages/analysis/README.md +++ b/packages/analysis/README.md @@ -8,21 +8,20 @@ For more information about Jitar: * [Visit our website](https://jitar.dev) * [Read the documentation](https://docs.jitar.dev). -## Known limitations +## TOOLS -1. Declaration of multiple values is not supported +Two tools are provided: -```ts -// Supported -const a = 1; -export { a } +* **Reflector** - Extensive reflection (modules, classes, functions, objects and instances) +* **Parser** - Code parsing (modules, statements, imports, exports, variables, functions and classes) -// Unsupported (will be supported) -const b = 2, c = 3; -export { b, c } -``` +Both tools return a shared based on the ECMAScript specification, but tailored to the Jitar runtime. + +## KNOWN LIMITATIONS + +The limitations below belong to the parser. -2. Generator as object properties are not supported +1. Generator as object properties are not supported ```ts // Supported @@ -45,7 +44,7 @@ class Bar } ``` -3. Destructuring not fully supported +2. Destructuring not fully supported ```ts // Supported diff --git a/packages/build/README.md b/packages/build/README.md index 1fa42ecb..c1f6a0cb 100644 --- a/packages/build/README.md +++ b/packages/build/README.md @@ -7,3 +7,10 @@ For more information about Jitar: * [Visit our website](https://jitar.dev) * [Read the documentation](https://docs.jitar.dev). + +## CONTENT + +Two tools are provided: + +* **BuildHelper** - Generates and returns module and segment code on request. +* **BuildManager** - Rewrites the whole application to the target folder. diff --git a/packages/build/src/BuildHelper.ts b/packages/build/src/BuildHelper.ts index ea1dbc31..dae024ad 100644 --- a/packages/build/src/BuildHelper.ts +++ b/packages/build/src/BuildHelper.ts @@ -3,11 +3,12 @@ import type { RuntimeConfiguration } from '@jitar/configuration'; import { Files, LocalFileManager } from '@jitar/sourcing'; import { Application, ApplicationReader } from './source'; -import { LocalModuleGenerator, RemoteModuleGenerator } from './target'; +import { LocalModuleGenerator, RemoteModuleGenerator, SegmentGenerator } from './target'; import { FileHelper } from './utils'; import ApplicationNotRead from './errors/ApplicationNotRead'; import ApplicationModuleNotFound from './errors/ApplicationModuleNotFound'; +import ApplicationSegmentNotFound from './errors/ApplicationSegmentNotFound'; import ProjectFileManager from './ProjectFileManager'; @@ -91,4 +92,23 @@ export default class BuildHelper return generator.generate(); } + + generateSegmentCode(segmentName: string): string + { + if (this.#application === undefined) + { + throw new ApplicationNotRead(); + } + + const segment = this.#application.segmentation.getSegment(segmentName); + + if (segment === undefined) + { + throw new ApplicationSegmentNotFound(segmentName); + } + + const generator = new SegmentGenerator(segment); + + return generator.generate(); + } } diff --git a/packages/build/src/BuildManager.ts b/packages/build/src/BuildManager.ts index 4d5823f0..32ffed1f 100644 --- a/packages/build/src/BuildManager.ts +++ b/packages/build/src/BuildManager.ts @@ -7,6 +7,8 @@ import { ApplicationReader } from './source'; import { ApplicationBuilder } from './target'; import { FileHelper } from './utils'; +import BuildFailed from './errors/BuildFailed'; + import ProjectFileManager from './ProjectFileManager'; export default class BuildManager @@ -43,16 +45,23 @@ export default class BuildManager async build(): Promise { - const sourceFileManager = this.#fileManager.source; - const resourceFileManager = this.#fileManager.resource; - const segmentFileManager = this.#fileManager.segment; + try + { + const sourceFileManager = this.#fileManager.source; + const resourceFileManager = this.#fileManager.resource; + const segmentFileManager = this.#fileManager.segment; - const moduleFiles = await sourceFileManager.filterWithIgnores(Files.MODULE_PATTERN, this.#fileManager.sourceIgnores); - const resourceFiles = await resourceFileManager.filter(Files.RESOURCE_PATTERN); - const segmentFiles = await segmentFileManager.filter(Files.SEGMENT_PATTERN); + const moduleFiles = await sourceFileManager.filterWithIgnores(Files.MODULE_PATTERN, this.#fileManager.sourceIgnores); + const resourceFiles = await resourceFileManager.filter(Files.RESOURCE_PATTERN); + const segmentFiles = await segmentFileManager.filter(Files.SEGMENT_PATTERN); - const applicationModel = await this.#applicationReader.read(moduleFiles, resourceFiles, segmentFiles); + const applicationModel = await this.#applicationReader.read(moduleFiles, resourceFiles, segmentFiles); - return this.#applicationBuilder.build(applicationModel); + await this.#applicationBuilder.build(applicationModel); + } + catch (error: unknown) + { + throw new BuildFailed(error); + } } } diff --git a/packages/build/src/errors/ApplicationSegmentNotFound.ts b/packages/build/src/errors/ApplicationSegmentNotFound.ts new file mode 100644 index 00000000..0d989e8d --- /dev/null +++ b/packages/build/src/errors/ApplicationSegmentNotFound.ts @@ -0,0 +1,8 @@ + +export default class ApplicationSegmentNotFound extends Error +{ + constructor(segmentName: string) + { + super(`Application segment not found '${segmentName}'`); + } +} diff --git a/packages/build/src/errors/BuildFailed.ts b/packages/build/src/errors/BuildFailed.ts new file mode 100644 index 00000000..0952b074 --- /dev/null +++ b/packages/build/src/errors/BuildFailed.ts @@ -0,0 +1,8 @@ + +export default class BuildFailed extends Error +{ + constructor(cause: unknown) + { + super('Build failed', { cause }); + } +} diff --git a/packages/build/src/source/index.ts b/packages/build/src/source/index.ts index 1a97c2dd..2ca30b24 100644 --- a/packages/build/src/source/index.ts +++ b/packages/build/src/source/index.ts @@ -1,5 +1,5 @@ export { Application, ApplicationReader } from './application'; export { Module } from './module'; -export { Segment, SegmentModule, SegmentImplementation, Segmentation } from './segment'; +export { Segment, SegmentModule, SegmentMember, SegmentClass, SegmentImplementation, Segmentation } from './segment'; export { ResourcesList } from './resource'; diff --git a/packages/build/src/source/module/Reader.ts b/packages/build/src/source/module/Reader.ts index da9b7bdc..774aa5a0 100644 --- a/packages/build/src/source/module/Reader.ts +++ b/packages/build/src/source/module/Reader.ts @@ -2,10 +2,10 @@ import { Parser } from '@jitar/analysis'; import type { FileManager } from '@jitar/sourcing'; -import FileNotLoaded from './errors/FileNotLoaded'; - +import ReadingModuleFailed from './errors/ReadingModuleFailed'; import Module from './models/Module'; import Repository from './models/Repository'; + import LocationRewriter from './LocationRewriter'; export default class Reader @@ -44,9 +44,7 @@ export default class Reader } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error); - - throw new FileNotLoaded(filename, message); + throw new ReadingModuleFailed(filename, error); } } diff --git a/packages/build/src/source/module/errors/FileNotLoaded.ts b/packages/build/src/source/module/errors/FileNotLoaded.ts deleted file mode 100644 index e7b9091d..00000000 --- a/packages/build/src/source/module/errors/FileNotLoaded.ts +++ /dev/null @@ -1,8 +0,0 @@ - -export default class FileNotLoaded extends Error -{ - constructor(filename: string, message: string) - { - super(`Failed to load module file '${filename}' because of: ${message}`); - } -} diff --git a/packages/build/src/source/module/errors/ReadingModuleFailed.ts b/packages/build/src/source/module/errors/ReadingModuleFailed.ts new file mode 100644 index 00000000..e0f3da19 --- /dev/null +++ b/packages/build/src/source/module/errors/ReadingModuleFailed.ts @@ -0,0 +1,8 @@ + +export default class ReadingModuleFailed extends Error +{ + constructor(filename: string, cause: unknown) + { + super(`Failed reading module from '${filename}'`, { cause }); + } +} diff --git a/packages/build/src/source/resource/Reader.ts b/packages/build/src/source/resource/Reader.ts index 51958d7e..08f0b197 100644 --- a/packages/build/src/source/resource/Reader.ts +++ b/packages/build/src/source/resource/Reader.ts @@ -4,8 +4,8 @@ import type { FileManager } from '@jitar/sourcing'; import { Files } from '../../definitions'; import { FileHelper } from '../../utils'; +import ReadingResourcesFailed from './errors/ReadingResourcesFailed'; import ResourcesList from './models/ResourcesList'; -import FileNotLoaded from './errors/FileNotLoaded'; import type ResourceFile from './types/File'; export default class ResourceReader @@ -42,9 +42,7 @@ export default class ResourceReader } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error); - - throw new FileNotLoaded(filename, message); + throw new ReadingResourcesFailed(filename, error); } } diff --git a/packages/build/src/source/resource/errors/FileNotLoaded.ts b/packages/build/src/source/resource/errors/FileNotLoaded.ts deleted file mode 100644 index 4e4c47ec..00000000 --- a/packages/build/src/source/resource/errors/FileNotLoaded.ts +++ /dev/null @@ -1,8 +0,0 @@ - -export default class FileNotLoaded extends Error -{ - constructor(filename: string, message: string) - { - super(`Failed to load resource file '${filename}' because of: ${message}`); - } -} diff --git a/packages/build/src/source/resource/errors/ReadingResourcesFailed.ts b/packages/build/src/source/resource/errors/ReadingResourcesFailed.ts new file mode 100644 index 00000000..ca3634e2 --- /dev/null +++ b/packages/build/src/source/resource/errors/ReadingResourcesFailed.ts @@ -0,0 +1,8 @@ + +export default class ReadingResourcesFailed extends Error +{ + constructor(filename: string, cause: unknown) + { + super(`Failed reading resources from '${filename}'`, { cause }); + } +} diff --git a/packages/build/src/source/segment/Reader.ts b/packages/build/src/source/segment/Reader.ts index f1b57cd2..2de5bcc3 100644 --- a/packages/build/src/source/segment/Reader.ts +++ b/packages/build/src/source/segment/Reader.ts @@ -10,6 +10,7 @@ import FunctionNotAsync from './errors/FunctionNotAsync'; import InvalidFilename from './errors/InvalidFilename'; import FileNotLoaded from './errors/FileNotLoaded'; import InvalidModuleExport from './errors/InvalidModuleExport'; +import ReadingSegmentFailed from './errors/ReadingSegmentFailed'; import Segmentation from './models/Segmentation'; import Segment from './models/Segment'; @@ -57,15 +58,22 @@ export default class SegmentReader async #read(filename: string): Promise { - const definition = await this.#loadSegmentDefinition(filename); - const name = this.#extractSegmentName(filename); + try + { + const definition = await this.#loadSegmentDefinition(filename); + const name = this.#extractSegmentName(filename); - const segment = new Segment(name); + const segment = new Segment(name); - this.#registerModules(segment, definition); - this.#registerMembers(segment); + this.#registerModules(segment, definition); + this.#registerMembers(segment); - return segment; + return segment; + } + catch (error: unknown) + { + throw new ReadingSegmentFailed(filename, error); + } } #extractSegmentName(filename: string): string diff --git a/packages/build/src/source/segment/errors/ReadingSegmentFailed.ts b/packages/build/src/source/segment/errors/ReadingSegmentFailed.ts new file mode 100644 index 00000000..886e95dd --- /dev/null +++ b/packages/build/src/source/segment/errors/ReadingSegmentFailed.ts @@ -0,0 +1,8 @@ + +export default class ReadingSegmentFailed extends Error +{ + constructor(filename: string, cause: unknown) + { + super(`Failed reading segment from '${filename}'`, { cause }); + } +} diff --git a/packages/build/src/source/segment/index.ts b/packages/build/src/source/segment/index.ts index 3aefb4d0..d875f3ab 100644 --- a/packages/build/src/source/segment/index.ts +++ b/packages/build/src/source/segment/index.ts @@ -1,6 +1,8 @@ export { default as Segment } from './models/Segment'; export { default as SegmentModule } from './models/Module'; +export { default as SegmentMember } from './models/Member'; +export { default as SegmentClass } from './models/Class'; export { default as SegmentProcedure } from './models/Procedure'; export { default as SegmentImplementation } from './models/Implementation'; export { default as Segmentation } from './models/Segmentation'; diff --git a/packages/build/src/source/segment/models/Module.ts b/packages/build/src/source/segment/models/Module.ts index 004ee932..3c15eeb4 100644 --- a/packages/build/src/source/segment/models/Module.ts +++ b/packages/build/src/source/segment/models/Module.ts @@ -29,6 +29,10 @@ export default class Module get members() { return this.#members; } + get classes() { return this.#members.filter(member => member instanceof Class); } + + get implementations() { return this.#members.filter(member => member instanceof Implementation); } + get segmented() { return this.#segmented; } hasClasses(): boolean @@ -36,19 +40,14 @@ export default class Module return this.#members.some(member => member instanceof Class); } - getClasses(): Class[] - { - return this.#members.filter(member => member instanceof Class); - } - hasImplementations(): boolean { return this.#members.some(member => member instanceof Implementation); } - getImplementations(): Implementation[] + importsImplementation(importKey: string): boolean { - return this.#members.filter(member => member instanceof Implementation); + return this.implementations.find(implementation => implementation.importKey === importKey) !== undefined; } addMember(members: Member): void diff --git a/packages/build/src/target/index.ts b/packages/build/src/target/index.ts index 9705a88f..98bdd3d9 100644 --- a/packages/build/src/target/index.ts +++ b/packages/build/src/target/index.ts @@ -2,3 +2,4 @@ export { default as ApplicationBuilder } from './application/Builder'; export { default as LocalModuleGenerator } from './module/LocalGenerator'; export { default as RemoteModuleGenerator } from './module/RemoteGenerator'; +export { default as SegmentGenerator } from './segment/Generator'; diff --git a/packages/build/src/target/module/Builder.ts b/packages/build/src/target/module/Builder.ts index 79a80a21..761cb0aa 100644 --- a/packages/build/src/target/module/Builder.ts +++ b/packages/build/src/target/module/Builder.ts @@ -4,6 +4,8 @@ import type { FileManager } from '@jitar/sourcing'; import type { Application, Module, Segment, Segmentation, ResourcesList } from '../../source'; import { FileHelper } from '../../utils'; +import BuildingModuleFailed from './errors/BuildingModuleFailed'; + import LocalGenerator from './LocalGenerator'; import RemoteGenerator from './RemoteGenerator'; @@ -30,30 +32,36 @@ export default class Builder async #buildModule(module: Module, resources: ResourcesList, segmentation: Segmentation): Promise { - const moduleSegments = segmentation.getSegments(module.filename); - - if (moduleSegments.length === 0) + try { - // For unsegmented modules we only need to build the common module. - // This will overwrite the original module file. + const moduleSegments = segmentation.getSegments(module.filename); - return this.#buildCommonModule(module, resources, segmentation); - } - - const segmentBuilds = moduleSegments.map(segment => this.#buildSegmentModule(module, resources, segment, segmentation)); + if (moduleSegments.length === 0) + { + // For unsegmented modules we only need to build the common module. + // This will overwrite the original module file. + + await this.#buildCommonModule(module, resources, segmentation); - const firstModuleSegment = moduleSegments[0]; - const segmentModule = firstModuleSegment.getModule(module.filename); + return; + } - const remoteBuild = segmentModule?.hasImplementations() - ? this.#buildRemoteModule(module, moduleSegments) - : Promise.resolve(); + const implementationSegments = moduleSegments.filter(segment => segment.getModule(module.filename)?.hasImplementations()); - await Promise.all([...segmentBuilds, remoteBuild]); + const commonBuild = this.#buildCommonModule(module, resources, segmentation); - // The segment files will replace the original module file, so we can delete it. + const segmentBuilds = implementationSegments.map(segment => this.#buildSegmentModule(module, resources, segment, segmentation)); - this.#targetFileManager.delete(module.filename); + const remoteBuild = implementationSegments.length > 0 + ? this.#buildRemoteModule(module, moduleSegments) + : Promise.resolve(); + + await Promise.all([commonBuild, ...segmentBuilds, remoteBuild]); + } + catch (error: unknown) + { + throw new BuildingModuleFailed(module.filename, error); + } } async #buildCommonModule(module: Module, resources: ResourcesList, segmentation: Segmentation): Promise diff --git a/packages/build/src/target/module/LocalGenerator.ts b/packages/build/src/target/module/LocalGenerator.ts index c4f12fbb..b80dfd86 100644 --- a/packages/build/src/target/module/LocalGenerator.ts +++ b/packages/build/src/target/module/LocalGenerator.ts @@ -105,7 +105,7 @@ export default class LocalGenerator return [this.#rewriteToCommon(targetModuleFilename, item, [])]; } - const { segmentKeys, remoteKeys, commonKeys } = this.#getModuleImportKeys(targetModuleFilename, item); + const { segmentKeys, remoteKeys, commonKeys } = this.#getModuleImplementationImportKeys(targetModuleFilename, item); const rewrites: T[] = []; @@ -127,10 +127,10 @@ export default class LocalGenerator return rewrites; } - #getModuleImportKeys(targetModuleFilename: string, item: ESImport | ESExport): ModuleImportKeys + #getModuleImplementationImportKeys(targetModuleFilename: string, item: ESImport | ESExport): ModuleImportKeys { - const moduleSegmentKeys = this.#getSegmentImportKeys(targetModuleFilename, this.#segment); - const moduleRemoteKeys = this.#getRemoteImportKeys(targetModuleFilename, moduleSegmentKeys); + const moduleSegmentKeys = this.#getSegmentImplementationImportKeys(targetModuleFilename, this.#segment); + const moduleRemoteKeys = this.#getRemoteImplementationImportKeys(targetModuleFilename, moduleSegmentKeys); const segmentKeys = this.#filterMemberKeys(item, moduleSegmentKeys); const remoteKeys = this.#filterMemberKeys(item, moduleRemoteKeys); @@ -139,7 +139,7 @@ export default class LocalGenerator return { segmentKeys, remoteKeys, commonKeys }; } - #getSegmentImportKeys(targetModuleFilename: string, segment?: Segment): string[] + #getSegmentImplementationImportKeys(targetModuleFilename: string, segment?: Segment): string[] { if (segment === undefined) { @@ -153,13 +153,15 @@ export default class LocalGenerator return []; } - return Object.keys(module.imports); + const keys = Object.keys(module.imports); + + return keys.filter(key => module.importsImplementation(key)); } - #getRemoteImportKeys(targetModuleFilename: string, segmentKeys: string[]): string[] + #getRemoteImplementationImportKeys(targetModuleFilename: string, segmentKeys: string[]): string[] { const segments = this.#segmentation.getSegments(targetModuleFilename).filter(segment => segment !== this.#segment); - const importKeys = segments.flatMap(segment => this.#getSegmentImportKeys(targetModuleFilename, segment)); + const importKeys = segments.flatMap(segment => this.#getSegmentImplementationImportKeys(targetModuleFilename, segment)); const uniqueKeys = [...new Set(importKeys)]; return uniqueKeys.filter(key => segmentKeys.includes(key) === false); diff --git a/packages/build/src/target/module/RemoteGenerator.ts b/packages/build/src/target/module/RemoteGenerator.ts index f4b68970..c72893a8 100644 --- a/packages/build/src/target/module/RemoteGenerator.ts +++ b/packages/build/src/target/module/RemoteGenerator.ts @@ -38,7 +38,7 @@ export default class RemoteGenerator #getImplementations(): Implementation[] { const segmentModules = this.#segments.map(segment => segment.getModule(this.#module.filename)); - const implementations = segmentModules.flatMap(segmentModule => segmentModule!.getImplementations()); + const implementations = segmentModules.flatMap(segmentModule => segmentModule!.implementations); // Implementation can be duplicated across segments // We need to ensure that each implementation is unique diff --git a/packages/build/src/target/module/errors/BuildingModuleFailed.ts b/packages/build/src/target/module/errors/BuildingModuleFailed.ts new file mode 100644 index 00000000..1df64152 --- /dev/null +++ b/packages/build/src/target/module/errors/BuildingModuleFailed.ts @@ -0,0 +1,8 @@ + +export default class BuildingModuleFailed extends Error +{ + constructor(filename: string, cause: unknown) + { + super(`Failed building module for '${filename}'`, { cause }); + } +} diff --git a/packages/build/src/target/segment/Builder.ts b/packages/build/src/target/segment/Builder.ts index 4cfeff6e..71e796a7 100644 --- a/packages/build/src/target/segment/Builder.ts +++ b/packages/build/src/target/segment/Builder.ts @@ -4,6 +4,8 @@ import type { FileManager } from '@jitar/sourcing'; import type { Application, Segment } from '../../source'; +import BuildingSegmentFailed from './errors/BuildingSegmentFailed'; + import Generator from './Generator'; export default class Builder @@ -29,13 +31,20 @@ export default class Builder async #buildSegment(segment: Segment): Promise { - const filename = `${segment.name}.segment.js`; + try + { + const filename = `${segment.name}.segment.js`; - const generator = new Generator(segment); - const code = generator.generate(); + const generator = new Generator(segment); + const code = generator.generate(); - await this.#targetFileManager.write(filename, code); + await this.#targetFileManager.write(filename, code); - this.#logger.info(`Built ${segment.name} segment (${segment.modules.length} modules, ${segment.procedures.length} procedures, ${segment.classes.length} classes)`); + this.#logger.info(`Built ${segment.name} segment (${segment.modules.length} modules, ${segment.procedures.length} procedures, ${segment.classes.length} classes)`); + } + catch (error: unknown) + { + throw new BuildingSegmentFailed(segment.name, error); + } } } diff --git a/packages/build/src/target/segment/Generator.ts b/packages/build/src/target/segment/Generator.ts index 376ba5f5..f4f5fd95 100644 --- a/packages/build/src/target/segment/Generator.ts +++ b/packages/build/src/target/segment/Generator.ts @@ -2,10 +2,9 @@ import { ESModule, ESFunction, ESIdentifierBinding, ESParameter, ESArrayBinding, ESObjectBinding, ESImport, ESModuleMember, ESVariable, ESExport, ESExpression } from '@jitar/analysis'; import { VersionParser } from '@jitar/execution'; -import type { Segment } from '../../source'; +import type { Segment, SegmentMember } from '../../source'; import { FileHelper } from '../../utils'; - const JITAR_MODULE = 'jitar'; const JITAR_IMPORTS = ['Segment', 'Class', 'Procedure', 'Implementation', 'Version', 'NamedParameter', 'ArrayParameter', 'ObjectParameter']; @@ -49,38 +48,60 @@ export default class Generator #createModuleImports(): ESImport[] { - const imports = []; - - // We only want to include modules that are defined in the segment configuration. - // The other modules contain classes and procedures that are re-exported by at least one segmented module. - // Adding them would cause a duplicate declaration error. - const segmentName = this.#segment.name; const modules = this.#segment.getSegmentedModules(); + const imports: ESImport[] = []; + for (const module of modules) { - const filename = this.#fileHelper.addSubExtension(module.filename, segmentName); - const from = `./${filename}`; - + const segmentedFilename = this.#fileHelper.addSubExtension(module.filename, segmentName); + const segmentedFrom = `./${segmentedFilename}`; + if (module.members.length === 0) { - const model = new ESImport([], from); + const model = new ESImport([], segmentedFrom); imports.push(model); continue; } + + const classes = module.classes; + const implementations = module.implementations; + + if (classes.length > 0) + { + const commonFrom = `./${module.filename}`; + + const model = this.#createModuleImport(classes, commonFrom); + + imports.push(model); + } - const members = module.members.map(member => new ESModuleMember(member.importKey, member.id)); - const model = new ESImport(members, from); + if (implementations.length > 0) + { + const model = this.#createModuleImport(implementations, segmentedFrom); - imports.push(model); + imports.push(model); + } } return imports; } + #createModuleImport(segmentMembers: SegmentMember[], from: string): ESImport + { + if (segmentMembers.length === 0) + { + return new ESImport([], from); + } + + const importMembers = segmentMembers.map(member => new ESModuleMember(member.importKey, member.id)); + + return new ESImport(importMembers, from); + } + #createExport(): ESExport { const member = new ESModuleMember('segment', 'default'); diff --git a/packages/build/src/target/segment/errors/BuildingSegmentFailed.ts b/packages/build/src/target/segment/errors/BuildingSegmentFailed.ts new file mode 100644 index 00000000..cda2a2fd --- /dev/null +++ b/packages/build/src/target/segment/errors/BuildingSegmentFailed.ts @@ -0,0 +1,8 @@ + +export default class BuildingSegmentFailed extends Error +{ + constructor(segmentName: string, cause: unknown) + { + super(`Failed building segment '${segmentName}'`, { cause }); + } +} diff --git a/packages/configuration/src/runtime/ConfigurationBuilder.ts b/packages/configuration/src/runtime/ConfigurationBuilder.ts index a7c4634d..311a305f 100644 --- a/packages/configuration/src/runtime/ConfigurationBuilder.ts +++ b/packages/configuration/src/runtime/ConfigurationBuilder.ts @@ -24,7 +24,7 @@ export default class ConfigurationBuilder if (validation.valid === false) { - throw new RuntimeConfigurationInvalid(validation); + throw new RuntimeConfigurationInvalid(filename, validation); } configuration.source ??= DefaultValues.SOURCE; diff --git a/packages/configuration/src/runtime/errors/RuntimeConfigurationInvalid.ts b/packages/configuration/src/runtime/errors/RuntimeConfigurationInvalid.ts index c1011b28..079958a9 100644 --- a/packages/configuration/src/runtime/errors/RuntimeConfigurationInvalid.ts +++ b/packages/configuration/src/runtime/errors/RuntimeConfigurationInvalid.ts @@ -3,10 +3,10 @@ import type { ValidationResult } from '@jitar/validation'; export default class RuntimeConfigurationInvalid extends Error { - public constructor(validation: ValidationResult) + public constructor(filename: string, validation: ValidationResult) { const errorMessages = validation.errors.join('\n'); - super(`Runtime configuration is invalid:\n${errorMessages}`); + super(`Invalid runtime configuration '${filename}'\n${errorMessages}`); } } diff --git a/packages/configuration/src/server/ConfigurationBuilder.ts b/packages/configuration/src/server/ConfigurationBuilder.ts index df47fb55..ac47e126 100644 --- a/packages/configuration/src/server/ConfigurationBuilder.ts +++ b/packages/configuration/src/server/ConfigurationBuilder.ts @@ -24,7 +24,7 @@ export default class ConfigurationBuilder if (validation.valid === false) { - throw new ServerConfigurationInvalid(validation); + throw new ServerConfigurationInvalid(filename, validation); } configuration.meta = diff --git a/packages/configuration/src/server/errors/ServerConfigurationInvalid.ts b/packages/configuration/src/server/errors/ServerConfigurationInvalid.ts index aff83195..1d776da8 100644 --- a/packages/configuration/src/server/errors/ServerConfigurationInvalid.ts +++ b/packages/configuration/src/server/errors/ServerConfigurationInvalid.ts @@ -5,10 +5,10 @@ const BREAK = '\n => '; export default class ServerConfigurationInvalid extends Error { - public constructor(validation: ValidationResult) + public constructor(filename: string, validation: ValidationResult) { const errorMessages = validation.errors.join(BREAK); - super(`Invalid server configuration:${BREAK}${errorMessages}`); + super(`Invalid server configuration '${filename}'${BREAK}${errorMessages}`); } } diff --git a/packages/configuration/test/runtime/ConfigurationBuilder.spec.ts b/packages/configuration/test/runtime/ConfigurationBuilder.spec.ts index a8ff7edc..1c86a0a1 100644 --- a/packages/configuration/test/runtime/ConfigurationBuilder.spec.ts +++ b/packages/configuration/test/runtime/ConfigurationBuilder.spec.ts @@ -37,6 +37,6 @@ describe('runtime/ConfigurationBuilder', () => { const promise = configurationBuilder.build(FILENAMES.INVALID); - await expect(promise).rejects.toEqual(new RuntimeConfigurationInvalid(VALIDATION_RESULT)); + await expect(promise).rejects.toEqual(new RuntimeConfigurationInvalid(FILENAMES.INVALID, VALIDATION_RESULT)); }); }); diff --git a/packages/configuration/test/server/ConfigurationBuilder.spec.ts b/packages/configuration/test/server/ConfigurationBuilder.spec.ts index ea6d392f..1474eed2 100644 --- a/packages/configuration/test/server/ConfigurationBuilder.spec.ts +++ b/packages/configuration/test/server/ConfigurationBuilder.spec.ts @@ -16,6 +16,6 @@ describe('server/ConfigurationBuilder', () => { const promise = configurationBuilder.build(FILENAMES.INVALID_CONFIGURATION); - await expect(promise).rejects.toEqual(new ServerConfigurationInvalid(VALIDATION_RESULT)); + await expect(promise).rejects.toEqual(new ServerConfigurationInvalid(FILENAMES.INVALID_CONFIGURATION, VALIDATION_RESULT)); }); }); diff --git a/packages/execution/README.md b/packages/execution/README.md index 297f0b60..33203b1b 100644 --- a/packages/execution/README.md +++ b/packages/execution/README.md @@ -7,3 +7,10 @@ For more information about Jitar: * [Visit our website](https://jitar.dev) * [Read the documentation](https://docs.jitar.dev). + +## ERROR HANDLING + +The execution manager distincts between execution and application errors. + +* **Execution errors** are thrown and need to be caught by its caller. +* **Application errors** are not a part of the execution process and therefore are caught and wrapped in a response. diff --git a/packages/execution/src/ExecutionManager.ts b/packages/execution/src/ExecutionManager.ts index d6e1f1d8..eaffab7d 100644 --- a/packages/execution/src/ExecutionManager.ts +++ b/packages/execution/src/ExecutionManager.ts @@ -7,6 +7,8 @@ import RunModes from './definitions/RunModes'; import ImplementationNotFound from './errors/ImplementationNotFound'; import InvalidSegment from './errors/InvalidSegment'; import ProcedureNotFound from './errors/ProcedureNotFound'; +import StartingExecutionManagerFailed from './errors/StartingExecutionManagerFailed'; +import StoppingExecutionManagerFailed from './errors/StoppingExecutionManagerFailed'; import type Runner from './interfaces/Runner'; @@ -39,12 +41,26 @@ export default class ExecutionManager implements Runner async start(): Promise { - return this.#loadSegments(); + try + { + await this.#loadSegments(); + } + catch (error: unknown) + { + throw new StartingExecutionManagerFailed(error); + } } async stop(): Promise { - return this.#clearSegments(); + try + { + return this.#clearSegments(); + } + catch (error: unknown) + { + throw new StoppingExecutionManagerFailed(error); + } } async loadSegment(filename: string): Promise diff --git a/packages/execution/src/errors/StartingExecutionManagerFailed.ts b/packages/execution/src/errors/StartingExecutionManagerFailed.ts new file mode 100644 index 00000000..72a62904 --- /dev/null +++ b/packages/execution/src/errors/StartingExecutionManagerFailed.ts @@ -0,0 +1,8 @@ + +export default class StartingExecutionManagerFailed extends Error +{ + constructor(cause: unknown) + { + super('Starting execution manager failed', { cause }); + } +} diff --git a/packages/execution/src/errors/StoppingExecutionManagerFailed.ts b/packages/execution/src/errors/StoppingExecutionManagerFailed.ts new file mode 100644 index 00000000..4d8f7928 --- /dev/null +++ b/packages/execution/src/errors/StoppingExecutionManagerFailed.ts @@ -0,0 +1,8 @@ + +export default class StoppingExecutionManagerFailed extends Error +{ + constructor(cause: unknown) + { + super('Stopping execution manager failed', { cause }); + } +} diff --git a/packages/health/src/HealthManager.ts b/packages/health/src/HealthManager.ts index e5ea1b34..09d1d247 100644 --- a/packages/health/src/HealthManager.ts +++ b/packages/health/src/HealthManager.ts @@ -3,7 +3,11 @@ import type { ModuleImporter } from '@jitar/sourcing'; import States from './definitions/States'; import type { State } from './definitions/States'; + import InvalidHealthCheck from './errors/InvalidHealthCheck'; +import StartingHealthManagerFailed from './errors/StartingHealthManagerFailed'; +import StoppingHealthManagerFailed from './errors/StoppingHealthManagerFailed'; + import type HealthCheck from './interfaces/HealthCheck'; type HealthCheckResult = @@ -47,7 +51,7 @@ export default class HealthManager { this.#state = States.STOPPED; - throw error; + throw new StartingHealthManagerFailed(error); } } @@ -70,7 +74,7 @@ export default class HealthManager { this.#state = States.STARTED; - throw error; + throw new StoppingHealthManagerFailed(error); } } diff --git a/packages/health/src/errors/StartingHealthManagerFailed.ts b/packages/health/src/errors/StartingHealthManagerFailed.ts new file mode 100644 index 00000000..bdf6722e --- /dev/null +++ b/packages/health/src/errors/StartingHealthManagerFailed.ts @@ -0,0 +1,8 @@ + +export default class StartingHealthManagerFailed extends Error +{ + constructor(cause: unknown) + { + super('Starting health manager failed', { cause }); + } +} diff --git a/packages/health/src/errors/StoppingHealthManagerFailed.ts b/packages/health/src/errors/StoppingHealthManagerFailed.ts new file mode 100644 index 00000000..dac0690c --- /dev/null +++ b/packages/health/src/errors/StoppingHealthManagerFailed.ts @@ -0,0 +1,8 @@ + +export default class StoppingHealthManagerFailed extends Error +{ + constructor(cause: unknown) + { + super('Stopping health manager failed', { cause }); + } +} diff --git a/packages/jitar/src/cli.ts b/packages/jitar/src/cli.ts index ed995051..0c70528a 100755 --- a/packages/jitar/src/cli.ts +++ b/packages/jitar/src/cli.ts @@ -1,8 +1,12 @@ #!/usr/bin/env node +import process from 'node:process'; + import { Cli } from '@jitar/cli'; import { Logger } from '@jitar/logging'; +const logger = new Logger(); + try { const cli = new Cli(); @@ -11,8 +15,7 @@ try } catch (error: unknown) { - const logger = new Logger(); - const message = error instanceof Error ? error.message : String(error); + logger.fatal(error); - logger.fatal(message); + process.exitCode = 1; } diff --git a/packages/logging/src/Logger.ts b/packages/logging/src/Logger.ts index 3069af69..2f93864f 100644 --- a/packages/logging/src/Logger.ts +++ b/packages/logging/src/Logger.ts @@ -73,7 +73,7 @@ export default class Logger const moment = new Date().toISOString(); const message = messages.map(value => this.#interpretValue(value)).join(' '); - return `[${logLevel}][${moment}] ${message}`; + return `[${moment}][${logLevel}] ${message}`; } #interpretValue(value: unknown, level = 0): string @@ -112,7 +112,13 @@ export default class Logger if (object instanceof Error) { - return object.stack ?? object.message; + const causeValue = object.cause !== undefined + ? this.#interpretValue(object.cause, level + 1) + : undefined; + + const cause = causeValue !== undefined ? `\n=>${causeValue}` : ''; + + return `${object.message}${cause}`; } return JSON.stringify(object); diff --git a/packages/logging/test/fixtures/values.fixture.ts b/packages/logging/test/fixtures/values.fixture.ts index 2e2354d1..7c8f858d 100644 --- a/packages/logging/test/fixtures/values.fixture.ts +++ b/packages/logging/test/fixtures/values.fixture.ts @@ -29,11 +29,11 @@ export const INPUT = export const OUTPUT = { - DEBUG_CATEGORY: /\[DEBUG\]\[.*\] debug/, - INFO_CATEGORY: /\[INFO\]\[.*\] info/, - WARN_CATEGORY: /\[WARN\]\[.*\] warn/, - ERROR_CATEGORY: /\[ERROR\]\[.*\] error/, - FATAL_CATEGORY: /\[FATAL\]\[.*\] fatal/, + DEBUG_CATEGORY: /\[.*\]\[DEBUG\] debug/, + INFO_CATEGORY: /\[.*\]\[INFO\] info/, + WARN_CATEGORY: /\[.*\]\[WARN\] warn/, + ERROR_CATEGORY: /\[.*\]\[ERROR\] error/, + FATAL_CATEGORY: /\[.*\]\[FATAL\] fatal/, STRING: /\[.*\]\[.*\] value/, NUMBER: /\[.*\]\[.*\] 1/, @@ -45,6 +45,6 @@ export const OUTPUT = NULL: /\[.*\]\[.*\] null/, NESTED_OBJECT: /\[.*\]\[.*\] {"key":{"key":{"key":"value"}}}/, NESTED_ARRAY: /\[.*\]\[.*\] \[\n {2}value,\n {2}\[\n {4}value,\n {4}\[\n {6}value\n {4}\]\n {2}\]\n\]/, - ERROR_WITH_STACKTRACE: /\[.*\]\[.*\] Stacktrace/, + ERROR_WITH_STACKTRACE: /\[.*\]\[.*\] error with stacktrace/, ERROR_WITHOUT_STACKTRACE: /\[.*\]\[.*\] error without stacktrace/ }; diff --git a/packages/middleware/src/MiddlewareManager.ts b/packages/middleware/src/MiddlewareManager.ts index bd48f589..4f1cc1b0 100644 --- a/packages/middleware/src/MiddlewareManager.ts +++ b/packages/middleware/src/MiddlewareManager.ts @@ -4,7 +4,11 @@ import type { ModuleImporter } from '@jitar/sourcing'; import States from './definitions/States'; import type { State } from './definitions/States'; + import InvalidMiddleware from './errors/InvalidMiddleware'; +import StartingMiddlewareManagerFailed from './errors/StartingMiddlewareManagerFailed'; +import StoppingMiddlewareManagerFailed from './errors/StoppingMiddlewareManagerFailed'; + import type Middleware from './interfaces/Middleware'; import type NextHandler from './types/NextHandler'; @@ -43,7 +47,7 @@ export default class MiddlewareManager { this.#state = States.STOPPED; - throw error; + throw new StartingMiddlewareManagerFailed(error); } } @@ -66,7 +70,7 @@ export default class MiddlewareManager { this.#state = States.STARTED; - throw error; + throw new StoppingMiddlewareManagerFailed(error); } } diff --git a/packages/middleware/src/errors/StartingMiddlewareManagerFailed.ts b/packages/middleware/src/errors/StartingMiddlewareManagerFailed.ts new file mode 100644 index 00000000..75f20111 --- /dev/null +++ b/packages/middleware/src/errors/StartingMiddlewareManagerFailed.ts @@ -0,0 +1,8 @@ + +export default class StartingMiddlewareManagerFailed extends Error +{ + constructor(cause: unknown) + { + super('Starting middleware manager failed', { cause }); + } +} diff --git a/packages/middleware/src/errors/StoppingMiddlewareManagerFailed.ts b/packages/middleware/src/errors/StoppingMiddlewareManagerFailed.ts new file mode 100644 index 00000000..5585cc53 --- /dev/null +++ b/packages/middleware/src/errors/StoppingMiddlewareManagerFailed.ts @@ -0,0 +1,8 @@ + +export default class StoppingMiddlewareManagerFailed extends Error +{ + constructor(cause: unknown) + { + super('Stopping middleware manager failed', { cause }); + } +} diff --git a/packages/plugin-vite/src/index.ts b/packages/plugin-vite/src/index.ts index a39704a0..911f1ef1 100644 --- a/packages/plugin-vite/src/index.ts +++ b/packages/plugin-vite/src/index.ts @@ -9,40 +9,8 @@ const JITAR_CLIENT_ID = 'jitar/client'; const JITAR_BUNDLE_ID = 'jitar-bundle'; const JITAR_BUNDLE_RESOLVE_ID = `\0${JITAR_BUNDLE_ID}`; -function assureExtension(filename: string) -{ - if (filename.endsWith('.js')) - { - return filename; - } - - return `${filename}.js`; -} - -function createJitarBundle(middlewares: string[], targetPath: string) -{ - const middlewareFiles = middlewares.map(name => assureExtension(path.join(targetPath, name))); - - const jitarImport = `import { ClientBuilder, HttpRemoteBuilder } from "${JITAR_CLIENT_ID}";`; - const middlewareImports = middlewareFiles.map((filename, index) => `import { default as $M${index} } from "${filename}";`).join(''); - const imports = [jitarImport, middlewareImports].join('\n'); - - const remoteUrl = 'const remoteUrl = document.location.origin;'; - const segmentsArray = `const segments = [];`; - const middlewareItems = middlewares.map((_, index) => `$M${index}`).join(', '); - const middlewareArray = `const middleware = [${middlewareItems}];`; - const declarations = [remoteUrl, segmentsArray, middlewareArray].join('\n'); - - const remoteBuilder = 'const remoteBuilder = new HttpRemoteBuilder();'; - const clientBuilder = 'const clientBuilder = new ClientBuilder(remoteBuilder);'; - const build = 'const client = clientBuilder.build({remoteUrl, segments, middleware});'; - const start = 'client.start();'; - const client = [remoteBuilder, clientBuilder, build, start].join('\n'); - - const exports = `export * from "${JITAR_CLIENT_ID}";`; - - return [imports, declarations, client, exports].join('\n'); -} +const APP_SEGMENT_ID = 'segment:'; +const APP_SEGMENT_RESOLVE_ID = `\0${APP_SEGMENT_ID}`; type PluginConfig = { projectRoot: string; @@ -142,6 +110,11 @@ export default function viteJitar(pluginConfig: PluginConfig): PluginOption return JITAR_BUNDLE_RESOLVE_ID; } + if (id.startsWith(APP_SEGMENT_ID)) + { + return `\0${id}`; + } + return null; }, @@ -151,7 +124,19 @@ export default function viteJitar(pluginConfig: PluginConfig): PluginOption { jitarImported = true; - return createJitarBundle(middlewares, paths.vite.output!); + return createJitarBundle(segments, middlewares, paths.vite.output!); + } + + if (id.startsWith(APP_SEGMENT_RESOLVE_ID)) + { + const segmentName = id.substring(APP_SEGMENT_RESOLVE_ID.length); + + const code = buildHelper + .generateSegmentCode(segmentName) + .replaceAll("from './", `from '${paths.project.source!}/`) + .replaceAll(`.${segmentName}.js`, '.js'); + + return code; } if (id.startsWith(paths.project.source!)) @@ -167,18 +152,7 @@ export default function viteJitar(pluginConfig: PluginConfig): PluginOption if (relativeId.endsWith('.js')) { - try - { - return buildHelper.generateModuleCode(relativeId, segments); - } - catch (error) - { - const message = error instanceof Error ? error.message : String(error); - - console.error('ERROR:', message); - - return null; - } + return buildHelper.generateModuleCode(relativeId, segments); } } @@ -203,3 +177,40 @@ export default function viteJitar(pluginConfig: PluginConfig): PluginOption } as PluginOption; } + +function assureExtension(filename: string) +{ + if (filename.endsWith('.js')) + { + return filename; + } + + return `${filename}.js`; +} + +function createJitarBundle(segments: string[], middlewares: string[], targetPath: string) +{ + const middlewareFiles = middlewares.map(name => assureExtension(path.join(targetPath, name))); + + const jitarImport = `import { ClientBuilder, HttpRemoteBuilder } from "${JITAR_CLIENT_ID}";`; + const segmentImports = segments.map((name, index) => `import { default as $S${index} } from "segment:${name}";`).join(''); + const middlewareImports = middlewareFiles.map((filename, index) => `import { default as $M${index} } from "${filename}";`).join(''); + const imports = [jitarImport, segmentImports, middlewareImports].join('\n'); + + const remoteUrl = 'const remoteUrl = document.location.origin;'; + const segmentItems = segments.map((_, index) => `$S${index}`).join(', '); + const segmentArray = `const segments = [${segmentItems}];`; + const middlewareItems = middlewares.map((_, index) => `$M${index}`).join(', '); + const middlewareArray = `const middleware = [${middlewareItems}];`; + const declarations = [remoteUrl, segmentArray, middlewareArray].join('\n'); + + const remoteBuilder = 'const remoteBuilder = new HttpRemoteBuilder();'; + const clientBuilder = 'const clientBuilder = new ClientBuilder(remoteBuilder);'; + const build = 'const client = clientBuilder.build({remoteUrl, segments, middleware});'; + const start = 'client.start();'; + const client = [remoteBuilder, clientBuilder, build, start].join('\n'); + + const exports = `export * from "${JITAR_CLIENT_ID}";`; + + return [imports, declarations, client, exports].join('\n'); +} diff --git a/packages/runtime/README.md b/packages/runtime/README.md index 8bb97320..458bf91c 100644 --- a/packages/runtime/README.md +++ b/packages/runtime/README.md @@ -7,3 +7,12 @@ For more information about Jitar: * [Visit our website](https://jitar.dev) * [Read the documentation](https://docs.jitar.dev). + +## IMPLEMENTATIONS + +The runtime implements a client-server architecture. + +* **Client** - wraps a local worker service with a remote gateway, supports middleware. +* **Server** - wraps a local proxy service with a configurable runner and provider service, supports middleware and resources. + +Both types are constructed by a builder. diff --git a/packages/runtime/src/client/Client.ts b/packages/runtime/src/client/Client.ts index 9c93cc6e..205ba082 100644 --- a/packages/runtime/src/client/Client.ts +++ b/packages/runtime/src/client/Client.ts @@ -5,6 +5,9 @@ import type { MiddlewareManager } from '@jitar/middleware'; import type { ScheduleManager } from '@jitar/scheduling'; import { LocalWorker, RemoteGateway, Remote, RequestPool } from '@jitar/services'; +import StartingClientFailed from './errors/StartingClientFailed'; +import StoppingClientFailed from './errors/StoppingClientFailed'; + import ProcedureRunner from '../ProcedureRunner'; import Runtime from '../Runtime'; @@ -47,16 +50,30 @@ export default class Client extends Runtime async start(): Promise { - await this.#setUp(); - - this.#requestPool.start(); + try + { + await this.#setUp(); + + this.#requestPool.start(); + } + catch (error: unknown) + { + throw new StartingClientFailed(error); + } } async stop(): Promise { - this.#requestPool.stop(); - - await this.#tearDown(); + try + { + this.#requestPool.stop(); + + await this.#tearDown(); + } + catch (error: unknown) + { + throw new StoppingClientFailed(error); + } } getTrustKey(): string | undefined diff --git a/packages/runtime/src/client/errors/StartingClientFailed.ts b/packages/runtime/src/client/errors/StartingClientFailed.ts new file mode 100644 index 00000000..2618480e --- /dev/null +++ b/packages/runtime/src/client/errors/StartingClientFailed.ts @@ -0,0 +1,8 @@ + +export default class StartingClientFailed extends Error +{ + constructor(cause: unknown) + { + super('Starting client failed', { cause }); + } +} diff --git a/packages/runtime/src/client/errors/StoppingClientFailed.ts b/packages/runtime/src/client/errors/StoppingClientFailed.ts new file mode 100644 index 00000000..8321e1cc --- /dev/null +++ b/packages/runtime/src/client/errors/StoppingClientFailed.ts @@ -0,0 +1,8 @@ + +export default class StoppingClientFailed extends Error +{ + constructor(cause: unknown) + { + super('Stopping client failed', { cause }); + } +} diff --git a/packages/runtime/src/server/Server.ts b/packages/runtime/src/server/Server.ts index 0834aea5..f52f1d49 100644 --- a/packages/runtime/src/server/Server.ts +++ b/packages/runtime/src/server/Server.ts @@ -13,6 +13,9 @@ import Runtime from '../Runtime'; import ContentTypes from './definitions/ContentTypes'; import StatusCodes from './definitions/StatusCodes'; +import StartingServerFailed from './errors/StartingServerFailed'; +import StoppingServerFailed from './errors/StoppingServerFailed'; + import AddWorkerRequest from './types/AddWorkerRequest'; import ProvideRequest from './types/ProvideRequest'; import RemoveWorkerRequest from './types/RemoveWorkerRequest'; @@ -63,21 +66,35 @@ export default class Server extends Runtime async start(): Promise { - await this.#setUp(); + try + { + await this.#setUp(); - this.#logger.info(`Server started at ${this.#proxy.url}`); + this.#logger.info(`Server started at ${this.#proxy.url}`); - if (this.#proxy.runner instanceof LocalWorker) + if (this.#proxy.runner instanceof LocalWorker) + { + this.#logger.info('RPC procedures:', this.#proxy.runner.getProcedureNames()); + } + } + catch (error: unknown) { - this.#logger.info('RPC procedures:', this.#proxy.runner.getProcedureNames()); + throw new StartingServerFailed(error); } } async stop(): Promise { - await this.#tearDown(); + try + { + await this.#tearDown(); - this.#logger.info('Server stopped'); + this.#logger.info('Server stopped'); + } + catch (error: unknown) + { + throw new StoppingServerFailed(error); + } } async getHealth(): Promise @@ -94,7 +111,7 @@ export default class Server extends Runtime { const message = error instanceof Error ? error.message : String(error); - this.#logger.error('Failed to get health:', message); + this.#logger.error(`Failed to get health (${message})`); return this.#respondError(error); } @@ -114,7 +131,7 @@ export default class Server extends Runtime { const message = error instanceof Error ? error.message : String(error); - this.#logger.error('Failed to get health status:', message); + this.#logger.error(`Failed to get health status (${message})`); return this.#respondError(error); } @@ -126,7 +143,7 @@ export default class Server extends Runtime { const file = await this.#proxy.provide(provideRequest.filename); - this.#logger.info('Provided file:', provideRequest.filename); + this.#logger.info(`Provided file:' ${provideRequest.filename}`); return this.#respondFile(file); } @@ -134,7 +151,7 @@ export default class Server extends Runtime { const message = error instanceof Error ? error.message : String(error); - this.#logger.warn('Failed to provide file:', message); + this.#logger.warn(`Failed to provide file: ${provideRequest.filename} (${message})`); return this.#respondError(error); } @@ -149,7 +166,7 @@ export default class Server extends Runtime // Middleware is only executed on external requests. const response = await this.#middlewareManager.handle(request); - this.#logger.info('Ran request:', request.fqn); + this.#logger.info(`Ran request: ${request.fqn}`); return this.#respondResponse(response); } @@ -157,7 +174,7 @@ export default class Server extends Runtime { const message = error instanceof Error ? error.message : String(error); - this.#logger.error('Failed run request:', message); + this.#logger.error(`Failed run request: ${runRequest.fqn} (${message})`); return this.#respondError(error); } @@ -179,7 +196,7 @@ export default class Server extends Runtime const id = await gateway.addWorker(worker); - this.#logger.info('Added worker:', worker.url); + this.#logger.info(`Added worker: ${worker.url}`); return this.#respondSuccess({ id }); } @@ -187,7 +204,7 @@ export default class Server extends Runtime { const message = error instanceof Error ? error.message : String(error); - this.#logger.error('Failed to add worker:', message); + this.#logger.error(`Failed to add worker: ${addRequest.url} (${message})`); return this.#respondError(error); } @@ -203,7 +220,7 @@ export default class Server extends Runtime await gateway.reportWorker(reportRequest.id, state); - this.#logger.debug('Reported worker:', reportRequest.id); + this.#logger.debug(`Reported worker: ${reportRequest.id}`); return this.#respondSuccess(); } @@ -211,7 +228,7 @@ export default class Server extends Runtime { const message = error instanceof Error ? error.message : String(error); - this.#logger.error('Failed to report worker:', message); + this.#logger.error(`Failed to report worker: ${reportRequest.id} (${message})`); return this.#respondError(error); } @@ -225,7 +242,7 @@ export default class Server extends Runtime await gateway.removeWorker(removeRequest.id); - this.#logger.info('Removed worker:', removeRequest.id); + this.#logger.info(`Removed worker: ${removeRequest.id}`); return this.#respondSuccess(); } @@ -233,7 +250,7 @@ export default class Server extends Runtime { const message = error instanceof Error ? error.message : String(error); - this.#logger.error('Failed to remove worker:', message); + this.#logger.error(`Failed to remove worker: ${removeRequest.id} (${message})`); return this.#respondError(error); } diff --git a/packages/runtime/src/server/errors/StartingServerFailed.ts b/packages/runtime/src/server/errors/StartingServerFailed.ts new file mode 100644 index 00000000..c1178103 --- /dev/null +++ b/packages/runtime/src/server/errors/StartingServerFailed.ts @@ -0,0 +1,8 @@ + +export default class StartingServerFailed extends Error +{ + constructor(cause: unknown) + { + super('Starting server failed', { cause }); + } +} diff --git a/packages/runtime/src/server/errors/StoppingServerFailed.ts b/packages/runtime/src/server/errors/StoppingServerFailed.ts new file mode 100644 index 00000000..66d801da --- /dev/null +++ b/packages/runtime/src/server/errors/StoppingServerFailed.ts @@ -0,0 +1,8 @@ + +export default class StoppingServerFailed extends Error +{ + constructor(cause: unknown) + { + super('Stopping server failed', { cause }); + } +} diff --git a/packages/services/README.md b/packages/services/README.md index 879dad5b..9c6bd1b1 100644 --- a/packages/services/README.md +++ b/packages/services/README.md @@ -7,3 +7,34 @@ For more information about Jitar: * [Visit our website](https://jitar.dev) * [Read the documentation](https://docs.jitar.dev). + +## SERVICES + +Two types of services, each with their own responsibilities. + +### Runner services + +Run procedures by their FQN. + +* **Worker** - Executes containing procedures locally, and delegates others to a remote gateway. +* **Gateway** - Forwards run requests to workers that contain the requested procedure. + +### Provider services + +Provide files by their filename. + +* **Repository** - Holds and provides files. + +### Hybrid services + +Run requests and provide files. + +* **Proxy** - Forwards run requests to a gateway (or worker) and provide requests to a repository. + +## SERIALIZATION + +Managed by the **local worker** via header `X-Jitar-Data-Encoding` with value `serialized`. + +If the incoming request contains this header, the data the worker deserializes the data before execution and will serialize the response data. + +Outgoing (remote) requests always set this header, and the worker serializes the request data before sending it. diff --git a/packages/sourcing/src/files/errors/FileNotFound.ts b/packages/sourcing/src/files/errors/FileNotFound.ts index 6246c422..342a8855 100644 --- a/packages/sourcing/src/files/errors/FileNotFound.ts +++ b/packages/sourcing/src/files/errors/FileNotFound.ts @@ -1,14 +1,8 @@ export default class FileNotFound extends Error { - readonly #filename: string; - constructor(filename: string) { - super(`The file '${filename}' could not be found`); - - this.#filename = filename; + super(`File could not be found from '${filename}'`); } - - get filename() { return this.#filename; } } diff --git a/packages/sourcing/src/files/errors/InvalidLocation.ts b/packages/sourcing/src/files/errors/InvalidLocation.ts index 185428ca..01a2b310 100644 --- a/packages/sourcing/src/files/errors/InvalidLocation.ts +++ b/packages/sourcing/src/files/errors/InvalidLocation.ts @@ -1,14 +1,8 @@ export default class InvalidPath extends Error { - readonly #location: string; - constructor(location: string) { - super(`Invalid location: ${location}`); - - this.#location = location; + super(`Invalid location '${location}'`); } - - get location() { return this.#location; } } diff --git a/packages/sourcing/src/modules/ImportManager.ts b/packages/sourcing/src/modules/ImportManager.ts index b39b2a83..28c1a2be 100644 --- a/packages/sourcing/src/modules/ImportManager.ts +++ b/packages/sourcing/src/modules/ImportManager.ts @@ -25,9 +25,7 @@ export default class ImportManager implements ModuleImporter } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error); - - throw new ModuleNotLoaded(location, message); + throw new ModuleNotLoaded(location, error); } } } diff --git a/packages/sourcing/src/modules/errors/ModuleNotLoaded.ts b/packages/sourcing/src/modules/errors/ModuleNotLoaded.ts index 949a9057..cb20dd3f 100644 --- a/packages/sourcing/src/modules/errors/ModuleNotLoaded.ts +++ b/packages/sourcing/src/modules/errors/ModuleNotLoaded.ts @@ -1,20 +1,8 @@ export default class ModuleNotLoaded extends Error { - readonly #url: string; - readonly #reason?: string; - - constructor(url: string, reason?: string) + constructor(url: string, cause: unknown) { - const postfix = reason !== undefined ? ` | ${reason}` : ''; - - super(`Module '${url}' could not be loaded${postfix}`); - - this.#url = url; - this.#reason = reason; + super(`Module could not be loaded from '${url}'`, { cause }); } - - get url() { return this.#url; } - - get reason() { return this.#reason; } } diff --git a/packages/validation/src/Validator.ts b/packages/validation/src/Validator.ts index c1abd158..c62a75f4 100644 --- a/packages/validation/src/Validator.ts +++ b/packages/validation/src/Validator.ts @@ -1,6 +1,6 @@ import ValidationResult from './types/ValidationResult'; -import ValidationScheme, { FieldValidation, GroupValidation, ListValidation, PrimitiveValidation } from './types/ValidationScheme'; +import ValidationScheme, { EnumValidation, FieldValidation, GroupValidation, ListValidation, PrimitiveValidation } from './types/ValidationScheme'; type Data = Record; @@ -92,6 +92,8 @@ export default class Validator return this.#validateGroup(key, value, scheme, errors); case 'list': return this.#validateList(key, value, scheme, errors); + case 'enum': + return this.#validateEnum(key, value, scheme, errors); } } @@ -167,6 +169,16 @@ export default class Validator } } + #validateEnum(key: string, value: unknown, scheme: EnumValidation, errors: string[]): void + { + if (scheme.options.includes(value) === false) + { + const options = scheme.options.map(option => String(option)); + + errors.push(`Field '${key}' is not one of: ${options.join(', ')}`); + } + } + #composeKey(parent: string, key: string): string { return parent === '' ? key : `${parent}.${key}`; diff --git a/packages/validation/src/types/ValidationScheme.ts b/packages/validation/src/types/ValidationScheme.ts index a3221dbe..471abe9c 100644 --- a/packages/validation/src/types/ValidationScheme.ts +++ b/packages/validation/src/types/ValidationScheme.ts @@ -1,24 +1,32 @@ -export type FieldValidation = PrimitiveValidation | GroupValidation | ListValidation; +export type FieldValidation = PrimitiveValidation | GroupValidation | ListValidation | EnumValidation; -export type PrimitiveValidation = +type BaseValidation = { - type: 'string' | 'integer' | 'real' | 'boolean' | 'url'; - required?: boolean; + readonly required?: boolean; +} + +export type PrimitiveValidation = BaseValidation & +{ + readonly type: 'string' | 'integer' | 'real' | 'boolean' | 'url'; }; -export type GroupValidation = +export type GroupValidation = BaseValidation & { - type: 'group'; - required?: boolean; - fields: Record; + readonly type: 'group'; + readonly fields: Record; } -export type ListValidation = +export type ListValidation = BaseValidation & +{ + readonly type: 'list'; + readonly items: PrimitiveValidation; +}; + +export type EnumValidation = BaseValidation & { - type: 'list'; - required?: boolean; - items: PrimitiveValidation; + readonly type: 'enum'; + readonly options: unknown[]; }; type ValidationScheme = Record; diff --git a/packages/validation/test/Validator.spec.ts b/packages/validation/test/Validator.spec.ts index a3eaa2ff..d69b2238 100644 --- a/packages/validation/test/Validator.spec.ts +++ b/packages/validation/test/Validator.spec.ts @@ -5,42 +5,42 @@ import { Validator } from '../src'; import { VALIDATION_SCHEMES, VALUES } from './fixtures'; +const strictValidator = new Validator(true); +const lenientValidator = new Validator(false); + describe('Validator', () => { describe('String values', () => { it('should accept a valid string', () => { - const validator = new Validator(); const data = { string: 'string' }; const scheme = VALIDATION_SCHEMES.STRING; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(true); + expect(result.valid).toBeTruthy(); }); it('should reject an invalid string', () => { - const validator = new Validator(); const data = { string: 123 }; const scheme = VALIDATION_SCHEMES.STRING; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(false); + expect(result.valid).toBeFalsy(); expect(result.errors).toContain(VALUES.MESSAGES.INVALID_STRING); }); it('should reject a missing required string', () => { - const validator = new Validator(); const data = { string: undefined }; const scheme = VALIDATION_SCHEMES.STRING; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(false); + expect(result.valid).toBeFalsy(); expect(result.errors).toContain(VALUES.MESSAGES.MISSING_STRING); }); }); @@ -49,36 +49,33 @@ describe('Validator', () => { it('should accept a valid integer', () => { - const validator = new Validator(); const data = { integer: 123 }; const scheme = VALIDATION_SCHEMES.INTEGER; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(true); + expect(result.valid).toBeTruthy(); }); it('should reject an invalid integer', () => { - const validator = new Validator(); const data = { integer: '123' }; const scheme = VALIDATION_SCHEMES.INTEGER; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(false); + expect(result.valid).toBeFalsy(); expect(result.errors).toContain(VALUES.MESSAGES.INVALID_INTEGER); }); it('should reject a missing required integer', () => { - const validator = new Validator(); const data = { integer: undefined }; const scheme = VALIDATION_SCHEMES.INTEGER; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(false); + expect(result.valid).toBeFalsy(); expect(result.errors).toContain(VALUES.MESSAGES.MISSING_INTEGER); }); }); @@ -87,36 +84,33 @@ describe('Validator', () => { it('should accept a valid real', () => { - const validator = new Validator(); const data = { real: 123.45 }; const scheme = VALIDATION_SCHEMES.REAL; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(true); + expect(result.valid).toBeTruthy(); }); it('should reject an invalid real', () => { - const validator = new Validator(); const data = { real: '123.45' }; const scheme = VALIDATION_SCHEMES.REAL; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(false); + expect(result.valid).toBeFalsy(); expect(result.errors).toContain(VALUES.MESSAGES.INVALID_REAL); }); it('should reject a missing required real', () => { - const validator = new Validator(); const data = { real: undefined }; const scheme = VALIDATION_SCHEMES.REAL; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(false); + expect(result.valid).toBeFalsy(); expect(result.errors).toContain(VALUES.MESSAGES.MISSING_REAL); }); }); @@ -125,36 +119,33 @@ describe('Validator', () => { it('should accept a valid boolean', () => { - const validator = new Validator(); const data = { boolean: true }; const scheme = VALIDATION_SCHEMES.BOOLEAN; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(true); + expect(result.valid).toBeTruthy(); }); it('should reject an invalid boolean', () => { - const validator = new Validator(); const data = { boolean: 'true' }; const scheme = VALIDATION_SCHEMES.BOOLEAN; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(false); + expect(result.valid).toBeFalsy(); expect(result.errors).toContain(VALUES.MESSAGES.INVALID_BOOLEAN); }); it('should reject a missing required boolean', () => { - const validator = new Validator(); const data = { boolean: undefined }; const scheme = VALIDATION_SCHEMES.BOOLEAN; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(false); + expect(result.valid).toBeFalsy(); expect(result.errors).toContain(VALUES.MESSAGES.MISSING_BOOLEAN); }); }); @@ -163,36 +154,33 @@ describe('Validator', () => { it('should accept a valid URL', () => { - const validator = new Validator(); const data = { url: 'https://example.com' }; const scheme = VALIDATION_SCHEMES.URL; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(true); + expect(result.valid).toBeTruthy(); }); it('should reject an invalid URL', () => { - const validator = new Validator(); const data = { url: 'example.com' }; const scheme = VALIDATION_SCHEMES.URL; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(false); + expect(result.valid).toBeFalsy(); expect(result.errors).toContain(VALUES.MESSAGES.INVALID_URL); }); it('should reject a missing required URL', () => { - const validator = new Validator(); const data = { url: undefined }; const scheme = VALIDATION_SCHEMES.URL; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(false); + expect(result.valid).toBeFalsy(); expect(result.errors).toContain(VALUES.MESSAGES.MISSING_URL); }); }); @@ -201,13 +189,12 @@ describe('Validator', () => { it('should accept missing optional values', () => { - const validator = new Validator(); const data = { string: undefined, integer: undefined, real: undefined, boolean: undefined, url: undefined }; const scheme = VALIDATION_SCHEMES.OPTIONAL; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(true); + expect(result.valid).toBeTruthy(); }); }); @@ -215,36 +202,33 @@ describe('Validator', () => { it('should accept a valid group', () => { - const validator = new Validator(); const data = { group: { string: 'source', integer: 123, boolean: true }}; const scheme = VALIDATION_SCHEMES.GROUP; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(true); + expect(result.valid).toBeTruthy(); }); it('should reject an invalid group', () => { - const validator = new Validator(); const data = { group: 'group' }; const scheme = VALIDATION_SCHEMES.GROUP; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(false); + expect(result.valid).toBeFalsy(); expect(result.errors).toContain(VALUES.MESSAGES.INVALID_GROUP); }); it('should reject a missing required group', () => { - const validator = new Validator(); const data = { group: undefined }; const scheme = VALIDATION_SCHEMES.GROUP; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(false); + expect(result.valid).toBeFalsy(); expect(result.errors).toContain(VALUES.MESSAGES.MISSING_GROUP); }); }); @@ -253,63 +237,93 @@ describe('Validator', () => { it('should accept a valid list', () => { - const validator = new Validator(); const data = { list: ['item1', 'item2', 'item3'] }; const scheme = VALIDATION_SCHEMES.LIST; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(true); + expect(result.valid).toBeTruthy(); }); it('should reject an invalid list', () => { - const validator = new Validator(); const data = { list: 'item1' }; const scheme = VALIDATION_SCHEMES.LIST; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(false); + expect(result.valid).toBeFalsy(); expect(result.errors).toContain(VALUES.MESSAGES.INVALID_LIST); }); it('should reject an invalid list item', () => { - const validator = new Validator(); const data = { list: ['item1', 123, 'item3'] }; const scheme = VALIDATION_SCHEMES.LIST; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(false); + expect(result.valid).toBeFalsy(); expect(result.errors).toContain(VALUES.MESSAGES.INVALID_LIST_ITEM); }); it('should reject a missing required list', () => { - const validator = new Validator(); const data = { list: undefined }; const scheme = VALIDATION_SCHEMES.LIST; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(false); + expect(result.valid).toBeFalsy(); expect(result.errors).toContain(VALUES.MESSAGES.MISSING_LIST); }); }); + describe('Enum values', () => + { + it('should accept a valid enum value', () => + { + const data = { enum: 'two' }; + const scheme = VALIDATION_SCHEMES.ENUM; + + const result = strictValidator.validate(data, scheme); + + expect(result.valid).toBeTruthy(); + }); + + it('should reject an invalid enum value', () => + { + const data = { enum: 'four' }; + const scheme = VALIDATION_SCHEMES.ENUM; + + const result = strictValidator.validate(data, scheme); + + expect(result.valid).toBeFalsy(); + expect(result.errors).toContain(VALUES.MESSAGES.INVALID_ENUM_ITEM); + }); + + it('should reject a missing required enum value', () => + { + const data = { enum: undefined }; + const scheme = VALIDATION_SCHEMES.ENUM; + + const result = strictValidator.validate(data, scheme); + + expect(result.valid).toBeFalsy(); + expect(result.errors).toContain(VALUES.MESSAGES.MISSING_ENUM); + }); + }); + describe('Complex values', () => { it('should accept a valid complex group', () => { - const validator = new Validator(); const data = { complex: { source: 'source', integer: 123, boolean: true, list: ['item1', 'item2', 'item3'] }}; const scheme = VALIDATION_SCHEMES.COMPLEX; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(true); + expect(result.valid).toBeTruthy(); }); }); @@ -317,13 +331,12 @@ describe('Validator', () => { it('should reject extra fields', () => { - const validator = new Validator(); const data = { string: 'string', integer: 2, extra: 'extra' }; const scheme = VALIDATION_SCHEMES.STRICT; - const result = validator.validate(data, scheme); + const result = strictValidator.validate(data, scheme); - expect(result.valid).toBe(false); + expect(result.valid).toBeFalsy(); expect(result.errors).toContain(VALUES.MESSAGES.EXTRA_FIELD); }); }); @@ -332,13 +345,12 @@ describe('Validator', () => { it('should accept extra fields', () => { - const validator = new Validator(false); const data = { string: 'string', extra: 'extra' }; const scheme = VALIDATION_SCHEMES.LENIENT; - const result = validator.validate(data, scheme); + const result = lenientValidator.validate(data, scheme); - expect(result.valid).toBe(true); + expect(result.valid).toBeTruthy(); }); }); }); diff --git a/packages/validation/test/fixtures/validationSchemes.fixture.ts b/packages/validation/test/fixtures/validationSchemes.fixture.ts index dba2ddd4..5de94350 100644 --- a/packages/validation/test/fixtures/validationSchemes.fixture.ts +++ b/packages/validation/test/fixtures/validationSchemes.fixture.ts @@ -51,6 +51,14 @@ export const VALIDATION_SCHEMES: Record = } }, + ENUM: { + enum: { + type: 'enum', + required: true, + options: ['one', 'two', 'three'] + } + }, + COMPLEX: { complex: { type: 'group', diff --git a/packages/validation/test/fixtures/values.fixture.ts b/packages/validation/test/fixtures/values.fixture.ts index 7d7c98d1..104fce34 100644 --- a/packages/validation/test/fixtures/values.fixture.ts +++ b/packages/validation/test/fixtures/values.fixture.ts @@ -18,6 +18,8 @@ export const VALUES = INVALID_LIST: `Field 'list' is not a list`, INVALID_LIST_ITEM: `Field 'list.1' is not a string`, MISSING_LIST: `Field 'list' is required`, + INVALID_ENUM_ITEM: `Field 'enum' is not one of: one, two, three`, + MISSING_ENUM: `Field 'enum' is required`, EXTRA_FIELD: `Unknown field 'extra'`, }, };