Skip to content

Commit

Permalink
minor (#508): Fixed many bugs in the @kipper/config package
Browse files Browse the repository at this point in the history
  • Loading branch information
Luna-Klatzer committed Feb 13, 2024
1 parent c6cdca1 commit 9a37144
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 80 deletions.
2 changes: 2 additions & 0 deletions kipper/config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"version": "0.11.0-alpha.1",
"author": "Luna-Klatzer @Luna-Klatzer",
"dependencies": {
"deepmerge": "4.3.1",
"is-plain-object": "5.0.0",
"semver": "7.6.0",
"tslib": "~2.6.2"
},
Expand Down
34 changes: 34 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.

9 changes: 7 additions & 2 deletions kipper/config/src/abstract/config-file.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { KipperEncoding } from "@kipper/cli";
import { ConfigErrorMetaData, JSONSyntaxError } from "../errors";

/**
* An abstract class that represents a configuration file.
Expand All @@ -10,10 +11,14 @@ export abstract class ConfigFile {
public readonly fileName: string;
public readonly encoding: KipperEncoding;

protected constructor(content: string, fileName: string, encoding: KipperEncoding) {
protected constructor(content: string, fileName: string, encoding: KipperEncoding, meta?: ConfigErrorMetaData) {
this.content = content;
this.parsedJSON = JSON.parse(content);
this.fileName = fileName;
this.encoding = encoding;
try {
this.parsedJSON = JSON.parse(content);
} catch (e) {
throw new JSONSyntaxError(String(e), { fileName, refChain: meta?.refChain ?? [] });
}
}
}
124 changes: 86 additions & 38 deletions kipper/config/src/abstract/config-interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,44 @@ import { ConfigFile } from "./config-file";
import { ConfigErrorMetaData, ConfigValidationError, UnknownFieldError } from "../errors";
import { EvaluatedKipperConfigFile, RawEvaluatedKipperConfigFile } from "../evaluated-kipper-config-file";

/**
* A type that represents a configuration scheme definition.
* @since 0.11.0
*/
export type ConfigInterpreterSchemeDefinition = {
type: "string" | "number" | "boolean";
required: boolean;
// eslint-disable-next-line no-mixed-spaces-and-tabs
}
| {
type: "array";
required: boolean;
itemType: "string" | "number" | "boolean";
// eslint-disable-next-line no-mixed-spaces-and-tabs
}
| {
type: "object";
required: boolean;
properties: ConfigInterpreterScheme;
// eslint-disable-next-line no-mixed-spaces-and-tabs
}
| {
type: "union";
required: boolean;
possibilities: (
{ type: "string" | "number" | "boolean" } |
{ type: "array"; itemType: "string" | "number" | "boolean" } |
{ type: "object"; properties: ConfigInterpreterScheme }
)[]
// eslint-disable-next-line no-mixed-spaces-and-tabs
};

/**
* A type that represents a configuration scheme.
* @since 0.11.0
*/
export type ConfigInterpreterScheme = {
[key: string]:
| {
type: "string" | "number" | "boolean";
required: boolean;
// eslint-disable-next-line no-mixed-spaces-and-tabs
}
| {
type: "array";
required: boolean;
itemType: "string" | "number" | "boolean";
// eslint-disable-next-line no-mixed-spaces-and-tabs
}
| {
type: "object";
required: boolean;
properties: ConfigInterpreterScheme;
// eslint-disable-next-line no-mixed-spaces-and-tabs
};
[key: string]: ConfigInterpreterSchemeDefinition;
};

/**
Expand Down Expand Up @@ -111,26 +126,7 @@ export abstract class ConfigInterpreter<SchemeT extends ConfigInterpreterScheme,
if (schemeValue.required && configValue === undefined && !(parentConfig && key in parentConfig)) {
throw new ConfigValidationError(`Missing required field "${key}"`, meta);
} else if (configValue) {
if (schemeValue.type === "object") {
this.validateConfigBasedOffSchemeRecursive(configValue, schemeValue.properties, meta);
} else if (schemeValue.type === "array") {
if (!Array.isArray(configValue)) {
throw new ConfigValidationError(`Field "${key}" is not an array, but it should be`, meta);
}

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}"`,
meta,
);
}
}
} else {
if (typeof configValue !== schemeValue.type) {
throw new ConfigValidationError(`Field "${key}" is not of type "${schemeValue.type}"`, meta);
}
}
this.checkType(schemeValue, configValue, key, meta);
}
}

Expand All @@ -141,5 +137,57 @@ export abstract class ConfigInterpreter<SchemeT extends ConfigInterpreterScheme,
});
}

private checkType(
schemeValue: ConfigInterpreterSchemeDefinition,
configValue: any,
key: string,
meta: ConfigErrorMetaData,
) {
if (schemeValue.type === "union") {
const possibilities = schemeValue.possibilities;

// Ensure that at least one type is valid
let valid = false;
for (const possibility of possibilities) {
try {
this.checkType({...possibility, required: schemeValue.required}, configValue, key, meta);
valid = true;
break;
} catch (e) {
if (!(e instanceof ConfigValidationError)) {
throw e;
}
}
}

if (!valid) {
throw new ConfigValidationError(
`Field "${key}" is not of any of the types '${possibilities.map((p) => p.type).join(", ")}'`,
meta,
);
}
} else {
if (
(schemeValue.type === "array" && !Array.isArray(configValue)) ||
(schemeValue.type !== "array" && schemeValue.type !== typeof configValue)
) {
throw new ConfigValidationError(`Field "${key}" is not of type "${schemeValue.type}"`, meta);
}

if (schemeValue.type === "object") {
this.validateConfigBasedOffSchemeRecursive(configValue, schemeValue.properties, meta);
} else if (schemeValue.type === "array") {
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}"`,
meta,
);
}
}
}
}
}

abstract loadConfig(config: ConfigFile): Promise<OutputT>;
}
14 changes: 12 additions & 2 deletions kipper/config/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { clean } from "semver";
* The metadata for a config error, which is used to provide verbose error messages.
* @since 0.11.0
*/
export type ConfigErrorMetaData = { fileName: string; parentFiles: string[] };
export type ConfigErrorMetaData = { fileName: string; refChain: string[] };

/**
* Generic error for the '@kipper/config' package.
* @since 0.11.0
*/
export class ConfigError {
public constructor(public message: string, public meta?: ConfigErrorMetaData) {
this.message = `${message}${meta ? ` (${[meta.fileName, ...meta.parentFiles].join(" -> ")})` : ""}`;
this.message = `${message}${meta ? ` (${[...meta.refChain, meta.fileName].join(" -> ")})` : ""}`;
}
}

Expand Down Expand Up @@ -46,6 +46,16 @@ export class ConfigInterpreterError extends ConfigError {
}
}

