@optique/core
Version:
Type-safe combinatorial command-line interface parser
325 lines (323 loc) • 10.4 kB
JavaScript
import { message, text } from "./message.js";
//#region src/valueparser.ts
/**
* A predicate function that checks if an object is a {@link ValueParser}.
* @param object The object to check.
* @return `true` if the object is a {@link ValueParser}, `false` otherwise.
*/
function isValueParser(object) {
return typeof object === "object" && object != null && "metavar" in object && typeof object.metavar === "string" && "parse" in object && typeof object.parse === "function" && "format" in object && typeof object.format === "function";
}
/**
* Creates a {@link ValueParser} that accepts one of multiple
* string values, so-called enumerated values.
*
* This parser validates that the input string matches one of
* the specified values. If the input does not match any of the values,
* it returns an error message indicating the valid options.
* @param values An array of valid string values that this parser can accept.
* @param options Configuration options for the choice parser.
* @returns A {@link ValueParser} that checks if the input matches one of the
* specified values.
*/
function choice(values, options = {}) {
const normalizedValues = options.caseInsensitive ? values.map((v) => v.toLowerCase()) : values;
return {
metavar: options.metavar ?? "TYPE",
parse(input) {
const normalizedInput = options.caseInsensitive ? input.toLowerCase() : input;
const index = normalizedValues.indexOf(normalizedInput);
if (index < 0) return {
success: false,
error: message`Expected one of ${values.join(", ")}, but got ${input}.`
};
return {
success: true,
value: values[index]
};
},
format(value) {
return value;
}
};
}
/**
* Creates a {@link ValueParser} for strings.
*
* This parser validates that the input is a string and optionally checks
* if it matches a specified regular expression pattern.
* @param options Configuration options for the string parser.
* @returns A {@link ValueParser} that parses strings according to the
* specified options.
*/
function string(options = {}) {
return {
metavar: options.metavar ?? "STRING",
parse(input) {
if (options.pattern != null && !options.pattern.test(input)) return {
success: false,
error: message`Expected a string matching pattern ${text(options.pattern.source)}, but got ${input}.`
};
return {
success: true,
value: input
};
},
format(value) {
return value;
}
};
}
/**
* Creates a ValueParser for parsing integer values from strings.
*
* This function provides two modes of operation:
*
* - Regular mode: Returns JavaScript numbers
* (safe up to `Number.MAX_SAFE_INTEGER`)
* - `bigint` mode: Returns `bigint` values for arbitrarily large integers
*
* The parser validates that the input is a valid integer and optionally
* enforces minimum and maximum value constraints.
*
* @example
* ```typescript
* // Create a parser for regular integers
* const portParser = integer({ min: 1, max: 0xffff });
*
* // Create a parser for BigInt values
* const bigIntParser = integer({ type: "bigint", min: 0n });
*
* // Use the parser
* const result = portParser.parse("8080");
* if (result.success) {
* console.log(`Port: ${result.value}`);
* } else {
* console.error(result.error);
* }
* ```
*
* @param options Configuration options specifying the type and constraints.
* @returns A {@link ValueParser} that converts string input to the specified
* integer type.
*/
function integer(options) {
if (options?.type === "bigint") return {
metavar: options.metavar ?? "INTEGER",
parse(input) {
let value;
try {
value = BigInt(input);
} catch (e) {
if (e instanceof SyntaxError) return {
success: false,
error: message`Expected a valid integer, but got ${input}.`
};
throw e;
}
if (options.min != null && value < options.min) return {
success: false,
error: message`Expected a value greater than or equal to ${text(options.min.toLocaleString("en"))}, but got ${input}.`
};
else if (options.max != null && value > options.max) return {
success: false,
error: message`Expected a value less than or equal to ${text(options.max.toLocaleString("en"))}, but got ${input}.`
};
return {
success: true,
value
};
},
format(value) {
return value.toString();
}
};
return {
metavar: options?.metavar ?? "INTEGER",
parse(input) {
if (!input.match(/^\d+$/)) return {
success: false,
error: message`Expected a valid integer, but got ${input}.`
};
const value = Number.parseInt(input);
if (options?.min != null && value < options.min) return {
success: false,
error: message`Expected a value greater than or equal to ${text(options.min.toLocaleString("en"))}, but got ${input}.`
};
else if (options?.max != null && value > options.max) return {
success: false,
error: message`Expected a value less than or equal to ${text(options.max.toLocaleString("en"))}, but got ${input}.`
};
return {
success: true,
value
};
},
format(value) {
return value.toString();
}
};
}
/**
* Creates a {@link ValueParser} for floating-point numbers.
*
* This parser validates that the input is a valid floating-point number
* and optionally enforces minimum and maximum value constraints.
* @param options Configuration options for the float parser.
* @returns A {@link ValueParser} that parses strings into floating-point
* numbers.
*/
function float(options = {}) {
const floatRegex = /^[+-]?(?:(?:\d+\.?\d*)|(?:\d*\.\d+))(?:[eE][+-]?\d+)?$/;
return {
metavar: options.metavar ?? "NUMBER",
parse(input) {
let value;
const lowerInput = input.toLowerCase();
if (lowerInput === "nan" && options.allowNaN) value = NaN;
else if ((lowerInput === "infinity" || lowerInput === "+infinity") && options.allowInfinity) value = Infinity;
else if (lowerInput === "-infinity" && options.allowInfinity) value = -Infinity;
else if (floatRegex.test(input)) {
value = Number(input);
if (Number.isNaN(value)) return {
success: false,
error: message`Expected a valid number, but got ${input}.`
};
} else return {
success: false,
error: message`Expected a valid number, but got ${input}.`
};
if (options.min != null && value < options.min) return {
success: false,
error: message`Expected a value greater than or equal to ${text(options.min.toLocaleString("en"))}, but got ${input}.`
};
else if (options.max != null && value > options.max) return {
success: false,
error: message`Expected a value less than or equal to ${text(options.max.toLocaleString("en"))}, but got ${input}.`
};
return {
success: true,
value
};
},
format(value) {
return value.toString();
}
};
}
/**
* Creates a {@link ValueParser} for URL values.
*
* This parser validates that the input is a well-formed URL and optionally
* restricts the allowed protocols. The parsed result is a JavaScript `URL`
* object.
* @param options Configuration options for the URL parser.
* @returns A {@link ValueParser} that converts string input to `URL` objects.
*/
function url(options = {}) {
const allowedProtocols = options.allowedProtocols?.map((p) => p.toLowerCase());
return {
metavar: options.metavar ?? "URL",
parse(input) {
if (!URL.canParse(input)) return {
success: false,
error: message`Invalid URL: ${input}.`
};
const url$1 = new URL(input);
if (allowedProtocols != null && !allowedProtocols.includes(url$1.protocol)) return {
success: false,
error: message`URL protocol ${url$1.protocol} is not allowed. Allowed protocols: ${allowedProtocols.join(", ")}.`
};
return {
success: true,
value: url$1
};
},
format(value) {
return value.href;
}
};
}
/**
* Creates a {@link ValueParser} for locale values.
*
* This parser validates that the input is a well-formed locale identifier
* according to the Unicode Locale Identifier standard (BCP 47).
* The parsed result is a JavaScript `Intl.Locale` object.
* @param options Configuration options for the locale parser.
* @returns A {@link ValueParser} that converts string input to `Intl.Locale`
* objects.
*/
function locale(options = {}) {
return {
metavar: options.metavar ?? "LOCALE",
parse(input) {
let locale$1;
try {
locale$1 = new Intl.Locale(input);
} catch (e) {
if (e instanceof RangeError) return {
success: false,
error: message`Invalid locale: ${input}.`
};
throw e;
}
return {
success: true,
value: locale$1
};
},
format(value) {
return value.baseName;
}
};
}
/**
* Creates a {@link ValueParser} for UUID values.
*
* This parser validates that the input is a well-formed UUID string in the
* standard format: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` where each `x`
* is a hexadecimal digit. The parser can optionally restrict to specific
* UUID versions.
*
* @param options Configuration options for the UUID parser.
* @returns A {@link ValueParser} that converts string input to {@link Uuid}
* strings.
*/
function uuid(options = {}) {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return {
metavar: options.metavar ?? "UUID",
parse(input) {
if (!uuidRegex.test(input)) return {
success: false,
error: message`Expected a valid UUID in format ${"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}, but got ${input}.`
};
if (options.allowedVersions != null && options.allowedVersions.length > 0) {
const versionChar = input.charAt(14);
const version = parseInt(versionChar, 16);
if (!options.allowedVersions.includes(version)) {
let expectedVersions = message``;
let i = 0;
for (const v of options.allowedVersions) {
expectedVersions = i < 1 ? message`${expectedVersions}${v.toLocaleString("en")}` : i + 1 >= options.allowedVersions.length ? message`${expectedVersions}, or ${v.toLocaleString("en")}` : message`${expectedVersions}, ${v.toLocaleString("en")}`;
i++;
}
return {
success: false,
error: message`Expected UUID version ${expectedVersions}, but got version ${version.toLocaleString("en")}.`
};
}
}
return {
success: true,
value: input
};
},
format(value) {
return value;
}
};
}
//#endregion
export { choice, float, integer, isValueParser, locale, string, url, uuid };