fix ns.prompt typechecking

Introduced some new type assertion functions, initial step in larger type safety rework.
This commit is contained in:
omuretsu 2022-10-14 11:00:29 -04:00
parent 6c84d64e67
commit ad0be471ce
4 changed files with 121 additions and 61 deletions

@ -65,6 +65,9 @@ export const helpers = {
failOnHacknetServer, failOnHacknetServer,
}; };
/** Will probably remove the below function in favor of a different approach to object type assertion.
* This method cannot be used to handle optional properties. */
export function assertObjectType<T extends object>( export function assertObjectType<T extends object>(
ctx: NetscriptContext, ctx: NetscriptContext,
name: string, name: string,

@ -61,14 +61,7 @@ import { NetscriptCorporation } from "./NetscriptFunctions/Corporation";
import { NetscriptFormulas } from "./NetscriptFunctions/Formulas"; import { NetscriptFormulas } from "./NetscriptFunctions/Formulas";
import { NetscriptStockMarket } from "./NetscriptFunctions/StockMarket"; import { NetscriptStockMarket } from "./NetscriptFunctions/StockMarket";
import { NetscriptGrafting } from "./NetscriptFunctions/Grafting"; import { NetscriptGrafting } from "./NetscriptFunctions/Grafting";
import { import { NS, RecentScript as IRecentScript, BasicHGWOptions, ProcessInfo } from "./ScriptEditor/NetscriptDefinitions";
NS,
RecentScript as IRecentScript,
BasicHGWOptions,
ProcessInfo,
MoneySource as IMoneySource,
MoneySources as IMoneySources,
} from "./ScriptEditor/NetscriptDefinitions";
import { NetscriptSingularity } from "./NetscriptFunctions/Singularity"; import { NetscriptSingularity } from "./NetscriptFunctions/Singularity";
import { dialogBoxCreate } from "./ui/React/DialogBox"; import { dialogBoxCreate } from "./ui/React/DialogBox";
@ -83,6 +76,7 @@ import { InternalAPI, wrapAPI } from "./Netscript/APIWrapper";
import { INetscriptExtra } from "./NetscriptFunctions/Extra"; import { INetscriptExtra } from "./NetscriptFunctions/Extra";
import { ScriptDeath } from "./Netscript/ScriptDeath"; import { ScriptDeath } from "./Netscript/ScriptDeath";
import { getBitNodeMultipliers } from "./BitNode/BitNode"; import { getBitNodeMultipliers } from "./BitNode/BitNode";
import { assert, arrayAssert, stringAssert, objectAssert } from "./utils/helpers/typeAssertion";
// "Enums" as object // "Enums" as object
export const enums = { export const enums = {
@ -1759,60 +1753,81 @@ const base: InternalAPI<NS> = {
throw new Error(`variant must be one of ${Object.values(ToastVariant).join(", ")}`); throw new Error(`variant must be one of ${Object.values(ToastVariant).join(", ")}`);
SnackbarEvents.emit(message, variant as ToastVariant, duration); SnackbarEvents.emit(message, variant as ToastVariant, duration);
}, },
prompt: prompt: (ctx) => (_txt, _options) => {
(ctx) => const options: { type?: string; choices?: string[] } = {};
(_txt, options = {}) => { _options ??= options;
const txt = helpers.string(ctx, "txt", _txt); const txt = helpers.string(ctx, "txt", _txt);
const optionsValidator: { type?: string; options?: string[] } = {}; assert(_options, objectAssert, (type) =>
assertObjectType(ctx, "options", options, optionsValidator); helpers.makeRuntimeErrorMsg(ctx, `Invalid type for options: ${type}, should be object.`, "TYPE"),
);
return new Promise(function (resolve) { if (_options.type !== undefined) {
PromptEvent.emit({ assert(_options.type, stringAssert, (type) =>
txt: txt, helpers.makeRuntimeErrorMsg(ctx, `Invalid type for options.type: ${type}, should be string.`, "TYPE"),
options, );
resolve: resolve, options.type = _options.type;
}); const validTypes = ["boolean", "text", "select"];
}); if (!["boolean", "text", "select"].includes(options.type)) {
}, throw helpers.makeRuntimeErrorMsg(
wget: ctx,
(ctx) => `Invalid value for options.type: ${options.type}. Must be one of ${validTypes.join(", ")}.`,
async (_url, _target, _hostname = ctx.workerScript.hostname) => { );
const url = helpers.string(ctx, "url", _url);
const target = helpers.string(ctx, "target", _target);
const hostname = helpers.string(ctx, "hostname", _hostname);
if (!isScriptFilename(target) && !target.endsWith(".txt")) {
helpers.log(ctx, () => `Invalid target file: '${target}'. Must be a script or text file.`);
return Promise.resolve(false);
} }
const s = helpers.getServer(ctx, hostname); if (options.type === "select") {
return new Promise(function (resolve) { assert(_options.choices, arrayAssert, (type) =>
$.get( helpers.makeRuntimeErrorMsg(
url, ctx,
function (data) { `Invalid type for options.choices: ${type}. If options.type is "select", options.choices must be an array.`,
let res; "TYPE",
if (isScriptFilename(target)) { ),
res = s.writeToScriptFile(target, data); );
} else { options.choices = _options.choices.map((choice, i) => helpers.string(ctx, `options.choices[${i}]`, choice));
res = s.writeToTextFile(target, data); }
} }
if (!res.success) { return new Promise(function (resolve) {
helpers.log(ctx, () => "Failed."); PromptEvent.emit({
return resolve(false); txt: txt,
} options,
if (res.overwritten) { resolve: resolve,
helpers.log(ctx, () => `Successfully retrieved content and overwrote '${target}' on '${hostname}'`);
return resolve(true);
}
helpers.log(ctx, () => `Successfully retrieved content to new file '${target}' on '${hostname}'`);
return resolve(true);
},
"text",
).fail(function (e) {
helpers.log(ctx, () => JSON.stringify(e));
return resolve(false);
});
}); });
}, });
},
wget: (ctx) => async (_url, _target, _hostname) => {
const url = helpers.string(ctx, "url", _url);
const target = helpers.string(ctx, "target", _target);
const hostname = _hostname ? helpers.string(ctx, "hostname", _hostname) : ctx.workerScript.hostname;
if (!isScriptFilename(target) && !target.endsWith(".txt")) {
helpers.log(ctx, () => `Invalid target file: '${target}'. Must be a script or text file.`);
return Promise.resolve(false);
}
const s = helpers.getServer(ctx, hostname);
return new Promise(function (resolve) {
$.get(
url,
function (data) {
let res;
if (isScriptFilename(target)) {
res = s.writeToScriptFile(target, data);
} else {
res = s.writeToTextFile(target, data);
}
if (!res.success) {
helpers.log(ctx, () => "Failed.");
return resolve(false);
}
if (res.overwritten) {
helpers.log(ctx, () => `Successfully retrieved content and overwrote '${target}' on '${hostname}'`);
return resolve(true);
}
helpers.log(ctx, () => `Successfully retrieved content to new file '${target}' on '${hostname}'`);
return resolve(true);
},
"text",
).fail(function (e) {
helpers.log(ctx, () => JSON.stringify(e));
return resolve(false);
});
});
},
getFavorToDonate: () => () => { getFavorToDonate: () => () => {
return Math.floor(CONSTANTS.BaseFavorToDonate * BitNodeMultipliers.RepToDonateToFaction); return Math.floor(CONSTANTS.BaseFavorToDonate * BitNodeMultipliers.RepToDonateToFaction);
}, },

@ -6613,7 +6613,7 @@ export interface NS {
*/ */
prompt( prompt(
txt: string, txt: string,
options?: { type?: "boolean" | "text" | "select" | undefined; choices?: string[] }, options?: { type?: "boolean" | "text" | "select"; choices?: string[] },
): Promise<boolean | string>; ): Promise<boolean | string>;
/** /**

@ -0,0 +1,42 @@
// Various functions for asserting types.
/** Function for providing custom error message to throw for a type assertion.
* @param v: Value to assert type of
* @param assertFn: Typechecking function to use for asserting type of v.
* @param msgFn: Function to use to generate an error message if an error is produced. */
export function assert<T>(
v: unknown,
assertFn: (v: unknown) => asserts v is T,
msgFn: (type: string) => string,
): asserts v is T {
try {
assertFn(v);
} catch (type: unknown) {
if (type !== "string") type = "unknown";
throw msgFn(type as string);
}
}
/** Returns the friendlyType of v. arrays are "array" and null is "null". */
export function getFriendlyType(v: unknown): string {
return v === null ? "null" : Array.isArray(v) ? "array" : typeof v;
}
//All assertion functions used here should return the friendlyType of the input.
/** For non-objects, and for array/null, throws the friendlyType of v. */
export function objectAssert(v: unknown): asserts v is Partial<Record<string, unknown>> {
const type = getFriendlyType(v);
if (type !== "object") throw type;
}
/** For non-string, throws the friendlyType of v. */
export function stringAssert(v: unknown): asserts v is string {
const type = getFriendlyType(v);
if (type !== "string") throw type;
}
/** For non-array, throws the friendlyType of v. */
export function arrayAssert(v: unknown): asserts v is unknown[] {
if (!Array.isArray(v)) throw getFriendlyType(v);
}