/**
* Error that is thrown whenever a file is not found.
* @since 0.11.0
*/
export class JSONSyntaxError extends ConfigInterpreterError {
public constructor(details: string, meta: ConfigErrorMetaData) {
super(`Invalid JSON syntax ~ ${details}`, meta);
}
}

/**
* Error that is thrown whenever a referenced directory or file is not found.
* @since 0.11.0
Expand Down
2 changes: 1 addition & 1 deletion kipper/config/src/evaluated-kipper-config-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ export class EvaluatedKipperConfigFile implements RawEvaluatedKipperConfigFile {
compiler: RawEvaluatedKipperConfigFile["compiler"];

public constructor(config: RawEvaluatedKipperConfigFile) {
this.raw = config;
this.basePath = config.basePath;
this.outDir = config.outDir;
this.srcDir = config.srcDir;
this.files = config.files;
this.resources = config.resources;
this.compiler = config.compiler;
this.raw = config;
}
}
18 changes: 13 additions & 5 deletions kipper/config/src/kipper-config-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ import { ConfigFile } from "./abstract/config-file";
import * as path from "node:path";
import * as fs from "node:fs/promises";
import { ensureExistsHasPermAndIsOfType } from "./tools";
import { ConfigErrorMetaData } from "./errors";

/**
* 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);
protected constructor(
content: string,
fileName: string = "<string>",
encoding: KipperEncoding = "utf8",
meta?: ConfigErrorMetaData
) {
super(content, fileName, encoding, meta);
}

/**
Expand All @@ -28,13 +34,15 @@ export class KipperConfigFile extends ConfigFile {
* Create a new KipperConfigFile from a file.
* @param file The file to read.
* @param encoding The encoding of the file.
* @param meta The metadata for the error. This is primarily only used when resolving extension files, and does not
* need to be provided when manually creating a KipperConfigFile.
* @since 0.11.0
*/
static async fromFile(file: string, encoding: KipperEncoding): Promise<KipperConfigFile> {
await ensureExistsHasPermAndIsOfType(file, "r", "file");
static async fromFile(file: string, encoding: KipperEncoding, meta?: ConfigErrorMetaData): Promise<KipperConfigFile> {
await ensureExistsHasPermAndIsOfType(file, "r", "file", meta);

const fileContent = await fs.readFile(file, { encoding });
const fileName = path.basename(file);
return new KipperConfigFile(fileContent, fileName, encoding);
return new KipperConfigFile(fileContent, fileName, encoding, meta);
}
}
Loading

0 comments on commit 9a37144

Please sign in to comment.