From 13d9b60ac2d90134aec094f484cf725c9f36b298 Mon Sep 17 00:00:00 2001 From: ZeRiix Date: Wed, 22 Apr 2026 15:01:13 +0200 Subject: [PATCH 1/2] feat(09): add execOptions --- package-lock.json | 8 +- package.json | 2 +- scripts/command/create.ts | 44 +++++---- scripts/command/error.ts | 45 ++++++++- scripts/command/exec.ts | 4 +- scripts/command/execOptions.ts | 81 +++++++++++++++ scripts/command/help.ts | 159 ++++++++++++++++++------------ scripts/command/index.ts | 1 + scripts/command/options/array.ts | 32 +++--- scripts/command/options/base.ts | 18 ++-- scripts/command/options/simple.ts | 14 +-- tests/command/error.test.ts | 68 ++++++++++++- tests/command/execOptions.test.ts | 127 ++++++++++++++++++++++++ tests/command/help.test.ts | 37 ++++++- 14 files changed, 516 insertions(+), 124 deletions(-) create mode 100644 scripts/command/execOptions.ts create mode 100644 tests/command/execOptions.test.ts diff --git a/package-lock.json b/package-lock.json index 082f068..0e6a749 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "node": ">=22.15.1" }, "peerDependencies": { - "@duplojs/utils": ">=1.6.3 <2.0.0" + "@duplojs/utils": ">=1.6.5 <2.0.0" } }, "docs": { @@ -1086,9 +1086,9 @@ "link": true }, "node_modules/@duplojs/utils": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@duplojs/utils/-/utils-1.6.3.tgz", - "integrity": "sha512-83qJLc1D+l9cXVRjPSdq7H+/yTrwAidWYKBU+DoBX3iYppDodpk54D1M2dqoFQ16MKYco8CR0f5GxGWN2r5KWQ==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@duplojs/utils/-/utils-1.6.5.tgz", + "integrity": "sha512-IvjhYJ8Rw/ZWGEEETZtXapZGzm0+SDrMdwFCeHauGA/0UmPRheyctRMpC8ctD8CDYT1rnOuS8i6oBhMadgxcIQ==", "license": "MIT", "peer": true, "workspaces": [ diff --git a/package.json b/package.json index 33a02cb..3f325a3 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "@duplojs/dev-tools": "0.2.0" }, "peerDependencies": { - "@duplojs/utils": ">=1.6.3 <2.0.0" + "@duplojs/utils": ">=1.6.5 <2.0.0" }, "workspaces": [ "integration", diff --git a/scripts/command/create.ts b/scripts/command/create.ts index d1dfbd0..86c19c8 100644 --- a/scripts/command/create.ts +++ b/scripts/command/create.ts @@ -1,25 +1,29 @@ -import { type SimplifyTopLevel, type Kind, type AnyFunction, type RemoveKind, A, DP, E, unwrap, type MaybePromise, O } from "@duplojs/utils"; +import { type SimplifyTopLevel, type Kind, type AnyFunction, type RemoveKind, unwrap, type MaybePromise } from "@duplojs/utils"; +import * as AA from "@duplojs/utils/array"; +import * as OO from "@duplojs/utils/object"; +import * as DDP from "@duplojs/utils/dataParser"; +import * as EE from "@duplojs/utils/either"; import { createBooleanOption, type Option } from "./options"; import { createDuplojsServerUtilsKind } from "@scripts/kind"; import type { EligibleDataParser } from "./types"; import { exitProcess } from "@scripts/common/exitProcess"; -import { addIssue, addDataParserError, createError, interpretError, popErrorPath, setErrorPath, SymbolCommandError, type CommandError } from "./error"; -import { logHelp } from "./help"; +import { addIssue, addDataParserError, createError, interpretCommandError, popErrorPath, setErrorPath, SymbolCommandError, type CommandError } from "./error"; +import { logCommandHelp } from "./help"; export type Subject = ( | EligibleDataParser - | DP.DataParserArray< + | DDP.DataParserArray< SimplifyTopLevel< - & Omit + & Omit & { readonly element: EligibleDataParser; } > > - | DP.AdvancedContract< - DP.DataParserTuple< + | DDP.AdvancedContract< + DDP.DataParserTuple< SimplifyTopLevel< - & Omit + & Omit & { readonly shape: readonly [ EligibleDataParser, @@ -35,7 +39,7 @@ export type Subject = ( function printError(commandError: CommandError, error?: CommandError): SymbolCommandError { if (!error) { // eslint-disable-next-line no-console - console.error(interpretError(commandError)); + console.error(interpretCommandError(commandError)); exitProcess(1); } @@ -80,7 +84,7 @@ export interface CreateCommandExecuteParams< SymbolCommandError >["result"] }; - subject: DP.Output; + subject: DDP.Output; } /** @@ -129,7 +133,7 @@ export function create( setErrorPath(commandError, command.name, pathIndex); try { - result = await command.execute(A.shift(args), commandError); + result = await command.execute(AA.shift(args), commandError); } finally { popErrorPath(commandError); } @@ -148,13 +152,13 @@ export function create( if (help === SymbolCommandError) { return printError(commandError, error); } else if (help.result) { - logHelp(self); + logCommandHelp(self); return void exitProcess(0); } - const commandOptions = A.reduce( + const commandOptions = AA.reduce( self.options, - A.reduceFrom<{ + AA.reduceFrom<{ options: Record; restArgs: readonly string[]; }>({ @@ -169,7 +173,7 @@ export function create( } return next({ - options: O.override( + options: OO.override( lastValue.options, { [option.name]: optionResult.result, @@ -187,12 +191,12 @@ export function create( if (self.subject === null) { await execute({ options: commandOptions.options }); } else if ( - DP.identifier(self.subject, DP.arrayKind) - || DP.identifier(self.subject, DP.tupleKind) + DDP.identifier(self.subject, DDP.arrayKind) + || DDP.identifier(self.subject, DDP.tupleKind) ) { const subjectResult = self.subject.parse(commandOptions.restArgs); - if (E.isLeft(subjectResult)) { + if (EE.isLeft(subjectResult)) { addDataParserError( commandError, unwrap(subjectResult), @@ -208,7 +212,7 @@ export function create( options: commandOptions.options, subject: unwrap(subjectResult), }); - } else if (DP.identifier(self.subject, DP.dataParserKind)) { + } else if (DDP.identifier(self.subject, DDP.dataParserKind)) { if (commandOptions.restArgs.length > 1) { addIssue( commandError, @@ -225,7 +229,7 @@ export function create( const subjectResult = self.subject.parse(commandOptions.restArgs); - if (E.isLeft(subjectResult)) { + if (EE.isLeft(subjectResult)) { addDataParserError( commandError, unwrap(subjectResult), diff --git a/scripts/command/error.ts b/scripts/command/error.ts index dd23d1d..c7a56f9 100644 --- a/scripts/command/error.ts +++ b/scripts/command/error.ts @@ -1,4 +1,5 @@ -import { type DP, Printer } from "@duplojs/utils"; +import { Printer } from "@duplojs/utils"; +import type * as DDP from "@duplojs/utils/dataParser"; export interface CommandErrorIssue { readonly type: "command" | "option" | "subject"; @@ -61,7 +62,7 @@ export function popErrorPath( export function addDataParserError( error: CommandError, - parseError: DP.DataParserError, + parseError: DDP.DataParserError, params: { type: "option" | "subject"; target?: string; @@ -84,7 +85,7 @@ export function addDataParserError( return SymbolCommandError; } -export function interpretError( +export function interpretCommandError( error: CommandError, ): string { return Printer.renderParagraph( @@ -139,3 +140,41 @@ export function interpretError( ], ); } + +export function interpretExecOptionError( + error: CommandError, +): string { + return Printer.renderParagraph( + [ + Printer.colorizedBold("Invalid options", "red"), + error.issues.map( + (issue) => Printer.renderParagraph( + [ + issue.type === "option" + && issue.target + && Printer.render( + [ + Printer.indent(1), + Printer.colorizedBold("OPTION: ", "magenta"), + `--${issue.target}`, + ], + "", + ), + Printer.renderLine( + [ + Printer.colorizedBold("✖", "red"), + issue.parserPath && Printer.colorizedBold(issue.parserPath, "cyan"), + "expected", + Printer.colorized(issue.expected, "green"), + "but received", + Printer.colorized(Printer.stringify(issue.received), "red"), + ], + ), + issue.message !== undefined && `${Printer.indent(1)}↳ ${issue.message}`, + ], + ), + ), + error.issues.length === 0 && "No issue found", + ], + ); +} diff --git a/scripts/command/exec.ts b/scripts/command/exec.ts index 82a0b71..1d604a5 100644 --- a/scripts/command/exec.ts +++ b/scripts/command/exec.ts @@ -1,6 +1,6 @@ -import { type AnyFunction, type MaybePromise } from "@duplojs/utils"; +import type { AnyFunction, MaybePromise } from "@duplojs/utils"; import { type CreateCommandExecuteParams, type CreateCommandParams, type Subject, create } from "./create"; -import { type Option } from "./options"; +import type { Option } from "./options"; import { getProcessArguments } from "@scripts/common"; /** diff --git a/scripts/command/execOptions.ts b/scripts/command/execOptions.ts new file mode 100644 index 0000000..2d47b9a --- /dev/null +++ b/scripts/command/execOptions.ts @@ -0,0 +1,81 @@ +import * as AA from "@duplojs/utils/array"; +import * as OO from "@duplojs/utils/object"; +import { createError, interpretExecOptionError, SymbolCommandError } from "./error"; +import { logExecOptionHelp } from "./help"; +import { createBooleanOption, type Option } from "./options"; +import { exitProcess, getProcessArguments } from "@scripts/common"; + +type ComputeResult< + GenericOptions extends [Option, ...Option[]], +> = { + [GenericOptionName in GenericOptions[number]["name"]]: Exclude< + ReturnType< + Extract< + GenericOptions[number], + { name: GenericOptionName } + >["execute"] + >, + SymbolCommandError + >["result"] +}; + +const helpOption = createBooleanOption("help", { aliases: ["h"] }); + +export function execOptions< + GenericOptions extends [Option, ...Option[]], +>( + ...options: GenericOptions +): ComputeResult; + +export function execOptions( + ...options: [Option, ...Option[]] +) { + const processArguments = getProcessArguments(); + const error = createError("root"); + const help = helpOption.execute(processArguments, error); + + if (help === SymbolCommandError) { + // eslint-disable-next-line no-console + console.error(interpretExecOptionError(error)); + return void exitProcess(1); + } else if (help.result) { + logExecOptionHelp(options); + return void exitProcess(0); + } + + const result = AA.reduce( + options, + AA.reduceFrom<{ + options: Record; + restArgs: readonly string[]; + }>({ + options: {}, + restArgs: processArguments, + }), + ({ element: option, lastValue, next, exit }) => { + const optionResult = option.execute(lastValue.restArgs, error); + + if (optionResult === SymbolCommandError) { + return exit(SymbolCommandError); + } + + return next({ + options: OO.override( + lastValue.options, + { + [option.name]: optionResult.result, + }, + ), + restArgs: optionResult.argumentRest, + }); + }, + ); + + if (result === SymbolCommandError) { + // eslint-disable-next-line no-console + console.error(interpretExecOptionError(error)); + return void exitProcess(1); + } + + return result.options; +} diff --git a/scripts/command/help.ts b/scripts/command/help.ts index b6c5004..c880fbf 100644 --- a/scripts/command/help.ts +++ b/scripts/command/help.ts @@ -1,74 +1,78 @@ -import { A, DP, hasSomeKinds, isType, justReturn, P, pipe, Printer } from "@duplojs/utils"; +import { hasSomeKinds, isType, justReturn, pipe, Printer } from "@duplojs/utils"; +import * as AA from "@duplojs/utils/array"; +import * as PP from "@duplojs/utils/pattern"; +import * as DDP from "@duplojs/utils/dataParser"; import type { Command } from "./create"; +import type { Option } from "./options"; /** * @internal */ -export function formatSubject(subject: DP.DataParser): string { - return P.match(subject) +export function formatSubject(subject: DDP.DataParser): string { + return PP.match(subject) .when( - DP.identifier(DP.stringKind), + DDP.identifier(DDP.stringKind), justReturn("string"), ) .when( - DP.identifier(DP.numberKind), + DDP.identifier(DDP.numberKind), justReturn("number"), ) .when( - DP.identifier(DP.bigIntKind), + DDP.identifier(DDP.bigIntKind), justReturn("bigint"), ) .when( - DP.identifier(DP.dateKind), + DDP.identifier(DDP.dateKind), justReturn("date"), ) .when( - DP.identifier(DP.timeKind), + DDP.identifier(DDP.timeKind), justReturn("time"), ) .when( - DP.identifier(DP.nilKind), + DDP.identifier(DDP.nilKind), justReturn("null"), ) .when( - DP.identifier(DP.literalKind), + DDP.identifier(DDP.literalKind), (subject) => pipe( subject.definition.value, - A.map(String), - A.join(" | "), + AA.map(String), + AA.join(" | "), ), ) .when( - DP.identifier(DP.templateLiteralKind), + DDP.identifier(DDP.templateLiteralKind), (subject) => pipe( subject.definition.template, - A.map( - (part) => DP.identifier(part, DP.dataParserKind) + AA.map( + (part) => DDP.identifier(part, DDP.dataParserKind) ? `\${${formatSubject(part)}}` : String(part), ), - A.join(""), + AA.join(""), ), ) .when( - DP.identifier(DP.unionKind), + DDP.identifier(DDP.unionKind), (subject) => pipe( subject.definition.options, - A.map(formatSubject), - A.join(" | "), + AA.map(formatSubject), + AA.join(" | "), ), ) .when( - DP.identifier(DP.arrayKind), + DDP.identifier(DDP.arrayKind), (subject) => `${formatSubject(subject.definition.element)}[]`, ) .when( - DP.identifier(DP.tupleKind), + DDP.identifier(DDP.tupleKind), (subject) => { const parts = pipe( subject.definition.shape, - A.map(formatSubject), - A.join(", "), + AA.map(formatSubject), + AA.join(", "), ); const rest = subject.definition.rest ? `${parts ? ", " : ""}...${formatSubject(subject.definition.rest)}[]` @@ -80,6 +84,46 @@ export function formatSubject(subject: DP.DataParser): string { .otherwise(justReturn("unknown")); } +/** + * @internal + */ +export function renderOptionsHelp( + options: readonly Option[], + depth: number, +): string { + return Printer.renderParagraph( + [ + `${Printer.indent(depth)}${Printer.colorizedBold("OPTIONS:", "blue")}`, + AA.map( + options, + (option) => Printer.renderParagraph( + [ + AA.join( + [ + Printer.indent(depth), + Printer.dash, + Printer.colorized(` ${option.name}: `, "cyan"), + Printer.colorized( + pipe( + option.aliases, + AA.map((alias) => `-${alias},`), + AA.push(`--${option.name}`), + AA.join(" "), + ), + "gray", + ), + ], + "", + ), + option.description + && `${Printer.indent(depth)} ${option.description}`, + ], + ), + ), + ], + ); +} + /** * @internal */ @@ -104,55 +148,23 @@ export function renderCommandHelp( ); } - if (A.minElements(command.options, 1)) { - logs.push( - Printer.renderParagraph( - [ - `${Printer.indent(depth + 1)}${Printer.colorizedBold("OPTIONS:", "blue")}`, - A.map( - command.options, - (option) => Printer.renderParagraph( - [ - A.join( - [ - Printer.indent(depth + 1), - Printer.dash, - Printer.colorized(` ${option.name}: `, "cyan"), - Printer.colorized( - pipe( - option.aliases, - A.map((alias) => `-${alias},`), - A.push(`--${option.name}`), - A.join(" "), - ), - "gray", - ), - ], - "", - ), - option.description - && `${Printer.indent(depth + 1)} ${option.description}`, - ], - ), - ), - ], - ), - ); + if (AA.minElements(command.options, 1)) { + logs.push(renderOptionsHelp(command.options, depth + 1)); } if (isType(command.subject, "array")) { for (const childCommand of command.subject) { logs.push(...renderCommandHelp(childCommand, depth + 1)); } - } else if (DP.identifier(command.subject, DP.dataParserKind)) { + } else if (DDP.identifier(command.subject, DDP.dataParserKind)) { const formattedSubject = formatSubject(command.subject); logs.push( - A.join( + AA.join( [ Printer.indent(depth + 1), Printer.colorizedBold("SUBJECT:", "magenta"), - hasSomeKinds(command.subject, [DP.tupleKind, DP.arrayKind]) + hasSomeKinds(command.subject, [DDP.tupleKind, DDP.arrayKind]) ? formattedSubject : `<${formattedSubject}>`, ], @@ -164,7 +176,7 @@ export function renderCommandHelp( return logs; } -export function logHelp( +export function logCommandHelp( command: Command, depth = 0, ) { @@ -175,3 +187,28 @@ export function logHelp( ), ); } + +/** + * @internal + */ +export function renderExecOptionHelp( + options: readonly Option[], + depth: number, +): string[] { + return [ + `${Printer.indent(depth)}${Printer.colorizedBold("OPTION HELP", "green")}`, + renderOptionsHelp(options, depth + 1), + ]; +} + +export function logExecOptionHelp( + options: readonly Option[], + depth = 0, +) { + // eslint-disable-next-line no-console + console.log( + Printer.renderParagraph( + renderExecOptionHelp(options, depth), + ), + ); +} diff --git a/scripts/command/index.ts b/scripts/command/index.ts index 536cc58..3a85785 100644 --- a/scripts/command/index.ts +++ b/scripts/command/index.ts @@ -8,3 +8,4 @@ export * from "./create"; export * from "./exec"; export * from "./help"; export * from "./error"; +export * from "./execOptions"; diff --git a/scripts/command/options/array.ts b/scripts/command/options/array.ts index 1a224ee..43400e5 100644 --- a/scripts/command/options/array.ts +++ b/scripts/command/options/array.ts @@ -1,4 +1,8 @@ -import { type A, DP, E, pipe, S, unwrap } from "@duplojs/utils"; +import { pipe, unwrap } from "@duplojs/utils"; +import * as SS from "@duplojs/utils/string"; +import * as DDP from "@duplojs/utils/dataParser"; +import type * as AA from "@duplojs/utils/array"; +import * as EE from "@duplojs/utils/either"; import { initOption, type Option } from "./base"; import type { EligibleDataParser } from "../types"; import { addIssue, addDataParserError } from "../error"; @@ -26,11 +30,11 @@ export function createArrayOption< ): Option< GenericName, [ - ...A.CreateTuple< - DP.Output, + ...AA.CreateTuple< + DDP.Output, GenericMinValues >, - ...DP.Output[], + ...DDP.Output[], ] >; @@ -51,18 +55,18 @@ export function createArrayOption< ): Option< GenericName, | [ - ...A.CreateTuple< - DP.Output, + ...AA.CreateTuple< + DDP.Output, GenericMinValues >, - ...DP.Output[], + ...DDP.Output[], ] | undefined >; export function createArrayOption( name: string, - schema: DP.DataParser, + schema: DDP.DataParser, params?: { description?: string; aliases?: readonly string[]; @@ -74,16 +78,16 @@ export function createArrayOption( ) { const dataParser = pipe( schema, - DP.array, + DDP.array, (schema) => params?.min - ? schema.addChecker(DP.checkerArrayMin(params.min)) + ? schema.addChecker(DDP.checkerArrayMin(params.min)) : schema, (schema) => params?.max - ? schema.addChecker(DP.checkerArrayMax(params.max)) + ? schema.addChecker(DDP.checkerArrayMax(params.max)) : schema, (schema) => params?.required ? schema - : DP.optional(schema), + : DDP.optional(schema), ); return initOption( @@ -103,12 +107,12 @@ export function createArrayOption( } const values = value !== undefined - ? S.split(value, params?.separator ?? defaultSeparator) + ? SS.split(value, params?.separator ?? defaultSeparator) : undefined; const result = dataParser.parse(values); - if (E.isLeft(result)) { + if (EE.isLeft(result)) { return addDataParserError( error, unwrap(result), diff --git a/scripts/command/options/base.ts b/scripts/command/options/base.ts index 56e1f5c..f935df2 100644 --- a/scripts/command/options/base.ts +++ b/scripts/command/options/base.ts @@ -1,4 +1,6 @@ -import { A, type RemoveKind, S, type Kind } from "@duplojs/utils"; +import type { RemoveKind, Kind } from "@duplojs/utils"; +import * as AA from "@duplojs/utils/array"; +import * as SS from "@duplojs/utils/string"; import { createDuplojsServerUtilsKind } from "@scripts/kind"; import { addIssue, type CommandError, SymbolCommandError } from "../error"; @@ -50,11 +52,11 @@ export function initOption< args: readonly string[], error: CommandError, ) => { - const result = A.reduce( + const result = AA.reduce( args, - A.reduceFrom(null), + AA.reduceFrom(null), ({ element, next, exit, index }) => { - const extractResult = S.extract(element, regexOption); + const extractResult = SS.extract(element, regexOption); if (!extractResult) { return next(null); @@ -66,7 +68,7 @@ export function initOption< index, }; - if (self.name !== result.key && !A.includes(self.aliases, result.key)) { + if (self.name !== result.key && !AA.includes(self.aliases, result.key)) { return next(null); } @@ -93,7 +95,7 @@ export function initOption< }; } else if (self.hasValue) { const value = result.value ?? args[result.index + 1]; - const isOption = S.test(value ?? "", regexOption); + const isOption = SS.test(value ?? "", regexOption); if (isOption) { return addIssue( @@ -122,7 +124,7 @@ export function initOption< return { result: executeResult, - argumentRest: A.spliceDelete( + argumentRest: AA.spliceDelete( args, result.index, result.value === undefined && args[result.index + 1] !== undefined @@ -157,7 +159,7 @@ export function initOption< return { result: executeResult, - argumentRest: A.spliceDelete(args, result.index, 1), + argumentRest: AA.spliceDelete(args, result.index, 1), }; }, aliases: params?.aliases ?? [], diff --git a/scripts/command/options/simple.ts b/scripts/command/options/simple.ts index 4b86f9b..e1f01cc 100644 --- a/scripts/command/options/simple.ts +++ b/scripts/command/options/simple.ts @@ -1,4 +1,6 @@ -import { DP, E, unwrap } from "@duplojs/utils"; +import { unwrap } from "@duplojs/utils"; +import * as EE from "@duplojs/utils/either"; +import * as DDP from "@duplojs/utils/dataParser"; import { initOption, type Option } from "./base"; import type { EligibleDataParser } from "../types"; import { addIssue, addDataParserError } from "../error"; @@ -17,7 +19,7 @@ export function createOption< aliases?: readonly string[]; required: true; }, -): Option>; +): Option>; export function createOption< GenericName extends string, @@ -29,11 +31,11 @@ export function createOption< description?: string; aliases?: readonly string[]; }, -): Option | undefined>; +): Option | undefined>; export function createOption( name: string, - schema: DP.DataParser, + schema: DDP.DataParser, params?: { description?: string; aliases?: readonly string[]; @@ -42,7 +44,7 @@ export function createOption( ) { const dataParser = params?.required ? schema - : DP.optional(schema); + : DDP.optional(schema); return initOption( name, @@ -62,7 +64,7 @@ export function createOption( const result = dataParser.parse(value); - if (E.isLeft(result)) { + if (EE.isLeft(result)) { return addDataParserError( error, unwrap(result), diff --git a/tests/command/error.test.ts b/tests/command/error.test.ts index edbbedf..cdf2b4f 100644 --- a/tests/command/error.test.ts +++ b/tests/command/error.test.ts @@ -1,5 +1,5 @@ import { type ExpectType, DP, E, unwrap } from "@duplojs/utils"; -import { addDataParserError, addIssue, createError, interpretError, popErrorPath, setErrorPath, SymbolCommandError } from "@scripts/command/error"; +import { addDataParserError, addIssue, createError, interpretCommandError, interpretExecOptionError, popErrorPath, setErrorPath, SymbolCommandError } from "@scripts/command/error"; describe("error", () => { it("creates and mutates a command error path", () => { @@ -42,7 +42,7 @@ describe("error", () => { message: "Expected exactly one subject argument, received 2.", }, ]); - expect(interpretError(error)).toContain("Expected exactly one subject argument, received 2."); + expect(interpretCommandError(error)).toContain("Expected exactly one subject argument, received 2."); }); it("adds data parser issues and renders option and subject contexts", () => { @@ -84,7 +84,7 @@ describe("error", () => { ), ).toBe(SymbolCommandError); - const output = interpretError(error); + const output = interpretCommandError(error); expect(output).toContain("COMMAND:"); expect(output).toContain("root"); @@ -99,6 +99,66 @@ describe("error", () => { }); it("renders fallback text when no issue exists", () => { - expect(interpretError(createError("root"))).toContain("No issue found"); + expect(interpretCommandError(createError("root"))).toContain("No issue found"); + }); + + it("renders option errors for interpretExecOptionError", () => { + const error = createError("root"); + const optionResult = DP.boolean().parse("yes"); + + expect(E.isLeft(optionResult)).toBe(true); + if (!E.isLeft(optionResult)) { + return; + } + + expect( + addDataParserError( + error, + unwrap(optionResult), + { + type: "option", + target: "enabled", + }, + ), + ).toBe(SymbolCommandError); + + const output = interpretExecOptionError(error); + + expect(output).toContain("Invalid options"); + expect(output).not.toContain("COMMAND:"); + expect(output).toContain("OPTION:"); + expect(output).toContain("--enabled"); + expect(output).toContain("expected"); + expect(output).toContain("boolean"); + }); + + it("renders parser path and message for interpretExecOptionError", () => { + const error = createError("root"); + + expect( + addIssue( + error, + { + type: "option", + target: "payload", + parserPath: "[1]", + expected: "boolean", + received: "yes", + message: "Boolean value expected.", + }, + ), + ).toBe(SymbolCommandError); + + const output = interpretExecOptionError(error); + + expect(output).toContain("OPTION:"); + expect(output).toContain("--payload"); + expect(output).toContain("[1]"); + expect(output).toContain("↳"); + expect(output).toContain("Boolean value expected."); + }); + + it("renders fallback text when no issue exists for interpretExecOptionError", () => { + expect(interpretExecOptionError(createError("root"))).toContain("No issue found"); }); }); diff --git a/tests/command/execOptions.test.ts b/tests/command/execOptions.test.ts new file mode 100644 index 0000000..c63ba05 --- /dev/null +++ b/tests/command/execOptions.test.ts @@ -0,0 +1,127 @@ +import { DP, type ExpectType } from "@duplojs/utils"; +import { DServerCommand, TESTImplementation, setEnvironment } from "@scripts"; + +describe("execOptions", () => { + afterEach(() => { + setEnvironment("NODE"); + TESTImplementation.clear(); + vi.clearAllMocks(); + vi.restoreAllMocks(); + }); + + it("returns empty options when no options are provided", () => { + setEnvironment("TEST"); + const getProcessArgumentsSpy = vi.fn().mockReturnValue([]); + TESTImplementation.set("getProcessArguments", getProcessArgumentsSpy); + + const result = DServerCommand.execOptions(DServerCommand.createBooleanOption("test")); + + type _CheckResult = ExpectType< + typeof result, + { + test: boolean; + }, + "strict" + >; + + expect(getProcessArgumentsSpy).toHaveBeenCalledTimes(1); + expect(result).toEqual({ test: false }); + }); + + it("executes a single boolean option successfully", () => { + setEnvironment("TEST"); + const getProcessArgumentsSpy = vi.fn().mockReturnValue(["--verbose"]); + TESTImplementation.set("getProcessArguments", getProcessArgumentsSpy); + + const verboseOption = DServerCommand.createBooleanOption("verbose"); + const result = DServerCommand.execOptions(verboseOption); + + expect(getProcessArgumentsSpy).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + verbose: true, + }); + }); + + it("logs dedicated execOption help and exits when builtin help is used", () => { + setEnvironment("TEST"); + const getProcessArgumentsSpy = vi.fn().mockReturnValue(["-h"]); + const exitSpy = vi.fn(); + const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); + TESTImplementation.set("getProcessArguments", getProcessArgumentsSpy); + TESTImplementation.set("exitProcess", exitSpy); + + DServerCommand.execOptions(DServerCommand.createBooleanOption("verbose")); + + expect(getProcessArgumentsSpy).toHaveBeenCalledTimes(1); + expect(consoleLogSpy).toHaveBeenCalledTimes(1); + expect(String(consoleLogSpy.mock.calls[0]?.[0])).toContain("OPTION HELP"); + expect(String(consoleLogSpy.mock.calls[0]?.[0])).toContain("--verbose"); + expect(exitSpy).toHaveBeenCalledWith(0); + }); + + it("logs an error and exits when builtin help is malformed", () => { + setEnvironment("TEST"); + const getProcessArgumentsSpy = vi.fn().mockReturnValue(["--help=true"]); + const exitSpy = vi.fn(); + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); + TESTImplementation.set("getProcessArguments", getProcessArgumentsSpy); + TESTImplementation.set("exitProcess", exitSpy); + + const result = DServerCommand.execOptions(DServerCommand.createBooleanOption("verbose")); + + expect(getProcessArgumentsSpy).toHaveBeenCalledTimes(1); + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + expect(String(consoleErrorSpy.mock.calls[0]?.[0])).toContain("option without value --help"); + expect(exitSpy).toHaveBeenCalledWith(1); + expect(result).toBeUndefined(); + }); + + it("executes multiple options successfully", () => { + setEnvironment("TEST"); + const getProcessArgumentsSpy = vi.fn().mockReturnValue(["--verbose", "--debug"]); + TESTImplementation.set("getProcessArguments", getProcessArgumentsSpy); + + const verboseOption = DServerCommand.createBooleanOption("verbose"); + const debugOption = DServerCommand.createBooleanOption("debug"); + const result = DServerCommand.execOptions(verboseOption, debugOption); + + expect(getProcessArgumentsSpy).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + verbose: true, + debug: true, + }); + }); + + it("exits with error when option parsing fails", () => { + setEnvironment("TEST"); + const getProcessArgumentsSpy = vi.fn().mockReturnValue(["--count", "invalid"]); + const exitSpy = vi.fn(); + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + TESTImplementation.set("getProcessArguments", getProcessArgumentsSpy); + TESTImplementation.set("exitProcess", exitSpy); + + const countOption = DServerCommand.createOption("count", DP.coerce.number()); + const result = DServerCommand.execOptions(countOption); + + expect(getProcessArgumentsSpy).toHaveBeenCalledTimes(1); + expect(consoleErrorSpy).toHaveBeenCalled(); + expect(exitSpy).toHaveBeenCalledWith(1); + expect(result).toBeUndefined(); + + consoleErrorSpy.mockRestore(); + }); + + it("returns options with unmatched arguments", () => { + setEnvironment("TEST"); + const getProcessArgumentsSpy = vi.fn().mockReturnValue(["--verbose", "remaining", "args"]); + TESTImplementation.set("getProcessArguments", getProcessArgumentsSpy); + + const verboseOption = DServerCommand.createBooleanOption("verbose"); + const result = DServerCommand.execOptions(verboseOption); + + expect(getProcessArgumentsSpy).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + verbose: true, + }); + }); +}); diff --git a/tests/command/help.test.ts b/tests/command/help.test.ts index 6cc6b5c..f6f30e6 100644 --- a/tests/command/help.test.ts +++ b/tests/command/help.test.ts @@ -59,7 +59,7 @@ describe("help", () => { () => Promise.resolve(undefined), ); - DServerCommand.logHelp(command, 1); + DServerCommand.logCommandHelp(command, 1); expect(logSpy).toHaveBeenCalledTimes(1); expect(logSpy.mock.calls[0]).toEqual([ @@ -69,6 +69,41 @@ describe("help", () => { ]); }); + it("renders execOption help with a dedicated title and options block", () => { + const lines = DServerCommand.renderExecOptionHelp( + [ + DServerCommand.createBooleanOption("help", { aliases: ["h"] }), + DServerCommand.createBooleanOption("verbose", { + aliases: ["v"], + description: "Enable verbose mode", + }), + ], + 1, + ); + + expect(lines).toContain( + `${Printer.indent(1)}${Printer.colorizedBold("OPTION HELP", "green")}`, + ); + expect(lines.join("\n")).toContain(Printer.colorizedBold("OPTIONS:", "blue")); + expect(lines.join("\n")).toContain("--help"); + expect(lines.join("\n")).toContain("--verbose"); + expect(lines.join("\n")).toContain("Enable verbose mode"); + }); + + it("logs execOption help lines", () => { + const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); + const options = [DServerCommand.createBooleanOption("help", { aliases: ["h"] })]; + + DServerCommand.logExecOptionHelp(options, 1); + + expect(logSpy).toHaveBeenCalledTimes(1); + expect(logSpy.mock.calls[0]).toEqual([ + Printer.renderParagraph( + DServerCommand.renderExecOptionHelp(options, 1), + ), + ]); + }); + it("recursively renders child commands when subject is a command list", () => { const child = DServerCommand.create( "child", From 279b7df5712cf9c363c5d222cf9b94b057e1edc1 Mon Sep 17 00:00:00 2001 From: ZeRiix Date: Wed, 22 Apr 2026 19:47:34 +0200 Subject: [PATCH 2/2] feat(09): add jsDoc and doc to function execOptions --- .gitignore | 3 +- docs/en/v0/api/command/create.md | 4 +- docs/en/v0/api/command/exec.md | 4 +- docs/en/v0/api/command/execOptions.md | 48 ++++++ docs/en/v0/api/command/index.md | 3 + .../v0/api/command/execOptions/main.ts | 31 ++++ docs/fr/v0/api/command/create.md | 4 +- docs/fr/v0/api/command/exec.md | 4 +- docs/fr/v0/api/command/execOptions.md | 48 ++++++ docs/fr/v0/api/command/index.md | 3 + docs/libs/v0/command/create.cjs | 117 +++++++++++--- docs/libs/v0/command/create.d.ts | 16 +- docs/libs/v0/command/create.mjs | 99 +++++++++--- docs/libs/v0/command/error.cjs | 109 +++++++++++++ docs/libs/v0/command/error.d.ts | 26 +++ docs/libs/v0/command/error.mjs | 100 ++++++++++++ docs/libs/v0/command/errors.cjs | 40 ----- docs/libs/v0/command/errors.d.ts | 30 ---- docs/libs/v0/command/errors.mjs | 35 ---- docs/libs/v0/command/exec.d.ts | 4 +- docs/libs/v0/command/execOptions.cjs | 68 ++++++++ docs/libs/v0/command/execOptions.d.ts | 46 ++++++ docs/libs/v0/command/execOptions.mjs | 46 ++++++ docs/libs/v0/command/help.cjs | 153 ++++++++++++------ docs/libs/v0/command/help.d.ts | 4 +- docs/libs/v0/command/help.mjs | 127 +++++++++------ docs/libs/v0/command/index.cjs | 28 ++-- docs/libs/v0/command/index.d.ts | 4 +- docs/libs/v0/command/index.mjs | 6 +- docs/libs/v0/command/options/array.cjs | 55 +++++-- docs/libs/v0/command/options/array.d.ts | 11 +- docs/libs/v0/command/options/array.mjs | 36 +++-- docs/libs/v0/command/options/base.cjs | 91 ++++++++--- docs/libs/v0/command/options/base.d.ts | 9 +- docs/libs/v0/command/options/base.mjs | 71 +++++--- docs/libs/v0/command/options/simple.cjs | 45 +++++- docs/libs/v0/command/options/simple.d.ts | 6 +- docs/libs/v0/command/options/simple.mjs | 27 +++- docs/libs/v0/command/printer.cjs | 63 -------- docs/libs/v0/command/printer.d.ts | 32 ---- docs/libs/v0/command/printer.mjs | 63 -------- docs/libs/v0/dataParser/parsers/file.cjs | 24 ++- docs/libs/v0/dataParser/parsers/file.mjs | 24 ++- eslint.config.js | 1 + jsDoc/command/execOptions/example.ts | 26 +++ jsDoc/command/execOptions/index.md | 14 ++ scripts/command/execOptions.ts | 27 ++-- 47 files changed, 1256 insertions(+), 579 deletions(-) create mode 100644 docs/en/v0/api/command/execOptions.md create mode 100644 docs/examples/v0/api/command/execOptions/main.ts create mode 100644 docs/fr/v0/api/command/execOptions.md create mode 100644 docs/libs/v0/command/error.cjs create mode 100644 docs/libs/v0/command/error.d.ts create mode 100644 docs/libs/v0/command/error.mjs delete mode 100644 docs/libs/v0/command/errors.cjs delete mode 100644 docs/libs/v0/command/errors.d.ts delete mode 100644 docs/libs/v0/command/errors.mjs create mode 100644 docs/libs/v0/command/execOptions.cjs create mode 100644 docs/libs/v0/command/execOptions.d.ts create mode 100644 docs/libs/v0/command/execOptions.mjs delete mode 100644 docs/libs/v0/command/printer.cjs delete mode 100644 docs/libs/v0/command/printer.d.ts delete mode 100644 docs/libs/v0/command/printer.mjs create mode 100644 jsDoc/command/execOptions/example.ts create mode 100644 jsDoc/command/execOptions/index.md diff --git a/.gitignore b/.gitignore index 523b7c1..70a596b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules dist coverage -cache \ No newline at end of file +cache +.temp \ No newline at end of file diff --git a/docs/en/v0/api/command/create.md b/docs/en/v0/api/command/create.md index ddcb5ab..db32d98 100644 --- a/docs/en/v0/api/command/create.md +++ b/docs/en/v0/api/command/create.md @@ -1,8 +1,8 @@ --- outline: [2, 3] prev: - text: "exec" - link: "/en/v0/api/command/exec" + text: "execOptions" + link: "/en/v0/api/command/execOptions" next: text: "createBooleanOption" link: "/en/v0/api/command/createBooleanOption" diff --git a/docs/en/v0/api/command/exec.md b/docs/en/v0/api/command/exec.md index da8598f..558b8e3 100644 --- a/docs/en/v0/api/command/exec.md +++ b/docs/en/v0/api/command/exec.md @@ -4,8 +4,8 @@ prev: text: "Command" link: "/en/v0/api/command/" next: - text: "create" - link: "/en/v0/api/command/create" + text: "execOptions" + link: "/en/v0/api/command/execOptions" description: "Runs a CLI command from runtime arguments." --- diff --git a/docs/en/v0/api/command/execOptions.md b/docs/en/v0/api/command/execOptions.md new file mode 100644 index 0000000..e5cedfd --- /dev/null +++ b/docs/en/v0/api/command/execOptions.md @@ -0,0 +1,48 @@ +--- +outline: [2, 3] +prev: + text: "exec" + link: "/en/v0/api/command/exec" +next: + text: "create" + link: "/en/v0/api/command/create" +description: "Parses CLI options from runtime arguments." +--- + +# execOptions + +`execOptions` parses CLI options and returns the result as a `{ [optionName]: resultOption }` record. +If parsing fails, a detailed error is printed to the console. +The `--help` / `-h` output is generated automatically from the declared options. + +## Example + +```ts twoslash +// @version: 0 + +``` + +## Syntax + +```typescript +function execOptions< + GenericOptions extends [Option, ...Option[]] +>( + ...options: GenericOptions +): ComputeResult +``` + +## Parameters + +- `options` (`[Option, ...Option[]]`) : options to parse from runtime arguments. + +## Return value + +- `ComputeResult` : object whose keys are option names and whose values are the typed result of each parser. + +## See also + +- [`exec`](/en/v0/api/command/exec) - Runs a full command from runtime arguments. +- [`createBooleanOption`](/en/v0/api/command/createBooleanOption) - Builds a boolean flag option. +- [`createOption`](/en/v0/api/command/createOption) - Builds a single-value option. +- [`createArrayOption`](/en/v0/api/command/createArrayOption) - Builds an array option. diff --git a/docs/en/v0/api/command/index.md b/docs/en/v0/api/command/index.md index 83898ae..1cb5889 100644 --- a/docs/en/v0/api/command/index.md +++ b/docs/en/v0/api/command/index.md @@ -28,6 +28,9 @@ import * as SC from "@duplojs/server-utils/command"; ### [`exec`](/en/v0/api/command/exec) creates the root command, reads process arguments, and runs the command tree. +### [`execOptions`](/en/v0/api/command/execOptions) +only parses options from process arguments. + ## Command builder ### [`create`](/en/v0/api/command/create) diff --git a/docs/examples/v0/api/command/execOptions/main.ts b/docs/examples/v0/api/command/execOptions/main.ts new file mode 100644 index 0000000..34d65f2 --- /dev/null +++ b/docs/examples/v0/api/command/execOptions/main.ts @@ -0,0 +1,31 @@ +import { SC } from "@duplojs/server-utils"; +import { DP } from "@duplojs/utils"; + +const portOption = SC.createOption( + "port", + DP.coerce.number(), + { + description: "HTTP port", + required: true, + }, +); + +const verboseOption = SC.createBooleanOption( + "verbose", + { + aliases: ["v"], + description: "Enable verbose logs", + }, +); + +const options = SC.execOptions(portOption, verboseOption); +// ^? + + + + + + +if (options.verbose) { + console.log(`server starts on ${options.port}`); +} diff --git a/docs/fr/v0/api/command/create.md b/docs/fr/v0/api/command/create.md index df4583c..13eda2e 100644 --- a/docs/fr/v0/api/command/create.md +++ b/docs/fr/v0/api/command/create.md @@ -1,8 +1,8 @@ --- outline: [2, 3] prev: - text: "exec" - link: "/fr/v0/api/command/exec" + text: "execOptions" + link: "/fr/v0/api/command/execOptions" next: text: "createBooleanOption" link: "/fr/v0/api/command/createBooleanOption" diff --git a/docs/fr/v0/api/command/exec.md b/docs/fr/v0/api/command/exec.md index e1f2cdb..091727b 100644 --- a/docs/fr/v0/api/command/exec.md +++ b/docs/fr/v0/api/command/exec.md @@ -4,8 +4,8 @@ prev: text: "Command" link: "/fr/v0/api/command/" next: - text: "create" - link: "/fr/v0/api/command/create" + text: "execOptions" + link: "/fr/v0/api/command/execOptions" description: "Lance une commande CLI à partir des arguments du runtime." --- diff --git a/docs/fr/v0/api/command/execOptions.md b/docs/fr/v0/api/command/execOptions.md new file mode 100644 index 0000000..9000837 --- /dev/null +++ b/docs/fr/v0/api/command/execOptions.md @@ -0,0 +1,48 @@ +--- +outline: [2, 3] +prev: + text: "exec" + link: "/fr/v0/api/command/exec" +next: + text: "create" + link: "/fr/v0/api/command/create" +description: "Parse les options CLI depuis les arguments du runtime." +--- + +# execOptions + +`execOptions` parse les options CLI et retourne le résultat sous forme de record `{ [optionName]: resultOption }`. +Si le parsing échoue, une erreur détaillée est affichée dans la console. +Le `--help` / `-h` est généré automatiquement depuis les options déclarées. + +## Exemple + +```ts twoslash +// @version: 0 + +``` + +## Syntaxe + +```typescript +function execOptions< + GenericOptions extends [Option, ...Option[]] +>( + ...options: GenericOptions +): ComputeResult +``` + +## Paramètres + +- `options` (`[Option, ...Option[]]`) : options à parser depuis les arguments du runtime. + +## Valeur de retour + +- `ComputeResult` : objet dont les clés sont les noms d'options et les valeurs sont les résultats typés de chaque parser. + +## Voir aussi + +- [`exec`](/fr/v0/api/command/exec) - Lance une commande complète depuis les arguments du runtime. +- [`createBooleanOption`](/fr/v0/api/command/createBooleanOption) - Construit une option drapeau booléenne. +- [`createOption`](/fr/v0/api/command/createOption) - Construit une option à valeur unique. +- [`createArrayOption`](/fr/v0/api/command/createArrayOption) - Construit une option tableau. diff --git a/docs/fr/v0/api/command/index.md b/docs/fr/v0/api/command/index.md index ac31de1..5cfcc5d 100644 --- a/docs/fr/v0/api/command/index.md +++ b/docs/fr/v0/api/command/index.md @@ -28,6 +28,9 @@ import * as SC from "@duplojs/server-utils/command"; ### [`exec`](/fr/v0/api/command/exec) crée la commande racine, lit les arguments du process, puis exécute l'arbre de commandes. +### [`execOptions`](/fr/v0/api/command/execOptions) +parse uniquement les options depuis les arguments du process. + ## Construction de commandes ### [`create`](/fr/v0/api/command/create) diff --git a/docs/libs/v0/command/create.cjs b/docs/libs/v0/command/create.cjs index 76534f6..90d3033 100644 --- a/docs/libs/v0/command/create.cjs +++ b/docs/libs/v0/command/create.cjs @@ -1,12 +1,46 @@ 'use strict'; var utils = require('@duplojs/utils'); +var AA = require('@duplojs/utils/array'); +var OO = require('@duplojs/utils/object'); +var DDP = require('@duplojs/utils/dataParser'); +var EE = require('@duplojs/utils/either'); var kind = require('../kind.cjs'); +var exitProcess = require('../common/exitProcess.cjs'); +var error = require('./error.cjs'); var help = require('./help.cjs'); -var errors = require('./errors.cjs'); var boolean = require('./options/boolean.cjs'); -var exitProcess = require('../common/exitProcess.cjs'); +function _interopNamespaceDefault(e) { + var n = Object.create(null); + if (e) { + Object.keys(e).forEach(function (k) { + if (k !== 'default') { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { return e[k]; } + }); + } + }); + } + n.default = e; + return Object.freeze(n); +} + +var AA__namespace = /*#__PURE__*/_interopNamespaceDefault(AA); +var OO__namespace = /*#__PURE__*/_interopNamespaceDefault(OO); +var DDP__namespace = /*#__PURE__*/_interopNamespaceDefault(DDP); +var EE__namespace = /*#__PURE__*/_interopNamespaceDefault(EE); + +function printError(commandError, error$1) { + if (!error$1) { + // eslint-disable-next-line no-console + console.error(error.interpretCommandError(commandError)); + exitProcess.exitProcess(1); + } + return error.SymbolCommandError; +} const commandKind = kind.createDuplojsServerUtilsKind("command"); const helpOption = boolean.createBooleanOption("help", { aliases: ["h"] }); function create(...args) { @@ -18,57 +52,96 @@ function create(...args) { description: params.description ?? null, options: params.options ?? [], subject: params.subject ?? null, - execute: async (args) => { + execute: async (args, error$1) => { + const commandError = error$1 ?? error.createError(self.name); + const pathIndex = commandError.currentCommandPath.length; if (self.subject instanceof Array) { for (const command of self.subject) { if (args[0] === command.name) { - await command.execute(utils.A.shift(args)); - return; + let result = undefined; + error.setErrorPath(commandError, command.name, pathIndex); + try { + result = await command.execute(AA__namespace.shift(args), commandError); + } + finally { + error.popErrorPath(commandError); + } + if (result === error.SymbolCommandError) { + return printError(commandError, error$1); + } + return result; } } } - const help$1 = helpOption.execute(args); - if (help$1.result) { - help.logHelp(self); - exitProcess.exitProcess(0); - return; + const help$1 = helpOption.execute(args, commandError); + if (help$1 === error.SymbolCommandError) { + return printError(commandError, error$1); + } + else if (help$1.result) { + help.logCommandHelp(self); + return void exitProcess.exitProcess(0); } - const commandOptions = utils.A.reduce(self.options, utils.A.reduceFrom({ + const commandOptions = AA__namespace.reduce(self.options, AA__namespace.reduceFrom({ options: {}, restArgs: args, - }), ({ element: option, lastValue, next }) => { - const optionResult = option.execute(lastValue.restArgs); + }), ({ element: option, lastValue, next, exit }) => { + const optionResult = option.execute(lastValue.restArgs, commandError); + if (optionResult === error.SymbolCommandError) { + return exit(error.SymbolCommandError); + } return next({ - options: utils.O.override(lastValue.options, { + options: OO__namespace.override(lastValue.options, { [option.name]: optionResult.result, }), restArgs: optionResult.argumentRest, }); }); + if (commandOptions === error.SymbolCommandError) { + return printError(commandError, error$1); + } if (self.subject === null) { await execute({ options: commandOptions.options }); } - else if (utils.DP.identifier(self.subject, utils.DP.arrayKind) - || utils.DP.identifier(self.subject, utils.DP.tupleKind)) { + else if (DDP__namespace.identifier(self.subject, DDP__namespace.arrayKind) + || DDP__namespace.identifier(self.subject, DDP__namespace.tupleKind)) { + const subjectResult = self.subject.parse(commandOptions.restArgs); + if (EE__namespace.isLeft(subjectResult)) { + error.addDataParserError(commandError, utils.unwrap(subjectResult), { + type: "subject", + }); + return printError(commandError, error$1); + } await execute({ options: commandOptions.options, - subject: self.subject.parseOrThrow(commandOptions.restArgs), + subject: utils.unwrap(subjectResult), }); } - else if (utils.DP.identifier(self.subject, utils.DP.dataParserKind)) { + else if (DDP__namespace.identifier(self.subject, DDP__namespace.dataParserKind)) { if (commandOptions.restArgs.length > 1) { - throw new errors.CommandManyArgumentsError(commandOptions.restArgs.length); + error.addIssue(commandError, { + type: "command", + expected: "exactly one subject argument", + received: commandOptions.restArgs, + message: `Expected exactly one subject argument, received ${commandOptions.restArgs.length}.`, + }); + return printError(commandError, error$1); + } + const subjectResult = self.subject.parse(commandOptions.restArgs); + if (EE__namespace.isLeft(subjectResult)) { + error.addDataParserError(commandError, utils.unwrap(subjectResult), { + type: "subject", + }); + return printError(commandError, error$1); } await execute({ options: commandOptions.options, - subject: self.subject.parseOrThrow(commandOptions.restArgs), + subject: utils.unwrap(subjectResult), }); } else { await execute({}); } - exitProcess.exitProcess(0); - return; + return void exitProcess.exitProcess(0); }, }); return self; diff --git a/docs/libs/v0/command/create.d.ts b/docs/libs/v0/command/create.d.ts index 42539d3..3abc03d 100644 --- a/docs/libs/v0/command/create.d.ts +++ b/docs/libs/v0/command/create.d.ts @@ -1,9 +1,11 @@ -import { type SimplifyTopLevel, type Kind, DP, type MaybePromise } from "@duplojs/utils"; +import { type SimplifyTopLevel, type Kind, type MaybePromise } from "@duplojs/utils"; +import * as DDP from "@duplojs/utils/dataParser"; import { type Option } from "./options"; import type { EligibleDataParser } from "./types"; -export type Subject = (EligibleDataParser | DP.DataParserArray & { +import { SymbolCommandError, type CommandError } from "./error"; +export type Subject = (EligibleDataParser | DDP.DataParserArray & { readonly element: EligibleDataParser; -}>> | DP.AdvancedContract & { +}>> | DDP.AdvancedContract & { readonly shape: readonly [ EligibleDataParser, ...EligibleDataParser[] @@ -16,7 +18,7 @@ export interface Command extends Kind { readonly description: string | null; readonly subject: Subject | null | readonly Command[]; readonly options: readonly Option[]; - execute(args: readonly string[]): MaybePromise; + execute(args: readonly string[], error?: CommandError): Promise; } export interface CreateCommandParams { description?: string; @@ -25,11 +27,11 @@ export interface CreateCommandParams { options: { - [GenericOptionName in GenericOptions[number]["name"]]: ReturnType["execute"]>["result"]; + }>["execute"]>, SymbolCommandError>["result"]; }; - subject: DP.Output; + subject: DDP.Output; } /** * Create a command node. diff --git a/docs/libs/v0/command/create.mjs b/docs/libs/v0/command/create.mjs index 01deac7..e632047 100644 --- a/docs/libs/v0/command/create.mjs +++ b/docs/libs/v0/command/create.mjs @@ -1,10 +1,22 @@ -import { A, O, DP } from '@duplojs/utils'; +import { unwrap } from '@duplojs/utils'; +import * as AA from '@duplojs/utils/array'; +import * as OO from '@duplojs/utils/object'; +import * as DDP from '@duplojs/utils/dataParser'; +import * as EE from '@duplojs/utils/either'; import { createDuplojsServerUtilsKind } from '../kind.mjs'; -import { logHelp } from './help.mjs'; -import { CommandManyArgumentsError } from './errors.mjs'; -import { createBooleanOption } from './options/boolean.mjs'; import { exitProcess } from '../common/exitProcess.mjs'; +import { createError, setErrorPath, popErrorPath, SymbolCommandError, addDataParserError, addIssue, interpretCommandError } from './error.mjs'; +import { logCommandHelp } from './help.mjs'; +import { createBooleanOption } from './options/boolean.mjs'; +function printError(commandError, error) { + if (!error) { + // eslint-disable-next-line no-console + console.error(interpretCommandError(commandError)); + exitProcess(1); + } + return SymbolCommandError; +} const commandKind = createDuplojsServerUtilsKind("command"); const helpOption = createBooleanOption("help", { aliases: ["h"] }); function create(...args) { @@ -16,57 +28,96 @@ function create(...args) { description: params.description ?? null, options: params.options ?? [], subject: params.subject ?? null, - execute: async (args) => { + execute: async (args, error) => { + const commandError = error ?? createError(self.name); + const pathIndex = commandError.currentCommandPath.length; if (self.subject instanceof Array) { for (const command of self.subject) { if (args[0] === command.name) { - await command.execute(A.shift(args)); - return; + let result = undefined; + setErrorPath(commandError, command.name, pathIndex); + try { + result = await command.execute(AA.shift(args), commandError); + } + finally { + popErrorPath(commandError); + } + if (result === SymbolCommandError) { + return printError(commandError, error); + } + return result; } } } - const help = helpOption.execute(args); - if (help.result) { - logHelp(self); - exitProcess(0); - return; + const help = helpOption.execute(args, commandError); + if (help === SymbolCommandError) { + return printError(commandError, error); + } + else if (help.result) { + logCommandHelp(self); + return void exitProcess(0); } - const commandOptions = A.reduce(self.options, A.reduceFrom({ + const commandOptions = AA.reduce(self.options, AA.reduceFrom({ options: {}, restArgs: args, - }), ({ element: option, lastValue, next }) => { - const optionResult = option.execute(lastValue.restArgs); + }), ({ element: option, lastValue, next, exit }) => { + const optionResult = option.execute(lastValue.restArgs, commandError); + if (optionResult === SymbolCommandError) { + return exit(SymbolCommandError); + } return next({ - options: O.override(lastValue.options, { + options: OO.override(lastValue.options, { [option.name]: optionResult.result, }), restArgs: optionResult.argumentRest, }); }); + if (commandOptions === SymbolCommandError) { + return printError(commandError, error); + } if (self.subject === null) { await execute({ options: commandOptions.options }); } - else if (DP.identifier(self.subject, DP.arrayKind) - || DP.identifier(self.subject, DP.tupleKind)) { + else if (DDP.identifier(self.subject, DDP.arrayKind) + || DDP.identifier(self.subject, DDP.tupleKind)) { + const subjectResult = self.subject.parse(commandOptions.restArgs); + if (EE.isLeft(subjectResult)) { + addDataParserError(commandError, unwrap(subjectResult), { + type: "subject", + }); + return printError(commandError, error); + } await execute({ options: commandOptions.options, - subject: self.subject.parseOrThrow(commandOptions.restArgs), + subject: unwrap(subjectResult), }); } - else if (DP.identifier(self.subject, DP.dataParserKind)) { + else if (DDP.identifier(self.subject, DDP.dataParserKind)) { if (commandOptions.restArgs.length > 1) { - throw new CommandManyArgumentsError(commandOptions.restArgs.length); + addIssue(commandError, { + type: "command", + expected: "exactly one subject argument", + received: commandOptions.restArgs, + message: `Expected exactly one subject argument, received ${commandOptions.restArgs.length}.`, + }); + return printError(commandError, error); + } + const subjectResult = self.subject.parse(commandOptions.restArgs); + if (EE.isLeft(subjectResult)) { + addDataParserError(commandError, unwrap(subjectResult), { + type: "subject", + }); + return printError(commandError, error); } await execute({ options: commandOptions.options, - subject: self.subject.parseOrThrow(commandOptions.restArgs), + subject: unwrap(subjectResult), }); } else { await execute({}); } - exitProcess(0); - return; + return void exitProcess(0); }, }); return self; diff --git a/docs/libs/v0/command/error.cjs b/docs/libs/v0/command/error.cjs new file mode 100644 index 0000000..a029461 --- /dev/null +++ b/docs/libs/v0/command/error.cjs @@ -0,0 +1,109 @@ +'use strict'; + +var utils = require('@duplojs/utils'); + +const SymbolCommandError = Symbol.for("SymbolCommandError"); +function createError(commandName) { + return { + issues: [], + currentCommandPath: [commandName], + }; +} +function addIssue(error, issue) { + error.issues.push({ + ...issue, + commandPath: [...error.currentCommandPath], + }); + return SymbolCommandError; +} +function setErrorPath(error, value, index) { + error.currentCommandPath[index] = value; + return error; +} +function popErrorPath(error) { + error.currentCommandPath.pop(); + return error; +} +function addDataParserError(error, parseError, params) { + for (const issue of parseError.issues) { + error.issues.push({ + type: params.type, + commandPath: [...error.currentCommandPath], + target: params.target, + parserPath: issue.path || undefined, + expected: issue.expected, + received: issue.data, + message: issue.message, + }); + } + return SymbolCommandError; +} +function interpretCommandError(error) { + return utils.Printer.renderParagraph([ + utils.Printer.render([ + utils.Printer.colorizedBold("Command failed", "red"), + utils.Printer.back, + utils.Printer.indent(1), + utils.Printer.colorizedBold("COMMAND: ", "cyan"), + error.issues[0]?.commandPath.join(" ") ?? error.currentCommandPath.join(" "), + ], ""), + error.issues.map((issue) => utils.Printer.renderParagraph([ + issue.type === "option" + && issue.target + && utils.Printer.render([ + utils.Printer.indent(1), + utils.Printer.colorizedBold("OPTION: ", "magenta"), + `--${issue.target}`, + ], ""), + issue.type === "subject" + && issue.parserPath + && utils.Printer.render([ + utils.Printer.indent(1), + utils.Printer.colorizedBold("SUBJECT:", "magenta"), + ], ""), + utils.Printer.renderLine([ + utils.Printer.colorizedBold("✖", "red"), + issue.parserPath && utils.Printer.colorizedBold(issue.parserPath, "cyan"), + "expected", + utils.Printer.colorized(issue.expected, "green"), + "but received", + utils.Printer.colorized(utils.Printer.stringify(issue.received), "red"), + ]), + issue.message !== undefined && `${utils.Printer.indent(1)}↳ ${issue.message}`, + ])), + error.issues.length === 0 && "No issue found", + ]); +} +function interpretExecOptionError(error) { + return utils.Printer.renderParagraph([ + utils.Printer.colorizedBold("Invalid options", "red"), + error.issues.map((issue) => utils.Printer.renderParagraph([ + issue.type === "option" + && issue.target + && utils.Printer.render([ + utils.Printer.indent(1), + utils.Printer.colorizedBold("OPTION: ", "magenta"), + `--${issue.target}`, + ], ""), + utils.Printer.renderLine([ + utils.Printer.colorizedBold("✖", "red"), + issue.parserPath && utils.Printer.colorizedBold(issue.parserPath, "cyan"), + "expected", + utils.Printer.colorized(issue.expected, "green"), + "but received", + utils.Printer.colorized(utils.Printer.stringify(issue.received), "red"), + ]), + issue.message !== undefined && `${utils.Printer.indent(1)}↳ ${issue.message}`, + ])), + error.issues.length === 0 && "No issue found", + ]); +} + +exports.SymbolCommandError = SymbolCommandError; +exports.addDataParserError = addDataParserError; +exports.addIssue = addIssue; +exports.createError = createError; +exports.interpretCommandError = interpretCommandError; +exports.interpretExecOptionError = interpretExecOptionError; +exports.popErrorPath = popErrorPath; +exports.setErrorPath = setErrorPath; diff --git a/docs/libs/v0/command/error.d.ts b/docs/libs/v0/command/error.d.ts new file mode 100644 index 0000000..b7d9879 --- /dev/null +++ b/docs/libs/v0/command/error.d.ts @@ -0,0 +1,26 @@ +import type * as DDP from "@duplojs/utils/dataParser"; +export interface CommandErrorIssue { + readonly type: "command" | "option" | "subject"; + readonly commandPath: readonly string[]; + readonly target?: string; + readonly parserPath?: string; + readonly expected: string; + readonly received: unknown; + readonly message?: string; +} +export interface CommandError { + readonly issues: CommandErrorIssue[]; + readonly currentCommandPath: string[]; +} +export declare const SymbolCommandError: unique symbol; +export type SymbolCommandError = typeof SymbolCommandError; +export declare function createError(commandName: string): CommandError; +export declare function addIssue(error: CommandError, issue: Omit): typeof SymbolCommandError; +export declare function setErrorPath(error: CommandError, value: string, index: number): CommandError; +export declare function popErrorPath(error: CommandError): CommandError; +export declare function addDataParserError(error: CommandError, parseError: DDP.DataParserError, params: { + type: "option" | "subject"; + target?: string; +}): SymbolCommandError; +export declare function interpretCommandError(error: CommandError): string; +export declare function interpretExecOptionError(error: CommandError): string; diff --git a/docs/libs/v0/command/error.mjs b/docs/libs/v0/command/error.mjs new file mode 100644 index 0000000..3acfd24 --- /dev/null +++ b/docs/libs/v0/command/error.mjs @@ -0,0 +1,100 @@ +import { Printer } from '@duplojs/utils'; + +const SymbolCommandError = Symbol.for("SymbolCommandError"); +function createError(commandName) { + return { + issues: [], + currentCommandPath: [commandName], + }; +} +function addIssue(error, issue) { + error.issues.push({ + ...issue, + commandPath: [...error.currentCommandPath], + }); + return SymbolCommandError; +} +function setErrorPath(error, value, index) { + error.currentCommandPath[index] = value; + return error; +} +function popErrorPath(error) { + error.currentCommandPath.pop(); + return error; +} +function addDataParserError(error, parseError, params) { + for (const issue of parseError.issues) { + error.issues.push({ + type: params.type, + commandPath: [...error.currentCommandPath], + target: params.target, + parserPath: issue.path || undefined, + expected: issue.expected, + received: issue.data, + message: issue.message, + }); + } + return SymbolCommandError; +} +function interpretCommandError(error) { + return Printer.renderParagraph([ + Printer.render([ + Printer.colorizedBold("Command failed", "red"), + Printer.back, + Printer.indent(1), + Printer.colorizedBold("COMMAND: ", "cyan"), + error.issues[0]?.commandPath.join(" ") ?? error.currentCommandPath.join(" "), + ], ""), + error.issues.map((issue) => Printer.renderParagraph([ + issue.type === "option" + && issue.target + && Printer.render([ + Printer.indent(1), + Printer.colorizedBold("OPTION: ", "magenta"), + `--${issue.target}`, + ], ""), + issue.type === "subject" + && issue.parserPath + && Printer.render([ + Printer.indent(1), + Printer.colorizedBold("SUBJECT:", "magenta"), + ], ""), + Printer.renderLine([ + Printer.colorizedBold("✖", "red"), + issue.parserPath && Printer.colorizedBold(issue.parserPath, "cyan"), + "expected", + Printer.colorized(issue.expected, "green"), + "but received", + Printer.colorized(Printer.stringify(issue.received), "red"), + ]), + issue.message !== undefined && `${Printer.indent(1)}↳ ${issue.message}`, + ])), + error.issues.length === 0 && "No issue found", + ]); +} +function interpretExecOptionError(error) { + return Printer.renderParagraph([ + Printer.colorizedBold("Invalid options", "red"), + error.issues.map((issue) => Printer.renderParagraph([ + issue.type === "option" + && issue.target + && Printer.render([ + Printer.indent(1), + Printer.colorizedBold("OPTION: ", "magenta"), + `--${issue.target}`, + ], ""), + Printer.renderLine([ + Printer.colorizedBold("✖", "red"), + issue.parserPath && Printer.colorizedBold(issue.parserPath, "cyan"), + "expected", + Printer.colorized(issue.expected, "green"), + "but received", + Printer.colorized(Printer.stringify(issue.received), "red"), + ]), + issue.message !== undefined && `${Printer.indent(1)}↳ ${issue.message}`, + ])), + error.issues.length === 0 && "No issue found", + ]); +} + +export { SymbolCommandError, addDataParserError, addIssue, createError, interpretCommandError, interpretExecOptionError, popErrorPath, setErrorPath }; diff --git a/docs/libs/v0/command/errors.cjs b/docs/libs/v0/command/errors.cjs deleted file mode 100644 index 2a955da..0000000 --- a/docs/libs/v0/command/errors.cjs +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -var utils = require('@duplojs/utils'); -var kind = require('../kind.cjs'); - -class CommandManyArgumentsError extends utils.kindHeritage("command-many-arguments-error", kind.createDuplojsServerUtilsKind("command-many-arguments-error"), Error) { - restArgumentsLength; - constructor(restArgumentsLength) { - super({}, [`Expected exactly one subject argument, received ${restArgumentsLength}.`]); - this.restArgumentsLength = restArgumentsLength; - } -} -class CommandOptionValueLooksLikeOptionError extends utils.kindHeritage("command-option-value-looks-like-option-error", kind.createDuplojsServerUtilsKind("command-option-value-looks-like-option-error"), Error) { - optionName; - value; - constructor(optionName, value) { - super({}, [`Missing value for option "${optionName}": received another option token instead of a value.`]); - this.optionName = optionName; - this.value = value; - } -} -class CommandOptionValueNotRequiredError extends utils.kindHeritage("command-option-value-not-required-error", kind.createDuplojsServerUtilsKind("command-option-value-not-required-error"), Error) { - optionName; - constructor(optionName) { - super({}, [`Option "${optionName}" does not accept a value.`]); - this.optionName = optionName; - } -} -class CommandOptionRequiredError extends utils.kindHeritage("command-option-required-error", kind.createDuplojsServerUtilsKind("command-option-required-error"), Error) { - optionName; - constructor(optionName) { - super({}, [`Option "${optionName}" is required.`]); - this.optionName = optionName; - } -} - -exports.CommandManyArgumentsError = CommandManyArgumentsError; -exports.CommandOptionRequiredError = CommandOptionRequiredError; -exports.CommandOptionValueLooksLikeOptionError = CommandOptionValueLooksLikeOptionError; -exports.CommandOptionValueNotRequiredError = CommandOptionValueNotRequiredError; diff --git a/docs/libs/v0/command/errors.d.ts b/docs/libs/v0/command/errors.d.ts deleted file mode 100644 index 692df3c..0000000 --- a/docs/libs/v0/command/errors.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -declare const CommandManyArgumentsError_base: new (params: { - "@DuplojsServerUtils/command-many-arguments-error"?: unknown; -}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; -export declare class CommandManyArgumentsError extends CommandManyArgumentsError_base { - restArgumentsLength: number; - constructor(restArgumentsLength: number); -} -declare const CommandOptionValueLooksLikeOptionError_base: new (params: { - "@DuplojsServerUtils/command-option-value-looks-like-option-error"?: unknown; -}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; -export declare class CommandOptionValueLooksLikeOptionError extends CommandOptionValueLooksLikeOptionError_base { - optionName: string; - value: string | undefined; - constructor(optionName: string, value: string | undefined); -} -declare const CommandOptionValueNotRequiredError_base: new (params: { - "@DuplojsServerUtils/command-option-value-not-required-error"?: unknown; -}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; -export declare class CommandOptionValueNotRequiredError extends CommandOptionValueNotRequiredError_base { - optionName: string; - constructor(optionName: string); -} -declare const CommandOptionRequiredError_base: new (params: { - "@DuplojsServerUtils/command-option-required-error"?: unknown; -}, parentParams: readonly [message?: string | undefined, options?: ErrorOptions | undefined]) => Error & import("@duplojs/utils").Kind, unknown> & import("@duplojs/utils").Kind, unknown>; -export declare class CommandOptionRequiredError extends CommandOptionRequiredError_base { - optionName: string; - constructor(optionName: string); -} -export {}; diff --git a/docs/libs/v0/command/errors.mjs b/docs/libs/v0/command/errors.mjs deleted file mode 100644 index 65990f7..0000000 --- a/docs/libs/v0/command/errors.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import { kindHeritage } from '@duplojs/utils'; -import { createDuplojsServerUtilsKind } from '../kind.mjs'; - -class CommandManyArgumentsError extends kindHeritage("command-many-arguments-error", createDuplojsServerUtilsKind("command-many-arguments-error"), Error) { - restArgumentsLength; - constructor(restArgumentsLength) { - super({}, [`Expected exactly one subject argument, received ${restArgumentsLength}.`]); - this.restArgumentsLength = restArgumentsLength; - } -} -class CommandOptionValueLooksLikeOptionError extends kindHeritage("command-option-value-looks-like-option-error", createDuplojsServerUtilsKind("command-option-value-looks-like-option-error"), Error) { - optionName; - value; - constructor(optionName, value) { - super({}, [`Missing value for option "${optionName}": received another option token instead of a value.`]); - this.optionName = optionName; - this.value = value; - } -} -class CommandOptionValueNotRequiredError extends kindHeritage("command-option-value-not-required-error", createDuplojsServerUtilsKind("command-option-value-not-required-error"), Error) { - optionName; - constructor(optionName) { - super({}, [`Option "${optionName}" does not accept a value.`]); - this.optionName = optionName; - } -} -class CommandOptionRequiredError extends kindHeritage("command-option-required-error", createDuplojsServerUtilsKind("command-option-required-error"), Error) { - optionName; - constructor(optionName) { - super({}, [`Option "${optionName}" is required.`]); - this.optionName = optionName; - } -} - -export { CommandManyArgumentsError, CommandOptionRequiredError, CommandOptionValueLooksLikeOptionError, CommandOptionValueNotRequiredError }; diff --git a/docs/libs/v0/command/exec.d.ts b/docs/libs/v0/command/exec.d.ts index 2d3365d..3dca9e0 100644 --- a/docs/libs/v0/command/exec.d.ts +++ b/docs/libs/v0/command/exec.d.ts @@ -1,6 +1,6 @@ -import { type MaybePromise } from "@duplojs/utils"; +import type { MaybePromise } from "@duplojs/utils"; import { type CreateCommandExecuteParams, type CreateCommandParams, type Subject } from "./create"; -import { type Option } from "./options"; +import type { Option } from "./options"; /** * Execute a root command from process arguments. * diff --git a/docs/libs/v0/command/execOptions.cjs b/docs/libs/v0/command/execOptions.cjs new file mode 100644 index 0000000..6c0e0ad --- /dev/null +++ b/docs/libs/v0/command/execOptions.cjs @@ -0,0 +1,68 @@ +'use strict'; + +var AA = require('@duplojs/utils/array'); +var OO = require('@duplojs/utils/object'); +var error = require('./error.cjs'); +var help = require('./help.cjs'); +var exitProcess = require('../common/exitProcess.cjs'); +var getProcessArguments = require('../common/getProcessArguments.cjs'); +var boolean = require('./options/boolean.cjs'); + +function _interopNamespaceDefault(e) { + var n = Object.create(null); + if (e) { + Object.keys(e).forEach(function (k) { + if (k !== 'default') { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { return e[k]; } + }); + } + }); + } + n.default = e; + return Object.freeze(n); +} + +var AA__namespace = /*#__PURE__*/_interopNamespaceDefault(AA); +var OO__namespace = /*#__PURE__*/_interopNamespaceDefault(OO); + +const helpOption = boolean.createBooleanOption("help", { aliases: ["h"] }); +function execOptions(...options) { + const processArguments = getProcessArguments.getProcessArguments(); + const error$1 = error.createError("root"); + const help$1 = helpOption.execute(processArguments, error$1); + if (help$1 === error.SymbolCommandError) { + // eslint-disable-next-line no-console + console.error(error.interpretExecOptionError(error$1)); + return void exitProcess.exitProcess(1); + } + else if (help$1.result) { + help.logExecOptionHelp(options); + return void exitProcess.exitProcess(0); + } + const result = AA__namespace.reduce(options, AA__namespace.reduceFrom({ + options: {}, + restArgs: processArguments, + }), ({ element: option, lastValue, next, exit }) => { + const optionResult = option.execute(lastValue.restArgs, error$1); + if (optionResult === error.SymbolCommandError) { + return exit(error.SymbolCommandError); + } + return next({ + options: OO__namespace.override(lastValue.options, { + [option.name]: optionResult.result, + }), + restArgs: optionResult.argumentRest, + }); + }); + if (result === error.SymbolCommandError) { + // eslint-disable-next-line no-console + console.error(error.interpretExecOptionError(error$1)); + return void exitProcess.exitProcess(1); + } + return result.options; +} + +exports.execOptions = execOptions; diff --git a/docs/libs/v0/command/execOptions.d.ts b/docs/libs/v0/command/execOptions.d.ts new file mode 100644 index 0000000..9847981 --- /dev/null +++ b/docs/libs/v0/command/execOptions.d.ts @@ -0,0 +1,46 @@ +import type { SimplifyTopLevel } from "@duplojs/utils"; +import { type Option } from "./options"; +type ComputeResult = SimplifyTopLevel<{ + [GenericOption in GenericOptions[number] as GenericOption extends Option ? GenericName : never]: GenericOption extends Option ? GenericResult : never; +}>; +/** + * Execute command options from process arguments. + * + * `execOptions` reads runtime arguments, executes each option parser, and returns an object keyed by option name. + * It also adds an automatic `--help` / `-h` manual generated from the declared options. + * + * ```ts + * const portOption = SC.createOption( + * "port", + * DP.coerce.number(), + * { + * description: "HTTP port", + * required: true, + * }, + * ); + * const portResult = SC.execOptions(portOption); + * // portResult.port: number + * + * const tagOption = SC.createArrayOption( + * "tag", + * DP.string(), + * { + * aliases: ["t"], + * required: true, + * }, + * ); + * const forceOption = SC.createBooleanOption("force"); + * + * const options = SC.execOptions(tagOption, forceOption); + * // options.tag: string[] | undefined + * ``` + * + * @remarks + * Use this helper when your CLI only needs option parsing and does not need a command subject. + * + * @see https://server-utils.duplojs.dev/en/v0/api/command/execOptions + * @namespace SC + * + */ +export declare function execOptions(...options: GenericOptions): ComputeResult; +export {}; diff --git a/docs/libs/v0/command/execOptions.mjs b/docs/libs/v0/command/execOptions.mjs new file mode 100644 index 0000000..66065e7 --- /dev/null +++ b/docs/libs/v0/command/execOptions.mjs @@ -0,0 +1,46 @@ +import * as AA from '@duplojs/utils/array'; +import * as OO from '@duplojs/utils/object'; +import { createError, SymbolCommandError, interpretExecOptionError } from './error.mjs'; +import { logExecOptionHelp } from './help.mjs'; +import { exitProcess } from '../common/exitProcess.mjs'; +import { getProcessArguments } from '../common/getProcessArguments.mjs'; +import { createBooleanOption } from './options/boolean.mjs'; + +const helpOption = createBooleanOption("help", { aliases: ["h"] }); +function execOptions(...options) { + const processArguments = getProcessArguments(); + const error = createError("root"); + const help = helpOption.execute(processArguments, error); + if (help === SymbolCommandError) { + // eslint-disable-next-line no-console + console.error(interpretExecOptionError(error)); + return void exitProcess(1); + } + else if (help.result) { + logExecOptionHelp(options); + return void exitProcess(0); + } + const result = AA.reduce(options, AA.reduceFrom({ + options: {}, + restArgs: processArguments, + }), ({ element: option, lastValue, next, exit }) => { + const optionResult = option.execute(lastValue.restArgs, error); + if (optionResult === SymbolCommandError) { + return exit(SymbolCommandError); + } + return next({ + options: OO.override(lastValue.options, { + [option.name]: optionResult.result, + }), + restArgs: optionResult.argumentRest, + }); + }); + if (result === SymbolCommandError) { + // eslint-disable-next-line no-console + console.error(interpretExecOptionError(error)); + return void exitProcess(1); + } + return result.options; +} + +export { execOptions }; diff --git a/docs/libs/v0/command/help.cjs b/docs/libs/v0/command/help.cjs index 787b700..f5657c5 100644 --- a/docs/libs/v0/command/help.cjs +++ b/docs/libs/v0/command/help.cjs @@ -1,24 +1,50 @@ 'use strict'; var utils = require('@duplojs/utils'); -var printer = require('./printer.cjs'); +var AA = require('@duplojs/utils/array'); +var PP = require('@duplojs/utils/pattern'); +var DDP = require('@duplojs/utils/dataParser'); +function _interopNamespaceDefault(e) { + var n = Object.create(null); + if (e) { + Object.keys(e).forEach(function (k) { + if (k !== 'default') { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { return e[k]; } + }); + } + }); + } + n.default = e; + return Object.freeze(n); +} + +var AA__namespace = /*#__PURE__*/_interopNamespaceDefault(AA); +var PP__namespace = /*#__PURE__*/_interopNamespaceDefault(PP); +var DDP__namespace = /*#__PURE__*/_interopNamespaceDefault(DDP); + +/** + * @internal + */ function formatSubject(subject) { - return utils.P.match(subject) - .when(utils.DP.identifier(utils.DP.stringKind), utils.justReturn("string")) - .when(utils.DP.identifier(utils.DP.numberKind), utils.justReturn("number")) - .when(utils.DP.identifier(utils.DP.bigIntKind), utils.justReturn("bigint")) - .when(utils.DP.identifier(utils.DP.dateKind), utils.justReturn("date")) - .when(utils.DP.identifier(utils.DP.timeKind), utils.justReturn("time")) - .when(utils.DP.identifier(utils.DP.nilKind), utils.justReturn("null")) - .when(utils.DP.identifier(utils.DP.literalKind), (subject) => utils.pipe(subject.definition.value, utils.A.map(String), utils.A.join(" | "))) - .when(utils.DP.identifier(utils.DP.templateLiteralKind), (subject) => utils.pipe(subject.definition.template, utils.A.map((part) => utils.DP.identifier(part, utils.DP.dataParserKind) + return PP__namespace.match(subject) + .when(DDP__namespace.identifier(DDP__namespace.stringKind), utils.justReturn("string")) + .when(DDP__namespace.identifier(DDP__namespace.numberKind), utils.justReturn("number")) + .when(DDP__namespace.identifier(DDP__namespace.bigIntKind), utils.justReturn("bigint")) + .when(DDP__namespace.identifier(DDP__namespace.dateKind), utils.justReturn("date")) + .when(DDP__namespace.identifier(DDP__namespace.timeKind), utils.justReturn("time")) + .when(DDP__namespace.identifier(DDP__namespace.nilKind), utils.justReturn("null")) + .when(DDP__namespace.identifier(DDP__namespace.literalKind), (subject) => utils.pipe(subject.definition.value, AA__namespace.map(String), AA__namespace.join(" | "))) + .when(DDP__namespace.identifier(DDP__namespace.templateLiteralKind), (subject) => utils.pipe(subject.definition.template, AA__namespace.map((part) => DDP__namespace.identifier(part, DDP__namespace.dataParserKind) ? `\${${formatSubject(part)}}` - : String(part)), utils.A.join(""))) - .when(utils.DP.identifier(utils.DP.unionKind), (subject) => utils.pipe(subject.definition.options, utils.A.map(formatSubject), utils.A.join(" | "))) - .when(utils.DP.identifier(utils.DP.arrayKind), (subject) => `${formatSubject(subject.definition.element)}[]`) - .when(utils.DP.identifier(utils.DP.tupleKind), (subject) => { - const parts = utils.pipe(subject.definition.shape, utils.A.map(formatSubject), utils.A.join(", ")); + : String(part)), AA__namespace.join(""))) + .when(DDP__namespace.identifier(DDP__namespace.unionKind), (subject) => utils.pipe(subject.definition.options, AA__namespace.map(formatSubject), AA__namespace.join(" | "))) + .when(DDP__namespace.identifier(DDP__namespace.arrayKind), (subject) => `${formatSubject(subject.definition.element)}[]`) + .when(DDP__namespace.identifier(DDP__namespace.tupleKind), (subject) => { + const parts = utils.pipe(subject.definition.shape, AA__namespace.map(formatSubject), AA__namespace.join(", ")); const rest = subject.definition.rest ? `${parts ? ", " : ""}...${formatSubject(subject.definition.rest)}[]` : ""; @@ -26,54 +52,77 @@ function formatSubject(subject) { }) .otherwise(utils.justReturn("unknown")); } -function logHelp(command, depth = 0) { - printer.Printer.render([ - printer.Printer.indent(depth), - printer.Printer.colorized("NAME:", "GREEN"), - command.name, +/** + * @internal + */ +function renderOptionsHelp(options, depth) { + return utils.Printer.renderParagraph([ + `${utils.Printer.indent(depth)}${utils.Printer.colorizedBold("OPTIONS:", "blue")}`, + AA__namespace.map(options, (option) => utils.Printer.renderParagraph([ + AA__namespace.join([ + utils.Printer.indent(depth), + utils.Printer.dash, + utils.Printer.colorized(` ${option.name}: `, "cyan"), + utils.Printer.colorized(utils.pipe(option.aliases, AA__namespace.map((alias) => `-${alias},`), AA__namespace.push(`--${option.name}`), AA__namespace.join(" ")), "gray"), + ], ""), + option.description + && `${utils.Printer.indent(depth)} ${option.description}`, + ])), ]); +} +/** + * @internal + */ +function renderCommandHelp(command, depth) { + const logs = []; + logs.push(`${utils.Printer.indent(depth)}${utils.Printer.colorizedBold("NAME:", "green")}${command.name}`); if (command.description) { - printer.Printer.render([ - printer.Printer.indent(depth + 1), - printer.Printer.colorized("DESCRIPTION:", "CYAN"), - printer.Printer.back, - printer.Printer.indent(depth + 1), - command.description, - ]); + logs.push(utils.Printer.renderParagraph([ + `${utils.Printer.indent(depth + 1)}${utils.Printer.colorizedBold("DESCRIPTION:", "cyan")}`, + `${utils.Printer.indent(depth + 1)}${command.description}`, + ])); } - if (utils.A.minElements(command.options, 1)) { - const optionLines = []; - command.options.forEach((option, index) => { - optionLines.push(printer.Printer.indent(depth + 1), printer.Printer.dash, printer.Printer.colorized(` ${option.name}: `, "cyan"), printer.Printer.colorizedOption(option, "gray")); - if (option.description) { - optionLines.push(printer.Printer.back, printer.Printer.indent(depth + 1), ` ${option.description}`); - } - if (index < command.options.length - 1) { - optionLines.push(printer.Printer.back); - } - }); - printer.Printer.render([ - printer.Printer.indent(depth + 1), - printer.Printer.colorized("OPTIONS:", "BLUE"), - printer.Printer.back, - ...optionLines, - ]); + if (AA__namespace.minElements(command.options, 1)) { + logs.push(renderOptionsHelp(command.options, depth + 1)); } if (utils.isType(command.subject, "array")) { for (const childCommand of command.subject) { - logHelp(childCommand, depth + 1); + logs.push(...renderCommandHelp(childCommand, depth + 1)); } } - else if (utils.DP.identifier(command.subject, utils.DP.dataParserKind)) { + else if (DDP__namespace.identifier(command.subject, DDP__namespace.dataParserKind)) { const formattedSubject = formatSubject(command.subject); - printer.Printer.render([ - printer.Printer.indent(depth + 1), - printer.Printer.colorized("SUBJECT:", "MAGENTA"), - utils.hasSomeKinds(command.subject, [utils.DP.tupleKind, utils.DP.arrayKind]) + logs.push(AA__namespace.join([ + utils.Printer.indent(depth + 1), + utils.Printer.colorizedBold("SUBJECT:", "magenta"), + utils.hasSomeKinds(command.subject, [DDP__namespace.tupleKind, DDP__namespace.arrayKind]) ? formattedSubject : `<${formattedSubject}>`, - ]); + ], "")); } + return logs; +} +function logCommandHelp(command, depth = 0) { + // eslint-disable-next-line no-console + console.log(utils.Printer.renderParagraph(renderCommandHelp(command, depth))); +} +/** + * @internal + */ +function renderExecOptionHelp(options, depth) { + return [ + `${utils.Printer.indent(depth)}${utils.Printer.colorizedBold("OPTION HELP", "green")}`, + renderOptionsHelp(options, depth + 1), + ]; +} +function logExecOptionHelp(options, depth = 0) { + // eslint-disable-next-line no-console + console.log(utils.Printer.renderParagraph(renderExecOptionHelp(options, depth))); } -exports.logHelp = logHelp; +exports.formatSubject = formatSubject; +exports.logCommandHelp = logCommandHelp; +exports.logExecOptionHelp = logExecOptionHelp; +exports.renderCommandHelp = renderCommandHelp; +exports.renderExecOptionHelp = renderExecOptionHelp; +exports.renderOptionsHelp = renderOptionsHelp; diff --git a/docs/libs/v0/command/help.d.ts b/docs/libs/v0/command/help.d.ts index b20b532..e8dc2c5 100644 --- a/docs/libs/v0/command/help.d.ts +++ b/docs/libs/v0/command/help.d.ts @@ -1,2 +1,4 @@ import type { Command } from "./create"; -export declare function logHelp(command: Command, depth?: number): void; +import type { Option } from "./options"; +export declare function logCommandHelp(command: Command, depth?: number): void; +export declare function logExecOptionHelp(options: readonly Option[], depth?: number): void; diff --git a/docs/libs/v0/command/help.mjs b/docs/libs/v0/command/help.mjs index 2d26486..ec6a46d 100644 --- a/docs/libs/v0/command/help.mjs +++ b/docs/libs/v0/command/help.mjs @@ -1,22 +1,27 @@ -import { A, isType, DP, hasSomeKinds, P, justReturn, pipe } from '@duplojs/utils'; -import { Printer } from './printer.mjs'; +import { justReturn, pipe, Printer, isType, hasSomeKinds } from '@duplojs/utils'; +import * as AA from '@duplojs/utils/array'; +import * as PP from '@duplojs/utils/pattern'; +import * as DDP from '@duplojs/utils/dataParser'; +/** + * @internal + */ function formatSubject(subject) { - return P.match(subject) - .when(DP.identifier(DP.stringKind), justReturn("string")) - .when(DP.identifier(DP.numberKind), justReturn("number")) - .when(DP.identifier(DP.bigIntKind), justReturn("bigint")) - .when(DP.identifier(DP.dateKind), justReturn("date")) - .when(DP.identifier(DP.timeKind), justReturn("time")) - .when(DP.identifier(DP.nilKind), justReturn("null")) - .when(DP.identifier(DP.literalKind), (subject) => pipe(subject.definition.value, A.map(String), A.join(" | "))) - .when(DP.identifier(DP.templateLiteralKind), (subject) => pipe(subject.definition.template, A.map((part) => DP.identifier(part, DP.dataParserKind) + return PP.match(subject) + .when(DDP.identifier(DDP.stringKind), justReturn("string")) + .when(DDP.identifier(DDP.numberKind), justReturn("number")) + .when(DDP.identifier(DDP.bigIntKind), justReturn("bigint")) + .when(DDP.identifier(DDP.dateKind), justReturn("date")) + .when(DDP.identifier(DDP.timeKind), justReturn("time")) + .when(DDP.identifier(DDP.nilKind), justReturn("null")) + .when(DDP.identifier(DDP.literalKind), (subject) => pipe(subject.definition.value, AA.map(String), AA.join(" | "))) + .when(DDP.identifier(DDP.templateLiteralKind), (subject) => pipe(subject.definition.template, AA.map((part) => DDP.identifier(part, DDP.dataParserKind) ? `\${${formatSubject(part)}}` - : String(part)), A.join(""))) - .when(DP.identifier(DP.unionKind), (subject) => pipe(subject.definition.options, A.map(formatSubject), A.join(" | "))) - .when(DP.identifier(DP.arrayKind), (subject) => `${formatSubject(subject.definition.element)}[]`) - .when(DP.identifier(DP.tupleKind), (subject) => { - const parts = pipe(subject.definition.shape, A.map(formatSubject), A.join(", ")); + : String(part)), AA.join(""))) + .when(DDP.identifier(DDP.unionKind), (subject) => pipe(subject.definition.options, AA.map(formatSubject), AA.join(" | "))) + .when(DDP.identifier(DDP.arrayKind), (subject) => `${formatSubject(subject.definition.element)}[]`) + .when(DDP.identifier(DDP.tupleKind), (subject) => { + const parts = pipe(subject.definition.shape, AA.map(formatSubject), AA.join(", ")); const rest = subject.definition.rest ? `${parts ? ", " : ""}...${formatSubject(subject.definition.rest)}[]` : ""; @@ -24,54 +29,72 @@ function formatSubject(subject) { }) .otherwise(justReturn("unknown")); } -function logHelp(command, depth = 0) { - Printer.render([ - Printer.indent(depth), - Printer.colorized("NAME:", "GREEN"), - command.name, +/** + * @internal + */ +function renderOptionsHelp(options, depth) { + return Printer.renderParagraph([ + `${Printer.indent(depth)}${Printer.colorizedBold("OPTIONS:", "blue")}`, + AA.map(options, (option) => Printer.renderParagraph([ + AA.join([ + Printer.indent(depth), + Printer.dash, + Printer.colorized(` ${option.name}: `, "cyan"), + Printer.colorized(pipe(option.aliases, AA.map((alias) => `-${alias},`), AA.push(`--${option.name}`), AA.join(" ")), "gray"), + ], ""), + option.description + && `${Printer.indent(depth)} ${option.description}`, + ])), ]); +} +/** + * @internal + */ +function renderCommandHelp(command, depth) { + const logs = []; + logs.push(`${Printer.indent(depth)}${Printer.colorizedBold("NAME:", "green")}${command.name}`); if (command.description) { - Printer.render([ - Printer.indent(depth + 1), - Printer.colorized("DESCRIPTION:", "CYAN"), - Printer.back, - Printer.indent(depth + 1), - command.description, - ]); + logs.push(Printer.renderParagraph([ + `${Printer.indent(depth + 1)}${Printer.colorizedBold("DESCRIPTION:", "cyan")}`, + `${Printer.indent(depth + 1)}${command.description}`, + ])); } - if (A.minElements(command.options, 1)) { - const optionLines = []; - command.options.forEach((option, index) => { - optionLines.push(Printer.indent(depth + 1), Printer.dash, Printer.colorized(` ${option.name}: `, "cyan"), Printer.colorizedOption(option, "gray")); - if (option.description) { - optionLines.push(Printer.back, Printer.indent(depth + 1), ` ${option.description}`); - } - if (index < command.options.length - 1) { - optionLines.push(Printer.back); - } - }); - Printer.render([ - Printer.indent(depth + 1), - Printer.colorized("OPTIONS:", "BLUE"), - Printer.back, - ...optionLines, - ]); + if (AA.minElements(command.options, 1)) { + logs.push(renderOptionsHelp(command.options, depth + 1)); } if (isType(command.subject, "array")) { for (const childCommand of command.subject) { - logHelp(childCommand, depth + 1); + logs.push(...renderCommandHelp(childCommand, depth + 1)); } } - else if (DP.identifier(command.subject, DP.dataParserKind)) { + else if (DDP.identifier(command.subject, DDP.dataParserKind)) { const formattedSubject = formatSubject(command.subject); - Printer.render([ + logs.push(AA.join([ Printer.indent(depth + 1), - Printer.colorized("SUBJECT:", "MAGENTA"), - hasSomeKinds(command.subject, [DP.tupleKind, DP.arrayKind]) + Printer.colorizedBold("SUBJECT:", "magenta"), + hasSomeKinds(command.subject, [DDP.tupleKind, DDP.arrayKind]) ? formattedSubject : `<${formattedSubject}>`, - ]); + ], "")); } + return logs; +} +function logCommandHelp(command, depth = 0) { + // eslint-disable-next-line no-console + console.log(Printer.renderParagraph(renderCommandHelp(command, depth))); +} +/** + * @internal + */ +function renderExecOptionHelp(options, depth) { + return [ + `${Printer.indent(depth)}${Printer.colorizedBold("OPTION HELP", "green")}`, + renderOptionsHelp(options, depth + 1), + ]; +} +function logExecOptionHelp(options, depth = 0) { + // eslint-disable-next-line no-console + console.log(Printer.renderParagraph(renderExecOptionHelp(options, depth))); } -export { logHelp }; +export { formatSubject, logCommandHelp, logExecOptionHelp, renderCommandHelp, renderExecOptionHelp, renderOptionsHelp }; diff --git a/docs/libs/v0/command/index.cjs b/docs/libs/v0/command/index.cjs index bb9a6b8..cda7ff9 100644 --- a/docs/libs/v0/command/index.cjs +++ b/docs/libs/v0/command/index.cjs @@ -1,10 +1,10 @@ 'use strict'; -var errors = require('./errors.cjs'); var create = require('./create.cjs'); var exec = require('./exec.cjs'); -var printer = require('./printer.cjs'); var help = require('./help.cjs'); +var error = require('./error.cjs'); +var execOptions = require('./execOptions.cjs'); var base = require('./options/base.cjs'); var boolean = require('./options/boolean.cjs'); var simple = require('./options/simple.cjs'); @@ -14,17 +14,23 @@ var array = require('./options/array.cjs'); * {@include command/index.md} */ -exports.CommandManyArgumentsError = errors.CommandManyArgumentsError; -exports.CommandOptionRequiredError = errors.CommandOptionRequiredError; -exports.CommandOptionValueLooksLikeOptionError = errors.CommandOptionValueLooksLikeOptionError; -exports.CommandOptionValueNotRequiredError = errors.CommandOptionValueNotRequiredError; exports.create = create.create; exports.exec = exec.exec; -Object.defineProperty(exports, "Printer", { - enumerable: true, - get: function () { return printer.Printer; } -}); -exports.logHelp = help.logHelp; +exports.formatSubject = help.formatSubject; +exports.logCommandHelp = help.logCommandHelp; +exports.logExecOptionHelp = help.logExecOptionHelp; +exports.renderCommandHelp = help.renderCommandHelp; +exports.renderExecOptionHelp = help.renderExecOptionHelp; +exports.renderOptionsHelp = help.renderOptionsHelp; +exports.SymbolCommandError = error.SymbolCommandError; +exports.addDataParserError = error.addDataParserError; +exports.addIssue = error.addIssue; +exports.createError = error.createError; +exports.interpretCommandError = error.interpretCommandError; +exports.interpretExecOptionError = error.interpretExecOptionError; +exports.popErrorPath = error.popErrorPath; +exports.setErrorPath = error.setErrorPath; +exports.execOptions = execOptions.execOptions; exports.initOption = base.initOption; exports.createBooleanOption = boolean.createBooleanOption; exports.createOption = simple.createOption; diff --git a/docs/libs/v0/command/index.d.ts b/docs/libs/v0/command/index.d.ts index 3759573..4d3beee 100644 --- a/docs/libs/v0/command/index.d.ts +++ b/docs/libs/v0/command/index.d.ts @@ -24,8 +24,8 @@ */ export * from "./types"; export * from "./options"; -export * from "./errors"; export * from "./create"; export * from "./exec"; -export * from "./printer"; export * from "./help"; +export * from "./error"; +export * from "./execOptions"; diff --git a/docs/libs/v0/command/index.mjs b/docs/libs/v0/command/index.mjs index b9f3acc..e5d2ca1 100644 --- a/docs/libs/v0/command/index.mjs +++ b/docs/libs/v0/command/index.mjs @@ -1,8 +1,8 @@ -export { CommandManyArgumentsError, CommandOptionRequiredError, CommandOptionValueLooksLikeOptionError, CommandOptionValueNotRequiredError } from './errors.mjs'; export { create } from './create.mjs'; export { exec } from './exec.mjs'; -export { Printer } from './printer.mjs'; -export { logHelp } from './help.mjs'; +export { formatSubject, logCommandHelp, logExecOptionHelp, renderCommandHelp, renderExecOptionHelp, renderOptionsHelp } from './help.mjs'; +export { SymbolCommandError, addDataParserError, addIssue, createError, interpretCommandError, interpretExecOptionError, popErrorPath, setErrorPath } from './error.mjs'; +export { execOptions } from './execOptions.mjs'; export { initOption } from './options/base.mjs'; export { createBooleanOption } from './options/boolean.mjs'; export { createOption } from './options/simple.mjs'; diff --git a/docs/libs/v0/command/options/array.cjs b/docs/libs/v0/command/options/array.cjs index c2aa816..71625bb 100644 --- a/docs/libs/v0/command/options/array.cjs +++ b/docs/libs/v0/command/options/array.cjs @@ -1,26 +1,63 @@ 'use strict'; var utils = require('@duplojs/utils'); +var SS = require('@duplojs/utils/string'); +var DDP = require('@duplojs/utils/dataParser'); +var EE = require('@duplojs/utils/either'); var base = require('./base.cjs'); -var errors = require('../errors.cjs'); +var error = require('../error.cjs'); + +function _interopNamespaceDefault(e) { + var n = Object.create(null); + if (e) { + Object.keys(e).forEach(function (k) { + if (k !== 'default') { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { return e[k]; } + }); + } + }); + } + n.default = e; + return Object.freeze(n); +} + +var SS__namespace = /*#__PURE__*/_interopNamespaceDefault(SS); +var DDP__namespace = /*#__PURE__*/_interopNamespaceDefault(DDP); +var EE__namespace = /*#__PURE__*/_interopNamespaceDefault(EE); const defaultSeparator = ","; function createArrayOption(name, schema, params) { - const dataParser = utils.pipe(schema, utils.DP.array, (schema) => params?.min - ? schema.addChecker(utils.DP.checkerArrayMin(params.min)) + const dataParser = utils.pipe(schema, DDP__namespace.array, (schema) => params?.min + ? schema.addChecker(DDP__namespace.checkerArrayMin(params.min)) : schema, (schema) => params?.max - ? schema.addChecker(utils.DP.checkerArrayMax(params.max)) + ? schema.addChecker(DDP__namespace.checkerArrayMax(params.max)) : schema, (schema) => params?.required ? schema - : utils.DP.optional(schema)); - return base.initOption(name, ({ isHere, value }) => { + : DDP__namespace.optional(schema)); + return base.initOption(name, ({ isHere, value }, error$1) => { if (!isHere && params?.required) { - throw new errors.CommandOptionRequiredError(name); + return error.addIssue(error$1, { + type: "option", + target: name, + expected: `required option --${name}`, + received: value, + message: `Option "${name}" is required.`, + }); } const values = value !== undefined - ? utils.S.split(value, params?.separator ?? defaultSeparator) + ? SS__namespace.split(value, params?.separator ?? defaultSeparator) : undefined; - return dataParser.parseOrThrow(values); + const result = dataParser.parse(values); + if (EE__namespace.isLeft(result)) { + return error.addDataParserError(error$1, utils.unwrap(result), { + type: "option", + target: name, + }); + } + return utils.unwrap(result); }, { description: params?.description, aliases: params?.aliases, diff --git a/docs/libs/v0/command/options/array.d.ts b/docs/libs/v0/command/options/array.d.ts index dd6c002..751ae0d 100644 --- a/docs/libs/v0/command/options/array.d.ts +++ b/docs/libs/v0/command/options/array.d.ts @@ -1,4 +1,5 @@ -import { type A, DP } from "@duplojs/utils"; +import * as DDP from "@duplojs/utils/dataParser"; +import type * as AA from "@duplojs/utils/array"; import { type Option } from "./base"; import type { EligibleDataParser } from "../types"; /** @@ -52,8 +53,8 @@ export declare function createArrayOption, GenericMinValues>, - ...DP.Output[] + ...AA.CreateTuple, GenericMinValues>, + ...DDP.Output[] ]>; export declare function createArrayOption(name: GenericName, schema: GenericSchema, params?: { description?: string; @@ -62,6 +63,6 @@ export declare function createArrayOption, GenericMinValues>, - ...DP.Output[] + ...AA.CreateTuple, GenericMinValues>, + ...DDP.Output[] ] | undefined>; diff --git a/docs/libs/v0/command/options/array.mjs b/docs/libs/v0/command/options/array.mjs index 0b81372..a776309 100644 --- a/docs/libs/v0/command/options/array.mjs +++ b/docs/libs/v0/command/options/array.mjs @@ -1,24 +1,40 @@ -import { pipe, DP, S } from '@duplojs/utils'; +import { pipe, unwrap } from '@duplojs/utils'; +import * as SS from '@duplojs/utils/string'; +import * as DDP from '@duplojs/utils/dataParser'; +import * as EE from '@duplojs/utils/either'; import { initOption } from './base.mjs'; -import { CommandOptionRequiredError } from '../errors.mjs'; +import { addIssue, addDataParserError } from '../error.mjs'; const defaultSeparator = ","; function createArrayOption(name, schema, params) { - const dataParser = pipe(schema, DP.array, (schema) => params?.min - ? schema.addChecker(DP.checkerArrayMin(params.min)) + const dataParser = pipe(schema, DDP.array, (schema) => params?.min + ? schema.addChecker(DDP.checkerArrayMin(params.min)) : schema, (schema) => params?.max - ? schema.addChecker(DP.checkerArrayMax(params.max)) + ? schema.addChecker(DDP.checkerArrayMax(params.max)) : schema, (schema) => params?.required ? schema - : DP.optional(schema)); - return initOption(name, ({ isHere, value }) => { + : DDP.optional(schema)); + return initOption(name, ({ isHere, value }, error) => { if (!isHere && params?.required) { - throw new CommandOptionRequiredError(name); + return addIssue(error, { + type: "option", + target: name, + expected: `required option --${name}`, + received: value, + message: `Option "${name}" is required.`, + }); } const values = value !== undefined - ? S.split(value, params?.separator ?? defaultSeparator) + ? SS.split(value, params?.separator ?? defaultSeparator) : undefined; - return dataParser.parseOrThrow(values); + const result = dataParser.parse(values); + if (EE.isLeft(result)) { + return addDataParserError(error, unwrap(result), { + type: "option", + target: name, + }); + } + return unwrap(result); }, { description: params?.description, aliases: params?.aliases, diff --git a/docs/libs/v0/command/options/base.cjs b/docs/libs/v0/command/options/base.cjs index cba5cc3..a185c5e 100644 --- a/docs/libs/v0/command/options/base.cjs +++ b/docs/libs/v0/command/options/base.cjs @@ -1,17 +1,38 @@ 'use strict'; -var utils = require('@duplojs/utils'); +var AA = require('@duplojs/utils/array'); +var SS = require('@duplojs/utils/string'); var kind = require('../../kind.cjs'); -var errors = require('../errors.cjs'); +var error = require('../error.cjs'); + +function _interopNamespaceDefault(e) { + var n = Object.create(null); + if (e) { + Object.keys(e).forEach(function (k) { + if (k !== 'default') { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { return e[k]; } + }); + } + }); + } + n.default = e; + return Object.freeze(n); +} + +var AA__namespace = /*#__PURE__*/_interopNamespaceDefault(AA); +var SS__namespace = /*#__PURE__*/_interopNamespaceDefault(SS); const optionKind = kind.createDuplojsServerUtilsKind("command-option"); const regexOption = /^(?-{1,2})(?[A-Za-z0-9][A-Za-z0-9_-]*)(?:=(?.*))?$/; function initOption(name, execute, params) { const self = optionKind.setTo({ name, - execute: (args) => { - const result = utils.A.reduce(args, utils.A.reduceFrom(null), ({ element, next, exit, index }) => { - const extractResult = utils.S.extract(element, regexOption); + execute: (args, error$1) => { + const result = AA__namespace.reduce(args, AA__namespace.reduceFrom(null), ({ element, next, exit, index }) => { + const extractResult = SS__namespace.extract(element, regexOption); if (!extractResult) { return next(null); } @@ -20,45 +41,69 @@ function initOption(name, execute, params) { value: extractResult.namedGroups?.value, index, }; - if (self.name !== result.key && !utils.A.includes(self.aliases, result.key)) { + if (self.name !== result.key && !AA__namespace.includes(self.aliases, result.key)) { return next(null); } return exit(result); }); if (!result) { + const executeResult = execute({ + isHere: false, + value: undefined, + }, error$1); + if (executeResult === error.SymbolCommandError) { + return error.SymbolCommandError; + } return { - result: execute({ - isHere: false, - value: undefined, - }), + result: executeResult, argumentRest: args, }; } else if (self.hasValue) { const value = result.value ?? args[result.index + 1]; - const isOption = utils.S.test(value ?? "", regexOption); + const isOption = SS__namespace.test(value ?? "", regexOption); if (isOption) { - throw new errors.CommandOptionValueLooksLikeOptionError(self.name, value); + return error.addIssue(error$1, { + type: "option", + target: self.name, + expected: `value for option --${self.name}`, + received: value, + message: `Missing value for option "${self.name}": received another option token instead of a value.`, + }); + } + const executeResult = execute({ + isHere: true, + value, + }, error$1); + if (executeResult === error.SymbolCommandError) { + return error.SymbolCommandError; } return { - result: execute({ - isHere: true, - value, - }), - argumentRest: utils.A.spliceDelete(args, result.index, result.value === undefined && args[result.index + 1] !== undefined + result: executeResult, + argumentRest: AA__namespace.spliceDelete(args, result.index, result.value === undefined && args[result.index + 1] !== undefined ? 2 : 1), }; } else if (!self.hasValue && result.value !== undefined) { - throw new errors.CommandOptionValueNotRequiredError(self.name); + return error.addIssue(error$1, { + type: "option", + target: self.name, + expected: `option without value --${self.name}`, + received: result.value, + message: `Option "${self.name}" does not accept a value.`, + }); + } + const executeResult = execute({ + isHere: true, + value: undefined, + }, error$1); + if (executeResult === error.SymbolCommandError) { + return error.SymbolCommandError; } return { - result: execute({ - isHere: true, - value: undefined, - }), - argumentRest: utils.A.spliceDelete(args, result.index, 1), + result: executeResult, + argumentRest: AA__namespace.spliceDelete(args, result.index, 1), }; }, aliases: params?.aliases ?? [], diff --git a/docs/libs/v0/command/options/base.d.ts b/docs/libs/v0/command/options/base.d.ts index 1a3f69d..f7b13cd 100644 --- a/docs/libs/v0/command/options/base.d.ts +++ b/docs/libs/v0/command/options/base.d.ts @@ -1,20 +1,21 @@ -import { type Kind } from "@duplojs/utils"; +import type { Kind } from "@duplojs/utils"; +import { type CommandError, SymbolCommandError } from "../error"; declare const optionKind: import("@duplojs/utils").KindHandler>; export interface Option extends Kind { readonly name: GenericName; readonly description: string | null; readonly aliases: readonly string[]; readonly hasValue: boolean; - execute(args: readonly string[]): { + execute(args: readonly string[], error: CommandError): { result: GenericExecuteOutputValue; argumentRest: readonly string[]; - }; + } | SymbolCommandError; } export interface InitOptionExecuteParams { isHere: boolean; value: string | undefined; } -export declare function initOption(name: GenericName, execute: (params: InitOptionExecuteParams) => GenericExecuteOutputValue, params?: { +export declare function initOption(name: GenericName, execute: (params: InitOptionExecuteParams, error: CommandError) => GenericExecuteOutputValue | SymbolCommandError, params?: { description?: string; aliases?: readonly string[]; hasValue?: boolean; diff --git a/docs/libs/v0/command/options/base.mjs b/docs/libs/v0/command/options/base.mjs index 12214c7..8ce7343 100644 --- a/docs/libs/v0/command/options/base.mjs +++ b/docs/libs/v0/command/options/base.mjs @@ -1,15 +1,16 @@ -import { A, S } from '@duplojs/utils'; +import * as AA from '@duplojs/utils/array'; +import * as SS from '@duplojs/utils/string'; import { createDuplojsServerUtilsKind } from '../../kind.mjs'; -import { CommandOptionValueLooksLikeOptionError, CommandOptionValueNotRequiredError } from '../errors.mjs'; +import { SymbolCommandError, addIssue } from '../error.mjs'; const optionKind = createDuplojsServerUtilsKind("command-option"); const regexOption = /^(?-{1,2})(?[A-Za-z0-9][A-Za-z0-9_-]*)(?:=(?.*))?$/; function initOption(name, execute, params) { const self = optionKind.setTo({ name, - execute: (args) => { - const result = A.reduce(args, A.reduceFrom(null), ({ element, next, exit, index }) => { - const extractResult = S.extract(element, regexOption); + execute: (args, error) => { + const result = AA.reduce(args, AA.reduceFrom(null), ({ element, next, exit, index }) => { + const extractResult = SS.extract(element, regexOption); if (!extractResult) { return next(null); } @@ -18,45 +19,69 @@ function initOption(name, execute, params) { value: extractResult.namedGroups?.value, index, }; - if (self.name !== result.key && !A.includes(self.aliases, result.key)) { + if (self.name !== result.key && !AA.includes(self.aliases, result.key)) { return next(null); } return exit(result); }); if (!result) { + const executeResult = execute({ + isHere: false, + value: undefined, + }, error); + if (executeResult === SymbolCommandError) { + return SymbolCommandError; + } return { - result: execute({ - isHere: false, - value: undefined, - }), + result: executeResult, argumentRest: args, }; } else if (self.hasValue) { const value = result.value ?? args[result.index + 1]; - const isOption = S.test(value ?? "", regexOption); + const isOption = SS.test(value ?? "", regexOption); if (isOption) { - throw new CommandOptionValueLooksLikeOptionError(self.name, value); + return addIssue(error, { + type: "option", + target: self.name, + expected: `value for option --${self.name}`, + received: value, + message: `Missing value for option "${self.name}": received another option token instead of a value.`, + }); + } + const executeResult = execute({ + isHere: true, + value, + }, error); + if (executeResult === SymbolCommandError) { + return SymbolCommandError; } return { - result: execute({ - isHere: true, - value, - }), - argumentRest: A.spliceDelete(args, result.index, result.value === undefined && args[result.index + 1] !== undefined + result: executeResult, + argumentRest: AA.spliceDelete(args, result.index, result.value === undefined && args[result.index + 1] !== undefined ? 2 : 1), }; } else if (!self.hasValue && result.value !== undefined) { - throw new CommandOptionValueNotRequiredError(self.name); + return addIssue(error, { + type: "option", + target: self.name, + expected: `option without value --${self.name}`, + received: result.value, + message: `Option "${self.name}" does not accept a value.`, + }); + } + const executeResult = execute({ + isHere: true, + value: undefined, + }, error); + if (executeResult === SymbolCommandError) { + return SymbolCommandError; } return { - result: execute({ - isHere: true, - value: undefined, - }), - argumentRest: A.spliceDelete(args, result.index, 1), + result: executeResult, + argumentRest: AA.spliceDelete(args, result.index, 1), }; }, aliases: params?.aliases ?? [], diff --git a/docs/libs/v0/command/options/simple.cjs b/docs/libs/v0/command/options/simple.cjs index 3de1f05..4b2eea5 100644 --- a/docs/libs/v0/command/options/simple.cjs +++ b/docs/libs/v0/command/options/simple.cjs @@ -1,18 +1,53 @@ 'use strict'; var utils = require('@duplojs/utils'); +var EE = require('@duplojs/utils/either'); +var DDP = require('@duplojs/utils/dataParser'); var base = require('./base.cjs'); -var errors = require('../errors.cjs'); +var error = require('../error.cjs'); + +function _interopNamespaceDefault(e) { + var n = Object.create(null); + if (e) { + Object.keys(e).forEach(function (k) { + if (k !== 'default') { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { return e[k]; } + }); + } + }); + } + n.default = e; + return Object.freeze(n); +} + +var EE__namespace = /*#__PURE__*/_interopNamespaceDefault(EE); +var DDP__namespace = /*#__PURE__*/_interopNamespaceDefault(DDP); function createOption(name, schema, params) { const dataParser = params?.required ? schema - : utils.DP.optional(schema); - return base.initOption(name, ({ isHere, value }) => { + : DDP__namespace.optional(schema); + return base.initOption(name, ({ isHere, value }, error$1) => { if (!isHere && params?.required) { - throw new errors.CommandOptionRequiredError(name); + return error.addIssue(error$1, { + type: "option", + target: name, + expected: `required option --${name}`, + received: value, + message: `Option "${name}" is required.`, + }); + } + const result = dataParser.parse(value); + if (EE__namespace.isLeft(result)) { + return error.addDataParserError(error$1, utils.unwrap(result), { + type: "option", + target: name, + }); } - return dataParser.parseOrThrow(value); + return utils.unwrap(result); }, { description: params?.description, aliases: params?.aliases, diff --git a/docs/libs/v0/command/options/simple.d.ts b/docs/libs/v0/command/options/simple.d.ts index 3b4fb5e..99e0477 100644 --- a/docs/libs/v0/command/options/simple.d.ts +++ b/docs/libs/v0/command/options/simple.d.ts @@ -1,4 +1,4 @@ -import { DP } from "@duplojs/utils"; +import * as DDP from "@duplojs/utils/dataParser"; import { type Option } from "./base"; import type { EligibleDataParser } from "../types"; /** @@ -44,8 +44,8 @@ export declare function createOption>; +}): Option>; export declare function createOption(name: GenericName, schema: GenericSchema, params?: { description?: string; aliases?: readonly string[]; -}): Option | undefined>; +}): Option | undefined>; diff --git a/docs/libs/v0/command/options/simple.mjs b/docs/libs/v0/command/options/simple.mjs index d79412f..b557d72 100644 --- a/docs/libs/v0/command/options/simple.mjs +++ b/docs/libs/v0/command/options/simple.mjs @@ -1,16 +1,31 @@ -import { DP } from '@duplojs/utils'; +import { unwrap } from '@duplojs/utils'; +import * as EE from '@duplojs/utils/either'; +import * as DDP from '@duplojs/utils/dataParser'; import { initOption } from './base.mjs'; -import { CommandOptionRequiredError } from '../errors.mjs'; +import { addIssue, addDataParserError } from '../error.mjs'; function createOption(name, schema, params) { const dataParser = params?.required ? schema - : DP.optional(schema); - return initOption(name, ({ isHere, value }) => { + : DDP.optional(schema); + return initOption(name, ({ isHere, value }, error) => { if (!isHere && params?.required) { - throw new CommandOptionRequiredError(name); + return addIssue(error, { + type: "option", + target: name, + expected: `required option --${name}`, + received: value, + message: `Option "${name}" is required.`, + }); } - return dataParser.parseOrThrow(value); + const result = dataParser.parse(value); + if (EE.isLeft(result)) { + return addDataParserError(error, unwrap(result), { + type: "option", + target: name, + }); + } + return unwrap(result); }, { description: params?.description, aliases: params?.aliases, diff --git a/docs/libs/v0/command/printer.cjs b/docs/libs/v0/command/printer.cjs deleted file mode 100644 index 89a20b2..0000000 --- a/docs/libs/v0/command/printer.cjs +++ /dev/null @@ -1,63 +0,0 @@ -'use strict'; - -var utils = require('@duplojs/utils'); - -exports.Printer = void 0; -(function (Printer) { - const codeColors = { - reset: "\x1b[0m", - red: "\x1b[31m", - green: "\x1b[32m", - yellow: "\x1b[33m", - blue: "\x1b[34m", - magenta: "\x1b[35m", - cyan: "\x1b[36m", - gray: "\x1b[90m", - bold: "\x1b[1m", - }; - Printer.colorsEnum = utils.createEnum([ - "red", - "RED", - "green", - "GREEN", - "yellow", - "YELLOW", - "blue", - "BLUE", - "magenta", - "MAGENTA", - "cyan", - "CYAN", - "gray", - "GRAY", - ]); - Printer.tab = "\t"; - Printer.back = "\n"; - Printer.dash = "-"; - const regexCapitalize = /[A-Z]/; - function colorized(input, color) { - const text = `${codeColors[utils.S.toLowerCase(color)]}${input}${codeColors.reset}`; - if (utils.S.test(color, regexCapitalize)) { - return `${codeColors.bold}${text}${codeColors.reset}`; - } - return text; - } - Printer.colorized = colorized; - function indent(level) { - return utils.S.repeat(Printer.tab, level); - } - Printer.indent = indent; - function parenthesize(input) { - return `(${input})`; - } - Printer.parenthesize = parenthesize; - function colorizedOption(option, color) { - return colorized(utils.pipe(option.aliases, utils.A.map((alias) => `-${alias},`), utils.A.push(`--${option.name}`), utils.A.join(" ")), color); - } - Printer.colorizedOption = colorizedOption; - function render(values) { - // eslint-disable-next-line no-console - console.log(...values); - } - Printer.render = render; -})(exports.Printer || (exports.Printer = {})); diff --git a/docs/libs/v0/command/printer.d.ts b/docs/libs/v0/command/printer.d.ts deleted file mode 100644 index adada29..0000000 --- a/docs/libs/v0/command/printer.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { type GetEnumValue } from "@duplojs/utils"; -import type { Option } from "./options"; -export declare namespace Printer { - const colorsEnum: { - red: "red"; - RED: "RED"; - green: "green"; - GREEN: "GREEN"; - yellow: "yellow"; - YELLOW: "YELLOW"; - blue: "blue"; - BLUE: "BLUE"; - magenta: "magenta"; - MAGENTA: "MAGENTA"; - cyan: "cyan"; - CYAN: "CYAN"; - gray: "gray"; - GRAY: "GRAY"; - toTuple: () => readonly ["red", "RED", "green", "GREEN", "yellow", "YELLOW", "blue", "BLUE", "magenta", "MAGENTA", "cyan", "CYAN", "gray", "GRAY"]; - has: (value: string) => value is "red" | "RED" | "green" | "GREEN" | "yellow" | "YELLOW" | "blue" | "BLUE" | "magenta" | "MAGENTA" | "cyan" | "CYAN" | "gray" | "GRAY"; - contract: (...args: import("@duplojs/utils").IsEqual extends true ? ["A value is duplicated."] : ["One of the values ​​is missing.", Exclude]) => /*elided*/ any; - }; - type ColorEnum = GetEnumValue; - const tab: "\t"; - const back: "\n"; - const dash: "-"; - function colorized(input: string, color: ColorEnum): string; - function indent(level: number): string; - function parenthesize(input: string): string; - function colorizedOption(option: Option, color: ColorEnum): string; - function render(values: string[]): void; -} diff --git a/docs/libs/v0/command/printer.mjs b/docs/libs/v0/command/printer.mjs deleted file mode 100644 index c6342e4..0000000 --- a/docs/libs/v0/command/printer.mjs +++ /dev/null @@ -1,63 +0,0 @@ -import { createEnum, S, pipe, A } from '@duplojs/utils'; - -var Printer; -(function (Printer) { - const codeColors = { - reset: "\x1b[0m", - red: "\x1b[31m", - green: "\x1b[32m", - yellow: "\x1b[33m", - blue: "\x1b[34m", - magenta: "\x1b[35m", - cyan: "\x1b[36m", - gray: "\x1b[90m", - bold: "\x1b[1m", - }; - Printer.colorsEnum = createEnum([ - "red", - "RED", - "green", - "GREEN", - "yellow", - "YELLOW", - "blue", - "BLUE", - "magenta", - "MAGENTA", - "cyan", - "CYAN", - "gray", - "GRAY", - ]); - Printer.tab = "\t"; - Printer.back = "\n"; - Printer.dash = "-"; - const regexCapitalize = /[A-Z]/; - function colorized(input, color) { - const text = `${codeColors[S.toLowerCase(color)]}${input}${codeColors.reset}`; - if (S.test(color, regexCapitalize)) { - return `${codeColors.bold}${text}${codeColors.reset}`; - } - return text; - } - Printer.colorized = colorized; - function indent(level) { - return S.repeat(Printer.tab, level); - } - Printer.indent = indent; - function parenthesize(input) { - return `(${input})`; - } - Printer.parenthesize = parenthesize; - function colorizedOption(option, color) { - return colorized(pipe(option.aliases, A.map((alias) => `-${alias},`), A.push(`--${option.name}`), A.join(" ")), color); - } - Printer.colorizedOption = colorizedOption; - function render(values) { - // eslint-disable-next-line no-console - console.log(...values); - } - Printer.render = render; -})(Printer || (Printer = {})); - -export { Printer }; diff --git a/docs/libs/v0/dataParser/parsers/file.cjs b/docs/libs/v0/dataParser/parsers/file.cjs index f3f994c..3a222f0 100644 --- a/docs/libs/v0/dataParser/parsers/file.cjs +++ b/docs/libs/v0/dataParser/parsers/file.cjs @@ -28,22 +28,21 @@ function file(params, definition) { if (self.definition.checkExist || self.definition.maxSize !== undefined || self.definition.minSize !== undefined) { - return utils.DP.SymbolDataParserErrorPromiseIssue; + return utils.DP.addIssue(error, "async data parser", data, self.definition.errorMessage); } let fileInterface$1 = data; if (self.definition.coerce && typeof fileInterface$1 === "string") { fileInterface$1 = fileInterface.createFileInterface(fileInterface$1); } if (!fileInterface.isFileInterface(fileInterface$1)) { - return utils.DP.SymbolDataParserErrorIssue; + return utils.DP.addIssue(error, "file", data, self.definition.errorMessage); } if (self.definition.mimeType && !self .definition .mimeType .test(fileInterface$1.getMimeType() ?? "")) { - utils.DP.addIssue(error, self, data, "Wrong mimeType."); - return utils.DP.SymbolDataParserError; + return utils.DP.addIssue(error, `file with mime type matching ${self.definition.mimeType.source}`, data, "Wrong mimeType."); } return fileInterface$1; }, @@ -53,38 +52,33 @@ function file(params, definition) { fileInterface$1 = fileInterface.createFileInterface(fileInterface$1); } if (!fileInterface.isFileInterface(fileInterface$1)) { - return utils.DP.SymbolDataParserErrorIssue; + return utils.DP.addIssue(error, "file", data, self.definition.errorMessage); } if (self.definition.mimeType && !self .definition .mimeType .test(fileInterface$1.getMimeType() ?? "")) { - utils.DP.addIssue(error, self, data, "Wrong mimeType."); - return utils.DP.SymbolDataParserError; + return utils.DP.addIssue(error, `file with mime type matching ${self.definition.mimeType.source}`, fileInterface$1, "Wrong mimeType."); } if (self.definition.checkExist || self.definition.maxSize !== undefined || self.definition.minSize !== undefined) { const resultStats = await fileInterface$1.stat(); if (utils.E.isLeft(resultStats)) { - utils.DP.addIssue(error, self, data, "File not exist."); - return utils.DP.SymbolDataParserError; + return utils.DP.addIssue(error, "existing file", fileInterface$1, "File not exist."); } const stat = utils.unwrap(resultStats); if (!stat.isFile) { - utils.DP.addIssue(error, self, data, "Is not file."); - return utils.DP.SymbolDataParserError; + return utils.DP.addIssue(error, "file", stat, "Is not file."); } if (self.definition.maxSize !== undefined && stat.sizeBytes > self.definition.maxSize) { - utils.DP.addIssue(error, self, data, "File is to large."); - return utils.DP.SymbolDataParserError; + return utils.DP.addIssue(error, `file with sizeBytes <= ${self.definition.maxSize}`, stat.sizeBytes, "File is to large."); } if (self.definition.minSize !== undefined && stat.sizeBytes < self.definition.minSize) { - utils.DP.addIssue(error, self, data, "File is to small."); - return utils.DP.SymbolDataParserError; + return utils.DP.addIssue(error, `file with sizeBytes >= ${self.definition.minSize}`, stat.sizeBytes, "File is to small."); } } return fileInterface$1; diff --git a/docs/libs/v0/dataParser/parsers/file.mjs b/docs/libs/v0/dataParser/parsers/file.mjs index 8a70a6c..78cf49a 100644 --- a/docs/libs/v0/dataParser/parsers/file.mjs +++ b/docs/libs/v0/dataParser/parsers/file.mjs @@ -26,22 +26,21 @@ function file(params, definition) { if (self.definition.checkExist || self.definition.maxSize !== undefined || self.definition.minSize !== undefined) { - return DP.SymbolDataParserErrorPromiseIssue; + return DP.addIssue(error, "async data parser", data, self.definition.errorMessage); } let fileInterface = data; if (self.definition.coerce && typeof fileInterface === "string") { fileInterface = createFileInterface(fileInterface); } if (!isFileInterface(fileInterface)) { - return DP.SymbolDataParserErrorIssue; + return DP.addIssue(error, "file", data, self.definition.errorMessage); } if (self.definition.mimeType && !self .definition .mimeType .test(fileInterface.getMimeType() ?? "")) { - DP.addIssue(error, self, data, "Wrong mimeType."); - return DP.SymbolDataParserError; + return DP.addIssue(error, `file with mime type matching ${self.definition.mimeType.source}`, data, "Wrong mimeType."); } return fileInterface; }, @@ -51,38 +50,33 @@ function file(params, definition) { fileInterface = createFileInterface(fileInterface); } if (!isFileInterface(fileInterface)) { - return DP.SymbolDataParserErrorIssue; + return DP.addIssue(error, "file", data, self.definition.errorMessage); } if (self.definition.mimeType && !self .definition .mimeType .test(fileInterface.getMimeType() ?? "")) { - DP.addIssue(error, self, data, "Wrong mimeType."); - return DP.SymbolDataParserError; + return DP.addIssue(error, `file with mime type matching ${self.definition.mimeType.source}`, fileInterface, "Wrong mimeType."); } if (self.definition.checkExist || self.definition.maxSize !== undefined || self.definition.minSize !== undefined) { const resultStats = await fileInterface.stat(); if (E.isLeft(resultStats)) { - DP.addIssue(error, self, data, "File not exist."); - return DP.SymbolDataParserError; + return DP.addIssue(error, "existing file", fileInterface, "File not exist."); } const stat = unwrap(resultStats); if (!stat.isFile) { - DP.addIssue(error, self, data, "Is not file."); - return DP.SymbolDataParserError; + return DP.addIssue(error, "file", stat, "Is not file."); } if (self.definition.maxSize !== undefined && stat.sizeBytes > self.definition.maxSize) { - DP.addIssue(error, self, data, "File is to large."); - return DP.SymbolDataParserError; + return DP.addIssue(error, `file with sizeBytes <= ${self.definition.maxSize}`, stat.sizeBytes, "File is to large."); } if (self.definition.minSize !== undefined && stat.sizeBytes < self.definition.minSize) { - DP.addIssue(error, self, data, "File is to small."); - return DP.SymbolDataParserError; + return DP.addIssue(error, `file with sizeBytes >= ${self.definition.minSize}`, stat.sizeBytes, "File is to small."); } } return fileInterface; diff --git a/eslint.config.js b/eslint.config.js index 1d5fdfc..7a3aa30 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -29,6 +29,7 @@ export default [ files: ["jsDoc/**/*.ts", "docs/examples/**/*.ts"], rules: { "@stylistic/js/line-comment-position": "off", + "@stylistic/js/no-multiple-empty-lines": "off", }, }, { diff --git a/jsDoc/command/execOptions/example.ts b/jsDoc/command/execOptions/example.ts new file mode 100644 index 0000000..97d092c --- /dev/null +++ b/jsDoc/command/execOptions/example.ts @@ -0,0 +1,26 @@ +import { SC } from "@scripts"; +import { DP } from "@duplojs/utils"; + +const portOption = SC.createOption( + "port", + DP.coerce.number(), + { + description: "HTTP port", + required: true, + }, +); +const portResult = SC.execOptions(portOption); +// portResult.port: number + +const tagOption = SC.createArrayOption( + "tag", + DP.string(), + { + aliases: ["t"], + required: true, + }, +); +const forceOption = SC.createBooleanOption("force"); + +const options = SC.execOptions(tagOption, forceOption); +// options.tag: string[] | undefined diff --git a/jsDoc/command/execOptions/index.md b/jsDoc/command/execOptions/index.md new file mode 100644 index 0000000..a2b1ced --- /dev/null +++ b/jsDoc/command/execOptions/index.md @@ -0,0 +1,14 @@ +Execute command options from process arguments. + +`execOptions` reads runtime arguments, executes each option parser, and returns an object keyed by option name. +It also adds an automatic `--help` / `-h` manual generated from the declared options. + +```ts +{@include command/execOptions/example.ts[4,26]} +``` + +@remarks +Use this helper when your CLI only needs option parsing and does not need a command subject. + +@see https://server-utils.duplojs.dev/en/v0/api/command/execOptions +@namespace SC diff --git a/scripts/command/execOptions.ts b/scripts/command/execOptions.ts index 2d47b9a..319b85c 100644 --- a/scripts/command/execOptions.ts +++ b/scripts/command/execOptions.ts @@ -1,3 +1,4 @@ +import type { SimplifyTopLevel } from "@duplojs/utils"; import * as AA from "@duplojs/utils/array"; import * as OO from "@duplojs/utils/object"; import { createError, interpretExecOptionError, SymbolCommandError } from "./error"; @@ -5,22 +6,22 @@ import { logExecOptionHelp } from "./help"; import { createBooleanOption, type Option } from "./options"; import { exitProcess, getProcessArguments } from "@scripts/common"; +const helpOption = createBooleanOption("help", { aliases: ["h"] }); + type ComputeResult< GenericOptions extends [Option, ...Option[]], -> = { - [GenericOptionName in GenericOptions[number]["name"]]: Exclude< - ReturnType< - Extract< - GenericOptions[number], - { name: GenericOptionName } - >["execute"] - >, - SymbolCommandError - >["result"] -}; - -const helpOption = createBooleanOption("help", { aliases: ["h"] }); +> = SimplifyTopLevel<{ + [GenericOption in GenericOptions[number] as GenericOption extends Option + ? GenericName + : never + ]: GenericOption extends Option + ? GenericResult + : never +}>; +/** + * {@include command/execOptions/index.md} + */ export function execOptions< GenericOptions extends [Option, ...Option[]], >(