-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
major (#508): Implemented core functionality of the '@kipper/config' …
…package
- Loading branch information
1 parent
04e7d79
commit 7f62eef
Showing
9 changed files
with
393 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import type { KipperEncoding } from "@kipper/cli"; | ||
|
||
/** | ||
* An abstract class that represents a configuration file. | ||
* @since 0.11.0 | ||
*/ | ||
export abstract class ConfigFile { | ||
public readonly content: string; | ||
public readonly parsedJSON: { [key: string]: any }; | ||
public readonly fileName: string; | ||
public readonly encoding: KipperEncoding; | ||
|
||
protected constructor(content: string, fileName: string, encoding: KipperEncoding) { | ||
this.content = content; | ||
this.parsedJSON = JSON.parse(content); | ||
this.fileName = fileName; | ||
this.encoding = encoding; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import { ConfigFile } from "./config-file"; | ||
import { ConfigValidationError } from "../errors"; | ||
|
||
/** | ||
* A type that represents a configuration scheme. | ||
* @since 0.11.0 | ||
*/ | ||
export type ConfigInterpreterScheme = { | ||
[key: string]: { | ||
type: "string" | "number" | "boolean"; | ||
required: boolean; | ||
} | { | ||
type: "array", | ||
required: boolean; | ||
itemType: "string" | "number" | "boolean"; | ||
} | { | ||
type: "object"; | ||
required: boolean; | ||
properties: ConfigInterpreterScheme; | ||
} | ||
}; | ||
|
||
/** | ||
* A type that directly corresponds to the provided scheme and translates it to a TypeScript type | ||
* i.e. a representation of what a valid config file would actually look like if it was parsed. | ||
*/ | ||
export type Config<Scheme extends ConfigInterpreterScheme> = { | ||
[key in keyof Scheme]: Scheme[key] extends { type: "string" } ? | ||
string : | ||
Scheme[key] extends { type: "number", required: false } ? | ||
number : | ||
Scheme[key] extends { type: "boolean" } ? | ||
boolean : | ||
Scheme[key] extends { type: "array", itemType: infer T } ? | ||
T[] : | ||
Scheme[key] extends { type: "object", properties: infer P extends ConfigInterpreterScheme } ? | ||
Config<P> : | ||
never; | ||
}; | ||
|
||
type example = Config<{ "smth": { type: "object", required: false, properties: { x: { type: "string", required: false } }}}> | ||
var example1: example = {} as example; | ||
|
||
example1.smth.x; | ||
|
||
/** | ||
* An abstract class that interprets a configuration file and returns a configuration object. | ||
* @since 0.11.0 | ||
*/ | ||
export abstract class ConfigInterpreter<SchemeT extends ConfigInterpreterScheme, OutputT> { | ||
scheme: SchemeT; | ||
|
||
protected constructor(scheme: SchemeT) { | ||
this.scheme = scheme; | ||
} | ||
|
||
/** | ||
* Validate a configuration file based off a scheme. | ||
* | ||
* This is intended for basic validation, and should not be used for complex validation. There a custom validation | ||
* methods should be implemented depending on the complexity of the configuration file. | ||
* @param config The configuration file to validate. | ||
* @param parentFiles The parent files of the configuration file. Used to allow for more descriptive error messages | ||
* when recursively validating configuration files. | ||
* @throws ConfigValidationError If the configuration file is invalid. | ||
* @protected | ||
* @since 0.11.0 | ||
*/ | ||
protected validateConfigBasedOffScheme(config: ConfigFile, parentFiles: string[]): void { | ||
this.validateConfigBasedOffSchemeRecursive(config, this.scheme, parentFiles); | ||
} | ||
|
||
/** | ||
* Raw recursive function to validate a configuration file based off a scheme. | ||
* @param configFile The configuration file to validate. | ||
* @param scheme The scheme to validate the configuration file against. | ||
* @param parentFiles The parent files of the configuration file. Used to allow for more descriptive error messages | ||
* when recursively validating configuration files. | ||
* @throws ConfigValidationError If the configuration file is invalid. | ||
* @private | ||
* @since 0.11.0 | ||
*/ | ||
private validateConfigBasedOffSchemeRecursive( | ||
configFile: ConfigFile, | ||
scheme: ConfigInterpreterScheme, | ||
parentFiles: string[] | ||
): void { | ||
for (const key in scheme) { | ||
const schemeValue = scheme[key]; | ||
const configValue = configFile.parsedJSON[key]; | ||
|
||
if (schemeValue.required && configValue === undefined) { | ||
throw new ConfigValidationError(`Missing required field "${key}"`, configFile.fileName, parentFiles); | ||
} | ||
|
||
if (schemeValue.type === "object") { | ||
this.validateConfigBasedOffSchemeRecursive( | ||
configValue, | ||
schemeValue.properties, | ||
parentFiles | ||
); | ||
} else if (schemeValue.type === "array") { | ||
if (!Array.isArray(configValue)) { | ||
throw new ConfigValidationError( | ||
`Field "${key}" is not an array, but it should be`, | ||
configFile.fileName, | ||
parentFiles | ||
); | ||
} | ||
|
||
for (const value of configValue) { | ||
if (typeof value !== schemeValue.itemType) { | ||
throw new ConfigValidationError( | ||
`Field "${key}" contains an item that is not of type "${schemeValue.itemType}"`, | ||
configFile.fileName, | ||
parentFiles | ||
); | ||
} | ||
} | ||
} else { | ||
if (typeof configValue !== schemeValue.type) { | ||
throw new ConfigValidationError( | ||
`Field "${key}" is not of type "${schemeValue.type}"`, | ||
configFile.fileName, | ||
parentFiles | ||
); | ||
} | ||
} | ||
} | ||
} | ||
|
||
abstract loadConfig(config: ConfigFile): Promise<OutputT>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
/** | ||
* A type that represents a processed configuration value. | ||
* @since 0.11.0 | ||
*/ | ||
export type EvaluatedConfigValue = undefined | string | number | boolean | Array<EvaluatedConfigValue> | EvaluatedConfigFile; | ||
|
||
/** | ||
* A type that represents a processed configuration file. | ||
* @since 0.11.0 | ||
*/ | ||
export interface EvaluatedConfigFile { | ||
[key: string]: EvaluatedConfigValue; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/** | ||
* Generic error for the '@kipper/config' package. | ||
* @since 0.11.0 | ||
*/ | ||
export class ConfigError { | ||
public constructor(public message: string, public fileName: string, parentFiles: string[] = []) { | ||
this.message = `${message} (${[fileName, ...parentFiles].join(" -> ")})`; | ||
this.fileName = fileName; | ||
} | ||
} | ||
|
||
/** | ||
* Error that is thrown whenever a validation error is encountered. | ||
* @since 0.11.0 | ||
*/ | ||
export class ConfigValidationError extends ConfigError { | ||
public constructor(validationError: string, fileName: string, parentFiles?: string[]) { | ||
super( | ||
`Encountered validation error in file ${fileName}: ${validationError}`, | ||
"ConfigValidationError", | ||
parentFiles, | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* Error that is thrown whenever an unknown field is encountered. | ||
* @since 0.11.0 | ||
*/ | ||
export class UnknownFieldError extends ConfigError { | ||
public constructor(unknownField: string, fileName: string, parentFiles?: string[]) { | ||
super( | ||
`Encountered unknown field in file ${fileName}: ${unknownField}`, | ||
"UnknownFieldError", | ||
parentFiles, | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* Error that is thrown whenever an extends file is not found. | ||
* @since 0.11.0 | ||
*/ | ||
export class UnableToFindExtendsFileError extends ConfigError { | ||
public constructor(fileName: string, parentFiles?: string[]) { | ||
super( | ||
`Unable to find extends file for file ${fileName}`, | ||
"UnableToFindExtendsFileError", | ||
parentFiles, | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { EvaluatedConfigFile, EvaluatedConfigValue } from "./abstract/evaluated-config-file"; | ||
|
||
/** | ||
* A type that represents a path-like object. | ||
* @since 0.11.0 | ||
*/ | ||
export type PathLike = string; | ||
|
||
export interface RawEvaluatedKipperConfigFile extends EvaluatedConfigFile { | ||
version: string; | ||
resources: Array<PathLike>; | ||
compiler: { | ||
target: string; | ||
}; | ||
} | ||
|
||
/** | ||
* A class that represents a processed Kipper config file. | ||
* @since 0.11.0 | ||
*/ | ||
export class EvaluatedKipperConfigFile implements RawEvaluatedKipperConfigFile { | ||
[key: string]: EvaluatedConfigValue; | ||
|
||
version: RawEvaluatedKipperConfigFile["version"]; | ||
resources: RawEvaluatedKipperConfigFile["resources"]; | ||
compiler: RawEvaluatedKipperConfigFile["compiler"]; | ||
|
||
public constructor(config: RawEvaluatedKipperConfigFile) { | ||
this.version = config.version; | ||
this.resources = config.resources; | ||
this.compiler = config.compiler; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import * as fs from 'fs/promises'; | ||
import type { KipperEncoding } from "@kipper/cli"; | ||
import { ConfigFile } from "./abstract/config-file"; | ||
import * as path from "node:path"; | ||
|
||
/** | ||
* A class that represents a Kipper config file. | ||
* @since 0.11.0 | ||
*/ | ||
export class KipperConfigFile extends ConfigFile { | ||
protected constructor(content: string, fileName: string = "<string>", encoding: KipperEncoding = "utf8") { | ||
super(content, fileName, encoding); | ||
} | ||
|
||
/** | ||
* Create a new KipperConfigFile from a string. | ||
* @param content The content of the file. | ||
* @param encoding The encoding of the file. As we are running in a JavaScript environment, the default | ||
* {@link encoding} is always assumed to be the internal encoding of the JavaScript environment i.e. UTF-16 (utf16le). | ||
* @since 0.11.0 | ||
*/ | ||
static fromString(content: string, encoding: KipperEncoding = "utf16le"): KipperConfigFile { | ||
return new KipperConfigFile(content, "<string>", encoding); | ||
} | ||
|
||
/** | ||
* Create a new KipperConfigFile from a file. | ||
* @param file The file to read. | ||
* @param encoding The encoding of the file. | ||
* @since 0.11.0 | ||
*/ | ||
static async fromFile(file: string, encoding: KipperEncoding): Promise<KipperConfigFile> { | ||
const fileContent = await fs.readFile(file, { encoding }); | ||
const fileName = path.basename(file); | ||
return new KipperConfigFile(fileContent, fileName, encoding); | ||
} | ||
} |
Oops, something went wrong.