From 910ceb0209ccbd90499781bb1a44ee013ffcdeee Mon Sep 17 00:00:00 2001 From: Enderson Maia Date: Mon, 14 Oct 2024 17:00:30 -0300 Subject: [PATCH] fixup! feat(cli): build based on configuration implement typed errors for config module --- apps/cli/src/config.ts | 96 ++++++++++++++++++++++++++++++------ apps/cli/test/config.test.ts | 91 +++++++++++++++++++++++++++++----- 2 files changed, 161 insertions(+), 26 deletions(-) diff --git a/apps/cli/src/config.ts b/apps/cli/src/config.ts index 9f58420c..86c5d4f7 100644 --- a/apps/cli/src/config.ts +++ b/apps/cli/src/config.ts @@ -3,6 +3,72 @@ import os from "os"; import { extname } from "path"; import { TomlPrimitive, parse as parseToml } from "smol-toml"; +/** + * Typed Errors + */ +export class InvalidBuilderError extends Error { + constructor(builder: any) { + super(`Invalid builder: ${builder}`); + this.name = "InvalidBuilder"; + } +} + +export class InvalidDriveFormatError extends Error { + constructor(format: any) { + super(`Invalid drive format: ${format}`); + this.name = "InvalidDriveFormatError"; + } +} + +export class InvalidEmptyDriveFormatError extends Error { + constructor(format: any) { + super(`Invalid empty drive format: ${format}`); + this.name = "InvalidEmptyDriveFormatError"; + } +} + +export class InvalidStringValueError extends Error { + constructor(value: any) { + super(`Invalid string value: ${value}`); + this.name = "InvalidStringValueError"; + } +} + +export class InvalidBooleanValueError extends Error { + constructor(value: any) { + super(`Invalid boolean value: ${value}`); + this.name = "InvalidBooleanValueError"; + } +} + +export class InvalidNumberValueError extends Error { + constructor(value: any) { + super(`Invalid number value: ${value}`); + this.name = "InvalidNumberValueError"; + } +} + +export class InvalidBytesValueError extends Error { + constructor(value: any) { + super(`Invalid bytes value: ${value}`); + this.name = "InvalidBytesValueError"; + } +} + +export class RequiredFieldError extends Error { + constructor(key: string) { + super(`Missing required field: ${key}`); + this.name = "RequiredFieldError"; + } +} + +export class InvalidStringArrayError extends Error { + constructor() { + super(`Invalid string array`); + this.name = "InvalidStringArrayError"; + } +} + /** * Configuration for drives of a Cartesi Machine. A drive may already exist or be built by a builder */ @@ -141,7 +207,7 @@ const parseBoolean = (value: TomlPrimitive, defaultValue: boolean): boolean => { } else if (typeof value === "boolean") { return value; } - throw new Error(`Invalid boolean value: ${value}`); + throw new InvalidBooleanValueError(value); }; const parseOptionalBoolean = (value: TomlPrimitive): boolean | undefined => { @@ -150,7 +216,7 @@ const parseOptionalBoolean = (value: TomlPrimitive): boolean | undefined => { } else if (typeof value === "boolean") { return value; } - throw new Error(`Invalid boolean value: ${value}`); + throw new InvalidBooleanValueError(value); }; const parseString = (value: TomlPrimitive, defaultValue: string): string => { @@ -159,7 +225,7 @@ const parseString = (value: TomlPrimitive, defaultValue: string): string => { } else if (typeof value === "string") { return value; } - throw new Error(`Invalid string value: ${value}`); + throw new InvalidStringValueError(value); }; const parseStringArray = (value: TomlPrimitive): string[] => { @@ -172,19 +238,19 @@ const parseStringArray = (value: TomlPrimitive): string[] => { if (typeof v === "string") { return v; } - throw new Error(`Invalid string value: ${v}`); + throw new InvalidStringValueError(v); }); } - throw new Error(`Invalid string array value: ${value}`); + throw new InvalidStringArrayError(); }; const parseRequiredString = (value: TomlPrimitive, key: string): string => { if (value === undefined) { - throw new Error(`Missing required value: ${key}`); + throw new RequiredFieldError(key); } else if (typeof value === "string") { return value; } - throw new Error(`Invalid string value: ${value}`); + throw new InvalidStringValueError(value); }; const parseOptionalString = (value: TomlPrimitive): string | undefined => { @@ -193,7 +259,7 @@ const parseOptionalString = (value: TomlPrimitive): string | undefined => { } else if (typeof value === "string") { return value; } - throw new Error(`Invalid string value: ${value}`); + throw new InvalidStringValueError(value); }; const parseOptionalStringBoolean = ( @@ -206,7 +272,7 @@ const parseOptionalStringBoolean = ( } else if (typeof value === "boolean") { return value; } - throw new Error(`Invalid string value: ${value}`); + throw new InvalidStringValueError(value); }; const parseOptionalNumber = (value: TomlPrimitive): bigint | undefined => { @@ -217,7 +283,7 @@ const parseOptionalNumber = (value: TomlPrimitive): bigint | undefined => { } else if (typeof value === "number") { return BigInt(value); } - throw new Error(`Invalid number value: ${value}`); + throw new InvalidNumberValueError(value); }; const parseBytes = (value: TomlPrimitive, defaultValue: number): number => { @@ -228,7 +294,7 @@ const parseBytes = (value: TomlPrimitive, defaultValue: number): number => { } else if (typeof value === "number" || typeof value === "string") { return bytes.parse(value); } - throw new Error(`Invalid bytes value: ${value}`); + throw new InvalidBytesValueError(value); }; const parseBuilder = (value: TomlPrimitive): Builder => { @@ -248,7 +314,7 @@ const parseBuilder = (value: TomlPrimitive): Builder => { return "tar"; } } - throw new Error(`Invalid builder: ${value}`); + throw new InvalidBuilderError(value as string); }; const parseFormat = (value: TomlPrimitive): DriveFormat => { @@ -262,7 +328,7 @@ const parseFormat = (value: TomlPrimitive): DriveFormat => { return "sqfs"; } } - throw new Error(`Invalid format: ${value}`); + throw new InvalidDriveFormatError(value as string); }; const parseEmptyFormat = (value: TomlPrimitive): "ext2" | "raw" => { @@ -276,7 +342,7 @@ const parseEmptyFormat = (value: TomlPrimitive): "ext2" | "raw" => { return "raw"; } } - throw new Error(`Invalid format: ${value}`); + throw new InvalidEmptyDriveFormatError(value as string); }; const parseMachine = (value: TomlPrimitive): MachineConfig => { @@ -313,7 +379,7 @@ export const getDriveFormat = (filename: string): DriveFormat => { case ".sqfs": return "sqfs"; default: - throw new Error(`Invalid drive format: ${extension}`); + throw new InvalidDriveFormatError(extension); } }; diff --git a/apps/cli/test/config.test.ts b/apps/cli/test/config.test.ts index 6d4fbcd5..7ddb0a79 100644 --- a/apps/cli/test/config.test.ts +++ b/apps/cli/test/config.test.ts @@ -1,5 +1,15 @@ import { describe, expect, it } from "vitest"; -import { defaultConfig, defaultMachineConfig, parse } from "../src/config.js"; +import { + defaultConfig, + defaultMachineConfig, + InvalidBooleanValueError, + InvalidBuilderError, + InvalidDriveFormatError, + InvalidEmptyDriveFormatError, + InvalidStringValueError, + parse, + RequiredFieldError, +} from "../src/config.js"; describe("config", () => { it("default config", () => { @@ -41,47 +51,106 @@ shared = true`); it("invalid drive: invalid builder", () => { expect(() => parse('[drives.root]\nbuilder = "invalid"')).toThrowError( - "Invalid builder: invalid", + new InvalidBuilderError("invalid"), ); expect(() => parse("[drives.root]\nbuilder = true")).toThrowError( - "Invalid builder: true", + new InvalidBuilderError(true), ); expect(() => parse("[drives.root]\nbuilder = 10")).toThrowError( - "Invalid builder: 10", + new InvalidBuilderError(10), ); expect(() => parse("[drives.root]\nbuilder = {}")).toThrowError( - "Invalid builder: [object Object]", + new InvalidBuilderError({}), ); }); it("invalid drive: invalid format", () => { expect(() => parse('[drives.root]\nformat = "invalid"')).toThrowError( - "Invalid format: invalid", + new InvalidDriveFormatError("invalid"), ); expect(() => parse("[drives.root]\nformat = true")).toThrowError( - "Invalid format: true", + new InvalidDriveFormatError(true), ); expect(() => parse("[drives.root]\nformat = 10")).toThrowError( - "Invalid format: 10", + new InvalidDriveFormatError(10), ); expect(() => parse("[drives.root]\nformat = {}")).toThrowError( - "Invalid format: [object Object]", + new InvalidDriveFormatError({}), + ); + }); + + it("invalid drive: invalid extension", () => { + const builderNone = ` + [drives.none] + builder = "none" + filename = "./games/doom.xyzfs" + mount = "/usr/local/games/doom" + `; + expect(() => parse(builderNone)).toThrowError( + new InvalidDriveFormatError(".xyzfs"), ); }); it("invalid drive: invalid mount", () => { expect(() => parse("[drives.data]\nmount = 42")).toThrowError( - "Invalid string value: 42", + new InvalidStringValueError(42), + ); + }); + + it("invalid empty drive: invalid fomat", () => { + expect(() => + parse("[drives.data]\nbuilder = 'empty'\nformat = 42"), + ).toThrowError(new InvalidEmptyDriveFormatError(42)); + }); + + it("invalid boolean value", () => { + expect(() => parse("[machine]\nno-rollup = 42")).toThrowError( + new InvalidBooleanValueError(42), + ); + }); + + it("invalid string value", () => { + const invalidTarDrive = ` + [drives.data] + builder = "tar" + filename = 42 # invalid + format = "ext2" + `; + expect(() => parse(invalidTarDrive)).toThrowError( + new InvalidStringValueError(42), + ); + }); + + it("required field", () => { + const invalidDirectoryDrive = ` + [drives.data] + builder = "directory" + # directory = '' # required + `; + expect(() => parse(invalidDirectoryDrive)).toThrowError( + new RequiredFieldError("directory"), //XXX: how to know which field was required ); }); it("machine-config", () => { - expect(parse("[machine]\nno-rollup = true")).toEqual({ + const config = ` + [machine] + no-rollup = true + `; + expect(parse(config)).toEqual({ ...defaultConfig(), machine: { ...defaultMachineConfig(), noRollup: true, }, }); + + const invalidConfig = ` + ${config} + bootargs = ["no4lvl", "quiet", false] + `; + expect(() => parse(invalidConfig)).toThrowError( + new InvalidStringValueError(false), + ); }); });