Skip to content

Commit

Permalink
major (#508): Implemented core functionality of the '@kipper/config' …
Browse files Browse the repository at this point in the history
…package
  • Loading branch information
Luna-Klatzer committed Feb 11, 2024
1 parent 04e7d79 commit 7f62eef
Show file tree
Hide file tree
Showing 9 changed files with 393 additions and 19 deletions.
22 changes: 3 additions & 19 deletions kipper/config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"version": "0.11.0-alpha.0",
"author": "Luna-Klatzer @Luna-Klatzer",
"devDependencies": {
"@kipper/cli": "workspace:~",
"typescript": "5.1.3"
},
"engines": {
Expand All @@ -24,19 +25,7 @@
"files": [
"lib",
"src",
"LICENSE",
"KipperLexer.g4",
"KipperParser.g4"
],
"size-limit": [
{
"path": "./kipper-standalone.min.js",
"limit": "200 kB"
},
{
"path": "./kipper-standalone.js",
"limit": "400 kB"
}
"LICENSE"
],
"license": "GPL-3.0-or-later",
"main": "lib/index.js",
Expand All @@ -45,14 +34,9 @@
"prepack": "pnpm run build",
"build": "tsc",
"version": "pnpm version",
"antlr4ts": "antlr4ts -visitor -o ./src/compiler/parser/antlr ./KipperLexer.g4 ./KipperParser.g4",
"postantlr4ts": "run-script-os",
"postantlr4ts:linux:macos:default": "cp ./src/compiler/parser/antlr/*.interp ./lib/compiler/parser/antlr/ && cp ./src/compiler/parser/antlr/*.tokens ./lib/compiler/parser/antlr/ && cp ./src/compiler/parser/antlr/*.tokens ./",
"postantlr4ts:windows": "copy .\\src\\compiler\\parser\\antlr\\*.interp .\\lib\\compiler\\parser\\antlr\\ /Y && copy .\\src\\compiler\\parser\\antlr\\*.tokens .\\lib\\compiler\\parser\\antlr\\ && copy .\\src\\compiler\\parser\\antlr\\*.tokens .\\",
"lint": "pnpm run tslint",
"lint:fix": "pnpm run tslint:fix",
"tslint": "eslint ./src/ --ext .ts --config ./../../.eslintrc",
"tslint:fix": "eslint ./src/ --ext .ts --config ./../../.eslintrc --fix",
"size-limit": "size-limit"
"tslint:fix": "eslint ./src/ --ext .ts --config ./../../.eslintrc --fix"
}
}
18 changes: 18 additions & 0 deletions kipper/config/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions kipper/config/src/abstract/config-file.ts
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;
}
}
133 changes: 133 additions & 0 deletions kipper/config/src/abstract/config-interpreter.ts
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>;
}
13 changes: 13 additions & 0 deletions kipper/config/src/abstract/evaluated-config-file.ts
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;
}
52 changes: 52 additions & 0 deletions kipper/config/src/errors.ts
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,
);
}
}
33 changes: 33 additions & 0 deletions kipper/config/src/evaluated-kipper-config-file.ts
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;
}
}
37 changes: 37 additions & 0 deletions kipper/config/src/kipper-config-file.ts
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);
}
}
Loading

0 comments on commit 7f62eef

Please sign in to comment.