From a09fa9feaf9dfdd19b4a3b3a15ad2854e190391e Mon Sep 17 00:00:00 2001 From: Maxwell Brown Date: Wed, 8 Nov 2023 19:31:39 -0500 Subject: [PATCH] Refactor library internals to fix numerous issues (#353) --- .changeset/shaggy-countries-kneel.md | 5 + .gitignore | 3 +- .vscode/settings.json | 3 +- .vscode/snippets.code-snippets | 8 +- examples/git.ts | 10 +- package.json | 41 +- pnpm-lock.yaml | 810 ++++++--- src/Args.ts | 205 +-- src/AutoCorrect.ts | 4 +- src/{BuiltInOption.ts => BuiltInOptions.ts} | 32 +- src/CliApp.ts | 6 +- src/CliConfig.ts | 29 +- src/Command.ts | 96 +- src/CommandDirective.ts | 18 +- src/HelpDoc.ts | 42 +- src/HelpDoc/Span.ts | 58 +- src/Options.ts | 284 ++- src/Parameter.ts | 34 + src/Primitive.ts | 124 +- src/ShellType.ts | 8 +- src/Usage.ts | 32 +- src/ValidationError.ts | 184 +- src/index.ts | 10 +- src/internal/args.ts | 864 ++++----- .../{builtInOption.ts => builtInOptions.ts} | 30 +- src/internal/cliApp.ts | 217 +-- src/internal/cliConfig.ts | 17 +- src/internal/command.ts | 1048 ++++++----- src/internal/commandDirective.ts | 4 +- src/internal/helpDoc.ts | 30 +- src/internal/helpDoc/span.ts | 117 +- src/internal/options.ts | 1589 ++++++++++------- src/internal/primitive.ts | 416 +++-- src/internal/shellType.ts | 4 +- src/internal/usage.ts | 209 ++- src/internal/validationError.ts | 139 +- test/AutoCorrect.test.ts | 27 + test/Command.test.ts | 380 ++++ test/Command.ts | 243 --- test/Options.test.ts | 420 +++++ test/Options.ts | 448 ----- test/Primitive.test.ts | 120 ++ test/utils/extend.ts | 126 -- test/utils/grep.ts | 11 +- test/utils/tail.ts | 2 +- test/utils/wc.ts | 20 +- vitest.config.ts | 2 +- 47 files changed, 4919 insertions(+), 3610 deletions(-) create mode 100644 .changeset/shaggy-countries-kneel.md rename src/{BuiltInOption.ts => BuiltInOptions.ts} (60%) create mode 100644 src/Parameter.ts rename src/internal/{builtInOption.ts => builtInOptions.ts} (69%) create mode 100644 test/AutoCorrect.test.ts create mode 100644 test/Command.test.ts delete mode 100644 test/Command.ts create mode 100644 test/Options.test.ts delete mode 100644 test/Options.ts create mode 100644 test/Primitive.test.ts delete mode 100644 test/utils/extend.ts diff --git a/.changeset/shaggy-countries-kneel.md b/.changeset/shaggy-countries-kneel.md new file mode 100644 index 0000000..9008dbf --- /dev/null +++ b/.changeset/shaggy-countries-kneel.md @@ -0,0 +1,5 @@ +--- +"@effect/cli": minor +--- + +refactor library internals to fix a number of different bugs diff --git a/.gitignore b/.gitignore index 4e92359..ae2ac12 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ dist/ /dist /Args /AutoCorrect -/BuiltInOption +/BuiltInOptions /CliApp /CliConfig /Command @@ -21,6 +21,7 @@ dist/ /Exists /HelpDoc /Options +/Parameter /Primitive /Prompt /ShellType diff --git a/.vscode/settings.json b/.vscode/settings.json index a9eb5e6..b144b0b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -53,7 +53,7 @@ "dist": true, "Args": true, "AutoCorrect": true, - "BuiltInOption": true, + "BuiltInOptions": true, "CliApp": true, "CliConfig": true, "Command": true, @@ -61,6 +61,7 @@ "Exists": true, "HelpDoc": true, "Options": true, + "Parameter": true, "Primitive": true, "Prompt": true, "ShellType": true, diff --git a/.vscode/snippets.code-snippets b/.vscode/snippets.code-snippets index 3e57cb6..87ad337 100644 --- a/.vscode/snippets.code-snippets +++ b/.vscode/snippets.code-snippets @@ -2,17 +2,17 @@ "Gen Function $": { "prefix": "gg", "body": [ - "Effect.gen(function*($) {", + "Effect.gen(function*(_) {", " $0", "})" ], - "description": "Generator Function with a $ parameter" + "description": "Generator Function with a _ parameter" }, "Gen Yield $": { "prefix": "yy", "body": [ - "yield* $($0)" + "yield* _($0)" ], - "description": "Yield generator calling $()" + "description": "Yield generator calling _()" } } diff --git a/examples/git.ts b/examples/git.ts index 56dac9a..ecb3173 100644 --- a/examples/git.ts +++ b/examples/git.ts @@ -52,13 +52,13 @@ export interface RemoveRemote extends Data.Case { export const RemoveRemote = Data.tagged("RemoveRemote") const add: Command.Command = pipe( - Command.make("add", { options: Options.boolean("m"), args: Args.text({ name: "directory" }) }), + Command.standard("add", { options: Options.boolean("m"), args: Args.text({ name: "directory" }) }), Command.withHelp(HelpDoc.p("Description of the `git add` subcommand")), Command.map(({ args: directory, options: modified }) => Add({ modified, directory })) ) const addRemote: Command.Command = pipe( - Command.make("add", { + Command.standard("add", { options: Options.all({ name: Options.text("name"), url: Options.text("url") @@ -69,20 +69,20 @@ const addRemote: Command.Command = pipe( ) const removeRemote: Command.Command = pipe( - Command.make("remove", { args: Args.text({ name: "name" }) }), + Command.standard("remove", { args: Args.text({ name: "name" }) }), Command.withHelp(HelpDoc.p("Description of the `git remote remove` subcommand")), Command.map(({ args: name }) => RemoveRemote({ name })) ) const remote: Command.Command = pipe( - Command.make("remote", { options: Options.alias(Options.boolean("verbose"), "v") }), + Command.standard("remote", { options: Options.boolean("verbose").pipe(Options.withAlias("v")) }), Command.withHelp("Description of the `git remote` subcommand"), Command.subcommands([addRemote, removeRemote]), Command.map(({ options: verbose, subcommand }) => Remote({ verbose, subcommand })) ) const git: Command.Command = pipe( - Command.make("git", { options: Options.alias(Options.boolean("version"), "v") }), + Command.standard("git", { options: Options.boolean("version").pipe(Options.withAlias("v")) }), Command.subcommands([add, remote]), Command.map(({ options: version, subcommand }) => Git({ version, subcommand })) ) diff --git a/package.json b/package.json index 8d0c972..86b9957 100644 --- a/package.json +++ b/package.json @@ -57,11 +57,11 @@ "effect": { "generateIndex": true }, - "packageManager": "pnpm@8.9.0", + "packageManager": "pnpm@8.10.2", "peerDependencies": { - "@effect/printer": "^0.22.1", - "@effect/printer-ansi": "^0.22.1", - "@effect/schema": "^0.47.1", + "@effect/printer": "^0.18.0", + "@effect/printer-ansi": "^0.18.0", + "@effect/schema": "^0.43.0", "effect": "2.0.0-next.54" }, "devDependencies": { @@ -69,26 +69,26 @@ "@babel/preset-typescript": "^7.23.2", "@changesets/changelog-github": "^0.4.8", "@changesets/cli": "^2.26.2", - "@effect/build-utils": "^0.1.9", - "@effect/docgen": "^0.2.1", + "@effect/build-utils": "^0.3.0", + "@effect/docgen": "^0.3.0", "@effect/eslint-plugin": "^0.1.2", "@effect/language-service": "^0.0.21", "@effect/printer": "^0.22.1", "@effect/printer-ansi": "^0.22.1", - "@effect/schema": "^0.47.1", + "@effect/schema": "^0.47.2", "@preconstruct/cli": "^2.8.1", "@types/chai": "^4.3.9", "@types/node": "^20.8.10", - "@typescript-eslint/eslint-plugin": "^6.9.1", - "@typescript-eslint/parser": "^6.9.1", + "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^6.10.0", "@vitejs/plugin-react": "^4.1.1", "@vitest/coverage-v8": "^0.34.6", "babel-plugin-annotate-pure-calls": "^0.4.0", "effect": "2.0.0-next.54", "error-stack-parser": "^2.1.4", - "eslint": "^8.52.0", + "eslint": "^8.53.0", "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-codegen": "0.17.0", + "eslint-plugin-codegen": "0.18.1", "eslint-plugin-deprecation": "^2.0.0", "eslint-plugin-import": "^2.29.0", "eslint-plugin-simple-import-sort": "^10.0.0", @@ -106,7 +106,7 @@ "dist", "Args", "AutoCorrect", - "BuiltInOption", + "BuiltInOptions", "CliApp", "CliConfig", "Command", @@ -114,6 +114,7 @@ "Exists", "HelpDoc", "Options", + "Parameter", "Primitive", "Prompt", "ShellType", @@ -141,11 +142,11 @@ "import": "./AutoCorrect/dist/effect-cli-AutoCorrect.cjs.mjs", "default": "./AutoCorrect/dist/effect-cli-AutoCorrect.cjs.js" }, - "./BuiltInOption": { - "types": "./dist/declarations/src/BuiltInOption.d.ts", - "module": "./BuiltInOption/dist/effect-cli-BuiltInOption.esm.js", - "import": "./BuiltInOption/dist/effect-cli-BuiltInOption.cjs.mjs", - "default": "./BuiltInOption/dist/effect-cli-BuiltInOption.cjs.js" + "./BuiltInOptions": { + "types": "./dist/declarations/src/BuiltInOptions.d.ts", + "module": "./BuiltInOptions/dist/effect-cli-BuiltInOptions.esm.js", + "import": "./BuiltInOptions/dist/effect-cli-BuiltInOptions.cjs.mjs", + "default": "./BuiltInOptions/dist/effect-cli-BuiltInOptions.cjs.js" }, "./CliApp": { "types": "./dist/declarations/src/CliApp.d.ts", @@ -195,6 +196,12 @@ "import": "./Options/dist/effect-cli-Options.cjs.mjs", "default": "./Options/dist/effect-cli-Options.cjs.js" }, + "./Parameter": { + "types": "./dist/declarations/src/Parameter.d.ts", + "module": "./Parameter/dist/effect-cli-Parameter.esm.js", + "import": "./Parameter/dist/effect-cli-Parameter.cjs.mjs", + "default": "./Parameter/dist/effect-cli-Parameter.cjs.js" + }, "./Primitive": { "types": "./dist/declarations/src/Primitive.d.ts", "module": "./Primitive/dist/effect-cli-Primitive.esm.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c04115..97ca405 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,11 +18,11 @@ devDependencies: specifier: ^2.26.2 version: 2.26.2 '@effect/build-utils': - specifier: ^0.1.9 - version: 0.1.9 + specifier: ^0.3.0 + version: 0.3.0 '@effect/docgen': - specifier: ^0.2.1 - version: 0.2.1(@types/node@20.8.10)(typescript@5.2.2) + specifier: ^0.3.0 + version: 0.3.0(fast-check@3.13.2)(tsx@3.14.0)(typescript@5.2.2) '@effect/eslint-plugin': specifier: ^0.1.2 version: 0.1.2 @@ -36,8 +36,8 @@ devDependencies: specifier: ^0.22.1 version: 0.22.1(@effect/typeclass@0.14.1)(effect@2.0.0-next.54) '@effect/schema': - specifier: ^0.47.1 - version: 0.47.1(effect@2.0.0-next.54)(fast-check@3.13.2) + specifier: ^0.47.2 + version: 0.47.2(effect@2.0.0-next.54)(fast-check@3.13.2) '@preconstruct/cli': specifier: ^2.8.1 version: 2.8.1 @@ -48,11 +48,11 @@ devDependencies: specifier: ^20.8.10 version: 20.8.10 '@typescript-eslint/eslint-plugin': - specifier: ^6.9.1 - version: 6.9.1(@typescript-eslint/parser@6.9.1)(eslint@8.52.0)(typescript@5.2.2) + specifier: ^6.10.0 + version: 6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.2.2) '@typescript-eslint/parser': - specifier: ^6.9.1 - version: 6.9.1(eslint@8.52.0)(typescript@5.2.2) + specifier: ^6.10.0 + version: 6.10.0(eslint@8.53.0)(typescript@5.2.2) '@vitejs/plugin-react': specifier: ^4.1.1 version: 4.1.1(vite@4.5.0) @@ -69,26 +69,26 @@ devDependencies: specifier: ^2.1.4 version: 2.1.4 eslint: - specifier: ^8.52.0 - version: 8.52.0 + specifier: ^8.53.0 + version: 8.53.0 eslint-import-resolver-typescript: specifier: ^3.6.1 - version: 3.6.1(@typescript-eslint/parser@6.9.1)(eslint-plugin-import@2.29.0)(eslint@8.52.0) + version: 3.6.1(@typescript-eslint/parser@6.10.0)(eslint-plugin-import@2.29.0)(eslint@8.53.0) eslint-plugin-codegen: - specifier: 0.17.0 - version: 0.17.0 + specifier: 0.18.1 + version: 0.18.1 eslint-plugin-deprecation: specifier: ^2.0.0 - version: 2.0.0(eslint@8.52.0)(typescript@5.2.2) + version: 2.0.0(eslint@8.53.0)(typescript@5.2.2) eslint-plugin-import: specifier: ^2.29.0 - version: 2.29.0(@typescript-eslint/parser@6.9.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) + version: 2.29.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.53.0) eslint-plugin-simple-import-sort: specifier: ^10.0.0 - version: 10.0.0(eslint@8.52.0) + version: 10.0.0(eslint@8.53.0) eslint-plugin-sort-destructure-keys: specifier: ^1.5.0 - version: 1.5.0(eslint@8.52.0) + version: 1.5.0(eslint@8.53.0) fast-check: specifier: ^3.13.2 version: 3.13.2 @@ -123,7 +123,7 @@ packages: engines: {node: '>=6.0.0'} dependencies: '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.20 + '@jridgewell/trace-mapping': 0.3.19 dev: true /@babel/code-frame@7.22.13: @@ -431,6 +431,13 @@ packages: '@babel/plugin-transform-typescript': 7.22.15(@babel/core@7.23.2) dev: true + /@babel/runtime@7.23.1: + resolution: {integrity: sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.0 + dev: true + /@babel/runtime@7.23.2: resolution: {integrity: sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==} engines: {node: '>=6.9.0'} @@ -481,7 +488,7 @@ packages: /@changesets/apply-release-plan@6.1.4: resolution: {integrity: sha512-FMpKF1fRlJyCZVYHr3CbinpZZ+6MwvOtWUuO8uo+svcATEoc1zRDcj23pAurJ2TZ/uVz1wFHH6K3NlACy0PLew==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@changesets/config': 2.3.1 '@changesets/get-version-range-type': 0.3.2 '@changesets/git': 2.0.0 @@ -499,7 +506,7 @@ packages: /@changesets/assemble-release-plan@5.2.4: resolution: {integrity: sha512-xJkWX+1/CUaOUWTguXEbCDTyWJFECEhmdtbkjhn5GVBGxdP/JwaHBIU9sW3FR6gD07UwZ7ovpiPclQZs+j+mvg==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@changesets/errors': 0.1.4 '@changesets/get-dependents-graph': 1.3.6 '@changesets/types': 5.2.1 @@ -527,7 +534,7 @@ packages: resolution: {integrity: sha512-dnWrJTmRR8bCHikJHl9b9HW3gXACCehz4OasrXpMp7sx97ECuBGGNjJhjPhdZNCvMy9mn4BWdplI323IbqsRig==} hasBin: true dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@changesets/apply-release-plan': 6.1.4 '@changesets/assemble-release-plan': 5.2.4 '@changesets/changelog-git': 0.1.14 @@ -542,8 +549,8 @@ packages: '@changesets/types': 5.2.1 '@changesets/write': 0.2.3 '@manypkg/get-packages': 1.1.3 - '@types/is-ci': 3.0.3 - '@types/semver': 7.5.4 + '@types/is-ci': 3.0.2 + '@types/semver': 7.5.3 ansi-colors: 4.1.3 chalk: 2.4.2 enquirer: 2.4.1 @@ -559,7 +566,7 @@ packages: semver: 7.5.4 spawndamnit: 2.0.0 term-size: 2.2.1 - tty-table: 4.2.3 + tty-table: 4.2.2 dev: true /@changesets/config@2.3.1: @@ -602,7 +609,7 @@ packages: /@changesets/get-release-plan@3.0.17: resolution: {integrity: sha512-6IwKTubNEgoOZwDontYc2x2cWXfr6IKxP3IhKeK+WjyD6y3M4Gl/jdQvBw+m/5zWILSOCAaGLu2ZF6Q+WiPniw==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@changesets/assemble-release-plan': 5.2.4 '@changesets/config': 2.3.1 '@changesets/pre': 1.0.14 @@ -618,7 +625,7 @@ packages: /@changesets/git@2.0.0: resolution: {integrity: sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@changesets/errors': 0.1.4 '@changesets/types': 5.2.1 '@manypkg/get-packages': 1.1.3 @@ -643,7 +650,7 @@ packages: /@changesets/pre@1.0.14: resolution: {integrity: sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@changesets/errors': 0.1.4 '@changesets/types': 5.2.1 '@manypkg/get-packages': 1.1.3 @@ -653,7 +660,7 @@ packages: /@changesets/read@0.5.9: resolution: {integrity: sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@changesets/git': 2.0.0 '@changesets/logger': 0.0.5 '@changesets/parse': 0.3.16 @@ -674,7 +681,7 @@ packages: /@changesets/write@0.2.3: resolution: {integrity: sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@changesets/types': 5.2.1 fs-extra: 7.0.1 human-id: 1.0.2 @@ -704,33 +711,34 @@ packages: resolution: {integrity: sha512-rPwwm/RrFIolz6xHa8Kzpshuwpe+xu/XcEw9iUmRF2tnyIwxxaW7XoFKaQ+GfPju81cKpH4vJeq7/2IizKvyjg==} dev: true - /@effect/build-utils@0.1.9: - resolution: {integrity: sha512-PmgMZxGgk5lweaYZpR/kC09eZpXOEth9hShr8xn5a5m4S7RiMUu7O1h19Tx/i2ZgpLa/GK1405nkROT5+5z8Sg==} + /@effect/build-utils@0.3.0: + resolution: {integrity: sha512-t8YEfEMGObi8vuCyUe1hVQLLZPzfV4K1c8LdRa9w+Uwk3CXASUER0crjvDoPTT2A7dclVlOdhCie2oyGav8YQQ==} engines: {node: '>=16.17.1'} hasBin: true dev: true - /@effect/docgen@0.2.1(@types/node@20.8.10)(typescript@5.2.2): - resolution: {integrity: sha512-GJbCkmY73KM3BDWI9/TjSuirY6FmWFyyA+GlluVTIv5mZ9zsuI2SyhHcBGnR2BFSRv5HQlWeiTMYk3tZEXk/pg==} + /@effect/docgen@0.3.0(fast-check@3.13.2)(tsx@3.14.0)(typescript@5.2.2): + resolution: {integrity: sha512-QxhLddKwnxZ/BA/1iZGs53yuKorCF6BJmPbs0XXliwbOzgrSIjrSsS+satyis3ChzrtQ3vY1xHNwB6hgSifauA==} engines: {node: '>=16.17.1'} hasBin: true peerDependencies: - typescript: ^5.x + tsx: ^3.14.0 + typescript: ^5.2.2 dependencies: + '@effect/platform-node': 0.28.2(@effect/schema@0.47.2)(effect@2.0.0-next.54) + '@effect/schema': 0.47.2(effect@2.0.0-next.54)(fast-check@3.13.2) + chalk: 5.3.0 doctrine: 3.0.0 - fs-extra: 11.1.1 + effect: 2.0.0-next.54 glob: 10.3.10 markdown-toc: github.com/effect-ts/markdown-toc/4bfeb0f140105440ea0d12df2fa23199cc3ec1d5 - prettier: 2.8.8 - ts-morph: 19.0.0 - ts-node: 10.9.1(@types/node@20.8.10)(typescript@5.2.2) - tsconfck: 2.1.2(typescript@5.2.2) + prettier: 3.0.3 + ts-morph: 20.0.0 + tsconfck: 3.0.0(typescript@5.2.2) tsx: 3.14.0 typescript: 5.2.2 transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - - '@types/node' + - fast-check dev: true /@effect/eslint-plugin@0.1.2: @@ -745,6 +753,32 @@ packages: resolution: {integrity: sha512-e8vfKbjnbYiyneBincEFS0tzXluopGK77OkVFbPRtUbNDS5tJfb+jiwOQEiqASDsadcZmd+9J9+Q6v/z7GuN2g==} dev: true + /@effect/platform-node@0.28.2(@effect/schema@0.47.2)(effect@2.0.0-next.54): + resolution: {integrity: sha512-KU+ta/gantpbD5lnZBcgTuu7lgxeHQCy0dvvY2iieOG2UMzo8yogBLTTrjlpcbruFsriXEkF5+/y2uzyb17SDg==} + engines: {node: '>=18.0.0'} + peerDependencies: + effect: 2.0.0-next.54 + dependencies: + '@effect/platform': 0.27.2(@effect/schema@0.47.2)(effect@2.0.0-next.54) + busboy: 1.6.0 + effect: 2.0.0-next.54 + mime: 3.0.0 + transitivePeerDependencies: + - '@effect/schema' + dev: true + + /@effect/platform@0.27.2(@effect/schema@0.47.2)(effect@2.0.0-next.54): + resolution: {integrity: sha512-Ya8clQi5FWdlrsyPfhsh4zldvDM+NZe50QPANVXByqOpAWTw3XgPNRTMom/Yx464+VTORN6Pc4q82THWat1iOA==} + peerDependencies: + '@effect/schema': ^0.47.1 + effect: 2.0.0-next.54 + dependencies: + '@effect/schema': 0.47.2(effect@2.0.0-next.54)(fast-check@3.13.2) + effect: 2.0.0-next.54 + find-my-way: 7.7.0 + path-browserify: 1.0.1 + dev: true + /@effect/printer-ansi@0.22.1(@effect/typeclass@0.14.1)(effect@2.0.0-next.54): resolution: {integrity: sha512-NDyilEalzy7skoGiXVTP7OkPAlG/q8B0p8rKf9e0v3a2NadLCQEm4wNKLSYPcZN/ABRQCz2Wpml4+bO75vFu+g==} peerDependencies: @@ -766,8 +800,8 @@ packages: effect: 2.0.0-next.54 dev: true - /@effect/schema@0.47.1(effect@2.0.0-next.54)(fast-check@3.13.2): - resolution: {integrity: sha512-aqh6U1Faqqi+iMENdCt3Wp/sYZH9QJLJPKUycOllzcjFmOnNlb9Yiw58LNhzLSoAqNOTUtHjx31EHk/P7h8tuQ==} + /@effect/schema@0.47.2(effect@2.0.0-next.54)(fast-check@3.13.2): + resolution: {integrity: sha512-AwZg9c/NCD2rKEQuWjQUwFMWBi7xokJyL3jid5jtiLB7kjmrtRnHBxleeOEVoSrdBMJu6yXle3HvdHXefyA2jQ==} peerDependencies: effect: 2.0.0-next.54 fast-check: ^3.13.2 @@ -982,13 +1016,13 @@ packages: dev: true optional: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.52.0): + /@eslint-community/eslint-utils@4.4.0(eslint@8.53.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 8.52.0 + eslint: 8.53.0 eslint-visitor-keys: 3.4.3 dev: true @@ -997,8 +1031,8 @@ packages: engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: true - /@eslint/eslintrc@2.1.2: - resolution: {integrity: sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==} + /@eslint/eslintrc@2.1.3: + resolution: {integrity: sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 @@ -1014,8 +1048,8 @@ packages: - supports-color dev: true - /@eslint/js@8.52.0: - resolution: {integrity: sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==} + /@eslint/js@8.53.0: + resolution: {integrity: sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true @@ -1067,8 +1101,8 @@ packages: resolution: {integrity: sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==} engines: {node: '>= 10.14.2'} dependencies: - '@types/istanbul-lib-coverage': 2.0.5 - '@types/istanbul-reports': 3.0.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 '@types/node': 20.8.10 '@types/yargs': 15.0.17 chalk: 4.1.2 @@ -1080,7 +1114,7 @@ packages: dependencies: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.20 + '@jridgewell/trace-mapping': 0.3.19 dev: true /@jridgewell/resolve-uri@3.1.1: @@ -1104,6 +1138,13 @@ packages: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} dev: true + /@jridgewell/trace-mapping@0.3.19: + resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /@jridgewell/trace-mapping@0.3.20: resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} dependencies: @@ -1121,7 +1162,7 @@ packages: /@manypkg/find-root@1.1.0: resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@types/node': 12.20.55 find-up: 4.1.0 fs-extra: 8.1.0 @@ -1130,7 +1171,7 @@ packages: /@manypkg/get-packages@1.1.3: resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.1 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 @@ -1187,7 +1228,7 @@ packages: enquirer: 2.4.1 estree-walker: 2.0.2 fast-deep-equal: 2.0.1 - fast-glob: 3.3.1 + fast-glob: 3.3.2 fs-extra: 9.1.0 is-ci: 2.0.0 is-reference: 1.2.1 @@ -1299,10 +1340,10 @@ packages: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true - /@ts-morph/common@0.20.0: - resolution: {integrity: sha512-7uKjByfbPpwuzkstL3L5MQyuXPSKdoNG93Fmi2JoDcTf3pEP731JdRFAduRVkOs8oqxPsXKA+ScrWkdQ8t/I+Q==} + /@ts-morph/common@0.21.0: + resolution: {integrity: sha512-ES110Mmne5Vi4ypUKrtVQfXFDtCsDXiUiGxF6ILVlE90dDD4fdpC1LSjydl/ml7xJWKSDZwUYD2zkOePMSrPBA==} dependencies: - fast-glob: 3.3.1 + fast-glob: 3.3.2 minimatch: 7.4.6 mkdirp: 2.1.6 path-browserify: 1.0.1 @@ -1353,8 +1394,8 @@ packages: '@babel/types': 7.23.0 dev: true - /@types/chai-subset@1.3.4: - resolution: {integrity: sha512-CCWNXrJYSUIojZ1149ksLl3AN9cmZ5djf+yUoVVV+NuYrtydItQVlL2ZDqyC6M6O9LWRnVf8yYDxbXHO2TfQZg==} + /@types/chai-subset@1.3.3: + resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} dependencies: '@types/chai': 4.3.9 dev: true @@ -1371,36 +1412,48 @@ packages: resolution: {integrity: sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw==} dev: true - /@types/is-ci@3.0.3: - resolution: {integrity: sha512-FdHbjLiN2e8fk9QYQyVYZrK8svUDJpxSaSWLUga8EZS1RGAvvrqM9zbVARBtQuYPeLgnJxM2xloOswPwj1o2cQ==} + /@types/is-ci@3.0.2: + resolution: {integrity: sha512-9PyP1rgCro6xO3R7zOEoMgx5U9HpLhIg1FFb9p2mWX/x5QI8KMuCWWYtCT1dUQpicp84OsxEAw3iqwIKQY5Pog==} dependencies: ci-info: 3.9.0 dev: true - /@types/istanbul-lib-coverage@2.0.5: - resolution: {integrity: sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==} + /@types/istanbul-lib-coverage@2.0.4: + resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} dev: true - /@types/istanbul-lib-report@3.0.2: - resolution: {integrity: sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w==} + /@types/istanbul-lib-coverage@2.0.6: + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + dev: true + + /@types/istanbul-lib-report@3.0.3: + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} dependencies: - '@types/istanbul-lib-coverage': 2.0.5 + '@types/istanbul-lib-coverage': 2.0.6 dev: true - /@types/istanbul-reports@3.0.3: - resolution: {integrity: sha512-1nESsePMBlf0RPRffLZi5ujYh7IH1BWL4y9pr+Bn3cJBdxz+RTP8bUFljLz9HvzhhOSWKdyBZ4DIivdL6rvgZg==} + /@types/istanbul-reports@3.0.4: + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} dependencies: - '@types/istanbul-lib-report': 3.0.2 + '@types/istanbul-lib-report': 3.0.3 + dev: true + + /@types/json-schema@7.0.13: + resolution: {integrity: sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==} dev: true - /@types/json-schema@7.0.14: - resolution: {integrity: sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==} + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true /@types/json5@0.0.29: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true + /@types/minimist@1.2.3: + resolution: {integrity: sha512-ZYFzrvyWUNhaPomn80dsMNgMeXxNWZBdkuG/hWlUvXvbdUH8ZERNBGXnU87McuGcWDsyzX2aChCv/SVN348k3A==} + dev: true + /@types/minimist@1.2.4: resolution: {integrity: sha512-Kfe/D3hxHTusnPNRbycJE1N77WHDsdS4AjUYIzlDzhDrS47NrwuL3YW4VITxwR7KCVpzwgy4Rbj829KSSQmwXQ==} dev: true @@ -1415,8 +1468,8 @@ packages: undici-types: 5.26.5 dev: true - /@types/normalize-package-data@2.4.3: - resolution: {integrity: sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg==} + /@types/normalize-package-data@2.4.2: + resolution: {integrity: sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A==} dev: true /@types/resolve@1.17.1: @@ -1425,12 +1478,16 @@ packages: '@types/node': 20.8.10 dev: true + /@types/semver@7.5.3: + resolution: {integrity: sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==} + dev: true + /@types/semver@7.5.4: resolution: {integrity: sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==} dev: true - /@types/stack-utils@2.0.2: - resolution: {integrity: sha512-g7CK9nHdwjK2n0ymT2CW698FuWJRIx+RP6embAzZ2Qi8/ilIrA1Imt2LVSeHUzKvpoi7BhmmQcXz95eS0f2JXw==} + /@types/stack-utils@2.0.3: + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} dev: true /@types/yargs-parser@21.0.2: @@ -1443,8 +1500,8 @@ packages: '@types/yargs-parser': 21.0.2 dev: true - /@typescript-eslint/eslint-plugin@6.9.1(@typescript-eslint/parser@6.9.1)(eslint@8.52.0)(typescript@5.2.2): - resolution: {integrity: sha512-w0tiiRc9I4S5XSXXrMHOWgHgxbrBn1Ro+PmiYhSg2ZVdxrAJtQgzU5o2m1BfP6UOn7Vxcc6152vFjQfmZR4xEg==} + /@typescript-eslint/eslint-plugin@6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-uoLj4g2OTL8rfUQVx2AFO1hp/zja1wABJq77P6IclQs6I/m9GLrm7jCdgzZkvWdDCQf1uEvoa8s8CupsgWQgVg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha @@ -1455,13 +1512,13 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 6.9.1(eslint@8.52.0)(typescript@5.2.2) - '@typescript-eslint/scope-manager': 6.9.1 - '@typescript-eslint/type-utils': 6.9.1(eslint@8.52.0)(typescript@5.2.2) - '@typescript-eslint/utils': 6.9.1(eslint@8.52.0)(typescript@5.2.2) - '@typescript-eslint/visitor-keys': 6.9.1 + '@typescript-eslint/parser': 6.10.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/scope-manager': 6.10.0 + '@typescript-eslint/type-utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.10.0 debug: 4.3.4 - eslint: 8.52.0 + eslint: 8.53.0 graphemer: 1.4.0 ignore: 5.2.4 natural-compare: 1.4.0 @@ -1472,8 +1529,8 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@6.9.1(eslint@8.52.0)(typescript@5.2.2): - resolution: {integrity: sha512-C7AK2wn43GSaCUZ9do6Ksgi2g3mwFkMO3Cis96kzmgudoVaKyt62yNzJOktP0HDLb/iO2O0n2lBOzJgr6Q/cyg==} + /@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -1482,27 +1539,35 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 6.9.1 - '@typescript-eslint/types': 6.9.1 - '@typescript-eslint/typescript-estree': 6.9.1(typescript@5.2.2) - '@typescript-eslint/visitor-keys': 6.9.1 + '@typescript-eslint/scope-manager': 6.10.0 + '@typescript-eslint/types': 6.10.0 + '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.10.0 debug: 4.3.4 - eslint: 8.52.0 + eslint: 8.53.0 typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/scope-manager@6.9.1: - resolution: {integrity: sha512-38IxvKB6NAne3g/+MyXMs2Cda/Sz+CEpmm+KLGEM8hx/CvnSRuw51i8ukfwB/B/sESdeTGet1NH1Wj7I0YXswg==} + /@typescript-eslint/scope-manager@6.10.0: + resolution: {integrity: sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.9.1 - '@typescript-eslint/visitor-keys': 6.9.1 + '@typescript-eslint/types': 6.10.0 + '@typescript-eslint/visitor-keys': 6.10.0 dev: true - /@typescript-eslint/type-utils@6.9.1(eslint@8.52.0)(typescript@5.2.2): - resolution: {integrity: sha512-eh2oHaUKCK58qIeYp19F5V5TbpM52680sB4zNSz29VBQPTWIlE/hCj5P5B1AChxECe/fmZlspAWFuRniep1Skg==} + /@typescript-eslint/scope-manager@6.7.5: + resolution: {integrity: sha512-GAlk3eQIwWOJeb9F7MKQ6Jbah/vx1zETSDw8likab/eFcqkjSD7BI75SDAeC5N2L0MmConMoPvTsmkrg71+B1A==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.7.5 + '@typescript-eslint/visitor-keys': 6.7.5 + dev: true + + /@typescript-eslint/type-utils@6.10.0(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-wYpPs3hgTFblMYwbYWPT3eZtaDOjbLyIYuqpwuLBBqhLiuvJ+9sEp2gNRJEtR5N/c9G1uTtQQL5AhV0fEPJYcg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -1511,10 +1576,10 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.9.1(typescript@5.2.2) - '@typescript-eslint/utils': 6.9.1(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) + '@typescript-eslint/utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2) debug: 4.3.4 - eslint: 8.52.0 + eslint: 8.53.0 ts-api-utils: 1.0.3(typescript@5.2.2) typescript: 5.2.2 transitivePeerDependencies: @@ -1531,8 +1596,13 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/types@6.9.1: - resolution: {integrity: sha512-BUGslGOb14zUHOUmDB2FfT6SI1CcZEJYfF3qFwBeUrU6srJfzANonwRYHDpLBuzbq3HaoF2XL2hcr01c8f8OaQ==} + /@typescript-eslint/types@6.10.0: + resolution: {integrity: sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + + /@typescript-eslint/types@6.7.5: + resolution: {integrity: sha512-WboQBlOXtdj1tDFPyIthpKrUb+kZf2VroLZhxKa/VlwLlLyqv/PwUNgL30BlTVZV1Wu4Asu2mMYPqarSO4L5ZQ==} engines: {node: ^16.0.0 || >=18.0.0} dev: true @@ -1578,8 +1648,29 @@ packages: - supports-color dev: true - /@typescript-eslint/typescript-estree@6.9.1(typescript@5.2.2): - resolution: {integrity: sha512-U+mUylTHfcqeO7mLWVQ5W/tMLXqVpRv61wm9ZtfE5egz7gtnmqVIw9ryh0mgIlkKk9rZLY3UHygsBSdB9/ftyw==} + /@typescript-eslint/typescript-estree@6.10.0(typescript@5.2.2): + resolution: {integrity: sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.10.0 + '@typescript-eslint/visitor-keys': 6.10.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/typescript-estree@6.7.5(typescript@5.2.2): + resolution: {integrity: sha512-NhJiJ4KdtwBIxrKl0BqG1Ur+uw7FiOnOThcYx9DpOGJ/Abc9z2xNzLeirCG02Ig3vkvrc2qFLmYSSsaITbKjlg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: typescript: '*' @@ -1587,8 +1678,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 6.9.1 - '@typescript-eslint/visitor-keys': 6.9.1 + '@typescript-eslint/types': 6.7.5 + '@typescript-eslint/visitor-keys': 6.7.5 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 @@ -1599,19 +1690,38 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@6.9.1(eslint@8.52.0)(typescript@5.2.2): - resolution: {integrity: sha512-L1T0A5nFdQrMVunpZgzqPL6y2wVreSyHhKGZryS6jrEN7bD9NplVAyMryUhXsQ4TWLnZmxc2ekar/lSGIlprCA==} + /@typescript-eslint/utils@6.10.0(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.52.0) - '@types/json-schema': 7.0.14 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) + '@types/json-schema': 7.0.15 '@types/semver': 7.5.4 - '@typescript-eslint/scope-manager': 6.9.1 - '@typescript-eslint/types': 6.9.1 - '@typescript-eslint/typescript-estree': 6.9.1(typescript@5.2.2) - eslint: 8.52.0 + '@typescript-eslint/scope-manager': 6.10.0 + '@typescript-eslint/types': 6.10.0 + '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) + eslint: 8.53.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/utils@6.7.5(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-pfRRrH20thJbzPPlPc4j0UNGvH1PjPlhlCMq4Yx7EGjV7lvEeGX0U6MJYe8+SyFutWgSHsdbJ3BXzZccYggezA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) + '@types/json-schema': 7.0.13 + '@types/semver': 7.5.3 + '@typescript-eslint/scope-manager': 6.7.5 + '@typescript-eslint/types': 6.7.5 + '@typescript-eslint/typescript-estree': 6.7.5(typescript@5.2.2) + eslint: 8.53.0 semver: 7.5.4 transitivePeerDependencies: - supports-color @@ -1634,11 +1744,19 @@ packages: eslint-visitor-keys: 3.4.3 dev: true - /@typescript-eslint/visitor-keys@6.9.1: - resolution: {integrity: sha512-MUaPUe/QRLEffARsmNfmpghuQkW436DvESW+h+M52w0coICHRfD6Np9/K6PdACwnrq1HmuLl+cSPZaJmeVPkSw==} + /@typescript-eslint/visitor-keys@6.10.0: + resolution: {integrity: sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.9.1 + '@typescript-eslint/types': 6.10.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@typescript-eslint/visitor-keys@6.7.5: + resolution: {integrity: sha512-3MaWdDZtLlsexZzDSdQWsFQ9l9nL8B80Z4fImSpyllFC/KLqWQRdEcB+gGGO+N3Q2uL40EsG66wZLsohPxNXvg==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.7.5 eslint-visitor-keys: 3.4.3 dev: true @@ -1673,7 +1791,7 @@ packages: istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 4.0.1 istanbul-reports: 3.1.6 - magic-string: 0.30.5 + magic-string: 0.30.4 picocolors: 1.0.0 std-env: 3.4.3 test-exclude: 6.0.0 @@ -1702,7 +1820,7 @@ packages: /@vitest/snapshot@0.34.6: resolution: {integrity: sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==} dependencies: - magic-string: 0.30.5 + magic-string: 0.30.4 pathe: 1.1.1 pretty-format: 29.7.0 dev: true @@ -1717,7 +1835,7 @@ packages: resolution: {integrity: sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==} dependencies: diff-sequences: 29.6.3 - loupe: 2.3.7 + loupe: 2.3.6 pretty-format: 29.7.0 dev: true @@ -1729,11 +1847,17 @@ packages: acorn: 8.11.2 dev: true - /acorn-walk@8.3.0: - resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==} + /acorn-walk@8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} engines: {node: '>=0.4.0'} dev: true + /acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + /acorn@8.11.2: resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} engines: {node: '>=0.4.0'} @@ -1813,7 +1937,7 @@ packages: /array-buffer-byte-length@1.0.0: resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 is-array-buffer: 3.0.2 dev: true @@ -1848,10 +1972,10 @@ packages: resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 define-properties: 1.2.1 - es-abstract: 1.22.3 - es-shim-unscopables: 1.0.2 + es-abstract: 1.22.2 + es-shim-unscopables: 1.0.0 dev: true /array.prototype.flatmap@1.3.2: @@ -1869,10 +1993,10 @@ packages: engines: {node: '>= 0.4'} dependencies: array-buffer-byte-length: 1.0.0 - call-bind: 1.0.5 + call-bind: 1.0.2 define-properties: 1.2.1 - es-abstract: 1.22.3 - get-intrinsic: 1.2.2 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 is-array-buffer: 3.0.2 is-shared-array-buffer: 1.0.2 dev: true @@ -1978,8 +2102,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001559 - electron-to-chromium: 1.4.575 + caniuse-lite: 1.0.30001561 + electron-to-chromium: 1.4.576 node-releases: 2.0.13 update-browserslist-db: 1.0.13(browserslist@4.22.1) dev: true @@ -2000,11 +2124,25 @@ packages: engines: {node: '>=6'} dev: true + /busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + dependencies: + streamsearch: 1.1.0 + dev: true + /cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} dev: true + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + dev: true + /call-bind@1.0.5: resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} dependencies: @@ -2032,8 +2170,8 @@ packages: engines: {node: '>=6'} dev: true - /caniuse-lite@1.0.30001559: - resolution: {integrity: sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA==} + /caniuse-lite@1.0.30001561: + resolution: {integrity: sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==} dev: true /chai@4.3.10: @@ -2044,7 +2182,7 @@ packages: check-error: 1.0.3 deep-eql: 4.1.3 get-func-name: 2.0.2 - loupe: 2.3.7 + loupe: 2.3.6 pathval: 1.1.1 type-detect: 4.0.8 dev: true @@ -2066,6 +2204,11 @@ packages: supports-color: 7.2.0 dev: true + /chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: true + /chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true @@ -2304,6 +2447,15 @@ packages: clone: 1.0.4 dev: true + /define-data-property@1.1.0: + resolution: {integrity: sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + gopd: 1.0.1 + has-property-descriptors: 1.0.0 + dev: true + /define-data-property@1.1.1: resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} engines: {node: '>= 0.4'} @@ -2317,8 +2469,8 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} dependencies: - define-data-property: 1.1.1 - has-property-descriptors: 1.0.1 + define-data-property: 1.1.0 + has-property-descriptors: 1.0.0 object-keys: 1.1.1 dev: true @@ -2554,8 +2706,8 @@ packages: resolution: {integrity: sha512-qROhKMxlm6fpa90YRfWSgKeelDfhaDq2igPK+pIKupGehiCnZH4vd2qrY71HVZ10qZgXxh0VXpGyDQxJC+EQqw==} dev: true - /electron-to-chromium@1.4.575: - resolution: {integrity: sha512-kY2BGyvgAHiX899oF6xLXSIf99bAvvdPhDoJwG77nxCSyWYuRH6e9a9a3gpXBvCs6lj4dQZJkfnW2hdKWHEISg==} + /electron-to-chromium@1.4.576: + resolution: {integrity: sha512-yXsZyXJfAqzWk1WKryr0Wl0MN2D47xodPvEEwlVePBnhU5E7raevLQR+E6b9JAD3GfL/7MbAL9ZtWQQPcLx7wA==} dev: true /emoji-regex@8.0.0: @@ -2594,6 +2746,51 @@ packages: stackframe: 1.3.4 dev: true + /es-abstract@1.22.2: + resolution: {integrity: sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.2 + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.1 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.4 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.1 + safe-array-concat: 1.0.1 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.8 + string.prototype.trimend: 1.0.7 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.11 + dev: true + /es-abstract@1.22.3: resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} engines: {node: '>= 0.4'} @@ -2639,6 +2836,15 @@ packages: which-typed-array: 1.1.13 dev: true + /es-set-tostringtag@2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.4 + has-tostringtag: 1.0.0 + dev: true + /es-set-tostringtag@2.0.2: resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} engines: {node: '>= 0.4'} @@ -2648,6 +2854,12 @@ packages: hasown: 2.0.0 dev: true + /es-shim-unscopables@1.0.0: + resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + dependencies: + has: 1.0.4 + dev: true + /es-shim-unscopables@1.0.2: resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} dependencies: @@ -2735,7 +2947,7 @@ packages: - supports-color dev: true - /eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.9.1)(eslint-plugin-import@2.29.0)(eslint@8.52.0): + /eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.10.0)(eslint-plugin-import@2.29.0)(eslint@8.53.0): resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -2744,12 +2956,12 @@ packages: dependencies: debug: 4.3.4 enhanced-resolve: 5.15.0 - eslint: 8.52.0 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.9.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) - eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.9.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) + eslint: 8.53.0 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.53.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.53.0) fast-glob: 3.3.1 get-tsconfig: 4.7.2 - is-core-module: 2.13.1 + is-core-module: 2.13.0 is-glob: 4.0.3 transitivePeerDependencies: - '@typescript-eslint/parser' @@ -2758,7 +2970,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.9.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0): + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.53.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} peerDependencies: @@ -2779,17 +2991,18 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 6.9.1(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.10.0(eslint@8.53.0)(typescript@5.2.2) debug: 3.2.7 - eslint: 8.52.0 + eslint: 8.53.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.9.1)(eslint-plugin-import@2.29.0)(eslint@8.52.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.10.0)(eslint-plugin-import@2.29.0)(eslint@8.53.0) transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-codegen@0.17.0: - resolution: {integrity: sha512-6DDDob+7PjyNJyy9ynHFFsLp0+aUtWbXiiT/SfU161NCxo1zevewq7VvtDiJh15gMBvVZSFs6hXqYJWT3NUZvA==} + /eslint-plugin-codegen@0.18.1: + resolution: {integrity: sha512-Sy5nJ7tMahHWygM02w2gAO70MX6Lp0ZK0PD9kMpPPGtoQhyS2n1oN7s9zLpDx5pmFDf3woj6LadqztNpJ5RepQ==} + engines: {node: '>=12.0.0'} dependencies: '@babel/core': 7.23.2 '@babel/generator': 7.12.17 @@ -2797,7 +3010,7 @@ packages: '@babel/traverse': 7.23.2 expect: 26.6.2 fp-ts: 2.16.1 - glob: 7.2.3 + glob: 10.3.10 io-ts: 2.2.20(fp-ts@2.16.1) io-ts-extra: 0.11.6 js-yaml: 3.14.1 @@ -2808,14 +3021,14 @@ packages: - supports-color dev: true - /eslint-plugin-deprecation@2.0.0(eslint@8.52.0)(typescript@5.2.2): + /eslint-plugin-deprecation@2.0.0(eslint@8.53.0)(typescript@5.2.2): resolution: {integrity: sha512-OAm9Ohzbj11/ZFyICyR5N6LbOIvQMp7ZU2zI7Ej0jIc8kiGUERXPNMfw2QqqHD1ZHtjMub3yPZILovYEYucgoQ==} peerDependencies: eslint: ^7.0.0 || ^8.0.0 typescript: ^4.2.4 || ^5.0.0 dependencies: - '@typescript-eslint/utils': 6.9.1(eslint@8.52.0)(typescript@5.2.2) - eslint: 8.52.0 + '@typescript-eslint/utils': 6.7.5(eslint@8.53.0)(typescript@5.2.2) + eslint: 8.53.0 tslib: 2.6.2 tsutils: 3.21.0(typescript@5.2.2) typescript: 5.2.2 @@ -2823,7 +3036,7 @@ packages: - supports-color dev: true - /eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.9.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0): + /eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.53.0): resolution: {integrity: sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==} engines: {node: '>=4'} peerDependencies: @@ -2833,16 +3046,16 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 6.9.1(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.10.0(eslint@8.53.0)(typescript@5.2.2) array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 debug: 3.2.7 doctrine: 2.1.0 - eslint: 8.52.0 + eslint: 8.53.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.9.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.53.0) hasown: 2.0.0 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -2858,21 +3071,21 @@ packages: - supports-color dev: true - /eslint-plugin-simple-import-sort@10.0.0(eslint@8.52.0): + /eslint-plugin-simple-import-sort@10.0.0(eslint@8.53.0): resolution: {integrity: sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw==} peerDependencies: eslint: '>=5.0.0' dependencies: - eslint: 8.52.0 + eslint: 8.53.0 dev: true - /eslint-plugin-sort-destructure-keys@1.5.0(eslint@8.52.0): + /eslint-plugin-sort-destructure-keys@1.5.0(eslint@8.53.0): resolution: {integrity: sha512-xGLyqHtbFXZNXQSvAiQ4ISBYokrbUywEhmaA50fKtSKgceCv5y3zjoNuZwcnajdM6q29Nxj+oXC9KcqfMsAPrg==} engines: {node: '>=6.0.0'} peerDependencies: eslint: 3 - 8 dependencies: - eslint: 8.52.0 + eslint: 8.53.0 natural-compare-lite: 1.4.0 dev: true @@ -2894,15 +3107,15 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint@8.52.0: - resolution: {integrity: sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==} + /eslint@8.53.0: + resolution: {integrity: sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.52.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) '@eslint-community/regexpp': 4.10.0 - '@eslint/eslintrc': 2.1.2 - '@eslint/js': 8.52.0 + '@eslint/eslintrc': 2.1.3 + '@eslint/js': 8.53.0 '@humanwhocodes/config-array': 0.11.13 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 @@ -3034,6 +3247,10 @@ packages: pure-rand: 6.0.4 dev: true + /fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + dev: true + /fast-deep-equal@2.0.1: resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} dev: true @@ -3053,6 +3270,17 @@ packages: micromatch: 4.0.5 dev: true + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} dev: true @@ -3061,6 +3289,12 @@ packages: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} dev: true + /fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + dependencies: + fast-decode-uri-component: 1.0.1 + dev: true + /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: @@ -3086,7 +3320,7 @@ packages: is-relative-path: 1.0.2 module-definition: 3.4.0 module-lookup-amd: 7.0.1 - resolve: 1.22.8 + resolve: 1.22.6 resolve-dependency-path: 2.0.0 sass-lookup: 3.0.0 stylus-lookup: 3.0.2 @@ -3114,6 +3348,15 @@ packages: to-regex-range: 5.0.1 dev: true + /find-my-way@7.7.0: + resolution: {integrity: sha512-+SrHpvQ52Q6W9f3wJoJBbAQULJuNEEQwBvlvYwACDhBTLOTMiQ0HYWh4+vC3OivGP2ENcTI1oKlFA2OepJNjhQ==} + engines: {node: '>=14'} + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 2.0.0 + dev: true + /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -3178,15 +3421,6 @@ packages: resolution: {integrity: sha512-by7U5W8dkIzcvDofUcO42yl9JbnHTEDBrzu3pt5fKT+Z4Oy85I21K80EYJYdjQGC2qum4Vo55Ag57iiIK4FYuA==} dev: true - /fs-extra@11.1.1: - resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} - engines: {node: '>=14.14'} - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.1 - dev: true - /fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -3227,6 +3461,10 @@ packages: dev: true optional: true + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} dev: true @@ -3235,9 +3473,9 @@ packages: resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.2 functions-have-names: 1.2.3 dev: true @@ -3275,6 +3513,15 @@ packages: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} dev: true + /get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + dependencies: + function-bind: 1.1.1 + has: 1.0.4 + has-proto: 1.0.1 + has-symbols: 1.0.3 + dev: true + /get-intrinsic@1.2.2: resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} dependencies: @@ -3292,8 +3539,8 @@ packages: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 + call-bind: 1.0.2 + get-intrinsic: 1.2.1 dev: true /get-tsconfig@4.7.2: @@ -3395,7 +3642,7 @@ packages: /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: - get-intrinsic: 1.2.2 + get-intrinsic: 1.2.1 dev: true /graceful-fs@4.2.11: @@ -3448,6 +3695,12 @@ packages: engines: {node: '>=8'} dev: true + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.1 + dev: true + /has-property-descriptors@1.0.1: resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} dependencies: @@ -3471,6 +3724,11 @@ packages: has-symbols: 1.0.3 dev: true + /has@1.0.4: + resolution: {integrity: sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==} + engines: {node: '>= 0.4.0'} + dev: true + /hasown@2.0.0: resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} engines: {node: '>= 0.4'} @@ -3549,6 +3807,15 @@ packages: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} dev: true + /internal-slot@1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.4 + side-channel: 1.0.4 + dev: true + /internal-slot@1.0.6: resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} engines: {node: '>= 0.4'} @@ -3576,8 +3843,8 @@ packages: /is-array-buffer@3.0.2: resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 + call-bind: 1.0.2 + get-intrinsic: 1.2.1 is-typed-array: 1.1.12 dev: true @@ -3595,7 +3862,7 @@ packages: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 has-tostringtag: 1.0.0 dev: true @@ -3622,6 +3889,12 @@ packages: ci-info: 3.9.0 dev: true + /is-core-module@2.13.0: + resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + dependencies: + has: 1.0.4 + dev: true + /is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} dependencies: @@ -3751,7 +4024,7 @@ packages: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 has-tostringtag: 1.0.0 dev: true @@ -3767,7 +4040,7 @@ packages: /is-shared-array-buffer@1.0.2: resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 dev: true /is-string@1.0.7: @@ -3795,7 +4068,7 @@ packages: resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} engines: {node: '>= 0.4'} dependencies: - which-typed-array: 1.1.13 + which-typed-array: 1.1.11 dev: true /is-unicode-supported@0.1.0: @@ -3815,7 +4088,7 @@ packages: /is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 dev: true /is-windows@1.0.2: @@ -3920,7 +4193,7 @@ packages: dependencies: '@babel/code-frame': 7.22.13 '@jest/types': 26.6.2 - '@types/stack-utils': 2.0.2 + '@types/stack-utils': 2.0.3 chalk: 4.1.2 graceful-fs: 4.2.11 micromatch: 4.0.5 @@ -4138,8 +4411,8 @@ packages: is-unicode-supported: 0.1.0 dev: true - /loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + /loupe@2.3.6: + resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} dependencies: get-func-name: 2.0.2 dev: true @@ -4212,6 +4485,13 @@ packages: sourcemap-codec: 1.4.8 dev: true + /magic-string@0.30.4: + resolution: {integrity: sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /magic-string@0.30.5: resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} engines: {node: '>=12'} @@ -4253,7 +4533,7 @@ packages: resolution: {integrity: sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==} engines: {node: '>=8'} dependencies: - '@types/minimist': 1.2.4 + '@types/minimist': 1.2.3 camelcase-keys: 6.2.2 decamelize-keys: 1.1.1 hard-rejection: 2.1.0 @@ -4300,6 +4580,12 @@ packages: picomatch: 2.3.1 dev: true + /mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + dev: true + /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -4370,7 +4656,7 @@ packages: /mlly@1.4.2: resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==} dependencies: - acorn: 8.11.2 + acorn: 8.10.0 pathe: 1.1.1 pkg-types: 1.0.3 ufo: 1.3.1 @@ -4464,7 +4750,7 @@ packages: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: hosted-git-info: 2.8.9 - resolve: 1.22.8 + resolve: 1.22.6 semver: 5.7.2 validate-npm-package-license: 3.0.4 dev: true @@ -4495,6 +4781,10 @@ packages: npm-normalize-package-bin: 1.0.1 dev: true + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: true + /object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} dev: true @@ -4508,7 +4798,7 @@ packages: resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 define-properties: 1.2.1 has-symbols: 1.0.3 object-keys: 1.1.1 @@ -4858,6 +5148,12 @@ packages: hasBin: true dev: true + /prettier@3.0.3: + resolution: {integrity: sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==} + engines: {node: '>=14'} + hasBin: true + dev: true + /pretty-format@26.6.2: resolution: {integrity: sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==} engines: {node: '>= 10'} @@ -4964,7 +5260,7 @@ packages: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} engines: {node: '>=8'} dependencies: - '@types/normalize-package-data': 2.4.3 + '@types/normalize-package-data': 2.4.2 normalize-package-data: 2.5.0 parse-json: 5.2.0 type-fest: 0.6.0 @@ -5017,7 +5313,7 @@ packages: resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 define-properties: 1.2.1 set-function-name: 2.0.1 dev: true @@ -5083,6 +5379,15 @@ packages: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} dev: true + /resolve@1.22.6: + resolution: {integrity: sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==} + hasBin: true + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + /resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true @@ -5100,6 +5405,11 @@ packages: signal-exit: 3.0.7 dev: true + /ret@0.2.2: + resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==} + engines: {node: '>=4'} + dev: true + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -5138,8 +5448,8 @@ packages: resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} engines: {node: '>=0.4'} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 + call-bind: 1.0.2 + get-intrinsic: 1.2.1 has-symbols: 1.0.3 isarray: 2.0.5 dev: true @@ -5155,11 +5465,17 @@ packages: /safe-regex-test@1.0.0: resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 + call-bind: 1.0.2 + get-intrinsic: 1.2.1 is-regex: 1.1.4 dev: true + /safe-regex2@2.0.0: + resolution: {integrity: sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==} + dependencies: + ret: 0.2.2 + dev: true + /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: true @@ -5208,9 +5524,9 @@ packages: resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} engines: {node: '>= 0.4'} dependencies: - define-data-property: 1.1.1 + define-data-property: 1.1.0 functions-have-names: 1.2.3 - has-property-descriptors: 1.0.1 + has-property-descriptors: 1.0.0 dev: true /set-getter@0.1.1: @@ -5247,9 +5563,9 @@ packages: /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 - object-inspect: 1.13.1 + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 dev: true /siginfo@2.0.0: @@ -5374,6 +5690,11 @@ packages: mixme: 0.5.9 dev: true + /streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + dev: true + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -5410,25 +5731,25 @@ packages: resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.2 dev: true /string.prototype.trimend@1.0.7: resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.2 dev: true /string.prototype.trimstart@1.0.7: resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.2 dev: true /string_decoder@1.1.1: @@ -5501,7 +5822,7 @@ packages: /strip-literal@1.3.0: resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} dependencies: - acorn: 8.11.2 + acorn: 8.10.0 dev: true /stylus-lookup@3.0.2: @@ -5638,10 +5959,10 @@ packages: engines: {node: '>=14.16'} dev: true - /ts-morph@19.0.0: - resolution: {integrity: sha512-D6qcpiJdn46tUqV45vr5UGM2dnIEuTGNxVhg0sk5NX11orcouwj6i1bMqZIz2mZTZB1Hcgy7C3oEVhAT+f6mbQ==} + /ts-morph@20.0.0: + resolution: {integrity: sha512-JVmEJy2Wow5n/84I3igthL9sudQ8qzjh/6i4tmYCm6IqYyKFlNbJZi7oBdjyqcWSWYRu3CtL0xbT6fS03ESZIg==} dependencies: - '@ts-morph/common': 0.20.0 + '@ts-morph/common': 0.21.0 code-block-writer: 12.0.0 dev: true @@ -5665,8 +5986,8 @@ packages: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 '@types/node': 20.8.10 - acorn: 8.11.2 - acorn-walk: 8.3.0 + acorn: 8.10.0 + acorn-walk: 8.2.0 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 @@ -5676,12 +5997,12 @@ packages: yn: 3.1.1 dev: true - /tsconfck@2.1.2(typescript@5.2.2): - resolution: {integrity: sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==} - engines: {node: ^14.13.1 || ^16 || >=18} + /tsconfck@3.0.0(typescript@5.2.2): + resolution: {integrity: sha512-w3wnsIrJNi7avf4Zb0VjOoodoO0woEqGgZGQm+LHH9przdUI+XDKsWAXwxHA1DaRTjeuZNcregSzr7RaA8zG9A==} + engines: {node: ^18 || >=20} hasBin: true peerDependencies: - typescript: ^4.3.5 || ^5.0.0 + typescript: ^5.0.0 peerDependenciesMeta: typescript: optional: true @@ -5747,8 +6068,8 @@ packages: fsevents: 2.3.3 dev: true - /tty-table@4.2.3: - resolution: {integrity: sha512-Fs15mu0vGzCrj8fmJNP7Ynxt5J7praPXqFN0leZeZBXJwkMxv9cb2D454k1ltrtUSJbZ4yH4e0CynsHLxmUfFA==} + /tty-table@4.2.2: + resolution: {integrity: sha512-2gvCArMZLxgvpZ2NvQKdnYWIFLe7I/z5JClMuhrDXunmKgSZcQKcZRjN9XjAFiToMz2pUo1dEIXyrm0AwgV5Tw==} engines: {node: '>=8.0.0'} hasBin: true dependencies: @@ -5797,8 +6118,8 @@ packages: resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 + call-bind: 1.0.2 + get-intrinsic: 1.2.1 is-typed-array: 1.1.12 dev: true @@ -5806,7 +6127,7 @@ packages: resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 for-each: 0.3.3 has-proto: 1.0.1 is-typed-array: 1.1.12 @@ -5817,7 +6138,7 @@ packages: engines: {node: '>= 0.4'} dependencies: available-typed-arrays: 1.0.5 - call-bind: 1.0.5 + call-bind: 1.0.2 for-each: 0.3.3 has-proto: 1.0.1 is-typed-array: 1.1.12 @@ -5826,7 +6147,7 @@ packages: /typed-array-length@1.0.4: resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 for-each: 0.3.3 is-typed-array: 1.1.12 dev: true @@ -5860,7 +6181,7 @@ packages: /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.2 has-bigints: 1.0.2 has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 @@ -5917,8 +6238,8 @@ packages: resolution: {integrity: sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==} engines: {node: '>=10.12.0'} dependencies: - '@jridgewell/trace-mapping': 0.3.20 - '@types/istanbul-lib-coverage': 2.0.5 + '@jridgewell/trace-mapping': 0.3.19 + '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 2.0.0 dev: true @@ -6019,20 +6340,20 @@ packages: optional: true dependencies: '@types/chai': 4.3.9 - '@types/chai-subset': 1.3.4 + '@types/chai-subset': 1.3.3 '@types/node': 20.8.10 '@vitest/expect': 0.34.6 '@vitest/runner': 0.34.6 '@vitest/snapshot': 0.34.6 '@vitest/spy': 0.34.6 '@vitest/utils': 0.34.6 - acorn: 8.11.2 - acorn-walk: 8.3.0 + acorn: 8.10.0 + acorn-walk: 8.2.0 cac: 6.7.14 chai: 4.3.10 debug: 4.3.4 local-pkg: 0.4.3 - magic-string: 0.30.5 + magic-string: 0.30.4 pathe: 1.1.1 picocolors: 1.0.0 std-env: 3.4.3 @@ -6096,6 +6417,17 @@ packages: path-exists: 4.0.0 dev: true + /which-typed-array@1.1.11: + resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: true + /which-typed-array@1.1.13: resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==} engines: {node: '>= 0.4'} diff --git a/src/Args.ts b/src/Args.ts index 7ff2832..e5b5855 100644 --- a/src/Args.ts +++ b/src/Args.ts @@ -1,14 +1,15 @@ /** * @since 1.0.0 */ -import type { Chunk, NonEmptyChunk } from "effect/Chunk" import type { Effect } from "effect/Effect" import type { Either } from "effect/Either" import type { Option } from "effect/Option" import type { Pipeable } from "effect/Pipeable" import type { NonEmptyReadonlyArray } from "effect/ReadonlyArray" +import type { CliConfig } from "./CliConfig" import type { HelpDoc } from "./HelpDoc" -import * as internal from "./internal/args" +import * as InternalArgs from "./internal/args" +import type { Parameter } from "./Parameter" import type { Usage } from "./Usage" import type { ValidationError } from "./ValidationError" @@ -16,7 +17,7 @@ import type { ValidationError } from "./ValidationError" * @since 1.0.0 * @category symbols */ -export const ArgsTypeId: unique symbol = internal.ArgsTypeId +export const ArgsTypeId: unique symbol = InternalArgs.ArgsTypeId /** * @since 1.0.0 @@ -30,7 +31,17 @@ export type ArgsTypeId = typeof ArgsTypeId * @since 1.0.0 * @category models */ -export interface Args extends Args.Variance, Pipeable {} +export interface Args extends Args.Variance, Parameter, Pipeable { + get maxSize(): number + get minSize(): number + get identifier(): Option + get usage(): Usage + validate( + args: ReadonlyArray, + config: CliConfig + ): Effect, A]> + addDescription(description: string): Args +} /** * @since 1.0.0 @@ -57,43 +68,94 @@ export declare namespace Args { /** * @since 1.0.0 - * @category combinators */ -export const addDescription: { - (description: string): (self: Args) => Args - (self: Args, description: string): Args -} = internal.addDescription +export declare namespace All { + /** + * @since 1.0.0 + */ + export type ArgsAny = Args + + /** + * @since 1.0.0 + */ + export type ReturnIterable> = [T] extends [Iterable>] + ? Args> + : never + + /** + * @since 1.0.0 + */ + export type ReturnTuple> = Args< + T[number] extends never ? [] + : { + -readonly [K in keyof T]: [T[K]] extends [Args.Variance] ? _A : never + } + > extends infer X ? X : never + + /** + * @since 1.0.0 + */ + export type ReturnObject = [T] extends [{ [K: string]: ArgsAny }] ? Args< + { + -readonly [K in keyof T]: [T[K]] extends [Args.Variance] ? _A : never + } + > + : never + + /** + * @since 1.0.0 + */ + export type Return< + Arg extends Iterable | Record + > = [Arg] extends [ReadonlyArray] ? ReturnTuple + : [Arg] extends [Iterable] ? ReturnIterable + : [Arg] extends [Record] ? ReturnObject + : never +} + +/** + * @since 1.0.0 + * @category refinements + */ +export const isArgs: (u: unknown) => u is Args = InternalArgs.isArgs + +/** + * @since 1.0.0 + * @category constructors + */ +export const all: > | Record>>(arg: Arg) => All.Return = + InternalArgs.all /** * @since 1.0.0 * @category combinators */ export const atLeast: { - (times: 0): (self: Args) => Args> - (times: number): (self: Args) => Args> - (self: Args, times: 0): Args> - (self: Args, times: number): Args> -} = internal.atLeast + (times: 0): (self: Args) => Args> + (times: number): (self: Args) => Args> + (self: Args, times: 0): Args> + (self: Args, times: number): Args> +} = InternalArgs.atLeast /** * @since 1.0.0 * @category combinators */ export const atMost: { - (times: number): (self: Args) => Args> - (self: Args, times: number): Args> -} = internal.atMost + (times: number): (self: Args) => Args> + (self: Args, times: number): Args> +} = InternalArgs.atMost /** * @since 1.0.0 * @category combinators */ export const between: { - (min: 0, max: number): (self: Args) => Args> - (min: number, max: number): (self: Args) => Args> - (self: Args, min: 0, max: number): Args> - (self: Args, min: number, max: number): Args> -} = internal.between + (min: 0, max: number): (self: Args) => Args> + (min: number, max: number): (self: Args) => Args> + (self: Args, min: 0, max: number): Args> + (self: Args, min: number, max: number): Args> +} = InternalArgs.between /** * Creates a boolean argument. @@ -103,7 +165,7 @@ export const between: { * @since 1.0.0 * @category constructors */ -export const boolean: (options?: Args.ArgsConfig) => Args = internal.boolean +export const boolean: (options?: Args.ArgsConfig) => Args = InternalArgs.boolean /** * Creates a choice argument. @@ -114,7 +176,7 @@ export const boolean: (options?: Args.ArgsConfig) => Args = internal.bo * @category constructors */ export const choice: (choices: NonEmptyReadonlyArray<[string, A]>, config?: Args.ArgsConfig) => Args = - internal.choice + InternalArgs.choice /** * Creates a date argument. @@ -124,7 +186,7 @@ export const choice: (choices: NonEmptyReadonlyArray<[string, A]>, config?: A * @since 1.0.0 * @category constructors */ -export const date: (config?: Args.ArgsConfig) => Args = internal.date +export const date: (config?: Args.ArgsConfig) => Args = InternalArgs.date /** * Creates a floating point number argument. @@ -134,13 +196,7 @@ export const date: (config?: Args.ArgsConfig) => Args = interna * @since 1.0.0 * @category constructors */ -export const float: (config?: Args.ArgsConfig) => Args = internal.float - -/** - * @since 1.0.0 - * @category getters - */ -export const helpDoc: (self: Args) => HelpDoc = internal.helpDoc +export const float: (config?: Args.ArgsConfig) => Args = InternalArgs.float /** * Creates an integer argument. @@ -150,13 +206,7 @@ export const helpDoc: (self: Args) => HelpDoc = internal.helpDoc * @since 1.0.0 * @category constructors */ -export const integer: (config?: Args.ArgsConfig) => Args = internal.integer - -/** - * @since 1.0.0 - * @category refinements - */ -export const isArgs: (u: unknown) => u is Args = internal.isArgs +export const integer: (config?: Args.ArgsConfig) => Args = InternalArgs.integer /** * @since 1.0.0 @@ -165,7 +215,7 @@ export const isArgs: (u: unknown) => u is Args = internal.isArgs export const map: { (f: (a: A) => B): (self: Args) => Args (self: Args, f: (a: A) => B): Args -} = internal.map +} = InternalArgs.map /** * @since 1.0.0 @@ -174,7 +224,7 @@ export const map: { export const mapOrFail: { (f: (a: A) => Either): (self: Args) => Args (self: Args, f: (a: A) => Either): Args -} = internal.mapOrFail +} = InternalArgs.mapOrFail /** * @since 1.0.0 @@ -183,19 +233,7 @@ export const mapOrFail: { export const mapTryCatch: { (f: (a: A) => B, onError: (e: unknown) => HelpDoc): (self: Args) => Args (self: Args, f: (a: A) => B, onError: (e: unknown) => HelpDoc): Args -} = internal.mapTryCatch - -/** - * @since 1.0.0 - * @category getters - */ -export const maxSize: (self: Args) => number = internal.maxSize - -/** - * @since 1.0.0 - * @category getters - */ -export const minSize: (self: Args) => number = internal.minSize +} = InternalArgs.mapTryCatch /** * Creates an empty argument. @@ -203,19 +241,20 @@ export const minSize: (self: Args) => number = internal.minSize * @since 1.0.0 * @category constructors */ -export const none: Args = internal.none +export const none: Args = InternalArgs.none /** * @since 1.0.0 * @category combinators */ -export const repeat: (self: Args) => Args> = internal.repeat +export const repeated: (self: Args) => Args> = InternalArgs.repeated /** * @since 1.0.0 * @category combinators */ -export const repeat1: (self: Args) => Args> = internal.repeat1 +export const repeatedAtLeastOnce: (self: Args) => Args> = + InternalArgs.repeatedAtLeastOnce /** * Creates a text argument. @@ -225,54 +264,4 @@ export const repeat1: (self: Args) => Args> = internal.re * @since 1.0.0 * @category constructors */ -export const text: (config?: Args.ArgsConfig) => Args = internal.text - -/** - * @since 1.0.0 - * @category getters - */ -export const uid: (self: Args) => Option = internal.uid - -/** - * @since 1.0.0 - * @category getters - */ -export const usage: (self: Args) => Usage = internal.usage - -/** - * @since 1.0.0 - * @category validation - */ -export const validate: { - ( - args: ReadonlyArray - ): (self: Args) => Effect, A]> - (self: Args, args: ReadonlyArray): Effect, A]> -} = internal.validate - -/** - * @since 1.0.0 - * @category zipping - */ -export const zip: { - (that: Args): (self: Args) => Args - (self: Args, that: Args): Args -} = internal.zip - -/** - * @since 1.0.0 - * @category zipping - */ -export const zipFlatten: { - (that: Args): >(self: Args) => Args<[...A, B]> - , B>(self: Args, that: Args): Args<[...A, B]> -} = internal.zipFlatten - -/** - * @since 1.0.0 - * @category zipping - */ -export const zipWith: { - (that: Args, f: (a: A, b: B) => C): (self: Args) => Args - (self: Args, that: Args, f: (a: A, b: B) => C): Args -} = internal.zipWith +export const text: (config?: Args.ArgsConfig) => Args = InternalArgs.text diff --git a/src/AutoCorrect.ts b/src/AutoCorrect.ts index b414d34..f50ecbc 100644 --- a/src/AutoCorrect.ts +++ b/src/AutoCorrect.ts @@ -3,11 +3,11 @@ */ import type { CliConfig } from "./CliConfig" -import * as internal from "./internal/autoCorrect" +import * as InternalAutoCorrect from "./internal/autoCorrect" /** * @since 1.0.0 * @category utilities */ export const levensteinDistance: (first: string, second: string, config: CliConfig) => number = - internal.levensteinDistance + InternalAutoCorrect.levensteinDistance diff --git a/src/BuiltInOption.ts b/src/BuiltInOptions.ts similarity index 60% rename from src/BuiltInOption.ts rename to src/BuiltInOptions.ts index 91a7906..db87279 100644 --- a/src/BuiltInOption.ts +++ b/src/BuiltInOptions.ts @@ -5,7 +5,7 @@ import type { Option } from "effect/Option" import type { Command } from "./Command" import type { HelpDoc } from "./HelpDoc" -import * as internal from "./internal/builtInOption" +import * as InternalBuiltInOptions from "./internal/builtInOptions" import type { Options } from "./Options" import type { ShellType } from "./ShellType" import type { Usage } from "./Usage" @@ -14,7 +14,7 @@ import type { Usage } from "./Usage" * @since 1.0.0 * @category models */ -export type BuiltInOption = ShowHelp | ShowCompletionScript | ShowCompletions | Wizard +export type BuiltInOptions = ShowHelp | ShowCompletionScript | ShowCompletions | ShowWizard /** * @since 1.0.0 @@ -50,8 +50,8 @@ export interface ShowCompletions { * @since 1.0.0 * @category models */ -export interface Wizard { - readonly _tag: "Wizard" +export interface ShowWizard { + readonly _tag: "ShowWizard" readonly commmand: Command } @@ -63,54 +63,56 @@ export const builtInOptions: ( command: Command, usage: Usage, helpDoc: HelpDoc -) => Options> = internal.builtInOptions +) => Options> = InternalBuiltInOptions.builtInOptions /** * @since 1.0.0 * @category refinements */ -export const isShowCompletionScript: (self: BuiltInOption) => self is ShowCompletionScript = - internal.isShowCompletionScript +export const isShowCompletionScript: (self: BuiltInOptions) => self is ShowCompletionScript = + InternalBuiltInOptions.isShowCompletionScript /** * @since 1.0.0 * @category refinements */ -export const isShowCompletions: (self: BuiltInOption) => self is ShowCompletions = internal.isShowCompletions +export const isShowCompletions: (self: BuiltInOptions) => self is ShowCompletions = + InternalBuiltInOptions.isShowCompletions /** * @since 1.0.0 * @category refinements */ -export const isShowHelp: (self: BuiltInOption) => self is ShowHelp = internal.isShowHelp +export const isShowHelp: (self: BuiltInOptions) => self is ShowHelp = InternalBuiltInOptions.isShowHelp /** * @since 1.0.0 * @category refinements */ -export const isWizard: (self: BuiltInOption) => self is Wizard = internal.isWizard +export const isShowWizard: (self: BuiltInOptions) => self is ShowWizard = InternalBuiltInOptions.isShowWizard /** * @since 1.0.0 * @category constructors */ -export const showCompletions: (index: number, shellType: ShellType) => BuiltInOption = internal.showCompletions +export const showCompletions: (index: number, shellType: ShellType) => BuiltInOptions = + InternalBuiltInOptions.showCompletions /** * @since 1.0.0 * @category constructors */ -export const showCompletionScript: (pathToExecutable: string, shellType: ShellType) => BuiltInOption = - internal.showCompletionScript +export const showCompletionScript: (pathToExecutable: string, shellType: ShellType) => BuiltInOptions = + InternalBuiltInOptions.showCompletionScript /** * @since 1.0.0 * @category constructors */ -export const showHelp: (usage: Usage, helpDoc: HelpDoc) => BuiltInOption = internal.showHelp +export const showHelp: (usage: Usage, helpDoc: HelpDoc) => BuiltInOptions = InternalBuiltInOptions.showHelp /** * @since 1.0.0 * @category constructors */ -export const wizard: (commmand: Command) => BuiltInOption = internal.wizard +export const showWizard: (commmand: Command) => BuiltInOptions = InternalBuiltInOptions.showWizard diff --git a/src/CliApp.ts b/src/CliApp.ts index 3c09d33..bf0b48b 100644 --- a/src/CliApp.ts +++ b/src/CliApp.ts @@ -5,7 +5,7 @@ import type { Effect } from "effect/Effect" import type { Command } from "./Command" import type { HelpDoc } from "./HelpDoc" import type { Span } from "./HelpDoc/Span" -import * as internal from "./internal/cliApp" +import * as InternalCliApp from "./internal/cliApp" import type { ValidationError } from "./ValidationError" /** @@ -34,7 +34,7 @@ export const make: ( summary?: Span | undefined footer?: HelpDoc | undefined } -) => CliApp = internal.make +) => CliApp = InternalCliApp.make /** * @since 1.0.0 @@ -50,4 +50,4 @@ export const run: { args: ReadonlyArray, f: (a: A) => Effect ): Effect -} = internal.run +} = InternalCliApp.run diff --git a/src/CliConfig.ts b/src/CliConfig.ts index ef044cb..f9eb10e 100644 --- a/src/CliConfig.ts +++ b/src/CliConfig.ts @@ -3,7 +3,7 @@ */ import type * as Context from "effect/Context" import type * as Layer from "effect/Layer" -import * as internal from "./internal/cliConfig" +import * as InternalCliConfig from "./internal/cliConfig" /** * Represents how arguments from the command-line are to be parsed. @@ -20,37 +20,52 @@ export interface CliConfig { * Threshold for when to show auto correct suggestions. */ readonly autoCorrectLimit: number + /** + * Whether or not to perform a final check of the command-line arguments for + * a built-in option, even if the provided command is not valid. + */ + readonly finalCheckBuiltIn: boolean + /** + * Whether or not to display all the names of an option in the usage of a + * particular command. + */ + readonly showAllNames: boolean + /** + * Whether or not to display the type of an option in the usage of a + * particular command. + */ + readonly showTypes: boolean } /** * @since 1.0.0 * @category context */ -export const CliConfig: Context.Tag = internal.Tag +export const CliConfig: Context.Tag = InternalCliConfig.Tag /** * @since 1.0.0 * @category constructors */ -export const defaultConfig: CliConfig = internal.defaultConfig +export const defaultConfig: CliConfig = InternalCliConfig.defaultConfig /** * @since 1.0.0 * @category context */ -export const defaultLayer: Layer.Layer = internal.defaultLayer +export const defaultLayer: Layer.Layer = InternalCliConfig.defaultLayer /** * @since 1.0.0 * @category context */ -export const layer: (config: CliConfig) => Layer.Layer = internal.layer +export const layer: (config?: Partial) => Layer.Layer = InternalCliConfig.layer /** * @since 1.0.0 * @category constructors */ -export const make: (isCaseSensitive: boolean, autoCorrectLimit: number) => CliConfig = internal.make +export const make: (params: Partial) => CliConfig = InternalCliConfig.make /** * @since 1.0.0 @@ -59,4 +74,4 @@ export const make: (isCaseSensitive: boolean, autoCorrectLimit: number) => CliCo export const normalizeCase: { (text: string): (self: CliConfig) => string (self: CliConfig, text: string): string -} = internal.normalizeCase +} = InternalCliConfig.normalizeCase diff --git a/src/Command.ts b/src/Command.ts index 300b547..bf74fca 100644 --- a/src/Command.ts +++ b/src/Command.ts @@ -4,7 +4,6 @@ import type { Effect } from "effect/Effect" import type { Either } from "effect/Either" import type { HashMap } from "effect/HashMap" -import type { HashSet } from "effect/HashSet" import type { Option } from "effect/Option" import type { Pipeable } from "effect/Pipeable" import type { NonEmptyReadonlyArray } from "effect/ReadonlyArray" @@ -12,8 +11,9 @@ import type { Args } from "./Args" import type { CliConfig } from "./CliConfig" import type { CommandDirective } from "./CommandDirective" import type { HelpDoc } from "./HelpDoc" -import * as internal from "./internal/command" +import * as InternalCommand from "./internal/command" import type { Options } from "./Options" +import type { Named } from "./Parameter" import type { Prompt } from "./Prompt" import type { Terminal } from "./Terminal" import type { Usage } from "./Usage" @@ -23,7 +23,7 @@ import type { ValidationError } from "./ValidationError" * @since 1.0.0 * @category symbols */ -export const CommandTypeId: unique symbol = internal.CommandTypeId +export const CommandTypeId: unique symbol = InternalCommand.CommandTypeId /** * @since 1.0.0 @@ -41,7 +41,11 @@ export type CommandTypeId = typeof CommandTypeId * @since 1.0.0 * @category models */ -export interface Command extends Command.Variance, Pipeable {} +export interface Command extends Command.Variance, Named, Pipeable { + get usage(): Usage + get subcommands(): HashMap> + parse(args: ReadonlyArray, config: CliConfig): Effect> +} /** * @since 1.0.0 @@ -70,12 +74,29 @@ export declare namespace Command { * @since 1.0.0 * @category models */ - export type Parsed = Command.ComputeParsedType<{ + export type ParsedStandardCommand = Command.ComputeParsedType<{ readonly name: Name readonly options: OptionsType readonly args: ArgsType }> + /** + * @since 1.0.0 + * @category models + */ + export type ParsedUserInputCommand = Command.ComputeParsedType<{ + readonly name: Name + readonly value: ValueType + }> + + /** + * @since 1.0.0 + * @category models + */ + export type ParsedSubcommand> = A[number] extends Command + ? GetParsedType + : never + /** * @since 1.0.0 * @category models @@ -95,26 +116,14 @@ export declare namespace Command { export type Subcommands>> = GetParsedType } -/** - * @since 1.0.0 - * @category getters - */ -export const getSubcommands: (self: Command) => HashMap> = internal.getSubcommands - -/** - * @since 1.0.0 - * @category getters - */ -export const helpDoc: (self: Command) => HelpDoc = internal.helpDoc - /** * @since 1.0.0 * @category constructors */ -export const make: ( +export const standard: ( name: Name, config?: Command.ConstructorConfig -) => Command<{ readonly name: Name; readonly options: OptionsType; readonly args: ArgsType }> = internal.make +) => Command<{ readonly name: Name; readonly options: OptionsType; readonly args: ArgsType }> = InternalCommand.standard /** * @since 1.0.0 @@ -123,22 +132,7 @@ export const make: ( export const map: { (f: (a: A) => B): (self: Command) => Command (self: Command, f: (a: A) => B): Command -} = internal.map - -/** - * @since 1.0.0 - * @category mapping - */ -export const mapOrFail: { - (f: (a: A) => Either): (self: Command) => Command - (self: Command, f: (a: A) => Either): Command -} = internal.mapOrFail - -/** - * @since 1.0.0 - * @category getters - */ -export const names: (self: Command) => HashSet = internal.names +} = InternalCommand.map /** * @since 1.0.0 @@ -147,7 +141,7 @@ export const names: (self: Command) => HashSet = internal.names export const orElse: { (that: Command): (self: Command) => Command (self: Command, that: Command): Command -} = internal.orElse +} = InternalCommand.orElse /** * @since 1.0.0 @@ -156,23 +150,7 @@ export const orElse: { export const orElseEither: { (that: Command): (self: Command) => Command> (self: Command, that: Command): Command> -} = internal.orElseEither - -/** - * @since 1.0.0 - * @category parsing - */ -export const parse: { - ( - args: ReadonlyArray, - config: CliConfig - ): (self: Command) => Effect> - ( - self: Command, - args: ReadonlyArray, - config: CliConfig - ): Effect> -} = internal.parse +} = InternalCommand.orElseEither /** * @since 1.0.0 @@ -181,7 +159,7 @@ export const parse: { export const prompt: ( name: Name, prompt: Prompt -) => Command<{ readonly name: Name; readonly value: A }> = internal.prompt +) => Command<{ readonly name: Name; readonly value: A }> = InternalCommand.prompt /** * @since 1.0.0 @@ -201,13 +179,7 @@ export const subcommands: { ): Command< Command.ComputeParsedType> }>> > -} = internal.subcommands - -/** - * @since 1.0.0 - * @category getters - */ -export const usage: (self: Command) => Usage = internal.usage +} = InternalCommand.subcommands /** * @since 1.0.0 @@ -216,4 +188,4 @@ export const usage: (self: Command) => Usage = internal.usage export const withHelp: { (help: string | HelpDoc): (self: Command) => Command (self: Command, help: string | HelpDoc): Command -} = internal.withHelp +} = InternalCommand.withHelp diff --git a/src/CommandDirective.ts b/src/CommandDirective.ts index c8bd57c..790a083 100644 --- a/src/CommandDirective.ts +++ b/src/CommandDirective.ts @@ -1,8 +1,8 @@ /** * @since 1.0.0 */ -import type { BuiltInOption } from "./BuiltInOption" -import * as internal from "./internal/commandDirective" +import type { BuiltInOptions } from "./BuiltInOptions" +import * as InternalCommandDirective from "./internal/commandDirective" /** * @since 1.0.0 @@ -16,7 +16,7 @@ export type CommandDirective = BuiltIn | UserDefined */ export interface BuiltIn { readonly _tag: "BuiltIn" - readonly option: BuiltInOption + readonly option: BuiltInOptions } /** @@ -33,19 +33,20 @@ export interface UserDefined { * @since 1.0.0 * @category constructors */ -export const builtIn: (option: BuiltInOption) => CommandDirective = internal.builtIn +export const builtIn: (option: BuiltInOptions) => CommandDirective = InternalCommandDirective.builtIn /** * @since 1.0.0 * @category refinements */ -export const isBuiltIn: (self: CommandDirective) => self is BuiltIn = internal.isBuiltIn +export const isBuiltIn: (self: CommandDirective) => self is BuiltIn = InternalCommandDirective.isBuiltIn /** * @since 1.0.0 * @category refinements */ -export const isUserDefined: (self: CommandDirective) => self is UserDefined = internal.isUserDefined +export const isUserDefined: (self: CommandDirective) => self is UserDefined = + InternalCommandDirective.isUserDefined /** * @since 1.0.0 @@ -54,10 +55,11 @@ export const isUserDefined: (self: CommandDirective) => self is UserDefine export const map: { (f: (a: A) => B): (self: CommandDirective) => CommandDirective (self: CommandDirective, f: (a: A) => B): CommandDirective -} = internal.map +} = InternalCommandDirective.map /** * @since 1.0.0 * @category constructors */ -export const userDefined: (leftover: ReadonlyArray, value: A) => CommandDirective = internal.userDefined +export const userDefined: (leftover: ReadonlyArray, value: A) => CommandDirective = + InternalCommandDirective.userDefined diff --git a/src/HelpDoc.ts b/src/HelpDoc.ts index 9d382a6..b3e520a 100644 --- a/src/HelpDoc.ts +++ b/src/HelpDoc.ts @@ -4,7 +4,7 @@ import type { AnsiDoc } from "@effect/printer-ansi/AnsiDoc" import type { NonEmptyReadonlyArray } from "effect/ReadonlyArray" import type { Span } from "./HelpDoc/Span" -import * as internal from "./internal/helpDoc" +import * as InternalHelpDoc from "./internal/helpDoc" /** * A `HelpDoc` models the full documentation for a command-line application. @@ -81,73 +81,73 @@ export interface Sequence { * @since 1.0.0 * @category refinements */ -export const isEmpty: (helpDoc: HelpDoc) => helpDoc is Empty = internal.isEmpty +export const isEmpty: (helpDoc: HelpDoc) => helpDoc is Empty = InternalHelpDoc.isEmpty /** * @since 1.0.0 * @category refinements */ -export const isHeader: (helpDoc: HelpDoc) => helpDoc is Header = internal.isHeader +export const isHeader: (helpDoc: HelpDoc) => helpDoc is Header = InternalHelpDoc.isHeader /** * @since 1.0.0 * @category refinements */ -export const isParagraph: (helpDoc: HelpDoc) => helpDoc is Paragraph = internal.isParagraph +export const isParagraph: (helpDoc: HelpDoc) => helpDoc is Paragraph = InternalHelpDoc.isParagraph /** * @since 1.0.0 * @category refinements */ -export const isDescriptionList: (helpDoc: HelpDoc) => helpDoc is DescriptionList = internal.isDescriptionList +export const isDescriptionList: (helpDoc: HelpDoc) => helpDoc is DescriptionList = InternalHelpDoc.isDescriptionList /** * @since 1.0.0 * @category refinements */ -export const isEnumeration: (helpDoc: HelpDoc) => helpDoc is Enumeration = internal.isEnumeration +export const isEnumeration: (helpDoc: HelpDoc) => helpDoc is Enumeration = InternalHelpDoc.isEnumeration /** * @since 1.0.0 * @category refinements */ -export const isSequence: (helpDoc: HelpDoc) => helpDoc is Sequence = internal.isSequence +export const isSequence: (helpDoc: HelpDoc) => helpDoc is Sequence = InternalHelpDoc.isSequence /** * @since 1.0.0 * @category constructors */ -export const empty: HelpDoc = internal.empty +export const empty: HelpDoc = InternalHelpDoc.empty /** * @since 1.0.0 * @category constructors */ -export const blocks: (helpDocs: Iterable) => HelpDoc = internal.blocks +export const blocks: (helpDocs: Iterable) => HelpDoc = InternalHelpDoc.blocks /** * @since 1.0.0 * @category constructors */ -export const h1: (value: string | Span) => HelpDoc = internal.h1 +export const h1: (value: string | Span) => HelpDoc = InternalHelpDoc.h1 /** * @since 1.0.0 * @category constructors */ -export const h2: (value: string | Span) => HelpDoc = internal.h2 +export const h2: (value: string | Span) => HelpDoc = InternalHelpDoc.h2 /** * @since 1.0.0 * @category constructors */ -export const h3: (value: string | Span) => HelpDoc = internal.h3 +export const h3: (value: string | Span) => HelpDoc = InternalHelpDoc.h3 /** * @since 1.0.0 * @category constructors */ -export const p: (value: string | Span) => HelpDoc = internal.p +export const p: (value: string | Span) => HelpDoc = InternalHelpDoc.p /** * @since 1.0.0 @@ -155,19 +155,19 @@ export const p: (value: string | Span) => HelpDoc = internal.p */ export const descriptionList: ( definitions: NonEmptyReadonlyArray<[Span, HelpDoc]> -) => HelpDoc = internal.descriptionList +) => HelpDoc = InternalHelpDoc.descriptionList /** * @since 1.0.0 * @category constructors */ -export const enumeration: (elements: NonEmptyReadonlyArray) => HelpDoc = internal.enumeration +export const enumeration: (elements: NonEmptyReadonlyArray) => HelpDoc = InternalHelpDoc.enumeration /** * @since 1.0.0 * @category getters */ -export const getSpan: (self: HelpDoc) => Span = internal.getSpan +export const getSpan: (self: HelpDoc) => Span = InternalHelpDoc.getSpan /** * @since 1.0.0 @@ -176,7 +176,7 @@ export const getSpan: (self: HelpDoc) => Span = internal.getSpan export const sequence: { (that: HelpDoc): (self: HelpDoc) => HelpDoc (self: HelpDoc, that: HelpDoc): HelpDoc -} = internal.sequence +} = InternalHelpDoc.sequence /** * @since 1.0.0 @@ -185,7 +185,7 @@ export const sequence: { export const orElse: { (that: HelpDoc): (self: HelpDoc) => HelpDoc (self: HelpDoc, that: HelpDoc): HelpDoc -} = internal.orElse +} = InternalHelpDoc.orElse /** * @since 1.0.0 @@ -194,16 +194,16 @@ export const orElse: { export const mapDescriptionList: { (f: (span: Span, helpDoc: HelpDoc) => readonly [Span, HelpDoc]): (self: HelpDoc) => HelpDoc (self: HelpDoc, f: (span: Span, helpDoc: HelpDoc) => readonly [Span, HelpDoc]): HelpDoc -} = internal.mapDescriptionList +} = InternalHelpDoc.mapDescriptionList /** * @since 1.0.0 * @category rendering */ -export const toAnsiDoc: (self: HelpDoc) => AnsiDoc = internal.toAnsiDoc +export const toAnsiDoc: (self: HelpDoc) => AnsiDoc = InternalHelpDoc.toAnsiDoc /** * @since 1.0.0 * @category rendering */ -export const toAnsiText: (self: HelpDoc) => string = internal.toAnsiText +export const toAnsiText: (self: HelpDoc) => string = InternalHelpDoc.toAnsiText diff --git a/src/HelpDoc/Span.ts b/src/HelpDoc/Span.ts index 2e4fc2b..59fee1c 100644 --- a/src/HelpDoc/Span.ts +++ b/src/HelpDoc/Span.ts @@ -1,7 +1,7 @@ /** * @since 1.0.0 */ -import * as internal from "../internal/helpDoc/span" +import * as InternalSpan from "../internal/helpDoc/span" /** * @since 1.0.0 @@ -73,53 +73,89 @@ export interface Sequence { readonly right: Span } +/** + * @since 1.0.0 + * @category refinements + */ +export const isError: (self: Span) => self is Error = InternalSpan.isError + +/** + * @since 1.0.0 + * @category refinements + */ +export const isSequence: (self: Span) => self is Sequence = InternalSpan.isSequence + +/** + * @since 1.0.0 + * @category refinements + */ +export const isStrong: (self: Span) => self is Strong = InternalSpan.isStrong + +/** + * @since 1.0.0 + * @category refinements + */ +export const isText: (self: Span) => self is Text = InternalSpan.isText + +/** + * @since 1.0.0 + * @category refinements + */ +export const isUri: (self: Span) => self is URI = InternalSpan.isUri + +/** + * @since 1.0.0 + * @category refinements + */ +export const isWeak: (self: Span) => self is Weak = InternalSpan.isWeak + /** * @since 1.0.0 * @category constructors */ -export const empty: Span = internal.empty +export const empty: Span = InternalSpan.empty /** * @since 1.0.0 * @category constructors */ -export const space: Span = internal.space +export const space: Span = InternalSpan.space /** * @since 1.0.0 * @category constructors */ -export const text: (value: string) => Span = internal.text +export const text: (value: string) => Span = InternalSpan.text /** * @since 1.0.0 * @category constructors */ -export const code: (value: string) => Span = internal.code +export const code: (value: string) => Span = InternalSpan.code /** * @since 1.0.0 * @category constructors */ -export const error: (value: string | Span) => Span = internal.error +export const error: (value: string | Span) => Span = InternalSpan.error /** * @since 1.0.0 * @category constructors */ -export const weak: (value: string | Span) => Span = internal.weak +export const weak: (value: string | Span) => Span = InternalSpan.weak /** * @since 1.0.0 * @category constructors */ -export const strong: (value: string | Span) => Span = internal.strong +export const strong: (value: string | Span) => Span = InternalSpan.strong /** * @since 1.0.0 * @category constructors */ -export const uri: (value: string) => Span = internal.uri +export const uri: (value: string) => Span = InternalSpan.uri /** * @since 1.0.0 @@ -128,10 +164,10 @@ export const uri: (value: string) => Span = internal.uri export const concat: { (that: Span): (self: Span) => Span (self: Span, that: Span): Span -} = internal.concat +} = InternalSpan.concat /** * @since 1.0.0 * @category combinators */ -export const spans: (spans: Iterable) => Span = internal.spans +export const spans: (spans: Iterable) => Span = InternalSpan.spans diff --git a/src/Options.ts b/src/Options.ts index e468de7..6a614bc 100644 --- a/src/Options.ts +++ b/src/Options.ts @@ -1,7 +1,6 @@ /** * @since 1.0.0 */ -import type { Chunk, NonEmptyChunk } from "effect/Chunk" import type { Effect } from "effect/Effect" import type { Either } from "effect/Either" import type { HashMap } from "effect/HashMap" @@ -10,7 +9,8 @@ import type { Pipeable } from "effect/Pipeable" import type { NonEmptyReadonlyArray } from "effect/ReadonlyArray" import type { CliConfig } from "./CliConfig" import type { HelpDoc } from "./HelpDoc" -import * as internal from "./internal/options" +import * as InternalOptions from "./internal/options" +import type { Input, Parameter } from "./Parameter" import type { Usage } from "./Usage" import type { ValidationError } from "./ValidationError" @@ -18,7 +18,7 @@ import type { ValidationError } from "./ValidationError" * @since 1.0.0 * @category symbols */ -export const OptionsTypeId: unique symbol = internal.OptionsTypeId +export const OptionsTypeId: unique symbol = InternalOptions.OptionsTypeId /** * @since 1.0.0 @@ -30,7 +30,14 @@ export type OptionsTypeId = typeof OptionsTypeId * @since 1.0.0 * @category models */ -export interface Options extends Options.Variance, Pipeable {} +export interface Options extends Options.Variance, Parameter, Pipeable { + get identifier(): Option + get usage(): Usage + get flattened(): ReadonlyArray + validate(args: HashMap>, config: CliConfig): Effect + /** @internal */ + modifySingle(f: <_>(single: InternalOptions.Single<_>) => InternalOptions.Single<_>): Options +} /** * @since 1.0.0 @@ -53,80 +60,85 @@ export declare namespace Options { export interface BooleanOptionConfig { readonly ifPresent?: boolean readonly negationNames?: NonEmptyReadonlyArray + readonly aliases?: NonEmptyReadonlyArray } } /** * @since 1.0.0 - * @category combinators */ -export const alias: { - (alias: string): (self: Options) => Options - (self: Options, alias: string): Options -} = internal.alias +export declare namespace All { + /** + * @since 1.0.0 + */ + export type OptionsAny = Options -/** - * @since 1.0.0 - * @category constructors - */ -export const all: { - >>( - self: Options, - ...args: T - ): Options< - readonly [ - A, - ...(T["length"] extends 0 ? [] - : Readonly<{ [K in keyof T]: [T[K]] extends [Options] ? A : never }>) - ] - > - >>( - args: [...T] - ): Options< + /** + * @since 1.0.0 + */ + export type ReturnIterable> = [T] extends [Iterable>] + ? Options> + : never + + /** + * @since 1.0.0 + */ + export type ReturnTuple> = Options< T[number] extends never ? [] - : Readonly<{ [K in keyof T]: [T[K]] extends [Options] ? A : never }> - > - }>>( - args: T - ): Options] ? A : never }>> -} = internal.all + : { + -readonly [K in keyof T]: [T[K]] extends [Options.Variance] ? _A : never + } + > extends infer X ? X : never -/** - * @since 1.0.0 - * @category combinators - */ -export const atLeast: { - (times: 0): (self: Options) => Options> - (times: number): (self: Options) => Options> - (self: Options, times: 0): Options> - (self: Options, times: number): Options> -} = internal.atLeast + /** + * @since 1.0.0 + */ + export type ReturnObject = [T] extends [{ [K: string]: OptionsAny }] ? Options< + { + -readonly [K in keyof T]: [T[K]] extends [Options.Variance] ? _A : never + } + > + : never + + /** + * @since 1.0.0 + */ + export type Return< + Arg extends Iterable | Record + > = [Arg] extends [ReadonlyArray] ? ReturnTuple + : [Arg] extends [Iterable] ? ReturnIterable + : [Arg] extends [Record] ? ReturnObject + : never +} + +// ============================================================================= +// Refinements +// ============================================================================= /** * @since 1.0.0 - * @category combinators + * @category refinements */ -export const atMost: { - (times: number): (self: Options) => Options> - (self: Options, times: number): Options> -} = internal.atMost +export const isOptions: (u: unknown) => u is Options = InternalOptions.isOptions + +// ============================================================================= +// Constructors +// ============================================================================= /** * @since 1.0.0 - * @category combinators + * @category constructors */ -export const between: { - (min: 0, max: number): (self: Options) => Options> - (min: number, max: number): (self: Options) => Options> - (self: Options, min: 0, max: number): Options> - (self: Options, min: number, max: number): Options> -} = internal.between +export const all: < + const Arg extends Iterable> | Record> +>(arg: Arg) => All.Return = InternalOptions.all /** * @since 1.0.0 * @category constructors */ -export const boolean: (name: string, options?: Options.BooleanOptionConfig) => Options = internal.boolean +export const boolean: (name: string, options?: Options.BooleanOptionConfig) => Options = + InternalOptions.boolean /** * Constructs command-line `Options` that represent a choice between several @@ -146,7 +158,7 @@ export const boolean: (name: string, options?: Options.BooleanOptionConfig) => O export const choice: >( name: string, choices: C -) => Options = internal.choice +) => Options = InternalOptions.choice /** * Constructs command-line `Options` that represent a choice between several @@ -178,110 +190,101 @@ export const choice: >( * @since 1.0.0 * @category constructors */ -export const choiceWithValue: >( +export const choiceWithValue: >( name: string, choices: C -) => Options = internal.choiceWithValue +) => Options = InternalOptions.choiceWithValue /** * @since 1.0.0 * @category constructors */ -export const date: (name: string) => Options = internal.date +export const date: (name: string) => Options = InternalOptions.date /** * @since 1.0.0 - * @category combinators + * @category constructors */ -export const filterMap: { - (f: (a: A) => Option, message: string): (self: Options) => Options - (self: Options, f: (a: A) => Option, message: string): Options -} = internal.filterMap +export const float: (name: string) => Options = InternalOptions.float /** * @since 1.0.0 * @category constructors */ -export const float: (name: string) => Options = internal.float +export const integer: (name: string) => Options = InternalOptions.integer /** * @since 1.0.0 - * @category getters + * @category constructors */ -export const helpDoc: (self: Options) => HelpDoc = internal.helpDoc +export const keyValueMap: (name: string) => Options> = InternalOptions.keyValueMap /** * @since 1.0.0 * @category constructors */ -export const integer: (name: string) => Options = internal.integer +export const none: Options = InternalOptions.none /** - * Returns `true` if the specified `Options` is a boolean flag, `false` - * otherwise. - * * @since 1.0.0 - * @category predicates + * @category constructors */ -export const isBool: (self: Options) => boolean = internal.isBool +export const text: (name: string) => Options = InternalOptions.text -/** - * @since 1.0.0 - * @category refinements - */ -export const isOptions: (u: unknown) => u is Options = internal.isOptions +// ============================================================================= +// Combinators +// ============================================================================= /** * @since 1.0.0 - * @category constructors + * @category combinators */ -export const keyValueMap: (name: string) => Options> = internal.keyValueMap +export const filterMap: { + (f: (a: A) => Option, message: string): (self: Options) => Options + (self: Options, f: (a: A) => Option, message: string): Options +} = InternalOptions.filterMap /** + * Returns `true` if the specified `Options` is a boolean flag, `false` + * otherwise. + * * @since 1.0.0 - * @category constructors + * @category combinators */ -export const keyValueMapFromOption: (argumentOption: Options) => Options> = - internal.keyValueMapFromOption +export const isBool: (self: Options) => boolean = InternalOptions.isBool /** * @since 1.0.0 - * @category mapping + * @category combinators */ export const map: { (f: (a: A) => B): (self: Options) => Options (self: Options, f: (a: A) => B): Options -} = internal.map +} = InternalOptions.map /** * @since 1.0.0 - * @category mapping + * @category combinators */ export const mapOrFail: { (f: (a: A) => Either): (self: Options) => Options (self: Options, f: (a: A) => Either): Options -} = internal.mapOrFail +} = InternalOptions.mapOrFail /** * @since 1.0.0 - * @category mapping + * @category combinators */ export const mapTryCatch: { (f: (a: A) => B, onError: (e: unknown) => HelpDoc): (self: Options) => Options (self: Options, f: (a: A) => B, onError: (e: unknown) => HelpDoc): Options -} = internal.mapTryCatch - -/** - * @since 1.0.0 - * @category constructors - */ -export const none: Options = internal.none +} = InternalOptions.mapTryCatch /** * @since 1.0.0 * @category combinators */ -export const optional: (self: Options) => Options> = internal.optional +export const optional: (self: Options) => Options> = InternalOptions.optional /** * @since 1.0.0 @@ -290,7 +293,7 @@ export const optional: (self: Options) => Options> = internal.op export const orElse: { (that: Options): (self: Options) => Options (self: Options, that: Options): Options -} = internal.orElse +} = InternalOptions.orElse /** * @since 1.0.0 @@ -299,95 +302,58 @@ export const orElse: { export const orElseEither: { (that: Options): (self: Options) => Options> (self: Options, that: Options): Options> -} = internal.orElseEither +} = InternalOptions.orElseEither /** * @since 1.0.0 * @category combinators */ -export const repeat: (self: Options) => Options> = internal.repeat - -/** - * @since 1.0.0 - * @category combinators - */ -export const repeat1: (self: Options) => Options> = internal.repeat1 - -/** - * @since 1.0.0 - * @category constructors - */ -export const text: (name: string) => Options = internal.text - -/** - * @since 1.0.0 - * @category getters - */ -export const uid: (self: Options) => Option = internal.uid - -/** - * @since 1.0.0 - * @category getters - */ -export const usage: (self: Options) => Usage = internal.usage - -/** - * @since 1.0.0 - * @category validation - */ export const validate: { ( args: ReadonlyArray, config: CliConfig - ): (self: Options) => Effect, A]> + ): ( + self: Options + ) => Effect, ReadonlyArray, A]> ( self: Options, args: ReadonlyArray, config: CliConfig - ): Effect, A]> -} = internal.validate + ): Effect, ReadonlyArray, A]> +} = InternalOptions.validate /** * @since 1.0.0 * @category combinators */ -export const withDefault: { - (value: B): (self: Options) => Options - (self: Options, value: B): Options -} = internal.withDefault +export const withAlias: { + (alias: string): (self: Options) => Options + (self: Options, alias: string): Options +} = InternalOptions.withAlias /** * @since 1.0.0 * @category combinators */ -export const withDescription: { - (description: string): (self: Options) => Options - (self: Options, description: string): Options -} = internal.withDescription - -/** - * @since 1.0.0 - * @category zipping - */ -export const zip: { - (that: Options): (self: Options) => Options - (self: Options, that: Options): Options -} = internal.zip +export const withDefault: { + (fallback: A): (self: Options) => Options + (self: Options, fallback: A): Options +} = InternalOptions.withDefault /** * @since 1.0.0 - * @category zipping + * @category combinators */ -export const zipFlatten: { - (that: Options): >(self: Options) => Options<[...A, B]> - , B>(self: Options, that: Options): Options<[...A, B]> -} = internal.zipFlatten +export const withDescription: { + (description: string): (self: Options) => Options + (self: Options, description: string): Options +} = InternalOptions.withDescription /** * @since 1.0.0 - * @category zipping + * @category combinators */ -export const zipWith: { - (that: Options, f: (a: A, b: B) => C): (self: Options) => Options - (self: Options, that: Options, f: (a: A, b: B) => C): Options -} = internal.zipWith +export const withPseudoName: { + (pseudoName: string): (self: Options) => Options + (self: Options, pseudoName: string): Options +} = InternalOptions.withPseudoName diff --git a/src/Parameter.ts b/src/Parameter.ts new file mode 100644 index 0000000..edda418 --- /dev/null +++ b/src/Parameter.ts @@ -0,0 +1,34 @@ +import type { Effect } from "effect/Effect" +import type { HashSet } from "effect/HashSet" +import type { CliConfig } from "./CliConfig" +import type { HelpDoc } from "./HelpDoc" +import type { ValidationError } from "./ValidationError" + +/** + * Abstraction employed by Wizard class. Parameter trait encompass `Command`, + * `Options` and `Args` interfaces. + * + * The `Wizard` processes subtypes of `Parameter` in different manners. + */ +export interface Parameter { + get help(): HelpDoc + get shortDescription(): string +} + +/** + * Input is used to obtain a parameter from user. + */ +export interface Input extends Parameter { + isValid(input: string, config: CliConfig): Effect> + parse( + args: ReadonlyArray, + config: CliConfig + ): Effect, ReadonlyArray]> +} + +/** + * Represent a parameter with name to be used as the options in Alternatives. + */ +export interface Named extends Parameter { + get names(): HashSet +} diff --git a/src/Primitive.ts b/src/Primitive.ts index 02cfcfb..5adf613 100644 --- a/src/Primitive.ts +++ b/src/Primitive.ts @@ -5,14 +5,15 @@ import type { Effect } from "effect/Effect" import type { Option } from "effect/Option" import type { Pipeable } from "effect/Pipeable" import type { NonEmptyReadonlyArray } from "effect/ReadonlyArray" +import type { CliConfig } from "./CliConfig" import type { Span } from "./HelpDoc/Span" -import * as internal from "./internal/primitive" +import * as InternalPrimitive from "./internal/primitive" /** * @since 1.0.0 * @category symbol */ -export const PrimitiveTypeId: unique symbol = internal.PrimitiveTypeId as PrimitiveTypeId +export const PrimitiveTypeId: unique symbol = InternalPrimitive.PrimitiveTypeId as PrimitiveTypeId /** * @since 1.0.0 @@ -28,7 +29,12 @@ export type PrimitiveTypeId = typeof PrimitiveTypeId * @since 1.0.0 * @category models */ -export type Primitive = Bool | Date | Choice | Float | Integer | Text +export interface Primitive extends Primitive.Variance { + get typeName(): string + get help(): Span + get choices(): Option + validate(value: Option, config: CliConfig): Effect +} /** * @since 1.0.0 @@ -56,142 +62,44 @@ export declare namespace Primitive { : never } -/** - * Represents a boolean value. - * - * True values can be passed as one of: `["true", "1", "y", "yes" or "on"]`. - * False value can be passed as one of: `["false", "o", "n", "no" or "off"]`. - * - * @since 1.0.0 - * @category models - */ -export interface Bool extends Primitive.Variance { - readonly _tag: "Bool" - /** - * The default value to use if the parameter is not provided. - */ - readonly defaultValue: Option -} - -/** - * Represents a date in ISO-8601 format, such as `2007-12-03T10:15:30`. - * - * @since 1.0.0 - * @category models - */ -export interface Date extends Primitive.Variance { - readonly _tag: "Date" -} - -/** - * Represents a value selected from set of allowed values. - * - * @since 1.0.0 - * @category models - */ -export interface Choice extends Primitive.Variance { - readonly _tag: "Choice" - /** - * The list of allowed parameter-value pairs. - */ - readonly choices: NonEmptyReadonlyArray -} - -/** - * Represents a floating point number. - * - * @since 1.0.0 - * @category models - */ -export interface Float extends Primitive.Variance { - readonly _tag: "Float" -} - -/** - * Represents an integer. - * - * @since 1.0.0 - * @category models - */ -export interface Integer extends Primitive.Variance { - readonly _tag: "Integer" -} - -/** - * Represents a string value. - * - * @since 1.0.0 - * @category models - */ -export interface Text extends Primitive.Variance { - readonly _tag: "Text" -} - /** * @since 1.0.0 * @category Predicates */ -export const isBool: (self: Primitive) => boolean = internal.isBool +export const isBool: (self: Primitive) => boolean = InternalPrimitive.isBool /** * @since 1.0.0 * @category constructors */ -export const boolean: (defaultValue: Option) => Primitive = internal.boolean +export const boolean: (defaultValue: Option) => Primitive = InternalPrimitive.boolean /** * @since 1.0.0 * @category constructors */ -export const choice: (choices: NonEmptyReadonlyArray) => Primitive = internal.choice - -/** - * @since 1.0.0 - * @category getters - */ -export const choices: (self: Primitive) => Option = internal.choices +export const choice: (alternatives: NonEmptyReadonlyArray<[string, A]>) => Primitive = InternalPrimitive.choice /** * @since 1.0.0 * @category constructors */ -export const date: Primitive = internal.date +export const date: Primitive = InternalPrimitive.date /** * @since 1.0.0 * @category constructors */ -export const float: Primitive = internal.float - -/** - * @since 1.0.0 - * @category getters - */ -export const helpDoc: (self: Primitive) => Span = internal.helpDoc +export const float: Primitive = InternalPrimitive.float /** * @since 1.0.0 * @category constructors */ -export const integer: Primitive = internal.integer +export const integer: Primitive = InternalPrimitive.integer /** * @since 1.0.0 * @category constructors */ -export const text: Primitive = internal.text - -/** - * @since 1.0.0 - * @category getters - */ -export const typeName: (self: Primitive) => string = internal.typeName - -/** - * @since 1.0.0 - * @category validation - */ -export const validate: { - (value: Option): (self: Primitive) => Effect - (self: Primitive, value: Option): Effect -} = internal.validate +export const text: Primitive = InternalPrimitive.text diff --git a/src/ShellType.ts b/src/ShellType.ts index 954f00c..6c29591 100644 --- a/src/ShellType.ts +++ b/src/ShellType.ts @@ -1,7 +1,7 @@ /** * @since 1.0.0 */ -import * as internal from "./internal/shellType" +import * as InternalShellType from "./internal/shellType" import type { Options } from "./Options" /** @@ -30,16 +30,16 @@ export interface ZShell { * @since 1.0.0 * @category constructors */ -export const bash: ShellType = internal.bash +export const bash: ShellType = InternalShellType.bash /** * @since 1.0.0 * @category constructors */ -export const zShell: ShellType = internal.zShell +export const zShell: ShellType = InternalShellType.zShell /** * @since 1.0.0 * @category options */ -export const shellOption: Options = internal.shellOption +export const shellOption: Options = InternalShellType.shellOption diff --git a/src/Usage.ts b/src/Usage.ts index cdf7c50..5267ca9 100644 --- a/src/Usage.ts +++ b/src/Usage.ts @@ -1,10 +1,11 @@ /** * @since 1.0.0 */ -import type { Chunk } from "effect/Chunk" import type { Option } from "effect/Option" +import type { CliConfig } from "./CliConfig" import type { HelpDoc } from "./HelpDoc" -import * as internal from "./internal/usage" +import type { Span } from "./HelpDoc/Span" +import * as InternalUsage from "./internal/usage" /** * @since 1.0.0 @@ -34,7 +35,7 @@ export interface Mixed { */ export interface Named { readonly _tag: "Named" - readonly names: Chunk + readonly names: ReadonlyArray readonly acceptedValues: Option } @@ -83,7 +84,7 @@ export interface Concat { export const alternation: { (that: Usage): (self: Usage) => Usage (self: Usage, that: Usage): Usage -} = internal.alternation +} = InternalUsage.alternation /** * @since 1.0.0 @@ -92,40 +93,49 @@ export const alternation: { export const concat: { (that: Usage): (self: Usage) => Usage (self: Usage, that: Usage): Usage -} = internal.concat +} = InternalUsage.concat /** * @since 1.0.0 * @category constructors */ -export const empty: Usage = internal.empty +export const empty: Usage = InternalUsage.empty + +/** + * @since 1.0.0 + * @category constructors + */ +export const enumerate: { + (config: CliConfig): (self: Usage) => ReadonlyArray + (self: Usage, config: CliConfig): ReadonlyArray +} = InternalUsage.enumerate /** * @since 1.0.0 * @category combinators */ -export const helpDoc: (self: Usage) => HelpDoc = internal.helpDoc +export const getHelp: (self: Usage) => HelpDoc = InternalUsage.getHelp /** * @since 1.0.0 * @category constructors */ -export const mixed: Usage = internal.mixed +export const mixed: Usage = InternalUsage.mixed /** * @since 1.0.0 * @category constructors */ -export const named: (names: Chunk, acceptedValues: Option) => Usage = internal.named +export const named: (names: ReadonlyArray, acceptedValues: Option) => Usage = InternalUsage.named /** * @since 1.0.0 * @category combinators */ -export const optional: (self: Usage) => Usage = internal.optional +export const optional: (self: Usage) => Usage = InternalUsage.optional /** * @since 1.0.0 * @category combinators */ -export const repeated: (self: Usage) => Usage = internal.repeated +export const repeated: (self: Usage) => Usage = InternalUsage.repeated diff --git a/src/ValidationError.ts b/src/ValidationError.ts index 98cf5dc..2a0a7d9 100644 --- a/src/ValidationError.ts +++ b/src/ValidationError.ts @@ -20,9 +20,99 @@ export type ValidationErrorTypeId = typeof ValidationErrorTypeId * @since 1.0.0 * @category models */ -export interface ValidationError extends ValidationError.Proto { - readonly type: ValidationError.Type - readonly error: HelpDoc +export type ValidationError = + | CommandMismatch + | CorrectedFlag + | InvalidArgument + | InvalidValue + | KeyValuesDetected + | MissingValue + | MissingFlag + | MissingSubcommand + | NoBuiltInMatch + | UnclusteredFlag + +/** + * @since 1.0.0 + * @category models + */ +export interface CommandMismatch extends ValidationError.Proto { + readonly _tag: "CommandMismatch" +} + +/** + * @since 1.0.0 + * @category models + */ +export interface CorrectedFlag extends ValidationError.Proto { + readonly _tag: "CorrectedFlag" +} + +/** + * @since 1.0.0 + * @category models + */ +export interface InvalidArgument extends ValidationError.Proto { + readonly _tag: "InvalidArgument" +} + +/** + * @since 1.0.0 + * @category models + */ +export interface InvalidValue extends ValidationError.Proto { + readonly _tag: "InvalidValue" +} + +/** + * @since 1.0.0 + * @category models + */ +export interface KeyValuesDetected extends ValidationError.Proto { + readonly _tag: "KeyValuesDetected" + readonly keyValues: ReadonlyArray +} + +/** + * @since 1.0.0 + * @category models + */ +export interface MissingFlag extends ValidationError.Proto { + readonly _tag: "MissingFlag" +} + +/** + * @since 1.0.0 + * @category models + */ +export interface MissingValue extends ValidationError.Proto { + readonly _tag: "MissingValue" +} + +/** + * @since 1.0.0 + * @category models + */ +export interface MissingSubcommand extends ValidationError.Proto { + readonly _tag: "MissingSubcommand" +} + +/** + * @since 1.0.0 + * @category models + */ +export interface NoBuiltInMatch extends ValidationError.Proto { + readonly _tag: "NoBuiltInMatch" +} + +/** + * @since 1.0.0 + * @category models + */ +export interface UnclusteredFlag extends ValidationError.Proto { + readonly _tag: "UnclusteredFlag" + readonly unclustered: ReadonlyArray + readonly rest: ReadonlyArray } /** @@ -35,74 +125,87 @@ export declare namespace ValidationError { */ export interface Proto { readonly [ValidationErrorTypeId]: ValidationErrorTypeId + readonly error: HelpDoc } - - /** - * @since 1.0.0 - * @category models - */ - export type Type = - | "ExtraneousValue" - | "InvalidValue" - | "MissingValue" - | "CommandMismatch" - | "MissingSubCommand" - | "InvalidArgument" } /** * @since 1.0.0 * @category refinements */ -export const isValidationError: (u: unknown) => u is ValidationError = internal.isValidationError +export const isCommandMismatch: (self: ValidationError) => self is CommandMismatch = internal.isCommandMismatch + +/** + * @since 1.0.0 + * @category refinements + */ +export const isCorrectedFlag: (self: ValidationError) => self is CorrectedFlag = internal.isCorrectedFlag /** * @since 1.0.0 * @category refinements */ -export const isExtraneousValue: (validationError: ValidationError) => boolean = internal.isExtraneousValue +export const isInvalidArgument: (self: ValidationError) => self is InvalidArgument = internal.isInvalidArgument /** * @since 1.0.0 - * @category predicates + * @category refinements */ -export const isInvalidValue: (validationError: ValidationError) => boolean = internal.isInvalidValue +export const isInvalidValue: (self: ValidationError) => self is InvalidValue = internal.isInvalidValue /** * @since 1.0.0 - * @category predicates + * @category refinements */ -export const isMissingValue: (validationError: ValidationError) => boolean = internal.isMissingValue +export const isKeyValuesDetected: (self: ValidationError) => self is KeyValuesDetected = internal.isKeyValuesDetected /** * @since 1.0.0 - * @category predicates + * @category refinements */ -export const isCommandMismatch: (validationError: ValidationError) => boolean = internal.isCommandMismatch +export const isMissingFlag: (self: ValidationError) => self is MissingFlag = internal.isMissingFlag /** * @since 1.0.0 - * @category predicates + * @category refinements */ -export const isMissingSubCommand: (validationError: ValidationError) => boolean = internal.isMissingSubCommand +export const isMissingValue: (self: ValidationError) => self is MissingValue = internal.isMissingValue /** * @since 1.0.0 - * @category predicates + * @category refinements */ -export const isInvalidArgument: (validationError: ValidationError) => boolean = internal.isInvalidArgument +export const isMissingSubcommand: (self: ValidationError) => self is MissingSubcommand = internal.isMissingSubcommand + +/** + * @since 1.0.0 + * @category refinements + */ +export const isNoBuiltInMatch: (self: ValidationError) => self is NoBuiltInMatch = internal.isNoBuiltInMatch + +/** + * @since 1.0.0 + * @category refinements + */ +export const isUnclusteredFlag: (self: ValidationError) => self is UnclusteredFlag = internal.isUnclusteredFlag /** * @since 1.0.0 * @category constructors */ -export const make: (type: ValidationError.Type, error: HelpDoc) => ValidationError = internal.make +export const commandMismatch: (error: HelpDoc) => ValidationError = internal.commandMismatch /** * @since 1.0.0 * @category constructors */ -export const extraneousValue: (error: HelpDoc) => ValidationError = internal.extraneousValue +export const correctedFlag: (error: HelpDoc) => ValidationError = internal.correctedFlag + +/** + * @since 1.0.0 + * @category constructors + */ +export const invalidArgument: (error: HelpDoc) => ValidationError = internal.invalidArgument /** * @since 1.0.0 @@ -110,6 +213,19 @@ export const extraneousValue: (error: HelpDoc) => ValidationError = internal.ext */ export const invalidValue: (error: HelpDoc) => ValidationError = internal.invalidValue +/** + * @since 1.0.0 + * @category constructors + */ +export const keyValuesDetected: (error: HelpDoc, keyValues: ReadonlyArray) => ValidationError = + internal.keyValuesDetected + +/** + * @since 1.0.0 + * @category constructors + */ +export const missingFlag: (error: HelpDoc) => ValidationError = internal.missingFlag + /** * @since 1.0.0 * @category constructors @@ -120,16 +236,20 @@ export const missingValue: (error: HelpDoc) => ValidationError = internal.missin * @since 1.0.0 * @category constructors */ -export const commandMismatch: (error: HelpDoc) => ValidationError = internal.commandMismatch +export const missingSubcommand: (error: HelpDoc) => ValidationError = internal.missingSubcommand /** * @since 1.0.0 * @category constructors */ -export const missingSubCommand: (error: HelpDoc) => ValidationError = internal.missingSubCommand +export const noBuiltInMatch: (error: HelpDoc) => ValidationError = internal.noBuiltInMatch /** * @since 1.0.0 * @category constructors */ -export const invalidArgument: (error: HelpDoc) => ValidationError = internal.invalidArgument +export const unclusteredFlag: ( + error: HelpDoc, + unclustered: ReadonlyArray, + rest: ReadonlyArray +) => ValidationError = internal.unclusteredFlag diff --git a/src/index.ts b/src/index.ts index 5c2a635..812070e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,7 @@ export * as AutoCorrect from "@effect/cli/AutoCorrect" /** * @since 1.0.0 */ -export * as BuiltInOption from "@effect/cli/BuiltInOption" +export * as BuiltInOptions from "@effect/cli/BuiltInOptions" /** * @since 1.0.0 @@ -48,6 +48,14 @@ export * as HelpDoc from "@effect/cli/HelpDoc" */ export * as Options from "@effect/cli/Options" +/** + * Abstraction employed by Wizard class. Parameter trait encompass `Command`, + * `Options` and `Args` interfaces. + * + * The `Wizard` processes subtypes of `Parameter` in different manners. + */ +export * as Parameter from "@effect/cli/Parameter" + /** * @since 1.0.0 */ diff --git a/src/internal/args.ts b/src/internal/args.ts index a79b4b8..b297ffe 100644 --- a/src/internal/args.ts +++ b/src/internal/args.ts @@ -1,21 +1,21 @@ -import * as Chunk from "effect/Chunk" import * as Effect from "effect/Effect" import * as Either from "effect/Either" import { dual } from "effect/Function" import * as Option from "effect/Option" import { pipeArguments } from "effect/Pipeable" import * as ReadonlyArray from "effect/ReadonlyArray" -import type { NonEmptyReadonlyArray } from "effect/ReadonlyArray" import type * as Args from "../Args" +import type * as CliConfig from "../CliConfig" import type * as HelpDoc from "../HelpDoc" +import type * as Parameter from "../Parameter" import type * as Primitive from "../Primitive" import type * as Usage from "../Usage" import type * as ValidationError from "../ValidationError" -import * as doc from "./helpDoc" -import * as span from "./helpDoc/span" -import * as primitive from "./primitive" -import * as _usage from "./usage" -import * as validationError from "./validationError" +import * as InternalHelpDoc from "./helpDoc" +import * as InternalSpan from "./helpDoc/span" +import * as InternalPrimitive from "./primitive" +import * as InternalUsage from "./usage" +import * as InternalValidationError from "./validationError" const ArgsSymbolKey = "@effect/cli/Args" @@ -24,119 +24,412 @@ export const ArgsTypeId: Args.ArgsTypeId = Symbol.for( ArgsSymbolKey ) as Args.ArgsTypeId +const proto = { + _A: (_: never) => _ +} + /** @internal */ -export type Op = Args.Args & Body & { - readonly _tag: Tag +export class Empty implements Args.Args { + readonly [ArgsTypeId] = proto + readonly _tag = "Empty" + + get minSize(): number { + return 0 + } + + get maxSize(): number { + return 0 + } + + get identifier(): Option.Option { + return Option.none() + } + + get help(): HelpDoc.HelpDoc { + return InternalHelpDoc.empty + } + + get usage(): Usage.Usage { + return InternalUsage.empty + } + + get shortDescription(): string { + return "" + } + + validate( + args: ReadonlyArray, + _config: CliConfig.CliConfig + ): Effect.Effect, void]> { + return Effect.succeed([args, undefined]) + } + + addDescription(_description: string): Args.Args { + return new Empty() + } + + pipe() { + return pipeArguments(this, arguments) + } } -const proto = { - [ArgsTypeId]: { - _A: (_: never) => _ - }, +/** @internal */ +export class Single implements Args.Args, Parameter.Input { + readonly [ArgsTypeId] = proto + readonly _tag = "Single" + + constructor( + readonly pseudoName: Option.Option, + readonly primitiveType: Primitive.Primitive, + readonly description: HelpDoc.HelpDoc = InternalHelpDoc.empty + ) {} + + get minSize(): number { + return 1 + } + + get maxSize(): number { + return 1 + } + + get identifier(): Option.Option { + return Option.some(this.name) + } + + get help(): HelpDoc.HelpDoc { + return InternalHelpDoc.descriptionList([[ + InternalSpan.weak(this.name), + InternalHelpDoc.sequence( + InternalHelpDoc.p(this.primitiveType.help), + this.description + ) + ]]) + } + + get usage(): Usage.Usage { + return InternalUsage.named(ReadonlyArray.of(this.name), this.primitiveType.choices) + } + + get shortDescription(): string { + return `Argument $name: ${InternalSpan.getText(InternalHelpDoc.getSpan(this.description))}` + } + + isValid( + input: string, + config: CliConfig.CliConfig + ): Effect.Effect> { + const args = ReadonlyArray.of(input) + return this.validate(args, config).pipe(Effect.as(args)) + } + + validate( + args: ReadonlyArray, + config: CliConfig.CliConfig + ): Effect.Effect, A]> { + return Effect.suspend(() => { + if (ReadonlyArray.isNonEmptyReadonlyArray(args)) { + const head = ReadonlyArray.headNonEmpty(args) + const tail = ReadonlyArray.tailNonEmpty(args) + return this.primitiveType.validate(Option.some(head), config).pipe( + Effect.mapBoth({ + onFailure: (text) => InternalHelpDoc.p(text), + onSuccess: (a) => [tail, a] as const + }) + ) + } + const choices = this.primitiveType.choices + if (Option.isSome(this.pseudoName) && Option.isSome(choices)) { + return Effect.fail(InternalHelpDoc.p( + `Missing argument <${this.pseudoName.value}> with choices ${choices.value}` + )) + } + if (Option.isSome(this.pseudoName)) { + return Effect.fail(InternalHelpDoc.p( + `Missing argument <${this.pseudoName.value}>` + )) + } + if (Option.isSome(choices)) { + return Effect.fail(InternalHelpDoc.p( + `Missing argument ${this.primitiveType.typeName} with choices ${choices.value}` + )) + } + return Effect.fail(InternalHelpDoc.p( + `Missing argument ${this.primitiveType.typeName}` + )) + }).pipe(Effect.mapError((help) => InternalValidationError.invalidArgument(help))) + } + + parse( + _args: ReadonlyArray, + _config: CliConfig.CliConfig + ): Effect.Effect, ReadonlyArray]> { + return Effect.succeed([ReadonlyArray.empty(), ReadonlyArray.empty()]) + } + + addDescription(description: string): Args.Args { + const desc = InternalHelpDoc.sequence(this.description, InternalHelpDoc.p(description)) + return new Single(this.pseudoName, this.primitiveType, desc) + } + + pipe() { + return pipeArguments(this, arguments) + } + + private get name(): string { + const name = Option.getOrElse(this.pseudoName, () => this.primitiveType.typeName) + return `<${name}>` + } +} + +export class Both implements Args.Args { + readonly [ArgsTypeId] = proto + readonly _tag = "Both" + + constructor( + readonly left: Args.Args, + readonly right: Args.Args + ) {} + + get minSize(): number { + return this.left.minSize + this.right.minSize + } + + get maxSize(): number { + return this.left.maxSize + this.right.maxSize + } + + get identifier(): Option.Option { + const ids = ReadonlyArray.compact([this.left.identifier, this.right.identifier]) + return ReadonlyArray.match(ids, { + onEmpty: () => Option.none(), + onNonEmpty: (ids) => Option.some(ReadonlyArray.join(ids, ", ")) + }) + } + + get help(): HelpDoc.HelpDoc { + return InternalHelpDoc.sequence(this.left.help, this.right.help) + } + + get usage(): Usage.Usage { + return InternalUsage.concat(this.left.usage, this.right.usage) + } + + get shortDescription(): string { + return "" + } + + validate( + args: ReadonlyArray, + config: CliConfig.CliConfig + ): Effect.Effect, readonly [A, B]]> { + return this.left.validate(args, config).pipe( + Effect.flatMap(([args, a]) => + this.right.validate(args, config).pipe( + Effect.map(([args, b]) => [args, [a, b]] as const) + ) + ) + ) + } + + addDescription(description: string): Args.Args { + return new Both( + this.left.addDescription(description), + this.right.addDescription(description) + ) + } + pipe() { return pipeArguments(this, arguments) } } /** @internal */ -export type Instruction = - | Empty - | Map - | Single - | Variadic - | Zip +export class Variadic implements Args.Args>, Parameter.Input { + readonly [ArgsTypeId] = proto + readonly _tag = "Variadic" + + constructor( + readonly value: Args.Args, + readonly min: Option.Option, + readonly max: Option.Option + ) {} + + get minSize(): number { + return Math.floor(Option.getOrElse(this.min, () => 0) * this.value.minSize) + } + + get maxSize(): number { + return Math.floor(Option.getOrElse(this.max, () => Number.MAX_SAFE_INTEGER / 2) * this.value.maxSize) + } + + get identifier(): Option.Option { + return this.value.identifier + } + + get help(): HelpDoc.HelpDoc { + return InternalHelpDoc.mapDescriptionList(this.value.help, (oldSpan, oldBlock) => { + const min = this.minSize + const max = this.maxSize + const newSpan = InternalSpan.text(Option.isSome(this.max) ? ` ${min} - ${max}` : min === 0 ? "..." : ` ${min}+`) + const newBlock = InternalHelpDoc.p( + Option.isSome(this.max) + ? `This argument must be repeated at least ${min} times and may be repeated up to ${max} times.` + : min === 0 + ? "This argument may be repeated zero or more times." + : `This argument must be repeated at least ${min} times.` + ) + return [InternalSpan.concat(oldSpan, newSpan), InternalHelpDoc.sequence(oldBlock, newBlock)] + }) + } + + get usage(): Usage.Usage { + return InternalUsage.repeated(this.value.usage) + } + + get shortDescription(): string { + return InternalHelpDoc.toAnsiText(this.help) + } + + validate( + args: ReadonlyArray, + config: CliConfig.CliConfig + ): Effect.Effect, ReadonlyArray]> { + const min1 = Option.getOrElse(this.min, () => 0) + const max1 = Option.getOrElse(this.max, () => Number.MAX_SAFE_INTEGER) + const loop = ( + args: ReadonlyArray, + acc: ReadonlyArray + ): Effect.Effect, ReadonlyArray]> => { + if (acc.length >= max1) { + return Effect.succeed([args, acc]) + } + return this.value.validate(args, config).pipe(Effect.matchEffect({ + onFailure: (failure) => + acc.length >= min1 && ReadonlyArray.isEmptyReadonlyArray(args) + ? Effect.succeed([args, acc]) + : Effect.fail(failure), + onSuccess: ([args, a]) => loop(args, ReadonlyArray.prepend(acc, a)) + })) + } + return loop(args, ReadonlyArray.empty()).pipe(Effect.map(([args, acc]) => [args, ReadonlyArray.reverse(acc)])) + } + + parse( + _args: ReadonlyArray, + _config: CliConfig.CliConfig + ): Effect.Effect, ReadonlyArray]> { + return Effect.succeed([ReadonlyArray.empty(), ReadonlyArray.empty()]) + } + + isValid( + input: string, + config: CliConfig.CliConfig + ): Effect.Effect> { + const args = input.split(" ") + return this.validate(args, config).pipe(Effect.as(args)) + } + + addDescription(description: string): Args.Args> { + return new Variadic(this.value.addDescription(description), this.min, this.max) + } + + pipe() { + return pipeArguments(this, arguments) + } +} /** @internal */ -export interface Empty extends Op<"Empty"> {} +export class Map implements Args.Args { + readonly [ArgsTypeId] = proto + readonly _tag = "Map" + + constructor( + readonly args: Args.Args, + readonly f: (value: A) => Either.Either + ) {} + + get minSize(): number { + return this.args.minSize + } + + get maxSize(): number { + return this.args.maxSize + } + + get identifier(): Option.Option { + return this.args.identifier + } + + get help(): HelpDoc.HelpDoc { + return this.args.help + } + + get usage(): Usage.Usage { + return this.args.usage + } + + get shortDescription(): string { + return this.args.shortDescription + } + + validate( + args: ReadonlyArray, + config: CliConfig.CliConfig + ): Effect.Effect, B]> { + return this.args.validate(args, config).pipe( + Effect.flatMap(([leftover, a]) => + Either.match(this.f(a), { + onLeft: (doc) => Effect.fail(InternalValidationError.invalidArgument(doc)), + onRight: (b) => Effect.succeed([leftover, b] as const) + }) + ) + ) + } + + addDescription(description: string): Args.Args { + return new Map(this.args.addDescription(description), this.f) + } + + pipe() { + return pipeArguments(this, arguments) + } +} + +// ============================================================================= +// Refinements +// ============================================================================= /** @internal */ -export interface Single extends - Op<"Single", { - readonly pseudoName: Option.Option - readonly primitiveType: Primitive.Primitive - readonly description: HelpDoc.HelpDoc - }> -{} +export const isArgs = (u: unknown): u is Args.Args => typeof u === "object" && u != null && ArgsTypeId in u /** @internal */ -export interface Map extends - Op<"Map", { - readonly value: Instruction - readonly f: (a: unknown) => Either.Either - }> -{} +export const isEmpty = (u: unknown): u is Empty => isArgs(u) && "_tag" in u && u._tag === "Empty" /** @internal */ -export interface Variadic extends - Op<"Variadic", { - readonly value: Instruction - readonly min: Option.Option - readonly max: Option.Option - }> -{} +export const isSingle = (u: unknown): u is Single => isArgs(u) && "_tag" in u && u._tag === "Single" /** @internal */ -export interface Zip extends - Op<"Zip", { - readonly left: Instruction - readonly right: Instruction - }> -{} +export const isBoth = (u: unknown): u is Both => isArgs(u) && "_tag" in u && u._tag === "Both" /** @internal */ -export const isArgs = (u: unknown): u is Args.Args => typeof u === "object" && u != null && ArgsTypeId in u +export const isVariadic = (u: unknown): u is Variadic => isArgs(u) && "_tag" in u && u._tag === "Variadic" -const addDescriptionMap: { - [K in Instruction["_tag"]]: (self: Extract, description: string) => Args.Args -} = { - Empty: (self) => self, - Single: (self, description) => - single(self.pseudoName, self.primitiveType, doc.sequence(self.description, doc.p(description))), - Map: (self, description) => mapOrFail(addDescriptionMap[self.value._tag](self.value as any, description), self.f), - Variadic: (self, description) => - variadic(addDescriptionMap[self.value._tag](self.value as any, description), self.min, self.max), - Zip: (self, description) => - zip( - addDescriptionMap[self.left._tag](self.left as any, description), - addDescriptionMap[self.right._tag](self.right as any, description) - ) -} +/** @internal */ +export const isMap = (u: unknown): u is Map => isArgs(u) && "_tag" in u && u._tag === "Map" + +// ============================================================================= +// Constructors +// ============================================================================= /** @internal */ -export const addDescription = dual< - (description: string) => (self: Args.Args) => Args.Args, - (self: Args.Args, description: string) => Args.Args ->(2, (self, description) => addDescriptionMap[(self as Instruction)._tag](self as any, description)) - -/* @internal */ -export const all: { - >>( - self: Args.Args, - ...args: T - ): Args.Args< - readonly [ - A, - ...(T["length"] extends 0 ? [] - : Readonly<{ [K in keyof T]: [T[K]] extends [Args.Args] ? A : never }>) - ] - > - >>( - args: [...T] - ): Args.Args< - T[number] extends never ? [] - : Readonly<{ [K in keyof T]: [T[K]] extends [Args.Args] ? A : never }> - > - }>>( - args: T - ): Args.Args< - Readonly<{ [K in keyof T]: [T[K]] extends [Args.Args] ? A : never }> - > -} = function() { +export const all: < + const Arg extends Iterable> | Record> +>(arg: Arg) => Args.All.Return = function() { if (arguments.length === 1) { if (isArgs(arguments[0])) { - return map(arguments[0], (x) => [x]) + return map(arguments[0], (x) => [x]) as any } else if (Array.isArray(arguments[0])) { - return tuple(arguments[0]) + return allTupled(arguments[0]) as any } else { const entries = Object.entries(arguments[0] as Readonly<{ [K: string]: Args.Args }>) let result = map(entries[0][1], (value) => ({ [entries[0][0]]: value })) @@ -145,102 +438,79 @@ export const all: { } const rest = entries.slice(1) for (const [key, options] of rest) { - result = zipWith(result, options, (record, value) => ({ ...record, [key]: value })) + result = map(new Both(result, options), ([record, value]) => ({ + ...record, + [key]: value + })) } return result as any } } - return tuple(arguments[0]) + return allTupled(arguments[0]) as any } /** @internal */ -export const atLeast = dual< - { - (times: 0): (self: Args.Args) => Args.Args> - (times: number): (self: Args.Args) => Args.Args> - }, - { - (self: Args.Args, times: 0): Args.Args> - (self: Args.Args, times: number): Args.Args> - } ->(2, (self, times) => variadic(self, Option.some(times), Option.none()) as any) +export const boolean = (config: Args.Args.ArgsConfig = {}): Args.Args => + new Single(Option.fromNullable(config.name), InternalPrimitive.boolean(Option.none())) /** @internal */ -export const atMost = dual< - (times: number) => (self: Args.Args) => Args.Args>, - (self: Args.Args, times: number) => Args.Args> ->(2, (self, times) => variadic(self, Option.none(), Option.some(times))) +export const choice = ( + choices: ReadonlyArray.NonEmptyReadonlyArray<[string, A]>, + config: Args.Args.ArgsConfig = {} +): Args.Args => new Single(Option.fromNullable(config.name), InternalPrimitive.choice(choices)) /** @internal */ -export const between = dual< - { - (min: 0, max: number): (self: Args.Args) => Args.Args> - (min: number, max: number): (self: Args.Args) => Args.Args> - }, - { - (self: Args.Args, min: 0, max: number): Args.Args> - (self: Args.Args, min: number, max: number): Args.Args> - } ->(3, (self, min, max) => variadic(self, Option.some(min), Option.some(max)) as any) +export const date = (config: Args.Args.ArgsConfig = {}): Args.Args => + new Single(Option.fromNullable(config.name), InternalPrimitive.date) /** @internal */ -export const boolean = (config: Args.Args.ArgsConfig = {}): Args.Args => - single(Option.fromNullable(config.name), primitive.boolean(Option.none())) +export const float = (config: Args.Args.ArgsConfig = {}): Args.Args => + new Single(Option.fromNullable(config.name), InternalPrimitive.float) /** @internal */ -export const choice = ( - choices: NonEmptyReadonlyArray, - config: Args.Args.ArgsConfig = {} -): Args.Args => single(Option.fromNullable(config.name), primitive.choice(choices)) +export const integer = (config: Args.Args.ArgsConfig = {}): Args.Args => + new Single(Option.fromNullable(config.name), InternalPrimitive.integer) /** @internal */ -export const date = (config: Args.Args.ArgsConfig = {}): Args.Args => - single(Option.fromNullable(config.name), primitive.date) +export const none: Args.Args = new Empty() /** @internal */ -export const float = (config: Args.Args.ArgsConfig = {}): Args.Args => - single(Option.fromNullable(config.name), primitive.float) - -const helpDocMap: { - [K in Instruction["_tag"]]: (self: Extract) => HelpDoc.HelpDoc -} = { - Empty: () => doc.empty, - Single: (self) => - doc.descriptionList([[ - span.weak(singleName(self)), - doc.sequence(doc.p(primitive.helpDoc(self.primitiveType)), self.description) - ]]), - Map: (self) => helpDocMap[self.value._tag](self.value as any), - Variadic: (self) => - doc.mapDescriptionList( - helpDocMap[self.value._tag](self.value as any), - (oldSpan, oldBlock) => { - const min = minSize(self) - const max = maxSize(self) - const newSpan = span.text(Option.isSome(self.max) ? ` ${min} - ${max}` : min === 0 ? "..." : ` ${min}+`) - const newBlock = doc.p( - Option.isSome(self.max) - ? `This argument must be repeated at least ${min} times and may be repeated up to ${max} times.` - : min === 0 - ? "This argument may be repeated zero or more times." - : `This argument must be repeated at least ${min} times.` - ) - return [span.concat(oldSpan, newSpan), doc.sequence(oldBlock, newBlock)] - } - ), - Zip: (self) => - doc.sequence( - helpDocMap[self.left._tag](self.left as any), - helpDocMap[self.right._tag](self.right as any) - ) -} +export const text = (config: Args.Args.ArgsConfig = {}): Args.Args => + new Single(Option.fromNullable(config.name), InternalPrimitive.text) + +// ============================================================================= +// Combinators +// ============================================================================= + +/** @internal */ +export const atLeast = dual< + { + (times: 0): (self: Args.Args) => Args.Args> + (times: number): (self: Args.Args) => Args.Args> + }, + { + (self: Args.Args, times: 0): Args.Args> + (self: Args.Args, times: number): Args.Args> + } +>(2, (self, times) => new Variadic(self, Option.some(times), Option.none()) as any) /** @internal */ -export const helpDoc = (self: Args.Args): HelpDoc.HelpDoc => helpDocMap[(self as Instruction)._tag](self as any) +export const atMost = dual< + (times: number) => (self: Args.Args) => Args.Args>, + (self: Args.Args, times: number) => Args.Args> +>(2, (self, times) => new Variadic(self, Option.none(), Option.some(times))) /** @internal */ -export const integer = (config: Args.Args.ArgsConfig = {}): Args.Args => - single(Option.fromNullable(config.name), primitive.integer) +export const between = dual< + { + (min: 0, max: number): (self: Args.Args) => Args.Args> + (min: number, max: number): (self: Args.Args) => Args.Args> + }, + { + (self: Args.Args, min: 0, max: number): Args.Args> + (self: Args.Args, min: number, max: number): Args.Args> + } +>(3, (self, min, max) => new Variadic(self, Option.some(min), Option.some(max)) as any) /** @internal */ export const map = dual< @@ -252,13 +522,7 @@ export const map = dual< export const mapOrFail = dual< (f: (a: A) => Either.Either) => (self: Args.Args) => Args.Args, (self: Args.Args, f: (a: A) => Either.Either) => Args.Args ->(2, (self, f) => { - const op = Object.create(proto) - op._tag = "Map" - op.value = self - op.f = f - return op -}) +>(2, (self, f) => new Map(self, f)) /** @internal */ export const mapTryCatch = dual< @@ -273,260 +537,42 @@ export const mapTryCatch = dual< } })) -const minSizeMap: { - [K in Instruction["_tag"]]: (self: Extract) => number -} = { - Empty: () => 0, - Single: () => 1, - Map: (self) => minSizeMap[self.value._tag](self.value as any), - Variadic: (self) => Option.getOrElse(self.min, () => 0) * minSizeMap[self.value._tag](self.value as any), - Zip: (self) => minSizeMap[self.left._tag](self.left as any) + minSizeMap[self.right._tag](self.right as any) -} - -/** @internal */ -export const minSize = (self: Args.Args): number => minSizeMap[(self as Instruction)._tag](self as any) - -const maxSizeMap: { - [K in Instruction["_tag"]]: (self: Extract) => number -} = { - Empty: () => 0, - Single: () => 1, - Map: (self) => maxSizeMap[self.value._tag](self.value as any), - Variadic: (self) => - Option.getOrElse(self.max, () => Math.floor(Number.MAX_SAFE_INTEGER / 2)) * - maxSizeMap[self.value._tag](self.value as any), - Zip: (self) => maxSizeMap[self.left._tag](self.left as any) + maxSizeMap[self.right._tag](self.right as any) -} - -/** @internal */ -export const maxSize = (self: Args.Args): number => maxSizeMap[(self as Instruction)._tag](self as any) - -/** @internal */ -export const none: Args.Args = (() => { - const op = Object.create(proto) - op._tag = "Empty" - return op -})() - /** @internal */ -export const repeat = (self: Args.Args): Args.Args> => variadic(self, Option.none(), Option.none()) +export const repeated = (self: Args.Args): Args.Args> => + new Variadic(self, Option.none(), Option.none()) /** @internal */ -export const repeat1 = (self: Args.Args): Args.Args> => - map(variadic(self, Option.some(1), Option.none()), (chunk) => { - if (Chunk.isNonEmpty(chunk)) { - return chunk +export const repeatedAtLeastOnce = (self: Args.Args): Args.Args> => + map(new Variadic(self, Option.some(1), Option.none()), (values) => { + if (ReadonlyArray.isNonEmptyReadonlyArray(values)) { + return values } - const message = Option.match(uid(self), { + const message = Option.match(self.identifier, { onNone: () => "An anonymous variadic argument", onSome: (identifier) => `The variadic option '${identifier}' ` }) throw new Error(`${message} is not respecting the required minimum of 1`) }) -/** @internal */ -export const text = (config: Args.Args.ArgsConfig = {}): Args.Args => - single(Option.fromNullable(config.name), primitive.text) - -const uidMap: { - [K in Instruction["_tag"]]: (self: Extract) => Option.Option -} = { - Empty: () => Option.none(), - Single: (self) => Option.some(singleName(self)), - Map: (self) => uidMap[self.value._tag](self.value as any), - Variadic: (self) => uidMap[self.value._tag](self.value as any), - Zip: (self) => combineUids(self.left, self.right) -} - -/** @internal */ -export const uid = (self: Args.Args): Option.Option => uidMap[(self as Instruction)._tag](self as any) - -const usageMap: { - [K in Instruction["_tag"]]: (self: Extract) => Usage.Usage -} = { - Empty: () => _usage.empty, - Single: (self) => _usage.named(Chunk.of(singleName(self)), primitive.choices(self.primitiveType)), - Map: (self) => usageMap[self.value._tag](self.value as any), - Variadic: (self) => _usage.repeated(usageMap[self.value._tag](self.value as any)), - Zip: (self) => - _usage.concat( - usageMap[self.left._tag](self.left as any), - usageMap[self.right._tag](self.right as any) - ) -} - -/** @internal */ -export const usage = (self: Args.Args): Usage.Usage => usageMap[(self as Instruction)._tag](self as any) - -const validateMap: { - [K in Instruction["_tag"]]: ( - self: Extract, - args: ReadonlyArray - ) => Effect.Effect, any]> -} = { - Empty: (_, args) => Effect.succeed([args, void 0]), - Single: (self, args) => { - if (ReadonlyArray.isNonEmptyReadonlyArray(args)) { - return Effect.mapBoth(primitive.validate(self.primitiveType, Option.some(args[0])), { - onFailure: (text) => validationError.invalidArgument(doc.p(text)), - onSuccess: (a) => [args.slice(1), a] - }) - } - const choices = primitive.choices(self.primitiveType) - let message = "" - if (Option.isSome(self.pseudoName) && Option.isSome(choices)) { - message = `Missing argument <${self.pseudoName.value}> with values: ${choices.value}` - } - if (Option.isSome(self.pseudoName) && Option.isNone(choices)) { - message = `Missing argument <${self.pseudoName.value}>` - } - if (Option.isNone(self.pseudoName) && Option.isSome(choices)) { - message = `Missing a ${primitive.typeName(self.primitiveType)} argument with values: ${choices.value}` - } - message = `Missing a ${primitive.typeName(self.primitiveType)} argument` - return Effect.fail(validationError.invalidArgument(doc.p(message))) - }, - Map: (self, args) => - Effect.flatMap( - validateMap[self.value._tag](self.value as any, args), - ([remainder, a]) => - Either.match(self.f(a), { - onLeft: (doc) => Effect.fail(validationError.invalidArgument(doc)), - onRight: (value) => Effect.succeed([remainder, value]) - }) - ), - Variadic: (self, args) => { - const min = Option.getOrElse(self.min, () => 0) - const max = Option.getOrElse(self.max, () => Infinity) - const loop = ( - args: ReadonlyArray, - acc: Chunk.Chunk - ): Effect.Effect, Chunk.Chunk]> => - acc.length >= max - ? Effect.succeed([args, acc]) - : Effect.matchEffect(validateMap[self.value._tag](self.value as any, args), { - onFailure: (error) => - acc.length >= min && ReadonlyArray.isEmptyReadonlyArray(args) ? - Effect.succeed([args, acc]) : - Effect.fail(error), - onSuccess: (tuple) => loop(tuple[0], Chunk.append(acc, tuple[1])) - }) - return loop(args, Chunk.empty()) - }, - Zip: (self, args) => - Effect.flatMap( - validateMap[self.left._tag](self.left as any, args), - ([args, a]) => - Effect.map( - validateMap[self.right._tag](self.right as any, args), - ([args, b]) => [args, [a, b]] - ) - ) -} - -/** @internal */ -export const validate = dual< - ( - args: ReadonlyArray - ) => ( - self: Args.Args - ) => Effect.Effect, A]>, - ( - self: Args.Args, - args: ReadonlyArray - ) => Effect.Effect, A]> ->(2, (self, args) => validateMap[(self as Instruction)._tag](self as any, args)) - -/** @internal */ -export const zip = dual< - (that: Args.Args) => (self: Args.Args) => Args.Args, - (self: Args.Args, that: Args.Args) => Args.Args ->(2, (self, that) => { - const op = Object.create(proto) - op._tag = "Zip" - op.left = self - op.right = that - return op -}) - -/** @internal */ -export const zipFlatten = dual< - ( - that: Args.Args - ) => >( - self: Args.Args - ) => Args.Args<[...A, B]>, - , B>( - self: Args.Args, - that: Args.Args - ) => Args.Args<[...A, B]> ->(2, (self, that) => map(zip(self, that), ([a, b]) => [...a, b])) - -/** @internal */ -export const zipWith = dual< - (that: Args.Args, f: (a: A, b: B) => C) => (self: Args.Args) => Args.Args, - (self: Args.Args, that: Args.Args, f: (a: A, b: B) => C) => Args.Args ->(3, (self, that, f) => map(zip(self, that), ([a, b]) => f(a, b))) - -const single = ( - pseudoName: Option.Option, - primitiveType: Primitive.Primitive, - description: HelpDoc.HelpDoc = doc.empty -): Args.Args => { - const op = Object.create(proto) - op._tag = "Single" - op.pseudoName = pseudoName - op.primitiveType = primitiveType - op.description = description - return op -} - -const singleName = (self: Single): string => - `<${Option.getOrElse(self.pseudoName, () => primitive.typeName(self.primitiveType))}>` - -const variadic = ( - value: Args.Args, - min: Option.Option, - max: Option.Option -): Args.Args> => { - const op = Object.create(proto) - op._tag = "Variadic" - op.value = value - op.min = min - op.max = max - return op -} - -const combineUids = (left: Instruction, right: Instruction): Option.Option => { - const l = uidMap[left._tag](left as any) - const r = uidMap[right._tag](right as any) - if (Option.isNone(l) && Option.isNone(r)) { - return Option.none() - } - if (Option.isNone(l) && Option.isSome(r)) { - return Option.some(r.value) - } - if (Option.isSome(l) && Option.isNone(r)) { - return Option.some(l.value) - } - return Option.some(`${(l as Option.Some).value}, ${(r as Option.Some).value}`) -} +// ============================================================================= +// Internals +// ============================================================================= -const tuple = >>(tuple: T): Args.Args< +const allTupled = >>(arg: T): Args.Args< { [K in keyof T]: [T[K]] extends [Args.Args] ? A : never } > => { - if (tuple.length === 0) { + if (arg.length === 0) { return none as any } - if (tuple.length === 1) { - return map(tuple[0], (x) => [x]) as any + if (arg.length === 1) { + return map(arg[0], (x) => [x]) as any } - let result = map(tuple[0], (x) => [x]) - for (let i = 1; i < tuple.length; i++) { - const args = tuple[i] - result = zipFlatten(result, args) + let result = map(arg[0], (x) => [x]) + for (let i = 1; i < arg.length; i++) { + const curr = arg[i] + result = map(new Both(result, curr), ([a, b]) => [...a, b]) } return result as any } diff --git a/src/internal/builtInOption.ts b/src/internal/builtInOptions.ts similarity index 69% rename from src/internal/builtInOption.ts rename to src/internal/builtInOptions.ts index 787a183..601a26f 100644 --- a/src/internal/builtInOption.ts +++ b/src/internal/builtInOptions.ts @@ -1,5 +1,5 @@ import * as Option from "effect/Option" -import type * as BuiltInOption from "../BuiltInOption" +import type * as BuiltInOptions from "../BuiltInOptions" import type * as Command from "../Command" import type * as HelpDoc from "../HelpDoc" import type * as Options from "../Options" @@ -9,7 +9,7 @@ import * as options from "./options" import * as _shellType from "./shellType" /** @internal */ -export const showCompletions = (index: number, shellType: ShellType.ShellType): BuiltInOption.BuiltInOption => ({ +export const showCompletions = (index: number, shellType: ShellType.ShellType): BuiltInOptions.BuiltInOptions => ({ _tag: "ShowCompletions", index, shellType @@ -19,47 +19,49 @@ export const showCompletions = (index: number, shellType: ShellType.ShellType): export const showCompletionScript = ( pathToExecutable: string, shellType: ShellType.ShellType -): BuiltInOption.BuiltInOption => ({ +): BuiltInOptions.BuiltInOptions => ({ _tag: "ShowCompletionScript", pathToExecutable, shellType }) /** @internal */ -export const showHelp = (usage: Usage.Usage, helpDoc: HelpDoc.HelpDoc): BuiltInOption.BuiltInOption => ({ +export const showHelp = (usage: Usage.Usage, helpDoc: HelpDoc.HelpDoc): BuiltInOptions.BuiltInOptions => ({ _tag: "ShowHelp", usage, helpDoc }) /** @internal */ -export const wizard = (commmand: Command.Command): BuiltInOption.BuiltInOption => ({ - _tag: "Wizard", +export const showWizard = (commmand: Command.Command): BuiltInOptions.BuiltInOptions => ({ + _tag: "ShowWizard", commmand }) /** @internal */ -export const isShowCompletionScript = (self: BuiltInOption.BuiltInOption): self is BuiltInOption.ShowCompletionScript => - self._tag === "ShowCompletionScript" +export const isShowCompletionScript = ( + self: BuiltInOptions.BuiltInOptions +): self is BuiltInOptions.ShowCompletionScript => self._tag === "ShowCompletionScript" /** @internal */ -export const isShowCompletions = (self: BuiltInOption.BuiltInOption): self is BuiltInOption.ShowCompletions => +export const isShowCompletions = (self: BuiltInOptions.BuiltInOptions): self is BuiltInOptions.ShowCompletions => self._tag === "ShowCompletions" /** @internal */ -export const isShowHelp = (self: BuiltInOption.BuiltInOption): self is BuiltInOption.ShowHelp => +export const isShowHelp = (self: BuiltInOptions.BuiltInOptions): self is BuiltInOptions.ShowHelp => self._tag === "ShowHelp" /** @internal */ -export const isWizard = (self: BuiltInOption.BuiltInOption): self is BuiltInOption.Wizard => self._tag === "Wizard" +export const isShowWizard = (self: BuiltInOptions.BuiltInOptions): self is BuiltInOptions.ShowWizard => + self._tag === "ShowWizard" /** @internal */ export const builtInOptions = ( command: Command.Command, usage: Usage.Usage, helpDoc: HelpDoc.HelpDoc -): Options.Options> => { - const help = options.alias(options.boolean("help"), "h") +): Options.Options> => { + const help = options.boolean("help").pipe(options.withAlias("h")) // TODO: after path/file primitives added // const completionScriptPath = options.optional(options.file("shell-completion-script")) const shellCompletionScriptPath = options.optional(options.text("shell-completion-script")) @@ -78,7 +80,7 @@ export const builtInOptions = ( return Option.some(showHelp(usage, helpDoc)) } if (builtIn.wizard) { - return Option.some(wizard(command)) + return Option.some(showWizard(command)) } if (Option.isSome(builtIn.shellCompletionScriptPath) && Option.isSome(builtIn.shellType)) { return Option.some(showCompletionScript(builtIn.shellCompletionScriptPath.value, builtIn.shellType.value)) diff --git a/src/internal/cliApp.ts b/src/internal/cliApp.ts index 8edfe83..eb99df0 100644 --- a/src/internal/cliApp.ts +++ b/src/internal/cliApp.ts @@ -3,24 +3,29 @@ import * as Context from "effect/Context" import * as Effect from "effect/Effect" import { dual, pipe } from "effect/Function" import * as Option from "effect/Option" -import type * as BuiltInOption from "../BuiltInOption" +import * as ReadonlyArray from "effect/ReadonlyArray" +import type * as BuiltInOptions from "../BuiltInOptions" import type * as CliApp from "../CliApp" +import type * as CliConfig from "../CliConfig" import type * as Command from "../Command" import type * as HelpDoc from "../HelpDoc" import type * as Span from "../HelpDoc/Span" import type * as ValidationError from "../ValidationError" -import * as cliConfig from "./cliConfig" -import * as command from "./command" -import * as commandDirective from "./commandDirective" -import * as doc from "./helpDoc" -import * as span from "./helpDoc/span" -import * as terminal from "./terminal" -import * as _usage from "./usage" -import * as validationError from "./validationError" +import * as InternalCliConfig from "./cliConfig" +import * as InternalCommand from "./command" +import * as InternalHelpDoc from "./helpDoc" +import * as InternalSpan from "./helpDoc/span" +import * as InternalTerminal from "./terminal" +import * as InternalUsage from "./usage" +import * as InternalValidationError from "./validationError" + +// ============================================================================= +// Constructors +// ============================================================================= const defaultConfig = { - summary: span.empty, - footer: doc.empty + summary: InternalSpan.empty, + footer: InternalHelpDoc.empty } /** @internal */ @@ -32,6 +37,10 @@ export const make = (config: { footer?: HelpDoc.HelpDoc }): CliApp.CliApp => Object.assign({}, defaultConfig, config) +// ============================================================================= +// Combinators +// ============================================================================= + /** @internal */ export const run = dual< ( @@ -43,104 +52,104 @@ export const run = dual< args: ReadonlyArray, f: (a: A) => Effect.Effect ) => Effect.Effect ->(3, (self, args, f) => +>(3, ( + self: CliApp.CliApp, + args: ReadonlyArray, + f: (a: A) => Effect.Effect +): Effect.Effect => Effect.contextWithEffect((context: Context.Context) => { - const config = Option.getOrElse(Context.getOption(context, cliConfig.Tag), () => cliConfig.defaultConfig) - return Effect.matchEffect( - command.parse(self.command, [...prefixCommand(self.command), ...args], config), - { - onFailure: (error) => Effect.zipRight(printDocs(error), Effect.fail(error)), - onSuccess: (directive) => - commandDirective.isUserDefined(directive) - ? f(directive.value) - : Effect.catchSome( - runBuiltIn(directive.option, self), - (error) => - validationError.isValidationError(error) ? - Option.some(Effect.zipRight(printDocs(error), Effect.fail(error))) : - Option.none() + const config = Option.getOrElse( + Context.getOption(context, InternalCliConfig.Tag), + () => InternalCliConfig.defaultConfig + ) + const prefixedArgs = ReadonlyArray.appendAll(prefixCommand(self.command), args) + return self.command.parse(prefixedArgs, config).pipe(Effect.matchEffect({ + onFailure: (e) => Effect.zipRight(printDocs(e.error), Effect.fail(e)), + onSuccess: (directive): Effect.Effect => { + switch (directive._tag) { + case "UserDefined": { + return f(directive.value) + } + case "BuiltIn": { + return handleBuiltInOption(self, directive.option, config).pipe( + Effect.catchSome((e) => + InternalValidationError.isValidationError(e) + ? Option.some(Effect.zipRight(printDocs(e.error), Effect.fail(e))) + : Option.none() + ) ) + } + } } - ) - }).pipe(Effect.provide(terminal.layer))) - -const prefixCommandMap: { - [K in command.Instruction["_tag"]]: (self: Extract) => ReadonlyArray -} = { - Single: (self) => [self.name], - Map: (self) => prefixCommandMap[self.command._tag](self.command as any), - OrElse: () => [], - Subcommands: (self) => prefixCommandMap[self.parent._tag](self.parent as any) -} + })) + }).pipe(Effect.provide(InternalTerminal.layer))) -const prefixCommand = (self: Command.Command): ReadonlyArray => - prefixCommandMap[(self as command.Instruction)._tag](self as any) +// ============================================================================= +// Internals +// ============================================================================= -const runBuiltInMap: { - [K in BuiltInOption.BuiltInOption["_tag"]]: ( - self: Extract, - cliApp: CliApp.CliApp - ) => Effect.Effect -} = { - ShowCompletions: () => - // case ShowCompletions(index, _) => - // envs.flatMap { envMap => - // val compWords = envMap.collect { - // case (idx, word) if idx.startsWith("COMP_WORD_") => - // (idx.drop("COMP_WORD_".length).toInt, word) - // }.toList.sortBy(_._1).map(_._2) +const printDocs = (error: HelpDoc.HelpDoc): Effect.Effect => + Console.log(InternalHelpDoc.toAnsiText(error)) - // Completion - // .complete(compWords, index, self.command, self.config) - // .flatMap { completions => - // ZIO.foreachDiscard(completions)(word => printLine(word)) - // } - // } - Console.log("Showing Completions"), - ShowCompletionScript: () => - // case ShowCompletionScript(path, shellType) => - // printLine( - // CompletionScript(path, if (self.command.names.nonEmpty) self.command.names else Set(self.name), shellType) - // ) - Console.log("Showing Completion Script"), - ShowHelp: (self, cliApp) => { - const banner = doc.h1(span.code(cliApp.name)) - const header = doc.p(span.concat(span.text(`${cliApp.name} v${cliApp.version} -- `), cliApp.summary)) - const usage = doc.sequence( - doc.h1("USAGE"), - doc.p(span.concat(span.text("$ "), doc.getSpan(_usage.helpDoc(self.usage)))) - ) - // TODO: add rendering of built-in options such as help - const helpDoc = pipe( - banner, - doc.sequence(header), - doc.sequence(usage), - doc.sequence(self.helpDoc), - doc.sequence(cliApp.footer) - ) - const helpText = doc.toAnsiText(helpDoc) - return Console.log(helpText) - }, - Wizard: () => - // val subcommands = command.getSubcommands - // for { - // subcommandName <- if (subcommands.size == 1) ZIO.succeed(subcommands.keys.head) - // else - // (print("Command" + subcommands.keys.mkString("(", "|", "): ")) *> readLine).orDie - // subcommand <- - // ZIO - // .fromOption(subcommands.get(subcommandName)) - // .orElseFail(ValidationError(ValidationErrorType.InvalidValue, HelpDoc.p("Invalid subcommand"))) - // args <- subcommand.generateArgs - // _ <- Console.printLine(s"Executing command: ${(prefix(self.command) ++ args).mkString(" ")}") - // result <- self.run(args) - // } yield result - Console.log("Running Wizard") +const handleBuiltInOption = ( + self: CliApp.CliApp, + builtIn: BuiltInOptions.BuiltInOptions, + config: CliConfig.CliConfig +): Effect.Effect => { + switch (builtIn._tag) { + case "ShowHelp": { + const banner = InternalHelpDoc.h1(InternalSpan.code(self.name)) + const header = InternalHelpDoc.p(InternalSpan.concat( + InternalSpan.text(`${self.name} ${self.version} -- `), + self.summary + )) + const usage = InternalHelpDoc.sequence( + InternalHelpDoc.h1("USAGE"), + pipe( + InternalUsage.enumerate(builtIn.usage, config), + ReadonlyArray.map((span) => InternalHelpDoc.p(InternalSpan.concat(InternalSpan.text("$ "), span))), + ReadonlyArray.reduceRight(InternalHelpDoc.empty, (left, right) => InternalHelpDoc.sequence(left, right)) + ) + ) + const helpDoc = pipe( + banner, + InternalHelpDoc.sequence(header), + InternalHelpDoc.sequence(usage), + InternalHelpDoc.sequence(builtIn.helpDoc), + InternalHelpDoc.sequence(self.footer) + ) + return Console.log(InternalHelpDoc.toAnsiText(helpDoc)) + } + case "ShowCompletionScript": { + return Console.log("Showing completion script") + } + case "ShowCompletions": { + return Console.log("Showing completions") + } + case "ShowWizard": { + return Console.log("Showing the wizard") + } + } } -const runBuiltIn = ( - self: BuiltInOption.BuiltInOption, - cliApp: CliApp.CliApp -): Effect.Effect => runBuiltInMap[self._tag](self as any, cliApp) - -const printDocs = (error: ValidationError.ValidationError) => Console.log(doc.toAnsiText(error.error)) +const prefixCommand = (self: Command.Command): ReadonlyArray => { + let command: Command.Command | undefined = self + let prefix: ReadonlyArray = ReadonlyArray.empty() + while (command !== undefined) { + if (InternalCommand.isStandard(command) || InternalCommand.isGetUserInput(command)) { + prefix = ReadonlyArray.of(command.name) + command = undefined + } + if (InternalCommand.isMap(command)) { + command = command.command + } + if (InternalCommand.isOrElse(command)) { + prefix = ReadonlyArray.empty() + command = undefined + } + if (InternalCommand.isSubcommands(command)) { + command = command.parent + } + } + return prefix +} diff --git a/src/internal/cliConfig.ts b/src/internal/cliConfig.ts index 9274adf..bc0ebb1 100644 --- a/src/internal/cliConfig.ts +++ b/src/internal/cliConfig.ts @@ -4,9 +4,9 @@ import * as Layer from "effect/Layer" import type * as CliConfig from "../CliConfig" /** @internal */ -export const make = (isCaseSensitive: boolean, autoCorrectLimit: number): CliConfig.CliConfig => ({ - isCaseSensitive, - autoCorrectLimit +export const make = (params: Partial = {}): CliConfig.CliConfig => ({ + ...defaultConfig, + ...params }) /** @internal */ @@ -14,16 +14,19 @@ export const Tag = Context.Tag() /** @internal */ export const defaultConfig: CliConfig.CliConfig = { - isCaseSensitive: true, - autoCorrectLimit: 2 + isCaseSensitive: false, + autoCorrectLimit: 2, + finalCheckBuiltIn: true, + showAllNames: true, + showTypes: true } /** @internal */ export const defaultLayer: Layer.Layer = Layer.succeed(Tag, defaultConfig) /** @internal */ -export const layer = (config: CliConfig.CliConfig): Layer.Layer => - Layer.succeed(Tag, config) +export const layer = (config: Partial = {}): Layer.Layer => + Layer.succeed(Tag, make(config)) /** @internal */ export const normalizeCase = dual< diff --git a/src/internal/command.ts b/src/internal/command.ts index 342f4bd..5919a82 100644 --- a/src/internal/command.ts +++ b/src/internal/command.ts @@ -1,4 +1,3 @@ -import * as Chunk from "effect/Chunk" import * as Effect from "effect/Effect" import * as Either from "effect/Either" import { dual, pipe } from "effect/Function" @@ -7,27 +6,27 @@ import * as HashSet from "effect/HashSet" import * as Option from "effect/Option" import { pipeArguments } from "effect/Pipeable" import * as ReadonlyArray from "effect/ReadonlyArray" -import type { NonEmptyReadonlyArray } from "effect/ReadonlyArray" import type * as Args from "../Args" import type * as CliConfig from "../CliConfig" import type * as Command from "../Command" import type * as CommandDirective from "../CommandDirective" -import type * as HelpDoc from "../HelpDoc" +import * as HelpDoc from "../HelpDoc" +import type * as Span from "../HelpDoc/Span" import type * as Options from "../Options" import type * as Prompt from "../Prompt" import type * as Terminal from "../Terminal" import type * as Usage from "../Usage" import type * as ValidationError from "../ValidationError" -import * as _args from "./args" -import * as builtInOption from "./builtInOption" -import * as cliConfig from "./cliConfig" -import * as commandDirective from "./commandDirective" -import * as doc from "./helpDoc" -import * as span from "./helpDoc/span" -import * as options from "./options" -import * as _prompt from "./prompt" -import * as _usage from "./usage" -import * as validationError from "./validationError" +import * as InternalArgs from "./args" +import * as InternalBuiltInOptions from "./builtInOptions" +import * as InternalCliConfig from "./cliConfig" +import * as InternalCommandDirective from "./commandDirective" +import * as InternalHelpDoc from "./helpDoc" +import * as InternalSpan from "./helpDoc/span" +import * as InternalOptions from "./options" +import * as InternalPrompt from "./prompt" +import * as InternalUsage from "./usage" +import * as InternalValidationError from "./validationError" const CommandSymbolKey = "@effect/cli/Command" @@ -36,378 +35,561 @@ export const CommandTypeId: Command.CommandTypeId = Symbol.for( CommandSymbolKey ) as Command.CommandTypeId -/** @internal */ -export type Op = Command.Command & Body & { - readonly _tag: Tag +const proto = { + _A: (_: never) => _ } -const proto = { - [CommandTypeId]: { - _ArgsType: (_: never) => _, - _OptionsType: (_: never) => _ - }, +/** @internal */ +export class Standard + implements Command.Command> +{ + readonly [CommandTypeId] = proto + readonly _tag = "Standard" + + constructor( + readonly name: Name, + readonly description: HelpDoc.HelpDoc, + readonly options: Options.Options, + readonly args: Args.Args + ) {} + + get names(): HashSet.HashSet { + return HashSet.make(this.name) + } + + get subcommands(): HashMap.HashMap> { + return HashMap.make([this.name, this]) + } + + get help(): HelpDoc.HelpDoc { + const header = InternalHelpDoc.isEmpty(this.description) + ? InternalHelpDoc.empty + : InternalHelpDoc.sequence(InternalHelpDoc.h1("DESCRIPTION"), this.description) + const argsHelp = this.args.help + const argsSection = InternalHelpDoc.isEmpty(argsHelp) + ? InternalHelpDoc.empty + : InternalHelpDoc.sequence(InternalHelpDoc.h1("ARGUMENTS"), argsHelp) + const optionsHelp = this.options.help + const optionsSection = InternalHelpDoc.isEmpty(optionsHelp) + ? InternalHelpDoc.empty + : InternalHelpDoc.sequence(InternalHelpDoc.h1("OPTIONS"), optionsHelp) + return InternalHelpDoc.sequence(header, InternalHelpDoc.sequence(argsSection, optionsSection)) + } + + get usage(): Usage.Usage { + return InternalUsage.concat( + InternalUsage.named(ReadonlyArray.of(this.name), Option.none()), + InternalUsage.concat(this.options.usage, this.args.usage) + ) + } + + get shortDescription(): string { + return InternalSpan.getText(InternalHelpDoc.getSpan(this.help)) + } + + parse( + args: ReadonlyArray, + config: CliConfig.CliConfig + ): Effect.Effect< + never, + ValidationError.ValidationError, + CommandDirective.CommandDirective> + > { + const parseCommandLine = ( + args: ReadonlyArray + ): Effect.Effect> => { + if (ReadonlyArray.isNonEmptyReadonlyArray(args)) { + const head = ReadonlyArray.headNonEmpty(args) + const tail = ReadonlyArray.tailNonEmpty(args) + const normalizedArgv0 = InternalCliConfig.normalizeCase(config, head) + const normalizedCommandName = InternalCliConfig.normalizeCase(config, this.name) + return Effect.succeed(tail).pipe( + Effect.when(() => normalizedArgv0 === normalizedCommandName), + Effect.flatten, + Effect.catchTag("NoSuchElementException", () => { + const error = InternalHelpDoc.p(`Missing command name: ${this.name}`) + return Effect.fail(InternalValidationError.commandMismatch(error)) + }) + ) + } + const error = InternalHelpDoc.p(`Missing command name: ${this.name}`) + return Effect.fail(InternalValidationError.commandMismatch(error)) + } + const parseBuiltInArgs = ( + args: ReadonlyArray + ): Effect.Effect> => { + const normalizedArgv0 = InternalCliConfig.normalizeCase(config, args[0]) + const normalizedCommandName = InternalCliConfig.normalizeCase(config, this.name) + if (normalizedArgv0 === normalizedCommandName) { + const options = InternalBuiltInOptions.builtInOptions(this, this.usage, this.help) + return InternalOptions.validate(options, ReadonlyArray.drop(args, 1), config).pipe( + Effect.flatMap((tuple) => tuple[2]), + Effect.catchTag("NoSuchElementException", () => { + const error = InternalHelpDoc.p("No built-in option was matched") + return Effect.fail(InternalValidationError.noBuiltInMatch(error)) + }), + Effect.map(InternalCommandDirective.builtIn) + ) + } + const error = InternalHelpDoc.p(`Missing command name: ${this.name}`) + return Effect.fail(InternalValidationError.commandMismatch(error)) + } + const parseUserDefinedArgs = ( + args: ReadonlyArray + ): Effect.Effect< + never, + ValidationError.ValidationError, + CommandDirective.CommandDirective> + > => + parseCommandLine(args).pipe(Effect.flatMap((commandOptionsAndArgs) => { + const [optionsAndArgs, forcedCommandArgs] = splitForcedArgs(commandOptionsAndArgs) + return InternalOptions.validate(this.options, optionsAndArgs, config).pipe( + Effect.flatMap(([error, commandArgs, optionsType]) => + this.args.validate(ReadonlyArray.appendAll(commandArgs, forcedCommandArgs), config).pipe( + Effect.catchAll((e) => + Option.match(error, { + onNone: () => Effect.fail(e), + onSome: (err) => Effect.fail(err) + }) + ), + Effect.map(([argsLeftover, argsType]) => + InternalCommandDirective.userDefined(argsLeftover, { + name: this.name, + options: optionsType, + args: argsType + }) + ) + ) + ) + ) + })) + const exhaustiveSearch = ( + args: ReadonlyArray + ): Effect.Effect> => { + if (ReadonlyArray.contains(args, "--help") || ReadonlyArray.contains(args, "-h")) { + return parseBuiltInArgs(ReadonlyArray.make(this.name, "--help")) + } + if (ReadonlyArray.contains(args, "--wizard") || ReadonlyArray.contains(args, "-w")) { + return parseBuiltInArgs(ReadonlyArray.make(this.name, "--wizard")) + } + const error = InternalHelpDoc.p(`Missing command name: ${this.name}`) + return Effect.fail(InternalValidationError.commandMismatch(error)) + } + return parseBuiltInArgs(args).pipe( + Effect.orElse(() => parseUserDefinedArgs(args)), + Effect.catchSome((e) => { + if (InternalValidationError.isValidationError(e)) { + if (config.finalCheckBuiltIn) { + return Option.some( + exhaustiveSearch(args).pipe( + Effect.catchSome((_) => + InternalValidationError.isValidationError(_) + ? Option.some(Effect.fail(e)) + : Option.none() + ) + ) + ) + } + return Option.some(Effect.fail(e)) + } + return Option.none() + }) + ) + } + pipe() { return pipeArguments(this, arguments) } } -/** @internal */ -export type Instruction = - | Single - | Map - | OrElse - | Subcommands +export class GetUserInput + implements Command.Command> +{ + readonly [CommandTypeId] = proto + readonly _tag = "GetUserInput" -/** @internal */ -export interface Single extends - Op<"Single", { - readonly name: string - readonly help: HelpDoc.HelpDoc - readonly type: { - readonly _tag: "Standard" - readonly options: Options.Options - readonly args: Args.Args - } | { - readonly _tag: "Prompt" - readonly prompt: Prompt.Prompt - } - }> -{} + constructor( + readonly name: Name, + readonly prompt: Prompt.Prompt + ) {} -/** @internal */ -export interface Map extends - Op<"Map", { - readonly command: Instruction - readonly f: (a: unknown) => Either.Either - }> -{} + get names(): HashSet.HashSet { + return HashSet.make(this.name) + } -/** @internal */ -export interface OrElse extends - Op<"OrElse", { - readonly left: Instruction - readonly right: Instruction - }> -{} + get subcommands(): HashMap.HashMap> { + return HashMap.make([this.name, this]) + } -/** @internal */ -export interface Subcommands extends - Op<"Subcommands", { - readonly parent: Instruction - readonly child: Instruction - }> -{} - -const getSubcommandsMap: { - [K in Instruction["_tag"]]: (self: Extract) => HashMap.HashMap> -} = { - Single: (self) => HashMap.make([self.name, self]), - Map: (self) => getSubcommandsMap[self.command._tag](self.command as any), - OrElse: (self) => - HashMap.union( - getSubcommandsMap[self.left._tag](self.left as any), - getSubcommandsMap[self.right._tag](self.right as any) - ), - Subcommands: (self) => getSubcommandsMap[self.child._tag](self.child as any) -} + get help(): HelpDoc.HelpDoc { + const header = InternalHelpDoc.h1("DESCRIPTION") + const content = InternalHelpDoc.p("This command will prompt the user for information") + return InternalHelpDoc.sequence(header, content) + } -/** @internal */ -export const getSubcommands = (self: Command.Command): HashMap.HashMap> => - getSubcommandsMap[(self as Instruction)._tag](self as any) - -const helpDocMap: { - [K in Instruction["_tag"]]: (self: Extract) => HelpDoc.HelpDoc -} = { - Single: (self) => { - const header = doc.isEmpty(self.help) - ? doc.empty - : doc.sequence(doc.h1("DESCRIPTION"), self.help) - if (self.type._tag === "Standard") { - const argsHelp = _args.helpDoc(self.type.args) - const argsSection = doc.isEmpty(argsHelp) - ? doc.empty - : doc.sequence(doc.h1("ARGUMENTS"), argsHelp) - const optionsHelp = options.helpDoc(self.type.options) - const optionsSection = doc.isEmpty(optionsHelp) - ? doc.empty - : doc.sequence(doc.h1("OPTIONS"), optionsHelp) - return doc.sequence(header, doc.sequence(argsSection, optionsSection)) - } - return doc.sequence(header, doc.p("This command will prompt the user for information")) - }, - Map: (self) => helpDocMap[self.command._tag](self.command as any), - OrElse: (self) => - doc.sequence( - helpDocMap[self.left._tag](self.left as any), - helpDocMap[self.right._tag](self.right as any) - ), - Subcommands: (self) => - doc.sequence( - helpDocMap[self.parent._tag](self.parent as any), - doc.sequence( - doc.h1("COMMANDS"), - subcommandsDescription(self.child, getSubcommandMaxSynopsisLength(self.child)) - ) + get usage(): Usage.Usage { + return InternalUsage.named(ReadonlyArray.of(this.name), Option.none()) + } + + get shortDescription(): string { + return "" + } + + parse( + args: ReadonlyArray, + _config: CliConfig.CliConfig + ): Effect.Effect< + Terminal.Terminal, + ValidationError.ValidationError, + CommandDirective.CommandDirective> + > { + return Effect.map( + InternalPrompt.run(this.prompt), + (value) => + InternalCommandDirective.userDefined(ReadonlyArray.drop(args, 1), { + name: this.name, + value + }) ) + } + + pipe() { + return pipeArguments(this, arguments) + } } /** @internal */ -export const helpDoc = (self: Command.Command): HelpDoc.HelpDoc => - helpDocMap[(self as Instruction)._tag](self as any) +export class Map implements Command.Command { + readonly [CommandTypeId] = proto + readonly _tag = "Map" -const defaultConstructorConfig = { - options: options.none, - args: _args.none -} + constructor( + readonly command: Command.Command, + readonly f: (value: A) => B + ) {} -/** @internal */ -export const make = ( - name: Name, - config: Command.Command.ConstructorConfig = defaultConstructorConfig as any -): Command.Command> => { - const { args, options } = { ...defaultConstructorConfig, ...config } - return standardSingleCommand(name, doc.empty, options, args) as any -} + get names(): HashSet.HashSet { + return this.command.names + } -/** @internal */ -export const map = dual< - (f: (a: A) => B) => (self: Command.Command) => Command.Command, - (self: Command.Command, f: (a: A) => B) => Command.Command ->(2, (self, f) => mapOrFail(self, (a) => Either.right(f(a)))) + get subcommands(): HashMap.HashMap> { + return this.command.subcommands + } -/** @internal */ -export const mapOrFail = dual< - ( - f: (a: A) => Either.Either - ) => (self: Command.Command) => Command.Command, - ( - self: Command.Command, - f: (a: A) => Either.Either - ) => Command.Command ->(2, (self, f) => { - const op = Object.create(proto) - op._tag = "Map" - op.command = self - op.f = f - return op -}) + get help(): HelpDoc.HelpDoc { + return this.command.help + } + + get usage(): Usage.Usage { + return this.command.usage + } -const namesMap: { - [K in Instruction["_tag"]]: (self: Extract) => HashSet.HashSet -} = { - Single: (self) => HashSet.make(self.name), - Map: (self) => namesMap[self.command._tag](self.command as any), - OrElse: (self) => - HashSet.union( - namesMap[self.left._tag](self.left as any), - namesMap[self.right._tag](self.right as any) - ), - Subcommands: (self) => namesMap[self.parent._tag](self.parent as any) + get shortDescription(): string { + return this.command.shortDescription + } + + parse( + args: ReadonlyArray, + config: CliConfig.CliConfig + ): Effect.Effect> { + return this.command.parse(args, config).pipe(Effect.map(InternalCommandDirective.map((a) => this.f(a)))) + } + + pipe() { + return pipeArguments(this, arguments) + } } /** @internal */ -export const names = (self: Command.Command): HashSet.HashSet => - namesMap[(self as Instruction)._tag](self as any) +export class OrElse implements Command.Command { + readonly [CommandTypeId] = proto + readonly _tag = "OrElse" -/** @internal */ -export const orElse = dual< - (that: Command.Command) => (self: Command.Command) => Command.Command, - (self: Command.Command, that: Command.Command) => Command.Command ->(2, (self, that) => { - const op = Object.create(proto) - op._tag = "OrElse" - op.left = self - op.right = that - return op -}) + constructor( + readonly left: Command.Command, + readonly right: Command.Command + ) {} -/** @internal */ -export const orElseEither = dual< - (that: Command.Command) => (self: Command.Command) => Command.Command>, - (self: Command.Command, that: Command.Command) => Command.Command> ->(2, (self, that) => orElse(map(self, Either.left), map(that, Either.right))) + get names(): HashSet.HashSet { + return HashSet.union(this.left.names, this.right.names) + } + + get subcommands(): HashMap.HashMap> { + return HashMap.union(this.left.subcommands, this.right.subcommands) + } -const parseMap: { - [K in Instruction["_tag"]]: ( - self: Extract, + get help(): HelpDoc.HelpDoc { + return InternalHelpDoc.sequence(this.left.help, this.right.help) + } + + get usage(): Usage.Usage { + return InternalUsage.mixed + } + + get shortDescription(): string { + return "" + } + + parse( args: ReadonlyArray, config: CliConfig.CliConfig - ) => Effect.Effect> -} = { - Single: (self, args, config) => { - const parseBuiltInArgs = - args[0] !== undefined && cliConfig.normalizeCase(config, self.name) === cliConfig.normalizeCase(config, args[0]) - ? pipe( - builtInOption.builtInOptions(self, usage(self), helpDoc(self)), - options.validate(args, config), - Effect.mapBoth({ - onFailure: (error) => error.error, - onSuccess: (tuple) => tuple[1] - }), - Effect.flatten, - Effect.map(commandDirective.builtIn) - ) + ): Effect.Effect> { + return this.left.parse(args, config).pipe(Effect.catchSome((e) => + InternalValidationError.isValidationError(e) + ? Option.some(this.right.parse(args, config)) : Option.none() - const parseUserDefinedArgs = pipe( - ReadonlyArray.isNonEmptyReadonlyArray(args) - ? pipe( - Effect.succeed(args.slice(1)), - Effect.when(() => cliConfig.normalizeCase(config, args[0]) === cliConfig.normalizeCase(config, self.name)), - Effect.flatten, - Effect.orElseFail(() => - validationError.commandMismatch(doc.p(span.error(`Missing command name: '${self.name}'`))) - ) - ) - : Effect.fail(validationError.commandMismatch(doc.p(span.error(`Missing command name: '${self.name}'`)))), - Effect.flatMap((commandOptionsAndArgs) => { - if (self.type._tag === "Standard") { - const standardCommand = self.type - return options.validate( - standardCommand.options, - unCluster(commandOptionsAndArgs), - config - ).pipe( - Effect.flatMap(([commandArgs, options]) => - _args.validate(standardCommand.args, commandArgs).pipe( - Effect.map(([argsLeftover, args]) => - commandDirective.userDefined(argsLeftover, { - name: self.name, - options, - args - }) - ) - ) - ) - ) - } + )) + } - return Effect.map(_prompt.run(self.type.prompt), (value) => - commandDirective.userDefined(args.slice(1), { - name: self.name, - value - } as any)) - }) - ) - return Effect.orElse(parseBuiltInArgs, () => parseUserDefinedArgs) - }, - Map: (self, args, config) => - Effect.flatMap( - parseMap[self.command._tag](self.command as any, args, config), - (directive) => { - if (commandDirective.isUserDefined(directive)) { - const either = self.f(directive.value) - return Either.isLeft(either) - ? Effect.fail(either.left) - : Effect.succeed(commandDirective.userDefined(directive.leftover, either.right)) - } - return Effect.succeed(directive) + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export class Subcommands, B extends Command.Command> + implements Command.Command> +{ + readonly [CommandTypeId] = proto + readonly _tag = "Subcommands" + + constructor( + readonly parent: A, + readonly child: B + ) {} + + get names(): HashSet.HashSet { + return this.parent.names + } + + get subcommands(): HashMap.HashMap> { + return this.child.subcommands + } + + get help(): HelpDoc.HelpDoc { + const getUsage = ( + command: Command.Command, + preceding: ReadonlyArray + ): ReadonlyArray<[Span.Span, Span.Span]> => { + if (isStandard(command)) { + const usage = InternalHelpDoc.getSpan(InternalUsage.getHelp(command.usage)) + const usages = ReadonlyArray.append(preceding, usage) + const finalUsage = ReadonlyArray.reduceRight( + usages, + InternalSpan.empty, + (acc, next) => + InternalSpan.isText(acc) && acc.value === "" + ? next + : InternalSpan.isText(next) && next.value === "" + ? acc + : InternalSpan.concat(acc, InternalSpan.concat(InternalSpan.space, next)) + ) + return ReadonlyArray.of([finalUsage, InternalHelpDoc.getSpan(command.description)]) } - ), - OrElse: (self, args, config) => - Effect.catchSome( - parseMap[self.left._tag](self.left as any, args, config), - (error) => - validationError.isCommandMismatch(error) - ? Option.some(parseMap[self.right._tag](self.right as any, args, config)) - : Option.none() - ), - Subcommands: (self, args, config) => { - const helpDirectiveForChild = Effect.flatMap( - parseMap[self.child._tag]( - self.child as any, - ReadonlyArray.isEmptyReadonlyArray(args) ? [] : args.slice(1), - config - ), - (directive) => { - if (commandDirective.isBuiltIn(directive) && builtInOption.isShowHelp(directive.option)) { - const availableNames = Array.from(names(self)) - const parentName = availableNames.length === 0 ? "" : availableNames[0] - return Effect.succeed(commandDirective.builtIn(builtInOption.showHelp( - _usage.concat( - _usage.named(Chunk.of(parentName), Option.none()), - directive.option.usage - ), - directive.option.helpDoc - ))) + // TODO: if (isPrompt(command)) {} + if (isMap(command)) { + return getUsage(command.command, preceding) + } + if (isOrElse(command)) { + return ReadonlyArray.appendAll( + getUsage(command.left, preceding), + getUsage(command.right, preceding) + ) + } + if (isSubcommands(command)) { + const parentUsage = getUsage(command.parent, preceding) + if (ReadonlyArray.isNonEmptyReadonlyArray(parentUsage)) { + const [usage] = ReadonlyArray.headNonEmpty(parentUsage) + const childUsage = getUsage(command.child, ReadonlyArray.append(preceding, usage)) + return ReadonlyArray.appendAll(parentUsage, childUsage) } - return Effect.fail(validationError.invalidArgument(doc.empty)) + return getUsage(command.child, preceding) } + throw new Error(`[BUG]: Subcommands.usage - unhandled command type: ${JSON.stringify(command)}`) + } + const printSubcommands = (subcommands: ReadonlyArray<[Span.Span, Span.Span]>): HelpDoc.HelpDoc => { + const maxUsageLength = ReadonlyArray.reduceRight( + subcommands, + 0, + (max, [usage]) => Math.max(InternalSpan.size(usage), max) + ) + const documents = ReadonlyArray.map(subcommands, ([usage, desc]) => + InternalHelpDoc.p( + InternalSpan.spans([ + usage, + InternalSpan.text(" ".repeat(maxUsageLength - InternalSpan.size(usage) + 2)), + desc + ]) + )) + if (ReadonlyArray.isNonEmptyReadonlyArray(documents)) { + return InternalHelpDoc.enumeration(documents) + } + throw new Error("[BUG]: Subcommands.usage - received empty list of subcommands to print") + } + return InternalHelpDoc.sequence( + this.parent.help, + InternalHelpDoc.sequence( + InternalHelpDoc.h1("COMMANDS"), + printSubcommands(getUsage(this.child, ReadonlyArray.empty())) + ) ) - const helpDirectiveForParent = Effect.succeed(commandDirective.builtIn(builtInOption.showHelp( - usage(self), - helpDoc(self) - ))) - const wizardDirectiveForChild = Effect.flatMap( - parseMap[self.child._tag]( - self.child as any, - ReadonlyArray.isEmptyReadonlyArray(args) ? [] : args.slice(1), - config - ), - (directive) => - commandDirective.isBuiltIn(directive) && builtInOption.isWizard(directive.option) - ? Effect.succeed(directive) - : Effect.fail(validationError.invalidArgument(doc.empty)) + } + + get usage(): Usage.Usage { + return InternalUsage.concat(this.parent.usage, this.child.usage) + } + + get shortDescription(): string { + return this.parent.shortDescription + } + + parse( + args: ReadonlyArray, + config: CliConfig.CliConfig + ): Effect.Effect< + Terminal.Terminal, + ValidationError.ValidationError, + CommandDirective.CommandDirective> + > { + const helpDirectiveForParent = Effect.succeed( + InternalCommandDirective.builtIn(InternalBuiltInOptions.showHelp(this.usage, this.help)) + ) + const helpDirectiveForChild = this.child.parse(args.slice(1), config).pipe(Effect.flatMap((directive) => { + if (InternalCommandDirective.isBuiltIn(directive) && InternalBuiltInOptions.isShowHelp(directive.option)) { + const parentName = Option.getOrElse(ReadonlyArray.head(Array.from(this.names)), () => "") + const newDirective = InternalCommandDirective.builtIn(InternalBuiltInOptions.showHelp( + InternalUsage.concat( + InternalUsage.named(ReadonlyArray.of(parentName), Option.none()), + directive.option.usage + ), + directive.option.helpDoc + )) + return Effect.succeed(newDirective) + } + return Effect.fail(InternalValidationError.invalidArgument(InternalHelpDoc.empty)) + })) + const wizardDirectiveForParent = Effect.succeed( + InternalCommandDirective.builtIn(InternalBuiltInOptions.showWizard(this)) ) - const wizardDirectiveForParent = Effect.succeed(commandDirective.builtIn(builtInOption.wizard(self))) - return pipe( - parseMap[self.parent._tag](self.parent as any, args, config), + const wizardDirectiveForChild = this.child.parse(args.slice(1), config).pipe(Effect.flatMap((directive) => { + if (InternalCommandDirective.isBuiltIn(directive) && InternalBuiltInOptions.isShowWizard(directive.option)) { + return Effect.succeed(directive) + } + return Effect.fail(InternalValidationError.invalidArgument(InternalHelpDoc.empty)) + })) + const subcommands = this.subcommands + const [parentArgs, childArgs] = ReadonlyArray.span(args, (name) => !HashMap.has(subcommands, name)) + return this.parent.parse(parentArgs, config).pipe( Effect.flatMap((directive) => { - if (commandDirective.isBuiltIn(directive)) { - if (builtInOption.isShowHelp(directive.option)) { - return Effect.orElse(helpDirectiveForChild, () => helpDirectiveForParent) - } - if (builtInOption.isWizard(directive.option)) { - return Effect.orElse(wizardDirectiveForChild, () => wizardDirectiveForParent) + switch (directive._tag) { + case "BuiltIn": { + if (InternalBuiltInOptions.isShowHelp(directive.option)) { + return Effect.orElse(helpDirectiveForChild, () => helpDirectiveForParent) + } + if (InternalBuiltInOptions.isShowWizard(directive.option)) { + return Effect.orElse(wizardDirectiveForChild, () => wizardDirectiveForParent) + } + return Effect.succeed(directive) } - return Effect.succeed(directive) - } - if (ReadonlyArray.isNonEmptyReadonlyArray(directive.leftover)) { - return Effect.map( - parseMap[self.child._tag](self.child as any, directive.leftover, config), - commandDirective.map((subcommand) => ({ + case "UserDefined": { + const args = ReadonlyArray.appendAll(directive.leftover, childArgs) + if (ReadonlyArray.isNonEmptyReadonlyArray(args)) { + return this.child.parse(args, config).pipe(Effect.mapBoth({ + onFailure: (err) => { + if (InternalValidationError.isCommandMismatch(err)) { + const parentName = Option.getOrElse(ReadonlyArray.head(Array.from(this.names)), () => "") + const subcommandNames = pipe( + ReadonlyArray.fromIterable(HashMap.keys(this.subcommands)), + ReadonlyArray.map((name) => `'${name}'`) + ) + const oneOf = subcommandNames.length === 1 ? "" : " one of" + const error = InternalHelpDoc.p( + `Invalid subcommand for ${parentName} - use${oneOf} ${ReadonlyArray.join(subcommandNames, ", ")}` + ) + return InternalValidationError.commandMismatch(error) + } + return err + }, + onSuccess: InternalCommandDirective.map((subcommand) => ({ + ...directive.value, + subcommand: Option.some(subcommand) + })) + })) + } + return Effect.succeed(InternalCommandDirective.userDefined(directive.leftover, { ...directive.value, - subcommand: Option.some(subcommand) + subcommand: Option.none() })) - ) + } } - return Effect.succeed(commandDirective.map(directive, () => ({ - ...directive.value, - subcommand: Option.none() - }))) }), Effect.catchSome(() => ReadonlyArray.isEmptyReadonlyArray(args) - ? Option.some(helpDirectiveForParent) - : Option.none() + ? Option.some(helpDirectiveForParent) : + Option.none() ) ) } + + pipe() { + return pipeArguments(this, arguments) + } } +// ============================================================================= +// Refinements +// ============================================================================= + /** @internal */ -export const parse = dual< - ( - args: ReadonlyArray, - config: CliConfig.CliConfig - ) => ( - self: Command.Command - ) => Effect.Effect>, - ( - self: Command.Command, - args: ReadonlyArray, - config: CliConfig.CliConfig - ) => Effect.Effect> ->(3, (self, args, config) => parseMap[(self as Instruction)._tag](self as any, args, config)) +export const isCommand = (u: unknown): u is Command.Command => + typeof u === "object" && u != null && CommandTypeId in u + +/** @internal */ +export const isStandard = (u: unknown): u is Standard => + isCommand(u) && "_tag" in u && u._tag === "Standard" + +/** @internal */ +export const isGetUserInput = (u: unknown): u is GetUserInput => + isCommand(u) && "_tag" in u && u._tag === "GetUserInput" /** @internal */ -export const prompt = (name: Name, prompt: Prompt.Prompt): Command.Command<{ - readonly name: Name - readonly value: A -}> => promptSingleCommand(name, doc.empty, prompt) +export const isMap = (u: unknown): u is Map => isCommand(u) && "_tag" in u && u._tag === "Map" + +/** @internal */ +export const isOrElse = (u: unknown): u is OrElse => + isCommand(u) && "_tag" in u && u._tag === "OrElse" + +/** @internal */ +export const isSubcommands = (u: unknown): u is Subcommands => + isCommand(u) && "_tag" in u && u._tag === "Subcommands" + +// ============================================================================= +// Constructors +// ============================================================================= + +/** @internal */ +export const prompt = ( + name: Name, + prompt: Prompt.Prompt +): Command.Command> => new GetUserInput(name, prompt) + +const defaultConstructorConfig = { + options: InternalOptions.none, + args: InternalArgs.none +} + +/** @internal */ +export const standard = ( + name: Name, + config: Command.Command.ConstructorConfig = defaultConstructorConfig as any +): Command.Command> => { + const { args, options } = { ...defaultConstructorConfig, ...config } + return new Standard(name, InternalHelpDoc.empty, options, args as any) as any +} /** @internal */ export const subcommands = dual< - >>( + >>( subcommands: [...Subcommands] ) => ( self: Command.Command @@ -416,7 +598,7 @@ export const subcommands = dual< A & Readonly<{ subcommand: Option.Option> }> > >, - >>( + >>( self: Command.Command, subcommands: [...Subcommands] ) => Command.Command< @@ -425,163 +607,73 @@ export const subcommands = dual< > > >(2, (self, subcommands) => { - const head = subcommands[0] - const tail = subcommands.slice(1) - if (tail.length === 0) { - return makeSubcommand(self, head) + if (ReadonlyArray.isNonEmptyReadonlyArray(subcommands)) { + const head = ReadonlyArray.headNonEmpty>(subcommands) + const tail = ReadonlyArray.tailNonEmpty>(subcommands) + if (ReadonlyArray.isNonEmptyReadonlyArray(tail)) { + const child = ReadonlyArray.reduce( + ReadonlyArray.tailNonEmpty(tail), + orElse(head, ReadonlyArray.headNonEmpty(tail)), + orElse + ) + return new Subcommands(self, child) as any + } + return new Subcommands(self, head) as any } - return makeSubcommand(self, tail.slice(1).reduce(orElse, orElse(head, tail[0]))) + throw new Error("[BUG]: Command.subcommands - received empty list of subcommands") }) -const usageMap: { - [K in Instruction["_tag"]]: (self: Extract) => Usage.Usage -} = { - Single: (self) => { - if (self.type._tag === "Standard") { - return _usage.concat( - _usage.named(Chunk.of(self.name), Option.none()), - _usage.concat( - options.usage(self.type.options), - _args.usage(self.type.args) - ) - ) - } - return _usage.named(Chunk.of(self.name), Option.none()) - }, - Map: (self) => usageMap[self.command._tag](self.command as any), - OrElse: () => _usage.mixed, - Subcommands: (self) => - _usage.concat( - usageMap[self.parent._tag](self.parent as any), - usageMap[self.child._tag](self.child as any) - ) -} +// ============================================================================= +// Combinators +// ============================================================================= /** @internal */ -export const usage = (self: Command.Command): Usage.Usage => usageMap[(self as Instruction)._tag](self as any) - -const withHelpMap: { - [K in Instruction["_tag"]]: ( - self: Extract, - help: string | HelpDoc.HelpDoc - ) => Command.Command -} = { - Single: (self, help) => { - if (self.type._tag === "Standard") { - return standardSingleCommand( - self.name, - typeof help === "string" ? doc.p(help) : help, - self.type.options, - self.type.args - ) - } - return promptSingleCommand( - self.name, - typeof help === "string" ? doc.p(help) : help, - self.type.prompt - ) - }, - Map: (self, help) => map(withHelpMap[self.command._tag](self.command as any, help), self.f), - OrElse: (self, help) => - orElse( - withHelpMap[self.left._tag](self.left as any, help), - withHelpMap[self.right._tag](self.right as any, help) - ), - Subcommands: (self, help) => makeSubcommand(withHelpMap[self.parent._tag](self.parent as any, help), self.child) -} +export const map = dual< + (f: (a: A) => B) => (self: Command.Command) => Command.Command, + (self: Command.Command, f: (a: A) => B) => Command.Command +>(2, (self, f) => new Map(self, f)) + +/** @internal */ +export const orElse = dual< + (that: Command.Command) => (self: Command.Command) => Command.Command, + (self: Command.Command, that: Command.Command) => Command.Command +>(2, (self, that) => new OrElse(self, that)) + +/** @internal */ +export const orElseEither = dual< + (that: Command.Command) => (self: Command.Command) => Command.Command>, + (self: Command.Command, that: Command.Command) => Command.Command> +>(2, (self, that) => orElse(map(self, Either.left), map(that, Either.right))) /** @internal */ export const withHelp = dual< (help: string | HelpDoc.HelpDoc) => (self: Command.Command) => Command.Command, (self: Command.Command, help: string | HelpDoc.HelpDoc) => Command.Command ->(2, (self, help) => withHelpMap[(self as Instruction)._tag](self as any, help)) - -const standardSingleCommand = ( - name: Name, - help: HelpDoc.HelpDoc, - options: Options.Options, - args: Args.Args -): Command.Command> => { - const op = Object.create(proto) - op._tag = "Single" - op.name = name - op.help = help - op.type = { - _tag: "Standard", - options, - args - } - return op -} - -const promptSingleCommand = ( - name: Name, - help: HelpDoc.HelpDoc, - prompt: Prompt.Prompt -): Command.Command<{ readonly name: Name; readonly value: A }> => { - const op = Object.create(proto) - op._tag = "Single" - op.name = name - op.help = help - op.type = { - _tag: "Prompt", - prompt - } - return op -} - -const makeSubcommand = (parent: Command.Command, child: Command.Command): Instruction => { - const op = Object.create(proto) - op._tag = "Subcommands" - op.parent = parent - op.child = child - return op -} +>(2, (self: Command.Command, help: string | HelpDoc.HelpDoc): Command.Command => { + if (isStandard(self)) { + const helpDoc = typeof help === "string" ? HelpDoc.p(help) : help + return new Standard(self.name, helpDoc, self.options, self.args) as Command.Command + } + // TODO: if (isPrompt(self)) {} + if (isMap(self)) { + return new Map(withHelp(self.command, help), self.f) as Command.Command + } + if (isOrElse(self)) { + // TODO: if both the left and right commands also have help defined, that + // help will be overwritten by this method which may be undesirable + return new OrElse(withHelp(self.left, help), withHelp(self.right, help)) as Command.Command + } + if (isSubcommands(self)) { + return new Subcommands(withHelp(self.parent, help), self.child) as Command.Command + } + throw new Error(`[BUG]: Command.withHelp - received unknown command type: ${JSON.stringify(self)}`) +}) -const clusteredOptionRegex = /^-{1}([^-]{2,}|$)/ - -const unCluster = (args: ReadonlyArray): ReadonlyArray => - ReadonlyArray.flatMap(args, (arg) => - clusteredOptionRegex.test(arg.trim()) - ? arg.substring(1).split("").map((c) => `-${c}`) - : ReadonlyArray.of(arg)) - -const subcommandMaxSynopsisLengthMap: { - [K in Instruction["_tag"]]: (self: Extract) => number -} = { - Single: (self) => span.size(doc.getSpan(_usage.helpDoc(usage(self)))), - Map: (self) => subcommandMaxSynopsisLengthMap[self.command._tag](self.command as any), - OrElse: (self) => - Math.max( - subcommandMaxSynopsisLengthMap[self.left._tag](self.left as any), - subcommandMaxSynopsisLengthMap[self.right._tag](self.right as any) - ), - Subcommands: (self) => subcommandMaxSynopsisLengthMap[self.parent._tag](self.parent as any) -} +// ============================================================================= +// Internals +// ============================================================================= -const getSubcommandMaxSynopsisLength = (self: Instruction): number => - subcommandMaxSynopsisLengthMap[self._tag](self as any) - -const subcommandDescriptionMap: { - [K in Instruction["_tag"]]: (self: Extract, maxSynopsisLength: number) => HelpDoc.HelpDoc -} = { - Single: (self, maxSynopsisLength) => { - const usageSpan = doc.getSpan(_usage.helpDoc(usage(self))) - return doc.p(span.spans([ - usageSpan, - span.text(" ".repeat(maxSynopsisLength - span.size(usageSpan) + 2)), - doc.getSpan(self.help) - ])) - }, - Map: (self, maxSynopsisLength) => subcommandDescriptionMap[self.command._tag](self.command as any, maxSynopsisLength), - OrElse: (self, maxSynopsisLength) => - doc.enumeration([ - subcommandDescriptionMap[self.left._tag](self.left as any, maxSynopsisLength), - subcommandDescriptionMap[self.right._tag](self.right as any, maxSynopsisLength) - ]), - Subcommands: (self, maxSynopsisLength) => - subcommandDescriptionMap[self.parent._tag](self.parent as any, maxSynopsisLength) +const splitForcedArgs = (args: ReadonlyArray): readonly [ReadonlyArray, ReadonlyArray] => { + const [remainingArgs, forcedArgs] = ReadonlyArray.span(args, (str) => str !== "--") + return [remainingArgs, ReadonlyArray.drop(forcedArgs, 1)] } - -const subcommandsDescription = (self: Instruction, maxSynopsisLength: number): HelpDoc.HelpDoc => - subcommandDescriptionMap[self._tag](self as any, maxSynopsisLength) diff --git a/src/internal/commandDirective.ts b/src/internal/commandDirective.ts index 27107a9..4a46c26 100644 --- a/src/internal/commandDirective.ts +++ b/src/internal/commandDirective.ts @@ -1,9 +1,9 @@ import { dual } from "effect/Function" -import type * as BuiltInOption from "../BuiltInOption" +import type * as BuiltInOption from "../BuiltInOptions" import type * as CommandDirective from "../CommandDirective" /** @internal */ -export const builtIn = (option: BuiltInOption.BuiltInOption): CommandDirective.CommandDirective => ({ +export const builtIn = (option: BuiltInOption.BuiltInOptions): CommandDirective.CommandDirective => ({ _tag: "BuiltIn", option }) diff --git a/src/internal/helpDoc.ts b/src/internal/helpDoc.ts index 354788c..89d7e03 100644 --- a/src/internal/helpDoc.ts +++ b/src/internal/helpDoc.ts @@ -4,10 +4,10 @@ import * as AnsiStyle from "@effect/printer-ansi/AnsiStyle" import * as Doc from "@effect/printer/Doc" import * as Optimize from "@effect/printer/Optimize" import { dual } from "effect/Function" -import * as RA from "effect/ReadonlyArray" +import * as ReadonlyArray from "effect/ReadonlyArray" import type * as HelpDoc from "../HelpDoc" import type * as Span from "../HelpDoc/Span" -import * as span from "./helpDoc/span" +import * as InternalSpan from "./helpDoc/span" /** @internal */ export const isEmpty = (helpDoc: HelpDoc.HelpDoc): helpDoc is HelpDoc.Empty => helpDoc._tag === "Empty" @@ -60,8 +60,8 @@ export const orElse = dual< /** @internal */ export const blocks = (helpDocs: Iterable): HelpDoc.HelpDoc => { - const elements = RA.fromIterable(helpDocs) - if (RA.isNonEmptyReadonlyArray(elements)) { + const elements = ReadonlyArray.fromIterable(helpDocs) + if (ReadonlyArray.isNonEmptyReadonlyArray(elements)) { return elements.slice(1).reduce(sequence, elements[0]) } return empty @@ -69,18 +69,18 @@ export const blocks = (helpDocs: Iterable): HelpDoc.HelpDoc => /** @internal */ export const getSpan = (self: HelpDoc.HelpDoc): Span.Span => - isHeader(self) || isParagraph(self) ? self.value : span.empty + isHeader(self) || isParagraph(self) ? self.value : InternalSpan.empty /** @internal */ export const descriptionList = ( - definitions: RA.NonEmptyReadonlyArray + definitions: ReadonlyArray.NonEmptyReadonlyArray ): HelpDoc.HelpDoc => ({ _tag: "DescriptionList", definitions }) /** @internal */ -export const enumeration = (elements: RA.NonEmptyReadonlyArray): HelpDoc.HelpDoc => ({ +export const enumeration = (elements: ReadonlyArray.NonEmptyReadonlyArray): HelpDoc.HelpDoc => ({ _tag: "Enumeration", elements }) @@ -88,28 +88,28 @@ export const enumeration = (elements: RA.NonEmptyReadonlyArray) /** @internal */ export const h1 = (value: string | Span.Span): HelpDoc.HelpDoc => ({ _tag: "Header", - value: typeof value === "string" ? span.text(value) : value, + value: typeof value === "string" ? InternalSpan.text(value) : value, level: 1 }) /** @internal */ export const h2 = (value: string | Span.Span): HelpDoc.HelpDoc => ({ _tag: "Header", - value: typeof value === "string" ? span.text(value) : value, + value: typeof value === "string" ? InternalSpan.text(value) : value, level: 2 }) /** @internal */ export const h3 = (value: string | Span.Span): HelpDoc.HelpDoc => ({ _tag: "Header", - value: typeof value === "string" ? span.text(value) : value, + value: typeof value === "string" ? InternalSpan.text(value) : value, level: 3 }) /** @internal */ export const p = (value: string | Span.Span): HelpDoc.HelpDoc => ({ _tag: "Paragraph", - value: typeof value === "string" ? span.text(value) : value + value: typeof value === "string" ? InternalSpan.text(value) : value }) /** @internal */ @@ -123,15 +123,15 @@ export const mapDescriptionList = dual< ) => HelpDoc.HelpDoc >(2, (self, f) => isDescriptionList(self) - ? descriptionList(RA.map(self.definitions, ([span, helpDoc]) => f(span, helpDoc))) + ? descriptionList(ReadonlyArray.map(self.definitions, ([span, helpDoc]) => f(span, helpDoc))) : self) const helpDocToAnsiDoc: { [K in HelpDoc.HelpDoc["_tag"]]: (self: Extract) => AnsiDoc.AnsiDoc } = { Empty: () => Doc.empty, - Paragraph: (self) => Doc.cat(span.toAnsiDoc(self.value), Doc.hardLine), - Header: (self) => Doc.cat(Doc.annotate(span.toAnsiDoc(self.value), AnsiStyle.bold), Doc.hardLine), + Paragraph: (self) => Doc.cat(InternalSpan.toAnsiDoc(self.value), Doc.hardLine), + Header: (self) => Doc.cat(Doc.annotate(InternalSpan.toAnsiDoc(self.value), AnsiStyle.bold), Doc.hardLine), Enumeration: (self) => Doc.indent( Doc.vsep(self.elements.map((doc) => @@ -145,7 +145,7 @@ const helpDocToAnsiDoc: { DescriptionList: (self) => Doc.vsep(self.definitions.map(([s, d]) => Doc.cats([ - Doc.annotate(span.toAnsiDoc(s), AnsiStyle.bold), + Doc.annotate(InternalSpan.toAnsiDoc(s), AnsiStyle.bold), Doc.empty, Doc.indent(helpDocToAnsiDoc[d._tag](d as any), 2) ]) diff --git a/src/internal/helpDoc/span.ts b/src/internal/helpDoc/span.ts index a958972..4ccdd92 100644 --- a/src/internal/helpDoc/span.ts +++ b/src/internal/helpDoc/span.ts @@ -3,7 +3,7 @@ import * as AnsiStyle from "@effect/printer-ansi/AnsiStyle" import * as Color from "@effect/printer-ansi/Color" import * as Doc from "@effect/printer/Doc" import { dual } from "effect/Function" -import * as RA from "effect/ReadonlyArray" +import * as ReadonlyArray from "effect/ReadonlyArray" import type * as Span from "../../HelpDoc/Span" /** @internal */ @@ -48,6 +48,27 @@ export const uri = (value: string): Span.Span => ({ value }) +/** @internal */ +export const isCode = (self: Span.Span): self is Span.Code => self._tag === "Code" + +/** @internal */ +export const isError = (self: Span.Span): self is Span.Error => self._tag === "Error" + +/** @internal */ +export const isSequence = (self: Span.Span): self is Span.Sequence => self._tag === "Sequence" + +/** @internal */ +export const isStrong = (self: Span.Span): self is Span.Strong => self._tag === "Strong" + +/** @internal */ +export const isText = (self: Span.Span): self is Span.Text => self._tag === "Text" + +/** @internal */ +export const isUri = (self: Span.Span): self is Span.URI => self._tag === "URI" + +/** @internal */ +export const isWeak = (self: Span.Span): self is Span.Weak => self._tag === "Weak" + /** @internal */ export const concat = dual< (that: Span.Span) => (self: Span.Span) => Span.Span, @@ -58,10 +79,28 @@ export const concat = dual< right: that })) +export const getText = (self: Span.Span): string => { + switch (self._tag) { + case "Text": + case "Code": + case "URI": { + return self.value + } + case "Error": + case "Weak": + case "Strong": { + return getText(self.value) + } + case "Sequence": { + return getText(self.left) + getText(self.right) + } + } +} + /** @internal */ export const spans = (spans: Iterable): Span.Span => { - const elements = RA.fromIterable(spans) - if (RA.isNonEmptyReadonlyArray(elements)) { + const elements = ReadonlyArray.fromIterable(spans) + if (ReadonlyArray.isNonEmptyReadonlyArray(elements)) { return elements.slice(1).reduce(concat, elements[0]) } return empty @@ -70,40 +109,48 @@ export const spans = (spans: Iterable): Span.Span => { /** @internal */ export const isEmpty = (self: Span.Span): boolean => size(self) === 0 -const sizeMap: { - [K in Span.Span["_tag"]]: (self: Extract) => number -} = { - Code: (self) => self.value.length, - Text: (self) => self.value.length, - Error: (self) => sizeMap[self.value._tag](self.value as any), - Weak: (self) => sizeMap[self.value._tag](self.value as any), - Strong: (self) => sizeMap[self.value._tag](self.value as any), - URI: (self) => self.value.length, - Sequence: (self) => { - const left = sizeMap[self.left._tag](self.left as any) - const right = sizeMap[self.right._tag](self.right as any) - return left + right +/** @internal */ +export const size = (self: Span.Span): number => { + switch (self._tag) { + case "Code": + case "Text": + case "URI": { + return self.value.length + } + case "Error": + case "Strong": + case "Weak": { + return size(self.value) + } + case "Sequence": { + return size(self.left) + size(self.right) + } } } /** @internal */ -export const size = (self: Span.Span): number => sizeMap[self._tag](self as any) - -const spanToAnsiDoc: { - [K in Span.Span["_tag"]]: (self: Extract) => AnsiDoc.AnsiDoc -} = { - Text: (self) => Doc.text(self.value), - Code: (self) => Doc.annotate(Doc.text(self.value), AnsiStyle.color(Color.white)), - Error: (self) => Doc.annotate(spanToAnsiDoc[self.value._tag](self.value as any), AnsiStyle.color(Color.red)), - Weak: (self) => Doc.annotate(spanToAnsiDoc[self.value._tag](self.value as any), AnsiStyle.dullColor(Color.black)), - Strong: (self) => Doc.annotate(spanToAnsiDoc[self.value._tag](self.value as any), AnsiStyle.bold), - URI: (self) => Doc.annotate(Doc.text(self.value), AnsiStyle.underlined), - Sequence: (self) => - Doc.cat( - spanToAnsiDoc[self.left._tag](self.left as any), - spanToAnsiDoc[self.right._tag](self.right as any) - ) +export const toAnsiDoc = (self: Span.Span): AnsiDoc.AnsiDoc => { + switch (self._tag) { + case "Text": { + return Doc.text(self.value) + } + case "Code": { + return Doc.annotate(Doc.text(self.value), AnsiStyle.color(Color.white)) + } + case "Error": { + return Doc.annotate(toAnsiDoc(self.value), AnsiStyle.color(Color.red)) + } + case "Weak": { + return Doc.annotate(toAnsiDoc(self.value), AnsiStyle.dullColor(Color.black)) + } + case "Strong": { + return Doc.annotate(toAnsiDoc(self.value), AnsiStyle.bold) + } + case "URI": { + return Doc.annotate(Doc.text(self.value), AnsiStyle.underlined) + } + case "Sequence": { + return Doc.cat(toAnsiDoc(self.left), toAnsiDoc(self.right)) + } + } } - -/** @internal */ -export const toAnsiDoc = (self: Span.Span): AnsiDoc.AnsiDoc => spanToAnsiDoc[self._tag](self as any) diff --git a/src/internal/options.ts b/src/internal/options.ts index ed4b6b6..31c297a 100644 --- a/src/internal/options.ts +++ b/src/internal/options.ts @@ -1,4 +1,4 @@ -import * as Chunk from "effect/Chunk" +import * as Schema from "@effect/schema/Schema" import * as Effect from "effect/Effect" import * as Either from "effect/Either" import { dual, pipe } from "effect/Function" @@ -6,20 +6,21 @@ import * as HashMap from "effect/HashMap" import * as Option from "effect/Option" import * as Order from "effect/Order" import { pipeArguments } from "effect/Pipeable" -import type { Predicate } from "effect/Predicate" -import * as RA from "effect/ReadonlyArray" +import * as ReadonlyArray from "effect/ReadonlyArray" import type * as CliConfig from "../CliConfig" import type * as HelpDoc from "../HelpDoc" import type * as Options from "../Options" +import type * as Parameter from "../Parameter" import type * as Primitive from "../Primitive" import type * as Usage from "../Usage" import type * as ValidationError from "../ValidationError" -import * as autoCorrect from "./autoCorrect" -import * as doc from "./helpDoc" -import * as span from "./helpDoc/span" -import * as primitive from "./primitive" -import * as _usage from "./usage" -import * as validationError from "./validationError" +import * as InternalAutoCorrect from "./autoCorrect" +import * as InternalCliConfig from "./cliConfig" +import * as InternalHelpDoc from "./helpDoc" +import * as InternalSpan from "./helpDoc/span" +import * as InternalPrimitive from "./primitive" +import * as InternalUsage from "./usage" +import * as InternalValidationError from "./validationError" const OptionsSymbolKey = "@effect/cli/Options" @@ -28,154 +29,683 @@ export const OptionsTypeId: Options.OptionsTypeId = Symbol.for( OptionsSymbolKey ) as Options.OptionsTypeId +const proto = { + _A: (_: never) => _ +} + /** @internal */ -export type Op = Options.Options & Body & { - readonly _tag: Tag +export class Empty implements Options.Options { + readonly [OptionsTypeId] = proto + readonly _tag = "Empty" + + get identifier(): Option.Option { + return Option.none() + } + + get flattened(): ReadonlyArray { + return ReadonlyArray.empty() + } + + get help(): HelpDoc.HelpDoc { + return InternalHelpDoc.empty + } + + get usage(): Usage.Usage { + return InternalUsage.empty + } + + get shortDescription(): string { + return "" + } + + modifySingle(_f: <_>(single: Single<_>) => Single<_>): Options.Options { + return new Empty() + } + + validate( + _args: HashMap.HashMap>, + _config: CliConfig.CliConfig + ): Effect.Effect { + return Effect.unit + } + + pipe() { + return pipeArguments(this, arguments) + } } -const proto = { - [OptionsTypeId]: { - _A: (_: never) => _ - }, +/** @internal */ +export class Single implements Options.Options, Parameter.Input { + readonly [OptionsTypeId] = proto + readonly _tag = "Single" + + constructor( + readonly name: string, + readonly aliases: ReadonlyArray, + readonly primitiveType: Primitive.Primitive, + readonly description: HelpDoc.HelpDoc = InternalHelpDoc.empty, + readonly pseudoName: Option.Option = Option.none() + ) {} + + get identifier(): Option.Option { + return Option.some(this.fullName) + } + + get flattened(): ReadonlyArray { + return ReadonlyArray.of(this) + } + + get help(): HelpDoc.HelpDoc { + return InternalHelpDoc.descriptionList(ReadonlyArray.of([ + InternalHelpDoc.getSpan(InternalUsage.getHelp(this.usage)), + InternalHelpDoc.sequence(InternalHelpDoc.p(this.primitiveType.help), this.description) + ])) + } + + get usage(): Usage.Usage { + const acceptedValues = InternalPrimitive.isBool(this.primitiveType) + ? Option.none() + : Option.orElse(this.primitiveType.choices, () => Option.some(this.placeholder)) + return InternalUsage.named(this.names, acceptedValues) + } + + get names(): ReadonlyArray { + return pipe( + ReadonlyArray.prepend(this.aliases, this.name), + ReadonlyArray.map((str) => this.makeFullName(str)), + ReadonlyArray.sort(Order.mapInput(Order.boolean, (tuple: readonly [boolean, string]) => !tuple[0])), + ReadonlyArray.map((tuple) => tuple[1]) + ) + } + + get shortDescription(): string { + return `Option ${this.name}. ${InternalSpan.getText(InternalHelpDoc.getSpan(this.description))}` + } + + modifySingle(f: <_>(single: Single<_>) => Single<_>): Options.Options { + return f(this) + } + + isValid( + input: string, + config: CliConfig.CliConfig + ): Effect.Effect> { + // There will always be at least one name in names + const args = ReadonlyArray.make(this.names[0]!, input) + return this.parse(args, config).pipe(Effect.as(args)) + } + + parse( + args: ReadonlyArray, + config: CliConfig.CliConfig + ): Effect.Effect, ReadonlyArray]> { + return processArgs(args).pipe( + Effect.flatMap((args) => { + if (ReadonlyArray.isNonEmptyReadonlyArray(args)) { + const head = ReadonlyArray.headNonEmpty(args) + const tail = ReadonlyArray.tailNonEmpty(args) + const normalizedArgv0 = InternalCliConfig.normalizeCase(config, head) + const normalizedNames = ReadonlyArray.map(this.names, (name) => InternalCliConfig.normalizeCase(config, name)) + if (ReadonlyArray.contains(normalizedNames, normalizedArgv0)) { + if (InternalPrimitive.isBool(this.primitiveType)) { + if (ReadonlyArray.isNonEmptyReadonlyArray(tail) && tail[0] === "true") { + return Effect.succeed([ReadonlyArray.make(head, "true"), ReadonlyArray.drop(tail, 1)]) + } + if (ReadonlyArray.isNonEmptyReadonlyArray(tail) && tail[0] === "false") { + return Effect.succeed([ReadonlyArray.make(head, "false"), ReadonlyArray.drop(tail, 1)]) + } + return Effect.succeed([ReadonlyArray.of(head), tail]) + } + if (ReadonlyArray.isNonEmptyReadonlyArray(tail)) { + return Effect.succeed([ReadonlyArray.make(head, tail[0]), ReadonlyArray.drop(tail, 1)]) + } + const error = InternalHelpDoc.p(`Expected a value following option: '${this.fullName}'`) + return Effect.fail(InternalValidationError.missingValue(error)) + } + const fullName = this.fullName + if ( + this.name.length > config.autoCorrectLimit + 1 && + InternalAutoCorrect.levensteinDistance(head, fullName, config) <= config.autoCorrectLimit + ) { + const error = InternalHelpDoc.p(`The flag '${head}' is not recognized. Did you mean '${fullName}'?`) + return Effect.fail(InternalValidationError.correctedFlag(error)) + } + const error = InternalHelpDoc.p(`Expected to find option: '${fullName}'`) + return Effect.fail(InternalValidationError.missingFlag(error)) + } + const error = InternalHelpDoc.p(`Expected to find option: '${this.fullName}'`) + return Effect.fail(InternalValidationError.missingFlag(error)) + }) + ) + } + + validate( + args: HashMap.HashMap>, + config: CliConfig.CliConfig + ): Effect.Effect { + const names = ReadonlyArray.filterMap(this.names, (name) => HashMap.get(args, name)) + if (ReadonlyArray.isNonEmptyReadonlyArray(names)) { + const head = ReadonlyArray.headNonEmpty(names) + const tail = ReadonlyArray.tailNonEmpty(names) + if (ReadonlyArray.isEmptyReadonlyArray(tail)) { + if (ReadonlyArray.isEmptyReadonlyArray(head)) { + return this.primitiveType.validate(Option.none(), config).pipe( + Effect.mapError((e) => InternalValidationError.invalidValue(InternalHelpDoc.p(e))) + ) + } + if ( + ReadonlyArray.isNonEmptyReadonlyArray(head) && + ReadonlyArray.isEmptyReadonlyArray(ReadonlyArray.tailNonEmpty(head)) + ) { + const value = ReadonlyArray.headNonEmpty(head) + return this.primitiveType.validate(Option.some(value), config).pipe( + Effect.mapError((e) => InternalValidationError.invalidValue(InternalHelpDoc.p(e))) + ) + } + return Effect.fail(InternalValidationError.keyValuesDetected(InternalHelpDoc.empty, head)) + } + const error = InternalHelpDoc.p(`More than one reference to option '${this.fullName}' detected`) + return Effect.fail(InternalValidationError.invalidValue(error)) + } + const error = InternalHelpDoc.p(`Expected to find option: '${this.fullName}'`) + return Effect.fail(InternalValidationError.missingValue(error)) + } + pipe() { return pipeArguments(this, arguments) } + + private get placeholder(): string { + const pseudoName = Option.getOrElse(this.pseudoName, () => this.primitiveType.typeName) + return `<${pseudoName}>` + } + + private get fullName(): string { + return this.makeFullName(this.name)[1] + } + + private makeFullName(str: string): readonly [boolean, string] { + return str.length === 1 ? [true, `-${str}`] : [false, `--${str}`] + } } /** @internal */ -export type Instruction = - | Empty - | Single - | Map - | OrElse - | KeyValueMap - | Variadic - | WithDefault - | ZipWith +export class Map implements Options.Options { + readonly [OptionsTypeId] = proto + readonly _tag = "Map" + + constructor( + readonly options: Options.Options, + readonly f: (a: A) => Either.Either + ) {} + + get identifier(): Option.Option { + return this.options.identifier + } + + get flattened(): ReadonlyArray { + return this.options.flattened + } + + get help(): HelpDoc.HelpDoc { + return this.options.help + } + + get usage(): Usage.Usage { + return this.options.usage + } + + get shortDescription(): string { + return this.options.shortDescription + } + + modifySingle(f: <_>(single: Single<_>) => Single<_>): Options.Options { + return new Map(this.options.modifySingle(f), this.f) + } + + validate( + args: HashMap.HashMap>, + config: CliConfig.CliConfig + ): Effect.Effect { + return this.options.validate(args, config).pipe(Effect.flatMap((a) => this.f(a))) + } + + pipe() { + return pipeArguments(this, arguments) + } +} /** @internal */ -export interface Empty extends Op<"Empty"> {} +export class OrElse implements Options.Options> { + readonly [OptionsTypeId] = proto + readonly _tag = "OrElse" + + constructor( + readonly left: Options.Options, + readonly right: Options.Options + ) {} + + get identifier(): Option.Option { + const ids = ReadonlyArray.compact([this.left.identifier, this.right.identifier]) + return ReadonlyArray.match(ids, { + onEmpty: () => Option.none(), + onNonEmpty: (ids) => Option.some(ReadonlyArray.join(ids, ", ")) + }) + } + + get flattened(): ReadonlyArray { + return ReadonlyArray.appendAll(this.left.flattened, this.right.flattened) + } + + get help(): HelpDoc.HelpDoc { + return InternalHelpDoc.sequence(this.left.help, this.right.help) + } + + get usage(): Usage.Usage { + return InternalUsage.alternation(this.left.usage, this.right.usage) + } + + get shortDescription(): string { + return "" + } + + modifySingle(f: <_>(single: Single<_>) => Single<_>): Options.Options> { + return new OrElse(this.left.modifySingle(f), this.right.modifySingle(f)) + } + + validate( + args: HashMap.HashMap>, + config: CliConfig.CliConfig + ): Effect.Effect> { + return this.left.validate(args, config).pipe( + Effect.matchEffect({ + onFailure: (err1) => + this.right.validate(args, config).pipe( + Effect.mapBoth({ + onFailure: (err2) => + // orElse option is only missing in case neither option was given + InternalValidationError.isMissingValue(err1) && InternalValidationError.isMissingValue(err2) + ? InternalValidationError.missingValue(InternalHelpDoc.sequence(err1.error, err2.error)) + : InternalValidationError.invalidValue(InternalHelpDoc.sequence(err1.error, err2.error)), + onSuccess: (b) => Either.right(b) + }) + ), + onSuccess: (a) => + this.right.validate(args, config).pipe(Effect.matchEffect({ + onFailure: () => Effect.succeed(Either.left(a)), + onSuccess: () => { + // The `identifier` will only be `None` for `Options.Empty`, which + // means the user would have had to purposefully compose + // `Options.Empty | otherArgument` + const leftUid = Option.getOrElse(this.left.identifier, () => "???") + const rightUid = Option.getOrElse(this.right.identifier, () => "???") + const error = InternalHelpDoc.p( + "Collision between two options detected - you can only specify " + + `one of either: ['${leftUid}', '${rightUid}']` + ) + return Effect.fail(InternalValidationError.invalidValue(error)) + } + })) + }) + ) + } + + pipe() { + return pipeArguments(this, arguments) + } +} /** @internal */ -export interface Single extends - Op<"Single", { - readonly name: string - readonly aliases: Chunk.Chunk - readonly primitiveType: Primitive.Primitive - readonly description: HelpDoc.HelpDoc - }> -{} +export class Both implements Options.Options { + readonly [OptionsTypeId] = proto + readonly _tag = "Both" + + constructor( + readonly left: Options.Options, + readonly right: Options.Options + ) {} + + get identifier(): Option.Option { + const ids = ReadonlyArray.compact([this.left.identifier, this.right.identifier]) + return ReadonlyArray.match(ids, { + onEmpty: () => Option.none(), + onNonEmpty: (ids) => Option.some(ReadonlyArray.join(ids, ", ")) + }) + } + + get flattened(): ReadonlyArray { + return ReadonlyArray.appendAll(this.left.flattened, this.right.flattened) + } + + get help(): HelpDoc.HelpDoc { + return InternalHelpDoc.sequence(this.left.help, this.right.help) + } + + get usage(): Usage.Usage { + return InternalUsage.concat(this.left.usage, this.right.usage) + } + + get shortDescription(): string { + return "" + } -export interface Map extends - Op<"Map", { - readonly value: Instruction - readonly f: (a: unknown) => Either.Either - }> -{} + modifySingle(f: <_>(single: Single<_>) => Single<_>): Options.Options { + return new Both(this.left.modifySingle(f), this.right.modifySingle(f)) + } + + validate( + args: HashMap.HashMap>, + config: CliConfig.CliConfig + ): Effect.Effect { + return this.left.validate(args, config).pipe( + Effect.catchAll((err1) => + this.right.validate(args, config).pipe(Effect.matchEffect({ + onFailure: (err2) => { + const error = InternalHelpDoc.sequence(err1.error, err2.error) + return Effect.fail(InternalValidationError.missingValue(error)) + }, + onSuccess: () => Effect.fail(err1) + })) + ), + Effect.zip(this.right.validate(args, config)) + ) + } + + pipe() { + return pipeArguments(this, arguments) + } +} /** @internal */ -export interface OrElse extends - Op<"OrElse", { - readonly left: Instruction - readonly right: Instruction - }> -{} +export class WithDefault implements Options.Options, Parameter.Input { + readonly [OptionsTypeId] = proto + readonly _tag = "WithDefault" + + constructor( + readonly options: Options.Options, + readonly fallback: A + ) {} + + get identifier(): Option.Option { + return this.options.identifier + } + + get flattened(): ReadonlyArray { + return this.options.flattened + } + + get help(): HelpDoc.HelpDoc { + return InternalHelpDoc.mapDescriptionList(this.options.help, (span, block) => { + const optionalDescription = Option.isOption(this.fallback) + ? Option.match(this.fallback, { + onNone: () => InternalHelpDoc.p("This setting is optional."), + onSome: () => InternalHelpDoc.p(`This setting is optional. Defaults to: ${this.fallback}`) + }) + : InternalHelpDoc.p("This setting is optional.") + return [span, InternalHelpDoc.sequence(block, optionalDescription)] as const + }) + } + + get usage(): Usage.Usage { + return InternalUsage.optional(this.options.usage) + } + + get shortDescription(): string { + return this.options.shortDescription + } + + modifySingle(f: <_>(single: Single<_>) => Single<_>): Options.Options { + return new WithDefault(this.options.modifySingle(f), this.fallback) + } + + isValid( + input: string, + _config: CliConfig.CliConfig + ): Effect.Effect> { + return Effect.sync(() => { + if (isBool(this.options)) { + if (Schema.is(InternalPrimitive.trueValues)(input)) { + const identifier = Option.getOrElse(this.options.identifier, () => "") + return ReadonlyArray.of(identifier) + } + return ReadonlyArray.empty() + } + if (input.length === 0) { + return ReadonlyArray.empty() + } + const identifier = Option.getOrElse(this.options.identifier, () => "") + return ReadonlyArray.make(identifier, input) + }) + } + + parse( + _args: ReadonlyArray, + _config: CliConfig.CliConfig + ): Effect.Effect, ReadonlyArray]> { + const error = InternalHelpDoc.p("Encountered an error in command design while parsing") + return Effect.fail(InternalValidationError.commandMismatch(error)) + } + + validate( + args: HashMap.HashMap>, + config: CliConfig.CliConfig + ): Effect.Effect { + return this.options.validate(args, config).pipe( + Effect.catchTag("MissingValue", () => Effect.succeed(this.fallback)) + ) + } + + pipe() { + return pipeArguments(this, arguments) + } +} /** @internal */ -export interface KeyValueMap extends - Op<"KeyValueMap", { - readonly argumentOption: Single - }> -{} +export class KeyValueMap implements Options.Options>, Parameter.Input { + readonly [OptionsTypeId] = proto + readonly _tag = "KeyValueMap" + + constructor(readonly argumentOption: Single) {} + + get identifier(): Option.Option { + return this.argumentOption.identifier + } + + get flattened(): ReadonlyArray { + return ReadonlyArray.of(this) + } + + get help(): HelpDoc.HelpDoc { + return this.argumentOption.help + } + + get usage(): Usage.Usage { + return this.argumentOption.usage + } + + get shortDescription(): string { + return this.argumentOption.shortDescription + } + + modifySingle(f: <_>(single: Single<_>) => Single<_>): Options.Options> { + return new KeyValueMap(f(this.argumentOption)) + } + + isValid( + input: string, + config: CliConfig.CliConfig + ): Effect.Effect> { + const identifier = Option.getOrElse(this.identifier, () => "") + const args = input.split(" ") + return this.validate(HashMap.make([identifier, args]), config).pipe( + Effect.as(ReadonlyArray.prepend(args, identifier)) + ) + } + + parse( + args: ReadonlyArray, + config: CliConfig.CliConfig + ): Effect.Effect, ReadonlyArray]> { + const names = ReadonlyArray.map( + this.argumentOption.names, + (name) => InternalCliConfig.normalizeCase(config, name) + ) + if (ReadonlyArray.isNonEmptyReadonlyArray(args)) { + const head = ReadonlyArray.headNonEmpty(args) + const tail = ReadonlyArray.tailNonEmpty(args) + if (ReadonlyArray.contains(names, head)) { + let keyValues: ReadonlyArray = ReadonlyArray.empty() + let leftover: ReadonlyArray = tail + while (ReadonlyArray.isNonEmptyReadonlyArray(leftover)) { + // Will either be the flag or a key/value pair + const flagOrKeyValue = ReadonlyArray.headNonEmpty(leftover).trim() + // The input can be in the form of "-d key1=value1 -d key2=value2" + if ( + leftover.length >= 2 && ReadonlyArray.contains( + names, + InternalCliConfig.normalizeCase(config, flagOrKeyValue) + ) + ) { + const keyValueString = leftover[1]!.trim() + const split = keyValueString.split("=") + if (split.length < 2 || split[1] === "" || split[1] === "=") { + break + } else { + keyValues = ReadonlyArray.prepend(keyValues, keyValueString) + leftover = leftover.slice(2) + } + // Or, it can be in the form of "-d key1=value1 key2=value2" + } else { + const split = flagOrKeyValue.split("=") + if (split.length < 2 || split[1] === "" || split[1] === "=") { + break + } else { + keyValues = ReadonlyArray.prepend(keyValues, flagOrKeyValue) + leftover = leftover.slice(1) + continue + } + } + } + return ReadonlyArray.isEmptyReadonlyArray(keyValues) + ? Effect.succeed([ReadonlyArray.empty(), args]) + : Effect.succeed([ReadonlyArray.prepend(keyValues, head), leftover]) + } + } + return Effect.succeed([ReadonlyArray.empty(), args]) + } + + validate( + args: HashMap.HashMap>, + config: CliConfig.CliConfig + ): Effect.Effect> { + const extractKeyValue = ( + keyValue: string + ): Effect.Effect => { + const split = keyValue.trim().split("=") + if (ReadonlyArray.isNonEmptyReadonlyArray(split) && split.length === 2 && split[1] !== "") { + return Effect.succeed(split as unknown as readonly [string, string]) + } + const error = InternalHelpDoc.p(`Expected a key/value pair but received '${keyValue}'`) + return Effect.fail(InternalValidationError.invalidArgument(error)) + } + return this.argumentOption.validate(args, config).pipe(Effect.matchEffect({ + onFailure: (e) => + InternalValidationError.isKeyValuesDetected(e) + ? Effect.forEach(e.keyValues, (kv) => extractKeyValue(kv)).pipe(Effect.map(HashMap.fromIterable)) + : Effect.fail(e), + onSuccess: (kv) => extractKeyValue(kv as string).pipe(Effect.map(HashMap.make)) + })) + } + + pipe() { + return pipeArguments(this, arguments) + } +} + +// ============================================================================= +// Refinements +// ============================================================================= /** @internal */ -export interface Variadic extends - Op<"Variadic", { - readonly options: Single - readonly min: Option.Option - readonly max: Option.Option - }> -{} +export const isOptions = (u: unknown): u is Options.Options => + typeof u === "object" && u != null && OptionsTypeId in u /** @internal */ -export interface WithDefault extends - Op<"WithDefault", { - readonly options: Instruction - readonly default: unknown - }> -{} +export const isEmpty = (u: unknown): u is Empty => isOptions(u) && "_tag" in u && u._tag === "Empty" /** @internal */ -export interface ZipWith extends - Op<"Zip", { - readonly left: Instruction - readonly right: Instruction - }> -{} +export const isSingle = (u: unknown): u is Single => isOptions(u) && "_tag" in u && u._tag === "Single" /** @internal */ -export const isOptions = (u: unknown): u is Options.Options => - typeof u === "object" && u != null && OptionsTypeId in u +export const isMap = (u: unknown): u is Map => isOptions(u) && "_tag" in u && u._tag === "Map" /** @internal */ -export const alias = dual< - (alias: string) => (self: Options.Options) => Options.Options, - (self: Options.Options, alias: string) => Options.Options ->(2, (self, alias) => - modifySingle(self as Instruction, (original) => { - const op = Object.create(proto) - op._tag = "Single" - op.name = original.name - op.aliases = Chunk.append(original.aliases, alias) - op.primitiveType = original.primitiveType - op.description = original.description - return op - })) +export const isOrElse = (u: unknown): u is OrElse => + isOptions(u) && "_tag" in u && u._tag === "OrElse" /** @internal */ -export const atLeast = dual< - { - (times: 0): (self: Options.Options) => Options.Options> - (times: number): (self: Options.Options) => Options.Options> - }, - { - (self: Options.Options, times: 0): Options.Options> - (self: Options.Options, times: number): Options.Options> - } ->(2, (self, times) => variadic(self, Option.some(times), Option.none()) as any) +export const isBoth = (u: unknown): u is Both => isOptions(u) && "_tag" in u && u._tag === "Both" /** @internal */ -export const atMost = dual< - (times: number) => (self: Options.Options) => Options.Options>, - (self: Options.Options, times: number) => Options.Options> ->(2, (self, times) => variadic(self, Option.none(), Option.some(times))) +export const isWithDefault = (u: unknown): u is WithDefault => + isOptions(u) && "_tag" in u && u._tag === "WithDefault" /** @internal */ -export const between = dual< - { - (min: 0, max: number): (self: Options.Options) => Options.Options> - (min: number, max: number): (self: Options.Options) => Options.Options> - }, - { - (self: Options.Options, min: 0, max: number): Options.Options> - (self: Options.Options, min: number, max: number): Options.Options> +export const isKeyValueMap = (u: unknown): u is KeyValueMap => isOptions(u) && "_tag" in u && u._tag === "KeyValueMap" + +// ============================================================================= +// Constructors +// ============================================================================= + +/** @internal */ +export const all: < + const Arg extends Iterable> | Record> +>(arg: Arg) => Options.All.Return = function() { + if (arguments.length === 1) { + if (isOptions(arguments[0])) { + return map(arguments[0], (x) => [x]) as any + } else if (Array.isArray(arguments[0])) { + return allTupled(arguments[0]) as any + } else { + const entries = Object.entries(arguments[0] as Readonly<{ [K: string]: Options.Options }>) + let result = map(entries[0][1], (value) => ({ [entries[0][0]]: value })) + if (entries.length === 1) { + return result as any + } + const rest = entries.slice(1) + for (const [key, options] of rest) { + result = map(new Both(result, options), ([record, value]) => ({ + ...record, + [key]: value + })) + } + return result as any + } } ->(3, (self, min, max) => variadic(self, Option.some(min), Option.some(max)) as any) + return allTupled(arguments[0]) as any +} const defaultBooleanOptions = { ifPresent: true, - negationNames: [] + negationNames: [], + aliases: [] } /** @internal */ export const boolean = (name: string, options: Options.Options.BooleanOptionConfig = {}): Options.Options => { - const { ifPresent, negationNames } = { ...defaultBooleanOptions, ...options } - const option = single(name, Chunk.empty(), primitive.boolean(Option.some(ifPresent))) - if (RA.isNonEmptyReadonlyArray(negationNames)) { - const negationOption = single( - negationNames[0], - Chunk.unsafeFromArray(negationNames.slice(1)), - primitive.boolean(Option.some(!ifPresent)) + const { aliases, ifPresent, negationNames } = { ...defaultBooleanOptions, ...options } + const option = new Single( + name, + aliases, + InternalPrimitive.boolean(Option.some(ifPresent)) + ) + if (ReadonlyArray.isNonEmptyReadonlyArray(negationNames)) { + const head = ReadonlyArray.headNonEmpty(negationNames) + const tail = ReadonlyArray.tailNonEmpty(negationNames) + const negationOption = new Single( + head, + tail, + InternalPrimitive.boolean(Option.some(!ifPresent)) ) return withDefault(orElse(option, negationOption), !ifPresent) } @@ -183,24 +713,23 @@ export const boolean = (name: string, options: Options.Options.BooleanOptionConf } /** @internal */ -export const choice = >( +export const choice = >( name: string, choices: C -): Options.Options => - single( - name, - Chunk.empty(), - primitive.choice(RA.map(choices, (choice) => [choice, choice] as const)) - ) +): Options.Options => { + const primitive = InternalPrimitive.choice(ReadonlyArray.map(choices, (choice) => [choice, choice])) + return new Single(name, ReadonlyArray.empty(), primitive) +} /** @internal */ -export const choiceWithValue = >( +export const choiceWithValue = >( name: string, choices: C -): Options.Options => single(name, Chunk.empty(), primitive.choice(choices)) +): Options.Options => new Single(name, ReadonlyArray.empty(), InternalPrimitive.choice(choices)) /** @internal */ -export const date = (name: string): Options.Options => single(name, Chunk.empty(), primitive.date) +export const date = (name: string): Options.Options => + new Single(name, ReadonlyArray.empty(), InternalPrimitive.date) /** @internal */ export const filterMap = dual< @@ -209,106 +738,66 @@ export const filterMap = dual< >(3, (self, f, message) => mapOrFail(self, (a) => Option.match(f(a), { - onNone: () => Either.left(validationError.invalidValue(doc.p(message))), + onNone: () => Either.left(InternalValidationError.invalidValue(InternalHelpDoc.p(message))), onSome: Either.right }))) /** @internal */ -export const float = (name: string): Options.Options => single(name, Chunk.empty(), primitive.float) - -const helpDocMap: { - [K in Instruction["_tag"]]: (self: Extract) => HelpDoc.HelpDoc -} = { - Empty: () => doc.empty, - Single: (self) => - doc.descriptionList(RA.of([ - doc.getSpan(_usage.helpDoc(usage(self))), - doc.sequence(doc.p(primitive.helpDoc(self.primitiveType)), self.description) - ])), - Map: (self) => helpDocMap[self.value._tag](self.value as any), - OrElse: (self) => { - const left = helpDocMap[self.left._tag](self.left as any) - const right = helpDocMap[self.right._tag](self.right as any) - return doc.sequence(left, right) - }, - KeyValueMap: (self) => helpDocMap[self.argumentOption._tag](self.argumentOption as any), - Variadic: (self) => helpDocMap[self.options._tag](self.options as any), - WithDefault: (self) => { - const helpDoc = helpDocMap[self.options._tag](self.options as any) - return doc.mapDescriptionList(helpDoc, (span, block) => [ - span, - doc.sequence( - block, - doc.p( - Option.isOption(self.default) ? - "This setting is optional." : - `This setting is optional. (Defaults to: '${JSON.stringify(self.default)}')` - ) - ) - ]) - }, - Zip: (self) => { - const left = helpDocMap[self.left._tag](self.left as any) - const right = helpDocMap[self.right._tag](self.right as any) - return doc.sequence(left, right) - } -} +export const float = (name: string): Options.Options => + new Single(name, ReadonlyArray.empty(), InternalPrimitive.float) /** @internal */ -export const helpDoc = (self: Options.Options): HelpDoc.HelpDoc => - helpDocMap[(self as Instruction)._tag](self as any) +export const integer = (name: string): Options.Options => + new Single(name, ReadonlyArray.empty(), InternalPrimitive.integer) /** @internal */ -export const integer = (name: string): Options.Options => single(name, Chunk.empty(), primitive.integer) - -const isBoolMap: { - [K in Instruction["_tag"]]: (self: Extract) => boolean -} = { - Empty: () => false, - Single: (self) => primitive.isBool(self.primitiveType), - Map: (self) => isBoolMap[self.value._tag](self.value as any), - OrElse: () => false, - KeyValueMap: () => false, - Variadic: () => false, - WithDefault: (self) => isBoolMap[self.options._tag](self.options as any), - Zip: () => false +export const keyValueMap = ( + option: string | Options.Options +): Options.Options> => { + if (typeof option === "string") { + const single = new Single(option, ReadonlyArray.empty(), InternalPrimitive.text) + return new KeyValueMap(single) + } + if (!isSingle(option)) { + throw new Error("InvalidArgumentException: the provided option must be a single option") + } else { + return new KeyValueMap(option as Single) + } } /** @internal */ -export const isBool = (self: Options.Options): boolean => isBoolMap[(self as Instruction)._tag](self as any) +export const none: Options.Options = new Empty() /** @internal */ -export const keyValueMap = (name: string): Options.Options> => { - const op = Object.create(proto) - op._tag = "KeyValueMap" - op.argumentOption = single(name, Chunk.empty(), primitive.text) - return op -} +export const text = (name: string): Options.Options => + new Single(name, ReadonlyArray.empty(), InternalPrimitive.text) + +// ============================================================================= +// Combinators +// ============================================================================= /** @internal */ -export const keyValueMapFromOption = ( - argumentOption: Options.Options -): Options.Options> => { - if ((argumentOption as Instruction)._tag !== "Single") { - throw new Error("argumentOption must be a single option") +export const isBool = (self: Options.Options): boolean => { + if (isEmpty(self)) { + return false + } + if (isWithDefault(self)) { + return isBool(self.options) + } + if (isSingle(self)) { + return InternalPrimitive.isBool(self.primitiveType) + } + if (isMap(self)) { + return isBool(self.options) } - const op = Object.create(proto) - op._tag = "KeyValueMap" - op.argumentOption = argumentOption - return op + return false } /** @internal */ export const map = dual< (f: (a: A) => B) => (self: Options.Options) => Options.Options, (self: Options.Options, f: (a: A) => B) => Options.Options ->(2, (self: Options.Options, f: (a: A) => B) => { - const op = Object.create(proto) - op._tag = "Map" - op.value = self - op.f = (a: A) => Either.right(f(a)) - return op -}) +>(2, (self, f) => new Map(self, (a) => Either.right(f(a)))) /** @internal */ export const mapOrFail = dual< @@ -319,13 +808,7 @@ export const mapOrFail = dual< self: Options.Options, f: (a: A) => Either.Either ) => Options.Options ->(2, (self, f) => { - const op = Object.create(proto) - op._tag = "Map" - op.value = self - op.f = f - return op -}) +>(2, (self, f) => new Map(self, f)) /** @internal */ export const mapTryCatch = dual< @@ -336,243 +819,25 @@ export const mapTryCatch = dual< try { return Either.right(f(a)) } catch (e) { - return Either.left(validationError.invalidValue(onError(e))) + return Either.left(InternalValidationError.invalidValue(onError(e))) } })) -/** @internal */ -export const none: Options.Options = (() => { - const op = Object.create(proto) - op._tag = "Empty" - return op -})() - /** @internal */ export const optional = (self: Options.Options): Options.Options> => withDefault(map(self, Option.some), Option.none()) /** @internal */ export const orElse = dual< - (that: Options.Options) => (self: Options.Options) => Options.Options, + (that: Options.Options) => (self: Options.Options) => Options.Options, (self: Options.Options, that: Options.Options) => Options.Options ->(2, (self, that) => map(orElseEither(self, that), Either.merge)) +>(2, (self, that) => orElseEither(self, that).pipe(map(Either.merge))) /** @internal */ export const orElseEither = dual< - (that: Options.Options) => (self: Options.Options) => Options.Options>, - (self: Options.Options, that: Options.Options) => Options.Options> ->(2, (self, that) => { - const op = Object.create(proto) - op._tag = "OrElse" - op.left = self - op.right = that - return op -}) - -/** @internal */ -export const repeat = (self: Options.Options): Options.Options> => - variadic(self, Option.none(), Option.none()) - -/** @internal */ -export const repeat1 = (self: Options.Options): Options.Options> => - variadic(self, Option.some(1), Option.none()) as any - -/** @internal */ -export const text = (name: string): Options.Options => single(name, Chunk.empty(), primitive.text) - -const uidMap: { - [K in Instruction["_tag"]]: (self: Extract) => Option.Option -} = { - Empty: () => Option.none(), - Single: (self) => Option.some(singleFullName(self)), - Map: (self) => uidMap[self.value._tag](self.value as any), - OrElse: (self) => combineUids(self.left, self.right), - KeyValueMap: (self) => uidMap[self.argumentOption._tag](self.argumentOption as any), - Variadic: (self) => uidMap[self.options._tag](self.options as any), - WithDefault: (self) => uidMap[self.options._tag](self.options as any), - Zip: (self) => combineUids(self.left, self.right) -} - -/** @internal */ -export const uid = (self: Options.Options): Option.Option => - uidMap[(self as Instruction)._tag](self as any) - -const usageMap: { - [K in Instruction["_tag"]]: (self: Extract) => Usage.Usage -} = { - Empty: () => _usage.empty, - Single: (self) => { - const names = singleNames(self) - const acceptedValues = primitive.isBool(self.primitiveType) - ? Option.none() - : Option.orElse( - primitive.choices(self.primitiveType), - () => Option.some(primitive.typeName(self.primitiveType)) - ) - return _usage.named(names, acceptedValues) - }, - Map: (self) => usageMap[self.value._tag](self.value as any), - OrElse: (self) => { - const left = usageMap[self.left._tag](self.left as any) - const right = usageMap[self.right._tag](self.right as any) - return _usage.alternation(left, right) - }, - KeyValueMap: (self) => usageMap[self.argumentOption._tag](self.argumentOption as any), - Variadic: (self) => _usage.optional(usageMap[self.options._tag](self.options as any)), - WithDefault: (self) => _usage.optional(usageMap[self.options._tag](self.options as any)), - Zip: (self) => { - const left = usageMap[self.left._tag](self.left as any) - const right = usageMap[self.right._tag](self.right as any) - return _usage.concat(left, right) - } -} - -/** @internal */ -export const usage = (self: Options.Options): Usage.Usage => usageMap[(self as Instruction)._tag](self as any) - -const validateMap: { - [K in Instruction["_tag"]]: ( - self: Extract, - args: ReadonlyArray, - config: CliConfig.CliConfig - ) => Effect.Effect, any]> -} = { - Empty: (_, args) => Effect.succeed([args, void 0]), - Single: (self, args, config) => { - if (RA.isNonEmptyReadonlyArray(args)) { - const [rest, supported] = processSingleArg(self, args[0], args.slice(1), config) - if (supported) { - if (primitive.isBool(self.primitiveType)) { - return Effect.mapBoth(primitive.validate(self.primitiveType, Option.none()), { - onFailure: (error) => validationError.invalidValue(doc.p(error)), - onSuccess: (a) => [rest, a] - }) - } - return Effect.mapBoth(primitive.validate(self.primitiveType, RA.head(rest)), { - onFailure: (error) => validationError.invalidValue(doc.p(error)), - onSuccess: (a) => [rest.slice(1), a] - }) - } - const fullName = singleFullName(self) - const distance = autoCorrect.levensteinDistance(args[0], fullName, config) - if (self.name.length > config.autoCorrectLimit + 1 && distance <= config.autoCorrectLimit) { - const message = `The flag '${args[0]}' is not recognized. Did you mean '${fullName}'?` - const error = validationError.invalidValue(doc.p(span.error(message))) - return Effect.fail(error) - } - return Effect.map( - validateMap[self._tag](self, rest, config), - (tuple) => [RA.prepend(tuple[0], args[0]), tuple[1]] - ) - } - const error = validationError.missingValue( - doc.p(span.error(`Expected to find option: '${singleFullName(self)}'`)) - ) - return Effect.fail(error) - }, - Map: (self, args, config) => - Effect.flatMap( - validateMap[self.value._tag](self.value as any, args, config), - (tuple) => - Either.match(self.f(tuple[1]), { - onLeft: Effect.fail, - onRight: (a) => Effect.succeed([tuple[0], a]) - }) - ), - OrElse: (self, args, config) => - Effect.matchEffect(validateMap[self.left._tag](self.left as any, args, config), { - onFailure: (error1) => - Effect.matchEffect(validateMap[self.right._tag](self.right as any, args, config), { - onFailure: (error2) => { - const message = doc.sequence(error1.error, error2.error) - // The option is only considered "missing" if neither option was given - if (validationError.isMissingValue(error1) && validationError.isMissingValue(error2)) { - return Effect.fail(validationError.missingValue(message)) - } - return Effect.fail(validationError.invalidValue(message)) - }, - onSuccess: (tuple) => Effect.succeed([tuple[0], Either.right(tuple[1])]) - }), - onSuccess: (tuple) => - Effect.matchEffect(validateMap[self.right._tag](self.right as any, tuple[0], config), { - onFailure: () => Effect.succeed([tuple[0], Either.left(tuple[1])]), - onSuccess: () => { - const left = uid(self.left) - const right = uid(self.right) - if (Option.isNone(left) || Option.isNone(right)) { - const message = "Collision between two options detected. Could not render option identifiers." - const error = validationError.invalidValue(doc.p(span.error(message))) - return Effect.fail(error) - } - const message = "Collision between two options detected." + - ` You can only specify one of either: ['${left.value}', '${right.value}'].` - const error = validationError.invalidValue(doc.p(span.error(message))) - return Effect.fail(error) - } - }) - }), - KeyValueMap: (self, args, config) => - Effect.map( - validateMap[self.argumentOption._tag](self.argumentOption as any, args, config), - (tuple) => processKeyValueMapArg(self, tuple[0], tuple[1], config) - ), - Variadic: (self, args, config) => { - const min = Option.getOrElse(self.min, () => 0) - const max = Option.getOrElse(self.max, () => Infinity) - const loop = ( - args: ReadonlyArray, - acc: Chunk.Chunk - ): Effect.Effect, Chunk.Chunk]> => - Effect.matchEffect(validateMap[self.options._tag](self.options as any, args, config), { - onFailure: (error) => { - if (!validationError.isMissingValue(error)) { - return Effect.fail(error) - } else if (acc.length < min) { - return Effect.fail(validationError.missingValue( - doc.p(span.error(`Expected at least ${min} value(s) for option: '${singleFullName(self.options)}'`)) - )) - } - - return Effect.succeed([args, acc]) - }, - onSuccess: (tuple) => { - acc = Chunk.append(acc, tuple[1]) - if (acc.length > max) { - return Effect.fail(validationError.extraneousValue( - doc.p(span.error(`Expected at most ${max} value(s) for option: '${singleFullName(self.options)}'`)) - )) - } - return loop(tuple[0], acc) - } - }) - return loop(args, Chunk.empty()) - }, - WithDefault: (self, args, config) => - Effect.catchSome( - validateMap[self.options._tag](self.options as any, args, config), - (error) => - validationError.isMissingValue(error) - ? Option.some(Effect.succeed([args, self.default])) - : Option.none() - ), - Zip: (self, args, config) => - pipe( - validateMap[self.left._tag](self.left as any, args, config), - Effect.catchAll( - (error1) => - Effect.matchEffect(validateMap[self.right._tag](self.right as any, args, config), { - onFailure: (error2) => Effect.fail(validationError.missingValue(doc.sequence(error1.error, error2.error))), - onSuccess: () => Effect.fail(error1) - }) - ), - Effect.flatMap(([args, a]) => - pipe( - validateMap[self.right._tag](self.right as any, args, config), - Effect.map(([args, b]) => [args, [a, b]]) - ) - ) - ) -} + (that: Options.Options) => (self: Options.Options) => Options.Options>, + (self: Options.Options, that: Options.Options) => Options.Options> +>(2, (self, that) => new OrElse(self, that)) /** @internal */ export const validate = dual< @@ -581,274 +846,266 @@ export const validate = dual< config: CliConfig.CliConfig ) => ( self: Options.Options - ) => Effect.Effect, A]>, + ) => Effect.Effect< + never, + ValidationError.ValidationError, + readonly [Option.Option, ReadonlyArray, A] + >, ( self: Options.Options, args: ReadonlyArray, config: CliConfig.CliConfig - ) => Effect.Effect, A]> ->(3, (self, args, config) => validateMap[(self as Instruction)._tag](self as any, args, config)) - -const variadic = ( - self: Options.Options, - min: Option.Option, - max: Option.Option -): Options.Options> => { - const op = Object.create(proto) - op._tag = "Variadic" - op.options = self - op.min = min - op.max = max - return op -} + ) => Effect.Effect< + never, + ValidationError.ValidationError, + readonly [Option.Option, ReadonlyArray, A] + > +>(3, (self, args, config) => + matchOptions(args, self.flattened, config).pipe( + Effect.flatMap(([error, commandArgs, matchedOptions]) => + self.validate(matchedOptions, config).pipe( + Effect.catchAll((e) => + Option.match(error, { + onNone: () => Effect.fail(e), + onSome: (err) => Effect.fail(err) + }) + ), + Effect.map((a) => [error, commandArgs, a] as const) + ) + ) + )) + +/** @internal */ +export const withAlias = dual< + (alias: string) => (self: Options.Options) => Options.Options, + (self: Options.Options, alias: string) => Options.Options +>(2, (self, alias) => + self.modifySingle((single) => { + const aliases = ReadonlyArray.append(single.aliases, alias) + return new Single(single.name, aliases, single.primitiveType, single.description, single.pseudoName) + })) /** @internal */ export const withDefault = dual< - (value: A) => (self: Options.Options) => Options.Options, - (self: Options.Options, value: A) => Options.Options ->(2, (self, value) => { - const op = Object.create(proto) - op._tag = "WithDefault" - op.options = self - op.default = value - return op -}) + (fallback: A) => (self: Options.Options) => Options.Options, + (self: Options.Options, fallback: A) => Options.Options +>(2, (self, fallback) => new WithDefault(self, fallback)) /** @internal */ export const withDescription = dual< (description: string) => (self: Options.Options) => Options.Options, (self: Options.Options, description: string) => Options.Options ->(2, (self, description) => - modifySingle(self as Instruction, (original) => { - const op = Object.create(proto) - op._tag = "Single" - op.name = original.name - op.aliases = original.aliases - op.primitiveType = original.primitiveType - op.description = doc.sequence(original.description, doc.p(description)) - return op +>(2, (self, desc) => + self.modifySingle((single) => { + const description = InternalHelpDoc.sequence(single.description, InternalHelpDoc.p(desc)) + return new Single(single.name, single.aliases, single.primitiveType, description, single.pseudoName) })) /** @internal */ -export const zip = dual< - (that: Options.Options) => (self: Options.Options) => Options.Options, - (self: Options.Options, that: Options.Options) => Options.Options ->(2, (self, that) => { - const op = Object.create(proto) - op._tag = "Zip" - op.left = self - op.right = that - return op -}) +export const withPseudoName = dual< + (pseudoName: string) => (self: Options.Options) => Options.Options, + (self: Options.Options, pseudoName: string) => Options.Options +>(2, (self, pseudoName) => + self.modifySingle((single) => + new Single( + single.name, + single.aliases, + single.primitiveType, + single.description, + Option.some(pseudoName) + ) + )) -/** @internal */ -export const zipFlatten = dual< - ( - that: Options.Options - ) => >( - self: Options.Options - ) => Options.Options<[...A, B]>, - , B>( - self: Options.Options, - that: Options.Options - ) => Options.Options<[...A, B]> ->(2, (self, that) => map(zip(self, that), ([a, b]) => [...a, b])) +// ============================================================================= +// Internals +// ============================================================================= -/** @internal */ -export const zipWith = dual< - (that: Options.Options, f: (a: A, b: B) => C) => (self: Options.Options) => Options.Options, - (self: Options.Options, that: Options.Options, f: (a: A, b: B) => C) => Options.Options ->(3, (self, that, f) => map(zip(self, that), ([a, b]) => f(a, b))) +const allTupled = >>(arg: T): Options.Options< + { + [K in keyof T]: [T[K]] extends [Options.Options] ? A : never + } +> => { + if (arg.length === 0) { + return none as any + } + if (arg.length === 1) { + return map(arg[0], (x) => [x]) as any + } + let result = map(arg[0], (x) => [x]) + for (let i = 1; i < arg.length; i++) { + const curr = arg[i] + result = map(new Both(result, curr), ([a, b]) => [...a, b]) + } + return result as any +} -/* @internal */ -export const all: { - >>( - self: Options.Options, - ...args: T - ): Options.Options< - readonly [ - A, - ...(T["length"] extends 0 ? [] - : Readonly<{ [K in keyof T]: [T[K]] extends [Options.Options] ? A : never }>) - ] - > - >>( - args: [...T] - ): Options.Options< - T[number] extends never ? [] - : Readonly<{ [K in keyof T]: [T[K]] extends [Options.Options] ? A : never }> - > - }>>( - args: T - ): Options.Options< - Readonly<{ [K in keyof T]: [T[K]] extends [Options.Options] ? A : never }> - > -} = function() { - if (arguments.length === 1) { - if (isOptions(arguments[0])) { - return map(arguments[0], (x) => [x]) - } else if (Array.isArray(arguments[0])) { - return tuple(arguments[0]) - } else { - const entries = Object.entries(arguments[0] as Readonly<{ [K: string]: Options.Options }>) - let result = map(entries[0][1], (value) => ({ [entries[0][0]]: value })) - if (entries.length === 1) { - return result as any - } - const rest = entries.slice(1) - for (const [key, options] of rest) { - result = zipWith(result, options, (record, value) => ({ ...record, [key]: value })) +const CLUSTERED_REGEX = /^-{1}([^-]{2,}$)/ +const FLAG_REGEX = /^(--[^=]+)(?:=(.+))?$/ + +const processArgs = ( + args: ReadonlyArray +): Effect.Effect> => { + if (ReadonlyArray.isNonEmptyReadonlyArray(args)) { + const head = ReadonlyArray.headNonEmpty(args) + const tail = ReadonlyArray.tailNonEmpty(args) + if (CLUSTERED_REGEX.test(head.trim())) { + const unclustered = head.substring(1).split("").map((c) => `-${c}`) + return Effect.fail(InternalValidationError.unclusteredFlag(InternalHelpDoc.empty, unclustered, tail)) + } + if (head.startsWith("--")) { + const result = FLAG_REGEX.exec(head) + if (result !== null && result[2] !== undefined) { + return Effect.succeed(ReadonlyArray.prependAll(tail, [result[1], result[2]])) } - return result as any } + return Effect.succeed(args) } - return tuple(arguments[0]) -} - -const single = ( - name: string, - aliases: Chunk.Chunk, - primitiveType: Primitive.Primitive, - description: HelpDoc.HelpDoc = doc.empty -): Options.Options => { - const op = Object.create(proto) - op._tag = "Single" - op.name = name - op.aliases = aliases - op.primitiveType = primitiveType - op.description = description - return op -} - -const singleModifierMap: { - [K in Instruction["_tag"]]: ( - self: Extract, - f: (single: Single) => Single - ) => Options.Options -} = { - Empty: (self) => self, - Single: (self, f) => f(self), - Map: (self, f) => mapOrFail(modifySingle(self.value, f), self.f), - OrElse: (self, f) => orElseEither(modifySingle(self.left, f), modifySingle(self.right, f)), - KeyValueMap: (self, f) => keyValueMapFromOption(f(self.argumentOption)), - Variadic: (self, f) => variadic(f(self.options), self.min, self.max), - WithDefault: (self, f) => withDefault(modifySingle(self.options, f), self.default), - Zip: (self, f) => zip(modifySingle(self.left, f), modifySingle(self.right, f)) + return Effect.succeed(ReadonlyArray.empty()) } -const singleFullName = (self: Single): string => makeSingleFullName(self.name)[1] - -const singleNames = (self: Single): Chunk.Chunk => - pipe( - Chunk.prepend(self.aliases, self.name), - Chunk.map(makeSingleFullName), - Chunk.sort(Order.mapInput(Order.boolean, (tuple: readonly [boolean, string]) => !tuple[0])), - Chunk.map((tuple) => tuple[1]) - ) - -const makeSingleFullName = (s: string): readonly [boolean, string] => - s.length === 1 ? [true, `-${s}`] : [false, `--${s}`] - -const modifySingle = (self: Instruction, f: (single: Single) => Single): Options.Options => - singleModifierMap[self._tag](self as any, f) - -const processKeyValueMapArg = ( - self: KeyValueMap, +/** + * Returns a possible `ValidationError` when parsing the commands, leftover + * arguments from `input` and a mapping between each flag and its values. + */ +const matchOptions = ( input: ReadonlyArray, - first: string, + options: ReadonlyArray, config: CliConfig.CliConfig -): readonly [ReadonlyArray, HashMap.HashMap] => { - const [remaining, chunk] = processVariadicArg(repeat(self.argumentOption) as any, input, first, config) - const createMapEntry = (input: string): readonly [string, string] => - input.split("=").slice(0, 2) as unknown as readonly [string, string] - return [remaining, HashMap.fromIterable(Chunk.map(chunk, createMapEntry))] +): Effect.Effect< + never, + never, + readonly [ + Option.Option, + ReadonlyArray, + HashMap.HashMap> + ] +> => { + if (ReadonlyArray.isNonEmptyReadonlyArray(input) && ReadonlyArray.isNonEmptyReadonlyArray(options)) { + return findOptions(input, options, config).pipe( + Effect.flatMap(([otherArgs, otherOptions, map1]) => { + if (HashMap.isEmpty(map1)) { + return Effect.succeed([Option.none(), input, map1] as const) + } + return matchOptions(otherArgs, otherOptions, config).pipe( + Effect.map(([error, otherArgs, map2]) => + [error, otherArgs, merge(map1, ReadonlyArray.fromIterable(map2))] as const + ) + ) + }), + Effect.catchAll((e) => Effect.succeed([Option.some(e), input, HashMap.empty()] as const)) + ) + } + return ReadonlyArray.isEmptyReadonlyArray(input) + ? Effect.succeed([Option.none(), ReadonlyArray.empty(), HashMap.empty()]) + : Effect.succeed([Option.none(), input, HashMap.empty()]) } -const processVariadicArg = ( - self: Variadic, +/** + * Returns the leftover arguments, leftover options, and a mapping between the + * first argument with its values if it corresponds to an option flag. + */ +const findOptions = ( input: ReadonlyArray, - first: string, + options: ReadonlyArray, config: CliConfig.CliConfig -): readonly [ReadonlyArray, Chunk.Chunk] => { - const max = Option.getOrElse(self.max, () => Infinity) - const makeFullName = (s: string): string => s.length === 1 ? `-${s}` : `--${s}` - const supports = (s: string, config: CliConfig.CliConfig): boolean => { - const argumentNames = Chunk.prepend( - Chunk.map(self.options.aliases, makeFullName), - makeFullName(self.options.name) +): Effect.Effect< + never, + ValidationError.ValidationError, + readonly [ + ReadonlyArray, + ReadonlyArray, + HashMap.HashMap> + ] +> => { + if (ReadonlyArray.isNonEmptyReadonlyArray(options)) { + const head = ReadonlyArray.headNonEmpty(options) + const tail = ReadonlyArray.tailNonEmpty(options) + return head.parse(input, config).pipe( + Effect.flatMap(([nameValues, leftover]) => { + if (ReadonlyArray.isNonEmptyReadonlyArray(nameValues)) { + const name = ReadonlyArray.headNonEmpty(nameValues) + const values: ReadonlyArray = ReadonlyArray.tailNonEmpty(nameValues) + return Effect.succeed([leftover, tail, HashMap.make([name, values])] as const) + } + return findOptions(leftover, tail, config).pipe( + Effect.map(([otherArgs, otherOptions, map]) => + [otherArgs, ReadonlyArray.prepend(otherOptions, head), map] as const + ) + ) + }), + Effect.catchTags({ + CorrectedFlag: (e) => + findOptions(input, tail, config).pipe( + Effect.catchSome(() => Option.some(Effect.fail(e))), + Effect.flatMap(([otherArgs, otherOptions, map]) => + Effect.fail(e).pipe( + Effect.when(() => HashMap.isEmpty(map)), + Effect.as([otherArgs, ReadonlyArray.prepend(otherOptions, head), map] as const) + ) + ) + ), + MissingFlag: () => + findOptions(input, tail, config).pipe( + Effect.map(([otherArgs, otherOptions, map]) => + [otherArgs, ReadonlyArray.prepend(otherOptions, head), map] as const + ) + ), + UnclusteredFlag: (e) => + matchUnclustered(e.unclustered, e.rest, options, config).pipe(Effect.catchAll(() => Effect.fail(e))) + }) ) - return config.isCaseSensitive - ? Chunk.contains(argumentNames, s) - : Chunk.some(argumentNames, (name) => name.toLowerCase() === s.toLowerCase()) - } - - const createChunk = (input: ReadonlyArray): Chunk.Chunk => - Chunk.fromIterable(RA.filter(input, (s) => !s.startsWith("-"))) - - const tuple = RA.span(RA.fromIterable(input), (s) => !s.startsWith("-") || supports(s, config)) - const remaining = tuple[1] - - let chunk: Chunk.Chunk = Chunk.prepend(createChunk(tuple[0]), first) - if (max < Infinity) { - chunk = Chunk.take(chunk, max) } - - return [remaining, chunk] + return Effect.succeed([input, ReadonlyArray.empty(), HashMap.empty()]) } -const processSingleArg = ( - self: Single, - arg: string, - remaining: ReadonlyArray, +const matchUnclustered = ( + input: ReadonlyArray, + tail: ReadonlyArray, + options: ReadonlyArray, config: CliConfig.CliConfig -): readonly [ReadonlyArray, boolean] => { - const process = (predicate: Predicate): readonly [ReadonlyArray, boolean] => { - if (predicate(arg)) { - return [remaining, true] - } - if (arg.startsWith("--")) { - const splitArg = arg.split("=") - return splitArg.length === 2 - ? [RA.prepend(remaining, splitArg[1]), predicate(splitArg[0])] - : [remaining, false] - } - return [remaining, false] - } - return config.isCaseSensitive - ? process((arg) => Chunk.contains(singleNames(self), arg)) - : process((arg) => Chunk.some(singleNames(self), (name) => name.toLowerCase() === arg.toLowerCase())) -} - -const combineUids = (left: Instruction, right: Instruction): Option.Option => { - const l = uidMap[left._tag](left as any) - const r = uidMap[right._tag](right as any) - if (Option.isNone(l) && Option.isNone(r)) { - return Option.none() - } - if (Option.isNone(l) && Option.isSome(r)) { - return Option.some(r.value) - } - if (Option.isSome(l) && Option.isNone(r)) { - return Option.some(l.value) +): Effect.Effect< + never, + ValidationError.ValidationError, + readonly [ + ReadonlyArray, + ReadonlyArray, + HashMap.HashMap> + ] +> => { + if (ReadonlyArray.isNonEmptyReadonlyArray(input)) { + const flag = ReadonlyArray.headNonEmpty(input) + const otherFlags = ReadonlyArray.tailNonEmpty(input) + return findOptions(ReadonlyArray.of(flag), options, config).pipe( + Effect.flatMap(([_, opts1, map1]) => { + if (HashMap.isEmpty(map1)) { + return Effect.fail( + InternalValidationError.unclusteredFlag(InternalHelpDoc.empty, ReadonlyArray.empty(), tail) + ) + } + return matchUnclustered(otherFlags, tail, opts1, config).pipe( + Effect.map(([_, opts2, map2]) => [tail, opts2, merge(map1, ReadonlyArray.fromIterable(map2))]) + ) + }) + ) } - return Option.some(`${(l as Option.Some).value}, ${(r as Option.Some).value}`) + return Effect.succeed([tail, options, HashMap.empty()]) } -const tuple = >>(tuple: T): Options.Options< - { - [K in keyof T]: [T[K]] extends [Options.Options] ? A : never +/** + * Sums the list associated with the same key. + */ +const merge = ( + map1: HashMap.HashMap>, + map2: ReadonlyArray]> +): HashMap.HashMap> => { + if (ReadonlyArray.isNonEmptyReadonlyArray(map2)) { + const head = ReadonlyArray.headNonEmpty(map2) + const tail = ReadonlyArray.tailNonEmpty(map2) + const newMap = Option.match(HashMap.get(map1, head[0]), { + onNone: () => HashMap.set(map1, head[0], head[1]), + onSome: (elems) => HashMap.set(map1, head[0], ReadonlyArray.appendAll(elems, head[1])) + }) + return merge(newMap, tail) } -> => { - if (tuple.length === 0) { - return none as any - } - if (tuple.length === 1) { - return map(tuple[0], (x) => [x]) as any - } - let result = map(tuple[0], (x) => [x]) - for (let i = 1; i < tuple.length; i++) { - const options = tuple[i] - result = zipFlatten(result, options) - } - return result as any + return map1 } diff --git a/src/internal/primitive.ts b/src/internal/primitive.ts index 68eada3..b34ec14 100644 --- a/src/internal/primitive.ts +++ b/src/internal/primitive.ts @@ -1,11 +1,14 @@ +import * as Schema from "@effect/schema/Schema" import * as Effect from "effect/Effect" -import { dual, pipe } from "effect/Function" +import { pipe } from "effect/Function" import * as Option from "effect/Option" import { pipeArguments } from "effect/Pipeable" -import type { NonEmptyReadonlyArray } from "effect/ReadonlyArray" +import * as ReadonlyArray from "effect/ReadonlyArray" +import type * as CliConfig from "../CliConfig" import type * as Span from "../HelpDoc/Span" import type * as Primitive from "../Primitive" -import * as span from "./helpDoc/span" +import * as InternalCliConfig from "./cliConfig" +import * as InternalSpan from "./helpDoc/span" const PrimitiveSymbolKey = "@effect/cli/Primitive" @@ -15,191 +18,296 @@ export const PrimitiveTypeId: Primitive.PrimitiveTypeId = Symbol.for( ) as Primitive.PrimitiveTypeId const proto = { - [PrimitiveTypeId]: { - _A: (_: never) => _ - }, + _A: (_: never) => _ +} + +/** @internal */ +export const trueValues = Schema.literal("true", "1", "y", "yes", "on") + +/** @internal */ +export const falseValues = Schema.literal("false", "0", "n", "no", "off") + +/** + * Represents a boolean value. + * + * True values can be passed as one of: `["true", "1", "y", "yes" or "on"]`. + * False value can be passed as one of: `["false", "o", "n", "no" or "off"]`. + * + * @internal + */ +export class Bool implements Primitive.Primitive { + readonly [PrimitiveTypeId] = proto + readonly _tag = "Bool" + + constructor(readonly defaultValue: Option.Option) {} + + get typeName(): string { + return "boolean" + } + + get help(): Span.Span { + return InternalSpan.text("A true or false value.") + } + + get choices(): Option.Option { + return Option.some("true | false") + } + + validate(value: Option.Option, config: CliConfig.CliConfig): Effect.Effect { + return Option.map(value, (str) => InternalCliConfig.normalizeCase(config, str)).pipe( + Option.match({ + onNone: () => Effect.orElseFail(this.defaultValue, () => `Missing default value for boolean parameter`), + onSome: (value) => + Schema.is(trueValues)(value) + ? Effect.succeed(true) + : Schema.is(falseValues)(value) + ? Effect.succeed(false) + : Effect.fail(`Unable to recognize '${value}' as a valid boolean`) + }) + ) + } + pipe() { return pipeArguments(this, arguments) } } -/** @internal */ -export const isBool = (self: Primitive.Primitive): boolean => self._tag === "Bool" +/** + * Represents a date in ISO-8601 format, such as `2007-12-03T10:15:30`. + * + * @internal + */ +export class Date implements Primitive.Primitive { + readonly [PrimitiveTypeId] = proto + readonly _tag = "Date" -/** @internal */ -export const boolean = (defaultValue: Option.Option): Primitive.Primitive => { - const op = Object.create(proto) - op._tag = "Bool" - op.defaultValue = defaultValue - return op + get typeName(): string { + return "date" + } + + get help(): Span.Span { + return InternalSpan.text("A date without a time-zone in the ISO-8601 format, such as 2007-12-03T10:15:30.") + } + + get choices(): Option.Option { + return Option.some("date") + } + + validate(value: Option.Option, _config: CliConfig.CliConfig): Effect.Effect { + return attempt( + value, + this.typeName, + Schema.parse(Schema.dateFromString(Schema.string)) + ) + } + + pipe() { + return pipeArguments(this, arguments) + } } -/** @internal */ -export const choice = (choices: NonEmptyReadonlyArray): Primitive.Primitive => { - const op = Object.create(proto) - op._tag = "Choice" - op.choices = choices - return op +export class Choice implements Primitive.Primitive { + readonly [PrimitiveTypeId] = proto + readonly _tag = "Choice" + + constructor( + readonly alternatives: ReadonlyArray.NonEmptyReadonlyArray + ) {} + + get typeName(): string { + return "choice" + } + + get help(): Span.Span { + const choices = pipe( + ReadonlyArray.map(this.alternatives, ([choice]) => choice), + ReadonlyArray.join(", ") + ) + return InternalSpan.text(`One of the following: ${choices}`) + } + + get choices(): Option.Option { + const choices = pipe( + ReadonlyArray.map(this.alternatives, ([choice]) => choice), + ReadonlyArray.join(" | ") + ) + return Option.some(choices) + } + + validate(value: Option.Option, _config: CliConfig.CliConfig): Effect.Effect { + return Effect.orElseFail( + value, + () => `Choice options to not have a default value` + ).pipe( + Effect.flatMap((value) => + ReadonlyArray.findFirst( + this.alternatives, + ([choice]) => choice === value + ) + ), + Effect.mapBoth({ + onFailure: () => { + const choices = pipe( + ReadonlyArray.map(this.alternatives, ([choice]) => choice), + ReadonlyArray.join(", ") + ) + return `Expected one of the following cases: ${choices}` + }, + onSuccess: ([, value]) => value + }) + ) + } + + pipe() { + return pipeArguments(this, arguments) + } } -const choicesMap: { - [K in Primitive.Primitive["_tag"]]: ( - self: Extract, { _tag: K }> - ) => Option.Option -} = { - Bool: () => Option.some("true | false"), - Date: () => Option.some("date"), - Float: () => Option.none(), - Integer: () => Option.none(), - Choice: (self) => Option.some(self.choices.map((tuple) => tuple[0]).join(" | ")), - Text: () => Option.none() +/** + * Represents a floating point number. + * + * @internal + */ +export class Float implements Primitive.Primitive { + readonly [PrimitiveTypeId] = proto + readonly _tag = "Float" + + get typeName(): string { + return "float" + } + + get help(): Span.Span { + return InternalSpan.text("A floating point number.") + } + + get choices(): Option.Option { + return Option.none() + } + + validate(value: Option.Option, _config: CliConfig.CliConfig): Effect.Effect { + const numberFromString = Schema.string.pipe(Schema.numberFromString) + return attempt(value, this.typeName, Schema.parse(numberFromString)) + } + + pipe() { + return pipeArguments(this, arguments) + } } +/** + * Represents an integer. + * + * @internal + */ +export class Integer implements Primitive.Primitive { + readonly [PrimitiveTypeId] = proto + readonly _tag = "Integer" + + get typeName(): string { + return "integer" + } + + get help(): Span.Span { + return InternalSpan.text("An integer.") + } + + get choices(): Option.Option { + return Option.none() + } + + validate(value: Option.Option, _config: CliConfig.CliConfig): Effect.Effect { + const intFromString = Schema.string.pipe(Schema.numberFromString, Schema.int()) + return attempt(value, this.typeName, Schema.parse(intFromString)) + } + + pipe() { + return pipeArguments(this, arguments) + } +} + +/** + * Represents a user-defined piece of text. + * + * @internal + */ +export class Text implements Primitive.Primitive { + readonly [PrimitiveTypeId] = proto + readonly _tag = "Text" + + get typeName(): string { + return "text" + } + + get help(): Span.Span { + return InternalSpan.text("A user-defined piece of text.") + } + + get choices(): Option.Option { + return Option.none() + } + + validate(value: Option.Option, _config: CliConfig.CliConfig): Effect.Effect { + return attempt(value, this.typeName, Schema.parse(Schema.string)) + } + + pipe() { + return pipeArguments(this, arguments) + } +} + +// ============================================================================= +// Constructors +// ============================================================================= + /** @internal */ -export const choices = (self: Primitive.Primitive): Option.Option => choicesMap[self._tag](self as any) +export const boolean = (defaultValue: Option.Option): Primitive.Primitive => new Bool(defaultValue) /** @internal */ -export const date: Primitive.Primitive = (() => { - const op = Object.create(proto) - op._tag = "Date" - return op -})() +export const choice = ( + alternatives: ReadonlyArray.NonEmptyReadonlyArray<[string, A]> +): Primitive.Primitive => new Choice(alternatives) /** @internal */ -export const float: Primitive.Primitive = (() => { - const op = Object.create(proto) - op._tag = "Float" - return op -})() - -const helpDocMap: { - [K in Primitive.Primitive["_tag"]]: (self: Extract, { _tag: K }>) => Span.Span -} = { - Bool: (_: Primitive.Bool) => span.text("A true or false value."), - Choice: (self: Primitive.Choice) => span.text(`One of: ${self.choices.map(([k]) => k).join(", ")}`), - Date: (_: Primitive.Date) => - span.text("A date without a time-zone in the ISO-8601 format, such as 2007-12-03T10:15:30."), - Float: (_: Primitive.Float) => span.text("A floating point number."), - Integer: (_: Primitive.Integer) => span.text("An integer."), - Text: (_: Primitive.Text) => span.text("A user-defined piece of text.") -} +export const date: Primitive.Primitive = new Date() /** @internal */ -export const helpDoc = (self: Primitive.Primitive): Span.Span => helpDocMap[self._tag](self as any) +export const float: Primitive.Primitive = new Float() /** @internal */ -export const integer: Primitive.Primitive = (() => { - const op = Object.create(proto) - op._tag = "Integer" - return op -})() +export const integer: Primitive.Primitive = new Integer() /** @internal */ -export const text: Primitive.Primitive = (() => { - const op = Object.create(proto) - op._tag = "Text" - return op -})() - -const trueValues: Record = { "true": true, "1": true, "y": true, "yes": true, "on": true } - -const falseValues: Record = { "false": true, "0": true, "n": true, "no": true, "off": true } - -const typeNameMap: { - [K in Primitive.Primitive["_tag"]]: string -} = { - Bool: "boolean", - Choice: "choice", - Date: "date", - Float: "float", - Integer: "integer", - Text: "text" -} +export const text: Primitive.Primitive = new Text() + +// ============================================================================= +// Refinements +// ============================================================================= /** @internal */ -export const typeName = (self: Primitive.Primitive): string => typeNameMap[self._tag] - -const validationMap: { - [K in Primitive.Primitive["_tag"]]: ( - self: Extract, { _tag: K }>, - value: Option.Option - ) => Effect.Effect, { _tag: K }>>> -} = { - Bool: (self: Primitive.Bool, value: Option.Option) => { - if (Option.isSome(value)) { - if (trueValues[value.value] !== undefined) { - return Effect.succeed(true) - } - if (falseValues[value.value] !== undefined) { - return Effect.succeed(false) - } - return Effect.fail(`${JSON.stringify(value.value)} cannot be recognized as valid boolean`) - } - return Effect.orElseFail(self.defaultValue, () => "Missing default value for boolean parameter") - }, - Choice: (self: Primitive.Choice, value: Option.Option) => - pipe( - Effect.orElseFail(value, () => "Choice options do not have a default value"), - Effect.flatMap((value) => { - const found = self.choices.find((tuple) => tuple[0] === value) - return found === undefined - ? Effect.fail(`Expected one of the following cases: ${self.choices.map((tuple) => tuple[0]).join(", ")}`) - : Effect.succeed(found[1]) - }) - ), - Date: (self: Primitive.Date, value: Option.Option) => - attempt( - value, - (string) => { - const ms = globalThis.Date.parse(string) - return Number.isNaN(ms) - ? Effect.fail(`${JSON.stringify(string)} is not a valid date`) - : Effect.succeed(new globalThis.Date(ms)) - }, - typeNameMap[self._tag] - ), - Float: (self: Primitive.Float, value: Option.Option) => - attempt( - value, - (string) => { - const n = Number.parseFloat(string) - return !Number.isNaN(n) && Number.isFinite(n) - ? Effect.succeed(n) - : Effect.fail(`Unable to parse float from "${string}"`) - }, - typeNameMap[self._tag] - ), - Integer: (self: Primitive.Integer, value: Option.Option) => - attempt( - value, - (string) => { - const n = Number.parseInt(string, 10) - return !Number.isNaN(n) && Number.isFinite(n) - ? Effect.succeed(n) - : Effect.fail(`Unable to parse integer from "${string}"`) - }, - typeNameMap[self._tag] - ), - Text: (self: Primitive.Text, value: Option.Option) => attempt(value, Effect.succeed, typeNameMap[self._tag]) -} +export const isPrimitive = (u: unknown): u is Primitive.Primitive => + typeof u === "object" && u != null && PrimitiveTypeId in u /** @internal */ -export const validate = dual< - (value: Option.Option) => (self: Primitive.Primitive) => Effect.Effect, - (self: Primitive.Primitive, value: Option.Option) => Effect.Effect ->(2, (self, value) => validationMap[self._tag](self as any, value)) +export const isBool = (self: Primitive.Primitive): boolean => + isPrimitive(self) && "_tag" in self && self._tag === "Bool" + +// ============================================================================= +// Internals +// ============================================================================= const attempt = ( option: Option.Option, - parse: (value: string) => Effect.Effect, - typeName: string + typeName: string, + parse: (value: string) => Effect.Effect ): Effect.Effect => - pipe( - Effect.orElseFail(option, () => `${typeName} options do not have a default value`), + Effect.orElseFail( + option, + () => `${typeName} options do not have a default value` + ).pipe( Effect.flatMap((value) => Effect.orElseFail( parse(value), - () => `${JSON.stringify(value)} is not a ${typeName}` + () => `'${value}' is not a ${typeName}` ) ) ) diff --git a/src/internal/shellType.ts b/src/internal/shellType.ts index c08470e..bfc58d4 100644 --- a/src/internal/shellType.ts +++ b/src/internal/shellType.ts @@ -1,6 +1,6 @@ import type * as Options from "../Options" import type * as ShellType from "../ShellType" -import * as options from "./options" +import * as InternalOptions from "./options" /** @internal */ export const bash: ShellType.ShellType = { @@ -13,7 +13,7 @@ export const zShell: ShellType.ShellType = { } /** @internal */ -export const shellOption: Options.Options = options.choiceWithValue("shell-type", [ +export const shellOption: Options.Options = InternalOptions.choiceWithValue("shell-type", [ ["sh", bash], ["bash", bash], ["zsh", zShell] diff --git a/src/internal/usage.ts b/src/internal/usage.ts index 39999a9..e9ddcca 100644 --- a/src/internal/usage.ts +++ b/src/internal/usage.ts @@ -1,11 +1,17 @@ -import * as Chunk from "effect/Chunk" -import { dual } from "effect/Function" +import { dual, pipe } from "effect/Function" import * as Option from "effect/Option" +import * as ReadonlyArray from "effect/ReadonlyArray" +import type * as CliConfig from "../CliConfig" import type * as HelpDoc from "../HelpDoc" import type * as Span from "../HelpDoc/Span" import type * as Usage from "../Usage" -import * as _helpDoc from "./helpDoc" -import * as span from "./helpDoc/span" +import * as InternalCliConfig from "./cliConfig" +import * as InternalHelpDoc from "./helpDoc" +import * as InternalSpan from "./helpDoc/span" + +// ============================================================================= +// Constructors +// ============================================================================= /** @internal */ export const empty: Usage.Usage = { @@ -19,7 +25,7 @@ export const mixed: Usage.Usage = { /** @internal */ export const named = ( - names: Chunk.Chunk, + names: ReadonlyArray, acceptedValues: Option.Option ): Usage.Usage => ({ _tag: "Named", @@ -58,43 +64,164 @@ export const concat = dual< right: that })) -const spanMap: { - [K in Usage.Usage["_tag"]]: (self: Extract) => Span.Span -} = { - Empty: () => span.text(""), - Mixed: () => span.text(""), - Named: (self) => { - const acceptedValues = Option.match(self.acceptedValues, { - onNone: () => span.empty, - onSome: (c) => span.concat(span.space, span.text(c)) - }) - const mainSpan = span.concat(span.text(Chunk.join(self.names, ", ")), acceptedValues) - return self.names.length > 1 - ? span.concat(span.text("("), span.concat(mainSpan, span.text(")"))) - : mainSpan - }, - Optional: (self) => { - const usage = spanMap[self.usage._tag](self.usage as any) - return span.concat(span.text("["), span.concat(usage, span.text("]"))) - }, - Repeated: (self) => { - const usage = spanMap[self.usage._tag](self.usage as any) - return span.concat(usage, span.text("...")) - }, - Alternation: (self) => { - const left = spanMap[self.left._tag](self.left as any) - const right = spanMap[self.right._tag](self.right as any) - return span.concat(left, span.concat(span.text("|"), right)) - }, - Concat: (self) => { - const left = spanMap[self.left._tag](self.left as any) - const right = spanMap[self.right._tag](self.right as any) - const separator = span.isEmpty(left) && span.isEmpty(right) - ? span.empty - : span.space - return span.concat(left, span.concat(separator, right)) +// ============================================================================= +// Combinators +// ============================================================================= + +/** @internal */ +export const getHelp = (self: Usage.Usage): HelpDoc.HelpDoc => { + const spans = enumerate(self, InternalCliConfig.defaultConfig) + if (ReadonlyArray.isNonEmptyReadonlyArray(spans)) { + const head = ReadonlyArray.headNonEmpty(spans) + const tail = ReadonlyArray.tailNonEmpty(spans) + if (ReadonlyArray.isNonEmptyReadonlyArray(tail)) { + return pipe( + ReadonlyArray.map(spans, (span) => InternalHelpDoc.p(span)), + ReadonlyArray.reduceRight(InternalHelpDoc.empty, (left, right) => InternalHelpDoc.sequence(left, right)) + ) + } + return InternalHelpDoc.p(head) } + return InternalHelpDoc.empty } /** @internal */ -export const helpDoc = (self: Usage.Usage): HelpDoc.HelpDoc => _helpDoc.p(spanMap[self._tag](self as any)) +export const enumerate = dual< + (config: CliConfig.CliConfig) => (self: Usage.Usage) => ReadonlyArray, + (self: Usage.Usage, config: CliConfig.CliConfig) => ReadonlyArray +>(2, (self, config) => render(simplify(self, config), config)) + +// ============================================================================= +// Internals +// ============================================================================= + +const simplify = (self: Usage.Usage, config: CliConfig.CliConfig): Usage.Usage => { + switch (self._tag) { + case "Empty": { + return empty + } + case "Mixed": { + return mixed + } + case "Named": { + if (Option.isNone(ReadonlyArray.head(render(self, config)))) { + return empty + } + return self + } + case "Optional": { + if (self.usage._tag === "Empty") { + return empty + } + const usage = simplify(self.usage, config) + return usage._tag === "Empty" ? empty : optional(usage) + } + case "Repeated": { + const usage = simplify(self.usage, config) + return usage._tag === "Empty" ? empty : repeated(usage) + } + case "Alternation": { + const leftUsage = simplify(self.left, config) + const rightUsage = simplify(self.right, config) + return leftUsage._tag === "Empty" + ? rightUsage + : rightUsage._tag === "Empty" + ? leftUsage + : alternation(leftUsage, rightUsage) + } + case "Concat": { + const leftUsage = simplify(self.left, config) + const rightUsage = simplify(self.right, config) + return leftUsage._tag === "Empty" + ? rightUsage + : rightUsage._tag === "Empty" + ? leftUsage + : concat(leftUsage, rightUsage) + } + } +} + +const render = (self: Usage.Usage, config: CliConfig.CliConfig): ReadonlyArray => { + switch (self._tag) { + case "Empty": { + return ReadonlyArray.of(InternalSpan.text("")) + } + case "Mixed": { + return ReadonlyArray.of(InternalSpan.text("")) + } + case "Named": { + const typeInfo = config.showTypes + ? Option.match(self.acceptedValues, { + onNone: () => InternalSpan.empty, + onSome: (s) => InternalSpan.concat(InternalSpan.space, InternalSpan.text(s)) + }) + : InternalSpan.empty + const namesToShow = config.showAllNames + ? self.names + : self.names.length > 1 + ? pipe( + ReadonlyArray.filter(self.names, (name) => name.startsWith("--")), + ReadonlyArray.head, + Option.map(ReadonlyArray.of), + Option.getOrElse(() => self.names) + ) + : self.names + const nameInfo = InternalSpan.text(ReadonlyArray.join(namesToShow, ", ")) + return config.showAllNames && self.names.length > 1 + ? ReadonlyArray.of(InternalSpan.spans([ + InternalSpan.text("("), + nameInfo, + typeInfo, + InternalSpan.text(")") + ])) + : ReadonlyArray.of(InternalSpan.concat(nameInfo, typeInfo)) + } + case "Optional": { + return ReadonlyArray.map(render(self.usage, config), (span) => + InternalSpan.spans([ + InternalSpan.text("["), + span, + InternalSpan.text("]") + ])) + } + case "Repeated": { + return ReadonlyArray.map( + render(self.usage, config), + (span) => InternalSpan.concat(span, InternalSpan.text("...")) + ) + } + case "Alternation": { + if ( + self.left._tag === "Repeated" || + self.right._tag === "Repeated" || + self.left._tag === "Concat" || + self.right._tag === "Concat" + ) { + return ReadonlyArray.appendAll( + render(self.left, config), + render(self.right, config) + ) + } + return ReadonlyArray.flatMap( + render(self.left, config), + (left) => + ReadonlyArray.map( + render(self.right, config), + (right) => InternalSpan.spans([left, InternalSpan.text("|"), right]) + ) + ) + } + case "Concat": { + const leftSpan = render(self.left, config) + const rightSpan = render(self.right, config) + const separator = + ReadonlyArray.isNonEmptyReadonlyArray(leftSpan) && ReadonlyArray.isNonEmptyReadonlyArray(rightSpan) + ? InternalSpan.space + : InternalSpan.empty + return ReadonlyArray.flatMap( + leftSpan, + (left) => ReadonlyArray.map(rightSpan, (right) => InternalSpan.spans([left, separator, right])) + ) + } + } +} diff --git a/src/internal/validationError.ts b/src/internal/validationError.ts index b8b5355..efbccc5 100644 --- a/src/internal/validationError.ts +++ b/src/internal/validationError.ts @@ -8,62 +8,141 @@ export const ValidationErrorTypeId: ValidationError.ValidationErrorTypeId = Symb ValidationErrorSymbolKey ) as ValidationError.ValidationErrorTypeId +const proto = (error: HelpDoc.HelpDoc): ValidationError.ValidationError.Proto => ({ + [ValidationErrorTypeId]: ValidationErrorTypeId, + error +}) + /** @internal */ export const isValidationError = (u: unknown): u is ValidationError.ValidationError => typeof u === "object" && u != null && ValidationErrorTypeId in u /** @internal */ -export const isExtraneousValue = (validationError: ValidationError.ValidationError): boolean => - validationError.type === "ExtraneousValue" +export const isCommandMismatch = (self: ValidationError.ValidationError): self is ValidationError.CommandMismatch => + self._tag === "CommandMismatch" /** @internal */ -export const isInvalidValue = (validationError: ValidationError.ValidationError): boolean => - validationError.type === "InvalidValue" +export const isCorrectedFlag = (self: ValidationError.ValidationError): self is ValidationError.CorrectedFlag => + self._tag === "CorrectedFlag" /** @internal */ -export const isMissingValue = (validationError: ValidationError.ValidationError): boolean => - validationError.type === "MissingValue" +export const isInvalidArgument = (self: ValidationError.ValidationError): self is ValidationError.InvalidArgument => + self._tag === "InvalidArgument" /** @internal */ -export const isCommandMismatch = (validationError: ValidationError.ValidationError): boolean => - validationError.type === "CommandMismatch" +export const isInvalidValue = (self: ValidationError.ValidationError): self is ValidationError.InvalidValue => + self._tag === "InvalidValue" /** @internal */ -export const isMissingSubCommand = (validationError: ValidationError.ValidationError): boolean => - validationError.type === "MissingSubCommand" +export const isKeyValuesDetected = (self: ValidationError.ValidationError): self is ValidationError.KeyValuesDetected => + self._tag === "KeyValuesDetected" /** @internal */ -export const isInvalidArgument = (validationError: ValidationError.ValidationError): boolean => - validationError.type === "InvalidArgument" +export const isMissingFlag = (self: ValidationError.ValidationError): self is ValidationError.MissingFlag => + self._tag === "MissingFlag" /** @internal */ -export const make = ( - type: ValidationError.ValidationError.Type, - error: HelpDoc.HelpDoc -): ValidationError.ValidationError => ({ - [ValidationErrorTypeId]: ValidationErrorTypeId, - type, - error -}) +export const isMissingValue = (self: ValidationError.ValidationError): self is ValidationError.MissingValue => + self._tag === "MissingValue" + +/** @internal */ +export const isMissingSubcommand = (self: ValidationError.ValidationError): self is ValidationError.MissingSubcommand => + self._tag === "MissingSubcommand" + +/** @internal */ +export const isNoBuiltInMatch = (self: ValidationError.ValidationError): self is ValidationError.NoBuiltInMatch => + self._tag === "NoBuiltInMatch" + +/** @internal */ +export const isUnclusteredFlag = (self: ValidationError.ValidationError): self is ValidationError.UnclusteredFlag => + self._tag === "UnclusteredFlag" + +/** @internal */ +export const commandMismatch = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => { + const op = Object.create(proto(error)) + op._tag = "CommandMismatch" + op.error = error + return op +} + +/** @internal */ +export const correctedFlag = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => { + const op = Object.create(proto(error)) + op._tag = "CorrectedFlag" + op.error = error + return op +} + +/** @internal */ +export const invalidArgument = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => { + const op = Object.create(proto(error)) + op._tag = "InvalidArgument" + op.error = error + return op +} + +/** @internal */ +export const invalidValue = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => { + const op = Object.create(proto(error)) + op._tag = "InvalidValue" + op.error = error + return op +} /** @internal */ -export const extraneousValue = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => - make("ExtraneousValue", error) +export const keyValuesDetected = ( + error: HelpDoc.HelpDoc, + keyValues: ReadonlyArray +): ValidationError.ValidationError => { + const op = Object.create(proto(error)) + op._tag = "KeyValuesDetected" + op.error = error + op.keyValues = keyValues + return op +} /** @internal */ -export const invalidValue = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => make("InvalidValue", error) +export const missingFlag = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => { + const op = Object.create(proto(error)) + op._tag = "MissingFlag" + op.error = error + return op +} /** @internal */ -export const missingValue = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => make("MissingValue", error) +export const missingValue = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => { + const op = Object.create(proto(error)) + op._tag = "MissingValue" + op.error = error + return op +} /** @internal */ -export const commandMismatch = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => - make("CommandMismatch", error) +export const missingSubcommand = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => { + const op = Object.create(proto(error)) + op._tag = "MissingSubcommand" + op.error = error + return op +} /** @internal */ -export const missingSubCommand = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => - make("MissingSubCommand", error) +export const noBuiltInMatch = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => { + const op = Object.create(proto(error)) + op._tag = "NoBuiltInMatch" + op.error = error + return op +} /** @internal */ -export const invalidArgument = (error: HelpDoc.HelpDoc): ValidationError.ValidationError => - make("InvalidArgument", error) +export const unclusteredFlag = ( + error: HelpDoc.HelpDoc, + unclustered: ReadonlyArray, + rest: ReadonlyArray +): ValidationError.ValidationError => { + const op = Object.create(proto(error)) + op._tag = "UnclusteredFlag" + op.error = error + op.unclustered = unclustered + op.rest = rest + return op +} diff --git a/test/AutoCorrect.test.ts b/test/AutoCorrect.test.ts new file mode 100644 index 0000000..49efc90 --- /dev/null +++ b/test/AutoCorrect.test.ts @@ -0,0 +1,27 @@ +import * as AutoCorrect from "@effect/cli/AutoCorrect" +import * as CliConfig from "@effect/cli/CliConfig" +import { describe, expect, it } from "vitest" + +describe("AutoCorrect", () => { + it("should calculate the correct Levenstein distance between two strings", () => { + expect(AutoCorrect.levensteinDistance("", "", CliConfig.defaultConfig)).toBe(0) + expect(AutoCorrect.levensteinDistance("--force", "", CliConfig.defaultConfig)).toBe(7) + expect(AutoCorrect.levensteinDistance("", "--force", CliConfig.defaultConfig)).toBe(7) + expect(AutoCorrect.levensteinDistance("--force", "force", CliConfig.defaultConfig)).toBe(2) + expect(AutoCorrect.levensteinDistance("--force", "--forc", CliConfig.defaultConfig)).toBe(1) + expect(AutoCorrect.levensteinDistance("foo", "bar", CliConfig.defaultConfig)).toBe(3) + // By default, the configuration is case-insensitive so options are normalized + expect(AutoCorrect.levensteinDistance("--force", "--Force", CliConfig.defaultConfig)).toBe(0) + }) + + it("should take into account the provided case-sensitivity", () => { + const config = CliConfig.make({ isCaseSensitive: true }) + expect(AutoCorrect.levensteinDistance("--force", "--force", config)).toBe(0) + expect(AutoCorrect.levensteinDistance("--FORCE", "--force", config)).toBe(5) + }) + + it("should calculate the correct Levenstein distance for non-ASCII characters", () => { + expect(AutoCorrect.levensteinDistance("とんかつ", "とかつ", CliConfig.defaultConfig)).toBe(1) + expect(AutoCorrect.levensteinDistance("¯\\_(ツ)_/¯", "_(ツ)_/¯", CliConfig.defaultConfig)).toBe(2) + }) +}) diff --git a/test/Command.test.ts b/test/Command.test.ts new file mode 100644 index 0000000..4bd7fc8 --- /dev/null +++ b/test/Command.test.ts @@ -0,0 +1,380 @@ +import * as Args from "@effect/cli/Args" +import * as BuiltInOptions from "@effect/cli/BuiltInOptions" +import * as CliConfig from "@effect/cli/CliConfig" +import * as Command from "@effect/cli/Command" +import * as CommandDirective from "@effect/cli/CommandDirective" +import * as HelpDoc from "@effect/cli/HelpDoc" +import * as Options from "@effect/cli/Options" +import * as Terminal from "@effect/cli/Terminal" +import * as Grep from "@effect/cli/test/utils/grep" +import * as Tail from "@effect/cli/test/utils/tail" +import * as WordCount from "@effect/cli/test/utils/wc" +import * as ValidationError from "@effect/cli/ValidationError" +import * as Doc from "@effect/printer/Doc" +import * as Render from "@effect/printer/Render" +import { Effect, Option, ReadonlyArray, String } from "effect" +import { describe, expect, it } from "vitest" + +const runEffect = (self: Effect.Effect): Promise => + Effect.provide(self, Terminal.layer).pipe(Effect.runPromise) + +describe("Command", () => { + describe("Standard Commands", () => { + it("should validate a command with options followed by arguments", () => + Effect.gen(function*(_) { + const args1 = ReadonlyArray.make("tail", "-n", "100", "foo.log") + const args2 = ReadonlyArray.make("grep", "--after", "2", "--before", "3", "fooBar") + const result1 = yield* _(Tail.command.parse(args1, CliConfig.defaultConfig)) + const result2 = yield* _(Grep.command.parse(args2, CliConfig.defaultConfig)) + const expected1 = { name: "tail", options: 100, args: "foo.log" } + const expected2 = { name: "grep", options: [2, 3], args: "fooBar" } + expect(result1).toEqual(CommandDirective.userDefined(ReadonlyArray.empty(), expected1)) + expect(result2).toEqual(CommandDirective.userDefined(ReadonlyArray.empty(), expected2)) + }).pipe(runEffect)) + + it("should provide auto-correct suggestions for misspelled options", () => + Effect.gen(function*(_) { + const args1 = ReadonlyArray.make("grep", "--afte", "2", "--before", "3", "fooBar") + const args2 = ReadonlyArray.make("grep", "--after", "2", "--efore", "3", "fooBar") + const args3 = ReadonlyArray.make("grep", "--afte", "2", "--efore", "3", "fooBar") + const result1 = yield* _(Effect.flip(Grep.command.parse(args1, CliConfig.defaultConfig))) + const result2 = yield* _(Effect.flip(Grep.command.parse(args2, CliConfig.defaultConfig))) + const result3 = yield* _(Effect.flip(Grep.command.parse(args3, CliConfig.defaultConfig))) + expect(result1).toEqual(ValidationError.correctedFlag(HelpDoc.p( + "The flag '--afte' is not recognized. Did you mean '--after'?" + ))) + expect(result2).toEqual(ValidationError.correctedFlag(HelpDoc.p( + "The flag '--efore' is not recognized. Did you mean '--before'?" + ))) + expect(result3).toEqual(ValidationError.correctedFlag(HelpDoc.p( + "The flag '--afte' is not recognized. Did you mean '--after'?" + ))) + }).pipe(runEffect)) + + it("should return an error if an option is missing", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("grep", "--a", "2", "--before", "3", "fooBar") + const result = yield* _(Effect.flip(Grep.command.parse(args, CliConfig.defaultConfig))) + expect(result).toEqual(ValidationError.missingValue(HelpDoc.sequence( + HelpDoc.p("Expected to find option: '--after'"), + HelpDoc.p("Expected to find option: '--before'") + ))) + }).pipe(runEffect)) + }) + + describe("Alternative Commands", () => { + it("should handle alternative commands", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.of("log") + const command = Command.standard("remote").pipe(Command.orElse(Command.standard("log"))) + const result = yield* _(command.parse(args, CliConfig.defaultConfig)) + const expected = { name: "log", options: void 0, args: void 0 } + expect(result).toEqual(CommandDirective.userDefined(ReadonlyArray.empty(), expected)) + }).pipe(runEffect)) + }) + + describe("Commands with Clustered Options", () => { + it("should treat clustered boolean options as un-clustered options", () => + Effect.gen(function*(_) { + const args1 = ReadonlyArray.make("wc", "-clw", "filename") + const args2 = ReadonlyArray.make("wc", "-c", "-l", "-w", "filename") + const result1 = yield* _(WordCount.command.parse(args1, CliConfig.defaultConfig)) + const result2 = yield* _(WordCount.command.parse(args2, CliConfig.defaultConfig)) + const expected = { name: "wc", options: [true, true, true, true], args: ["filename"] } + expect(result1).toEqual(CommandDirective.userDefined(ReadonlyArray.empty(), expected)) + expect(result2).toEqual(CommandDirective.userDefined(ReadonlyArray.empty(), expected)) + }).pipe(runEffect)) + + it("should not uncluster wrong clusters", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("wc", "-clk") + const result = yield* _(WordCount.command.parse(args, CliConfig.defaultConfig)) + const expected = { name: "wc", options: [false, false, false, true], args: ["-clk"] } + expect(result).toEqual(CommandDirective.userDefined(ReadonlyArray.empty(), expected)) + }).pipe(runEffect)) + + it("should not alter '-'", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("wc", "-") + const result = yield* _(WordCount.command.parse(args, CliConfig.defaultConfig)) + const expected = { name: "wc", options: [false, false, false, true], args: ["-"] } + expect(result).toEqual(CommandDirective.userDefined(ReadonlyArray.empty(), expected)) + }).pipe(runEffect)) + }) + + describe("Subcommands without Options or Arguments", () => { + const options = Options.boolean("verbose").pipe(Options.withAlias("v")) + + const git = Command.standard("git", { options }).pipe(Command.subcommands([ + Command.standard("remote"), + Command.standard("log") + ])) + + it("should match the top-level command if no subcommands are specified", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("git", "-v") + const result = yield* _(git.parse(args, CliConfig.defaultConfig)) + const expected = { name: "git", options: true, args: void 0, subcommand: Option.none() } + expect(result).toEqual(CommandDirective.userDefined([], expected)) + }).pipe(runEffect)) + + it("should match the first subcommand without any surplus arguments", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("git", "remote") + const result = yield* _(git.parse(args, CliConfig.defaultConfig)) + const expected = { + name: "git", + options: false, + args: void 0, + subcommand: Option.some({ name: "remote", options: void 0, args: void 0 }) + } + expect(result).toEqual(CommandDirective.userDefined(ReadonlyArray.empty(), expected)) + }).pipe(runEffect)) + + it("matches the first subcommand with a surplus option", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("git", "remote", "-v") + const result = yield* _(git.parse(args, CliConfig.defaultConfig)) + const expected = { + name: "git", + options: false, + args: void 0, + subcommand: Option.some({ name: "remote", options: void 0, args: void 0 }) + } + expect(result).toEqual(CommandDirective.userDefined(ReadonlyArray.of("-v"), expected)) + }).pipe(runEffect)) + + it("matches the second subcommand without any surplus arguments", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("git", "log") + const result = yield* _(git.parse(args, CliConfig.defaultConfig)) + const expected = { + name: "git", + options: false, + args: void 0, + subcommand: Option.some({ name: "log", options: void 0, args: void 0 }) + } + expect(result).toEqual(CommandDirective.userDefined(ReadonlyArray.empty(), expected)) + }).pipe(runEffect)) + + it("should return an error message for an unknown subcommand", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("git", "abc") + const result = yield* _(Effect.flip(git.parse(args, CliConfig.defaultConfig))) + expect(result).toEqual(ValidationError.commandMismatch(HelpDoc.p( + "Invalid subcommand for git - use one of 'log', 'remote'" + ))) + }).pipe(runEffect)) + }) + + describe("Subcommands with Options and Arguments", () => { + const options = Options.all([ + Options.boolean("i"), + Options.text("empty").pipe(Options.withDefault("drop")) + ]) + + const args = Args.all([Args.text(), Args.text()]) + + const git = Command.standard("git").pipe(Command.subcommands([ + Command.standard("rebase", { options, args }) + ])) + + it("should parse a subcommand with required options and arguments", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("git", "rebase", "-i", "upstream", "branch") + const result = yield* _(git.parse(args, CliConfig.defaultConfig)) + const expected = { + name: "git", + options: void 0, + args: void 0, + subcommand: Option.some({ name: "rebase", options: [true, "drop"], args: ["upstream", "branch"] }) + } + expect(result).toEqual(CommandDirective.userDefined(ReadonlyArray.empty(), expected)) + }).pipe(runEffect)) + + it("should parse a subcommand with required and optional options and arguments", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("git", "rebase", "-i", "--empty", "ask", "upstream", "branch") + const result = yield* _(git.parse(args, CliConfig.defaultConfig)) + const expected = { + name: "git", + options: void 0, + args: void 0, + subcommand: Option.some({ name: "rebase", options: [true, "ask"], args: ["upstream", "branch"] }) + } + expect(result).toEqual(CommandDirective.userDefined(ReadonlyArray.empty(), expected)) + }).pipe(runEffect)) + }) + + describe("Nested Subcommands", () => { + const command = Command.standard("command").pipe(Command.subcommands([ + Command.standard("sub").pipe(Command.subcommands([ + Command.standard("subsub", { options: Options.boolean("i"), args: Args.text() }) + ])) + ])) + + it("should properly parse deeply nested subcommands with options and arguments", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("command", "sub", "subsub", "-i", "text") + const result = yield* _(command.parse(args, CliConfig.defaultConfig)) + const expected = { + name: "command", + options: void 0, + args: void 0, + subcommand: Option.some({ + name: "sub", + options: void 0, + args: void 0, + subcommand: Option.some({ + name: "subsub", + options: true, + args: "text" + }) + }) + } + expect(result).toEqual(CommandDirective.userDefined(ReadonlyArray.empty(), expected)) + }).pipe(runEffect)) + }) + + describe("Help Documentation", () => { + it("should allow adding help documentation to a command", () => + Effect.gen(function*(_) { + const cmd = Command.standard("tldr").pipe(Command.withHelp("this is some help")) + const args = ReadonlyArray.of("tldr") + const result = yield* _(cmd.parse(args, CliConfig.defaultConfig)) + const expectedValue = { name: "tldr", options: void 0, args: void 0 } + const expectedDoc = HelpDoc.sequence( + HelpDoc.h1("DESCRIPTION"), + HelpDoc.p("this is some help") + ) + expect(result).toEqual(CommandDirective.userDefined(ReadonlyArray.empty(), expectedValue)) + expect(cmd.help).toEqual(expectedDoc) + }).pipe(runEffect)) + + it("should allow adding help documentation to subcommands", () => { + const cmd = Command.standard("command").pipe(Command.subcommands([ + Command.standard("sub").pipe(Command.withHelp("this is some help")) + ])) + const expected = HelpDoc.sequence(HelpDoc.h1("DESCRIPTION"), HelpDoc.p("this is some help")) + expect(cmd.help).not.toEqual(expected) + }) + + it("should correctly display help documentation for a command", () => { + const child3 = Command.standard("child3").pipe(Command.withHelp("help 3")) + const child2 = Command.standard("child2").pipe(Command.withHelp("help 2"), Command.orElse(child3)) + const child1 = Command.standard("child1").pipe(Command.subcommands([child2]), Command.withHelp("help 1")) + const parent = Command.standard("parent").pipe(Command.subcommands([child1])) + const result = Render.prettyDefault(Doc.unAnnotate(HelpDoc.toAnsiDoc(parent.help))) + expect(result).toBe(String.stripMargin( + `|COMMANDS + | + | - child1 help 1 + | + | - child2 child1 help 2 + | + | - child3 child1 help 3 + |` + )) + }) + }) + + describe("Built-In Options Processing", () => { + const command = Command.standard("command", { options: Options.text("a") }) + const params1 = ReadonlyArray.make("command", "--help") + const params2 = ReadonlyArray.make("command", "-h") + const params3 = ReadonlyArray.make("command", "--wizard") + const params4 = ReadonlyArray.make("command", "--shell-completion-index", "1", "--shell-type", "sh") + const params5 = ReadonlyArray.make("command", "-a", "--help") + const params6 = ReadonlyArray.make("command", "--help", "--wizard", "-b") + const params7 = ReadonlyArray.make("command", "-hdf", "--help") + const params8 = ReadonlyArray.make("command", "-af", "asdgf", "--wizard") + + const directiveType = (directive: CommandDirective.CommandDirective): string => { + if (CommandDirective.isBuiltIn(directive)) { + if (BuiltInOptions.isShowHelp(directive.option)) { + return "help" + } + if (BuiltInOptions.isShowWizard(directive.option)) { + return "wizard" + } + if (BuiltInOptions.isShowCompletions(directive.option)) { + return "completions" + } + if (BuiltInOptions.isShowCompletionScript(directive.option)) { + return "script" + } + } + return "user" + } + + it("should trigger built-in options if they are alone", () => + Effect.gen(function*(_) { + const result1 = yield* _(command.parse(params1, CliConfig.defaultConfig), Effect.map(directiveType)) + const result2 = yield* _(command.parse(params2, CliConfig.defaultConfig), Effect.map(directiveType)) + const result3 = yield* _(command.parse(params3, CliConfig.defaultConfig), Effect.map(directiveType)) + const result4 = yield* _(command.parse(params4, CliConfig.defaultConfig), Effect.map(directiveType)) + expect(result1).toBe("help") + expect(result2).toBe("help") + expect(result3).toBe("wizard") + expect(result4).toBe("completions") + }).pipe(runEffect)) + + it("should not trigger help if an option matches", () => + Effect.gen(function*(_) { + const result = yield* _(command.parse(params5, CliConfig.defaultConfig), Effect.map(directiveType)) + expect(result).toBe("user") + }).pipe(runEffect)) + + it("should trigger help even if not alone", () => + Effect.gen(function*(_) { + const result1 = yield* _(command.parse(params6, CliConfig.defaultConfig), Effect.map(directiveType)) + const result2 = yield* _(command.parse(params7, CliConfig.defaultConfig), Effect.map(directiveType)) + expect(result1).toBe("help") + expect(result2).toBe("help") + }).pipe(runEffect)) + + it("should trigger wizard even if not alone", () => + Effect.gen(function*(_) { + const result = yield* _(command.parse(params8, CliConfig.defaultConfig), Effect.map(directiveType)) + expect(result).toBe("wizard") + }).pipe(runEffect)) + }) + + describe("End of Command Options Symbol", () => { + const command = Command.standard("cmd", { + options: Options.all([ + Options.optional(Options.text("something")), + Options.boolean("verbose").pipe(Options.withAlias("v")) + ]), + args: Args.repeated(Args.text()) + }) + + it("should properly handle the end of command options symbol", () => + Effect.gen(function*(_) { + const args1 = ReadonlyArray.make("cmd", "-v", "--something", "abc", "something") + const args2 = ReadonlyArray.make("cmd", "-v", "--", "--something", "abc", "something") + const args3 = ReadonlyArray.make("cmd", "--", "-v", "--something", "abc", "something") + const result1 = yield* _(command.parse(args1, CliConfig.defaultConfig)) + const result2 = yield* _(command.parse(args2, CliConfig.defaultConfig)) + const result3 = yield* _(command.parse(args3, CliConfig.defaultConfig)) + const expected1 = { + name: "cmd", + options: [Option.some("abc"), true], + args: ReadonlyArray.of("something") + } + const expected2 = { + name: "cmd", + options: [Option.none(), true], + args: ReadonlyArray.make("--something", "abc", "something") + } + const expected3 = { + name: "cmd", + options: [Option.none(), false], + args: ReadonlyArray.make("-v", "--something", "abc", "something") + } + expect(result1).toEqual(CommandDirective.userDefined(ReadonlyArray.empty(), expected1)) + expect(result2).toEqual(CommandDirective.userDefined(ReadonlyArray.empty(), expected2)) + expect(result3).toEqual(CommandDirective.userDefined(ReadonlyArray.empty(), expected3)) + }).pipe(runEffect)) + }) +}) diff --git a/test/Command.ts b/test/Command.ts deleted file mode 100644 index 2bc42ec..0000000 --- a/test/Command.ts +++ /dev/null @@ -1,243 +0,0 @@ -import * as Args from "@effect/cli/Args" -import * as CliConfig from "@effect/cli/CliConfig" -import * as Command from "@effect/cli/Command" -import * as CommandDirective from "@effect/cli/CommandDirective" -import * as HelpDoc from "@effect/cli/HelpDoc" -import * as Span from "@effect/cli/HelpDoc/Span" -import * as Options from "@effect/cli/Options" -import * as it from "@effect/cli/test/utils/extend" -import * as Grep from "@effect/cli/test/utils/grep" -import * as Tail from "@effect/cli/test/utils/tail" -import * as WC from "@effect/cli/test/utils/wc" -import * as ValidationError from "@effect/cli/ValidationError" -import * as Effect from "effect/Effect" -import { pipe } from "effect/Function" -import * as Option from "effect/Option" -import { describe, expect } from "vitest" - -describe.concurrent("Command", () => { - it.effect("validates a command with options followed by args", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const args1 = ["tail", "-n", "100", "foo.log"] - const args2 = ["grep", "--after", "2", "--before", "3", "fooBar"] - const result1 = yield* $(Command.parse(Tail.command, args1, config)) - const result2 = yield* $(Command.parse(Grep.command, args2, config)) - const expected1 = { name: "tail", options: 100, args: "foo.log" } - const expected2 = { name: "grep", options: [2, 3], args: "fooBar" } - expect(result1).toEqual(CommandDirective.userDefined([], expected1)) - expect(result2).toEqual(CommandDirective.userDefined([], expected2)) - })) - - it.effect("provides auto-correct suggestions for misspelled options", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const args1 = ["grep", "--afte", "2", "--before", "3", "fooBar"] - const args2 = ["grep", "--after", "2", "--efore", "3", "fooBar"] - const args3 = ["grep", "--afte", "2", "--efore", "3", "fooBar"] - const result1 = yield* $(Effect.flip(Command.parse(Grep.command, args1, config))) - const result2 = yield* $(Effect.flip(Command.parse(Grep.command, args2, config))) - const result3 = yield* $(Effect.flip(Command.parse(Grep.command, args3, config))) - expect(result1).toEqual(ValidationError.invalidValue(HelpDoc.p(Span.error( - "The flag '--afte' is not recognized. Did you mean '--after'?" - )))) - expect(result2).toEqual(ValidationError.invalidValue(HelpDoc.p(Span.error( - "The flag '--efore' is not recognized. Did you mean '--before'?" - )))) - expect(result3).toEqual(ValidationError.missingValue(HelpDoc.sequence( - HelpDoc.p(Span.error("The flag '--afte' is not recognized. Did you mean '--after'?")), - HelpDoc.p(Span.error("The flag '--efore' is not recognized. Did you mean '--before'?")) - ))) - })) - - it.effect("shows an error if an option is missing", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const args = ["grep", "--a", "2", "--before", "3", "fooBar"] - const result = yield* $(Effect.flip(Command.parse(Grep.command, args, config))) - expect(result).toEqual(ValidationError.missingValue(HelpDoc.p(Span.error( - "Expected to find option: '--after'" - )))) - })) - - it.effect("should handle alternative commands", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const args = ["log"] - const command = Command.make("remote").pipe(Command.orElse(Command.make("log"))) - const result = yield* $(Command.parse(command, args, config)) - const expected = { name: "log", options: void 0, args: void 0 } - expect(result).toEqual(CommandDirective.userDefined([], expected)) - })) - - it.effect("should treat clustered boolean options as un-clustered options", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const args1 = ["wc", "-clw", "filename"] - const args2 = ["wc", "-c", "-l", "-w", "filename"] - const clustered = yield* $( - Effect.map( - Command.parse(WC.command, args1, config), - CommandDirective.map((result) => ({ ...result, args: Array.from(result.args) })) - ) - ) - const unclustered = yield* $( - Effect.map( - Command.parse(WC.command, args2, config), - CommandDirective.map((result) => ({ ...result, args: Array.from(result.args) })) - ) - ) - const expected = CommandDirective.userDefined([], { - name: "wc", - options: [true, true, true, false], - args: ["filename"] - }) - expect(clustered).toEqual(expected) - expect(unclustered).toEqual(expected) - })) - - describe.concurrent("Subcommands - no options or arguments", () => { - const git = Command.make("git", { options: Options.boolean("verbose").pipe(Options.alias("v")) }).pipe( - Command.subcommands([Command.make("remote"), Command.make("log")]) - ) - - it.effect("matches the top-level command if no subcommands are specified", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const args = ["git", "-v"] - const result = yield* $(Command.parse(git, args, config)) - const expected = { name: "git", options: true, args: void 0, subcommand: Option.none() } - expect(result).toEqual(CommandDirective.userDefined([], expected)) - })) - - it.effect("matches the first subcommand without any surplus arguments", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const args = ["git", "remote"] - const result = yield* $(Command.parse(git, args, config)) - const expected = { - name: "git", - options: false, - args: void 0, - subcommand: Option.some({ name: "remote", options: void 0, args: void 0 }) - } - expect(result).toEqual(CommandDirective.userDefined([], expected)) - })) - - it.effect("matches the first subcommand with a surplus option", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const args = ["git", "remote", "-m"] - const result = yield* $(Command.parse(git, args, config)) - const expected = { - name: "git", - options: false, - args: void 0, - subcommand: Option.some({ name: "remote", options: void 0, args: void 0 }) - } - expect(result).toEqual(CommandDirective.userDefined(["-m"], expected)) - })) - - it.effect("matches the second subcommand without any surplus arguments", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const args = ["git", "log"] - const result = yield* $(Command.parse(git, args, config)) - const expected = { - name: "git", - options: false, - args: void 0, - subcommand: Option.some({ name: "log", options: void 0, args: void 0 }) - } - expect(result).toEqual(CommandDirective.userDefined([], expected)) - })) - }) - - describe.concurrent("Subcommands - with options and arguments", () => { - const rebaseOptions = Options.boolean("i").pipe( - Options.zip(Options.text("empty").pipe(Options.withDefault("drop"))) - ) - const rebaseArgs = Args.zip(Args.text(), Args.text()) - const git = Command.make("git").pipe( - Command.subcommands([ - Command.make("rebase", { options: rebaseOptions, args: rebaseArgs }) - ]) - ) - - it.effect("subcommand with required options and arguments", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const args = ["git", "rebase", "-i", "upstream", "branch"] - const result = yield* $(Command.parse(git, args, config)) - const expected = { - name: "git", - options: void 0, - args: void 0, - subcommand: Option.some({ name: "rebase", options: [true, "drop"], args: ["upstream", "branch"] }) - } - expect(result).toEqual(CommandDirective.userDefined([], expected)) - })) - - it.effect("subcommand with required and optional options and arguments", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const args = ["git", "rebase", "-i", "--empty", "ask", "upstream", "branch"] - const result = yield* $(Command.parse(git, args, config)) - const expected = { - name: "git", - options: void 0, - args: void 0, - subcommand: Option.some({ name: "rebase", options: [true, "ask"], args: ["upstream", "branch"] }) - } - expect(result).toEqual(CommandDirective.userDefined([], expected)) - })) - - it.effect("subcommand that is unknown", () => - Effect.gen(function*($) { - const git = pipe( - Command.make("git", { options: Options.alias(Options.boolean("verbose"), "v") }), - Command.subcommands([ - Command.make("remote", { options: Options.alias(Options.boolean("verbose"), "v") }), - Command.make("log") - ]) - ) - const config = CliConfig.defaultConfig - const args = ["git", "abc"] - const result = yield* $(Effect.flip(Command.parse(git, args, config))) - expect(result).toEqual(ValidationError.commandMismatch(HelpDoc.p(Span.error("Missing command name: 'log'")))) - })) - }) - - describe.concurrent("Subcommands - nested", () => { - const command = Command.make("command").pipe( - Command.subcommands([ - Command.make("sub").pipe( - Command.subcommands([Command.make("subsub", { options: Options.boolean("i"), args: Args.text() })]) - ) - ]) - ) - - it.effect("deeply nested subcommands with an option and argument", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const args = ["command", "sub", "subsub", "-i", "text"] - const result = yield* $(Command.parse(command, args, config)) - const expected = { - name: "command", - options: void 0, - args: void 0, - subcommand: Option.some({ - name: "sub", - options: void 0, - args: void 0, - subcommand: Option.some({ - name: "subsub", - options: true, - args: "text" - }) - }) - } - expect(result).toEqual(CommandDirective.userDefined([], expected)) - })) - }) -}) diff --git a/test/Options.test.ts b/test/Options.test.ts new file mode 100644 index 0000000..4d3f73b --- /dev/null +++ b/test/Options.test.ts @@ -0,0 +1,420 @@ +import * as CliConfig from "@effect/cli/CliConfig" +import * as HelpDoc from "@effect/cli/HelpDoc" +import * as Options from "@effect/cli/Options" +import * as ValidationError from "@effect/cli/ValidationError" +import { Data, Effect, Either, HashMap, Option, ReadonlyArray } from "effect" +import { describe, expect, it } from "vitest" + +const firstName = Options.text("firstName").pipe(Options.withAlias("f")) +const lastName = Options.text("lastName") +const age = Options.integer("age") +const ageOptional = Options.optional(age) +const verbose = Options.boolean("verbose", { ifPresent: true }) +const defs = Options.keyValueMap("defs").pipe(Options.withAlias("d")) + +const validation = ( + options: Options.Options, + args: ReadonlyArray, + config: CliConfig.CliConfig +): Effect.Effect, A]> => + Options.validate(options, args, config).pipe(Effect.flatMap(([err, rest, a]) => + Option.match(err, { + onNone: () => Effect.succeed([rest, a]), + onSome: Effect.fail + }) + )) + +describe("Options", () => { + it("should validate without ambiguity", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("--firstName", "--lastName", "--lastName", "--firstName") + const result1 = yield* _(validation(Options.all([firstName, lastName]), args, CliConfig.defaultConfig)) + const result2 = yield* _(validation(Options.all([lastName, firstName]), args, CliConfig.defaultConfig)) + const expected1 = [ReadonlyArray.empty(), ReadonlyArray.make("--lastName", "--firstName")] + const expected2 = [ReadonlyArray.empty(), ReadonlyArray.make("--firstName", "--lastName")] + expect(result1).toEqual(expected1) + expect(result2).toEqual(expected2) + }).pipe(Effect.runPromise)) + + it("should not uncluster values", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("--firstName", "-ab") + const result = yield* _(validation(firstName, args, CliConfig.defaultConfig)) + const expected = [ReadonlyArray.empty(), "-ab"] + expect(result).toEqual(expected) + }).pipe(Effect.runPromise)) + + it("should return a HelpDoc if an option is not an exact match and it's a short option", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("--ag", "20") + const result = yield* _(Effect.flip(validation(age, args, CliConfig.defaultConfig))) + expect(result).toEqual(ValidationError.missingValue(HelpDoc.p( + "Expected to find option: '--age'" + ))) + }).pipe(Effect.runPromise)) + + it("validates a boolean option without a value", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("--verbose") + const result = yield* _(validation(verbose, args, CliConfig.defaultConfig)) + const expected = [ReadonlyArray.empty(), true] + expect(result).toEqual(expected) + }).pipe(Effect.runPromise)) + + it("validates a boolean option with a followup option", () => + Effect.gen(function*(_) { + const options = Options.all([Options.boolean("help"), Options.boolean("v")]) + const args1 = ReadonlyArray.empty() + const args2 = ReadonlyArray.make("--help") + const args3 = ReadonlyArray.make("--help", "-v") + const result1 = yield* _(validation(options, args1, CliConfig.defaultConfig)) + const result2 = yield* _(validation(options, args2, CliConfig.defaultConfig)) + const result3 = yield* _(validation(options, args3, CliConfig.defaultConfig)) + const expected1 = [ReadonlyArray.empty(), [false, false]] + const expected2 = [ReadonlyArray.empty(), [true, false]] + const expected3 = [ReadonlyArray.empty(), [true, true]] + expect(result1).toEqual(expected1) + expect(result2).toEqual(expected2) + expect(result3).toEqual(expected3) + }).pipe(Effect.runPromise)) + + it("validates a boolean option with negation", () => + Effect.gen(function*(_) { + const option = Options.boolean("verbose", { aliases: ["v"], negationNames: ["silent", "s"] }) + const result1 = yield* _(validation(option, [], CliConfig.defaultConfig)) + const result2 = yield* _(validation(option, ["--verbose"], CliConfig.defaultConfig)) + const result3 = yield* _(validation(option, ["-v"], CliConfig.defaultConfig)) + const result4 = yield* _(validation(option, ["--silent"], CliConfig.defaultConfig)) + const result5 = yield* _(validation(option, ["-s"], CliConfig.defaultConfig)) + const result6 = yield* _(Effect.flip(validation(option, ["--verbose", "--silent"], CliConfig.defaultConfig))) + const result7 = yield* _(Effect.flip(validation(option, ["-v", "-s"], CliConfig.defaultConfig))) + expect(result1).toEqual([[], false]) + expect(result2).toEqual([[], true]) + expect(result3).toEqual([[], true]) + expect(result4).toEqual([[], false]) + expect(result5).toEqual([[], false]) + expect(result6).toEqual(ValidationError.invalidValue(HelpDoc.p( + "Collision between two options detected - " + + "you can only specify one of either: ['--verbose', '--silent']" + ))) + expect(result7).toEqual(ValidationError.invalidValue(HelpDoc.p( + "Collision between two options detected - " + + "you can only specify one of either: ['--verbose', '--silent']" + ))) + }).pipe(Effect.runPromise)) + + it("does not validate collision of boolean options with negation", () => + Effect.gen(function*(_) { + const option = Options.boolean("v", { negationNames: ["s"] }) + const args = ReadonlyArray.make("-v", "-s") + const result = yield* _(Effect.flip(validation(option, args, CliConfig.defaultConfig))) + expect(result).toEqual(ValidationError.invalidValue(HelpDoc.p( + "Collision between two options detected - " + + "you can only specify one of either: ['-v', '-s']" + ))) + }).pipe(Effect.runPromise)) + + it("validates a text option", () => + Effect.gen(function*(_) { + const result = yield* _(validation(firstName, ["--firstName", "John"], CliConfig.defaultConfig)) + expect(result).toEqual([[], "John"]) + }).pipe(Effect.runPromise)) + + it("validates a text option with an alternative format", () => + Effect.gen(function*(_) { + const result = yield* _(validation(firstName, ["--firstName=John"], CliConfig.defaultConfig)) + expect(result).toEqual([[], "John"]) + }).pipe(Effect.runPromise)) + + it("validates a text option with an alias", () => + Effect.gen(function*(_) { + const result = yield* _(validation(firstName, ["-f", "John"], CliConfig.defaultConfig)) + expect(result).toEqual([[], "John"]) + }).pipe(Effect.runPromise)) + + it("validates an integer option", () => + Effect.gen(function*(_) { + const result = yield* _(validation(age, ["--age", "100"], CliConfig.defaultConfig)) + expect(result).toEqual([[], 100]) + }).pipe(Effect.runPromise)) + + it("validates an option and returns the remainder", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("--firstName", "John", "--lastName", "Doe") + const result = yield* _(validation(firstName, args, CliConfig.defaultConfig)) + expect(result).toEqual([["--lastName", "Doe"], "John"]) + }).pipe(Effect.runPromise)) + + it("does not validate when no valid values are passed", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("--lastName", "Doe") + const result = yield* _(Effect.either(validation(firstName, args, CliConfig.defaultConfig))) + expect(result).toEqual(Either.left(ValidationError.missingValue(HelpDoc.p( + "Expected to find option: '--firstName'" + )))) + }).pipe(Effect.runPromise)) + + it("does not validate when an option is passed without a corresponding value", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("--firstName") + const result = yield* _(Effect.either(validation(firstName, args, CliConfig.defaultConfig))) + expect(result).toEqual(Either.left(ValidationError.missingValue(HelpDoc.p( + "Expected a value following option: '--firstName'" + )))) + }).pipe(Effect.runPromise)) + + it("does not validate an invalid option value", () => + Effect.gen(function*(_) { + const option = Options.integer("t") + const args = ReadonlyArray.make("-t", "abc") + const result = yield* _(Effect.flip(validation(option, args, CliConfig.defaultConfig))) + expect(result).toEqual(ValidationError.invalidValue(HelpDoc.p("'abc' is not a integer"))) + }).pipe(Effect.runPromise)) + + it("does not validate an invalid option value even when there is a default", () => + Effect.gen(function*(_) { + const option = Options.withDefault(Options.integer("t"), 0) + const args = ReadonlyArray.make("-t", "abc") + const result = yield* _(Effect.flip(validation(option, args, CliConfig.defaultConfig))) + expect(result).toEqual(ValidationError.invalidValue(HelpDoc.p("'abc' is not a integer"))) + }).pipe(Effect.runPromise)) + + it("validates with case-sensitive configuration", () => + Effect.gen(function*(_) { + const config = CliConfig.make({ isCaseSensitive: true, autoCorrectLimit: 2 }) + const option = Options.text("Firstname").pipe(Options.withAlias("F")) + const args1 = ReadonlyArray.make("--Firstname", "John") + const args2 = ReadonlyArray.make("-F", "John") + const args3 = ReadonlyArray.make("--firstname", "John") + const args4 = ReadonlyArray.make("-f", "John") + const result1 = yield* _(validation(option, args1, config)) + const result2 = yield* _(validation(option, args2, config)) + const result3 = yield* _(Effect.flip(validation(option, args3, config))) + const result4 = yield* _(Effect.flip(validation(option, args4, config))) + expect(result1).toEqual([[], "John"]) + expect(result2).toEqual([[], "John"]) + expect(result3).toEqual(ValidationError.correctedFlag(HelpDoc.p( + "The flag '--firstname' is not recognized. Did you mean '--Firstname'?" + ))) + expect(result4).toEqual(ValidationError.missingValue(HelpDoc.p( + "Expected to find option: '--Firstname'" + ))) + }).pipe(Effect.runPromise)) + + it("validates an unsupplied optional option", () => + Effect.gen(function*(_) { + const result = yield* _(validation(ageOptional, [], CliConfig.defaultConfig)) + expect(result).toEqual([[], Option.none()]) + }).pipe(Effect.runPromise)) + + it("validates an unsupplied optional option with remainder", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("--bar", "baz") + const result = yield* _(validation(ageOptional, args, CliConfig.defaultConfig)) + expect(result).toEqual([args, Option.none()]) + }).pipe(Effect.runPromise)) + + it("validates a supplied optional option", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("--age", "20") + const result = yield* _(validation(ageOptional, args, CliConfig.defaultConfig)) + expect(result).toEqual([[], Option.some(20)]) + }).pipe(Effect.runPromise)) + + it("validates using all and returns the specified structure", () => + Effect.gen(function*(_) { + const option1 = Options.all({ + firstName: Options.text("firstName"), + lastName: Options.text("lastName") + }) + const option2 = Options.all([Options.text("firstName"), Options.text("lastName")]) + const args = ReadonlyArray.make("--firstName", "John", "--lastName", "Doe") + const result1 = yield* _(validation(option1, args, CliConfig.defaultConfig)) + const result2 = yield* _(validation(option2, args, CliConfig.defaultConfig)) + expect(result1).toEqual([[], { firstName: "John", lastName: "Doe" }]) + expect(result2).toEqual([[], ["John", "Doe"]]) + }).pipe(Effect.runPromise)) + + it("validate provides a suggestion if a provided option is close to a specified option", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("--firstme", "Alice") + const result = yield* _(Effect.flip(validation(firstName, args, CliConfig.defaultConfig))) + expect(result).toEqual(ValidationError.correctedFlag(HelpDoc.p( + "The flag '--firstme' is not recognized. Did you mean '--firstName'?" + ))) + }).pipe(Effect.runPromise)) + + it("validate provides a suggestion if a provided option with a default is close to a specified option", () => + Effect.gen(function*(_) { + const option = firstName.pipe(Options.withDefault("Jack")) + const args = ReadonlyArray.make("--firstme", "Alice") + const result = yield* _(Effect.flip(validation(option, args, CliConfig.defaultConfig))) + expect(result).toEqual(ValidationError.invalidValue(HelpDoc.p( + "The flag '--firstme' is not recognized. Did you mean '--firstName'?" + ))) + })) + + it("orElse - two options", () => + Effect.gen(function*(_) { + const option = Options.text("string").pipe( + Options.map(Either.left), + Options.orElse( + Options.integer("integer").pipe( + Options.map(Either.right) + ) + ) + ) + const args1 = ReadonlyArray.make("--integer", "2") + const args2 = ReadonlyArray.make("--string", "two") + const result1 = yield* _(validation(option, args1, CliConfig.defaultConfig)) + const result2 = yield* _(validation(option, args2, CliConfig.defaultConfig)) + expect(result1).toEqual([[], Either.right(2)]) + expect(result2).toEqual([[], Either.left("two")]) + }).pipe(Effect.runPromise)) + + it("orElse - option collision", () => + Effect.gen(function*(_) { + const option = Options.orElse(Options.text("string"), Options.integer("integer")) + const args = ReadonlyArray.make("--integer", "2", "--string", "two") + const result = yield* _(Effect.flip(validation(option, args, CliConfig.defaultConfig))) + expect(result).toEqual(ValidationError.invalidValue(HelpDoc.p( + "Collision between two options detected - " + + "you can only specify one of either: ['--string', '--integer']" + ))) + }).pipe(Effect.runPromise)) + + it("orElse - no options provided", () => + Effect.gen(function*(_) { + const option = Options.orElse(Options.text("string"), Options.integer("integer")) + const result = yield* _(Effect.flip(Options.validate(option, [], CliConfig.defaultConfig))) + const error = ValidationError.missingValue(HelpDoc.sequence( + HelpDoc.p("Expected to find option: '--string'"), + HelpDoc.p("Expected to find option: '--integer'") + )) + expect(result).toEqual(error) + }).pipe(Effect.runPromise)) + + it("orElse - invalid option provided with a default", () => + Effect.gen(function*(_) { + const option = Options.integer("min").pipe( + Options.orElse(Options.integer("max")), + Options.withDefault(0) + ) + const args = ReadonlyArray.make("--min", "abc") + const result = yield* _(Effect.flip(Options.validate(option, args, CliConfig.defaultConfig))) + const error = ValidationError.invalidValue(HelpDoc.sequence( + HelpDoc.p("'abc' is not a integer"), + HelpDoc.p("Expected to find option: '--max'") + )) + expect(result).toEqual(error) + }).pipe(Effect.runPromise)) + + it("keyValueMap - validates a missing option", () => + Effect.gen(function*(_) { + const result = yield* _(Effect.flip(validation(defs, [], CliConfig.defaultConfig))) + expect(result).toEqual(ValidationError.missingValue(HelpDoc.p( + "Expected to find option: '--defs'" + ))) + }).pipe(Effect.runPromise)) + + it("keyValueMap - validates repeated values", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("-d", "key1=v1", "-d", "key2=v2", "--verbose") + const result = yield* _(validation(defs, args, CliConfig.defaultConfig)) + expect(result).toEqual([["--verbose"], HashMap.make(["key1", "v1"], ["key2", "v2"])]) + }).pipe(Effect.runPromise)) + + it("keyValueMap - validates different key/values", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("--defs", "key1=v1", "key2=v2", "--verbose") + const result = yield* _(validation(defs, args, CliConfig.defaultConfig)) + expect(result).toEqual([["--verbose"], HashMap.make(["key1", "v1"], ["key2", "v2"])]) + }).pipe(Effect.runPromise)) + + it("keyValueMap - validates different key/values with alias", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("-d", "key1=v1", "key2=v2", "--verbose") + const result = yield* _(validation(defs, args, CliConfig.defaultConfig)) + expect(result).toEqual([["--verbose"], HashMap.make(["key1", "v1"], ["key2", "v2"])]) + }).pipe(Effect.runPromise)) + + it("keyValueMap - validate should keep non-key-value parameters that follow the key-value pairs (each preceded by alias -d)", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make( + "-d", + "key1=val1", + "-d", + "key2=val2", + "-d", + "key3=val3", + "arg1", + "arg2", + "--verbose" + ) + const result = yield* _(validation(defs, args, CliConfig.defaultConfig)) + expect(result).toEqual([ + ["arg1", "arg2", "--verbose"], + HashMap.make(["key1", "val1"], ["key2", "val2"], ["key3", "val3"]) + ]) + }).pipe(Effect.runPromise)) + + it("keyValueMap - validate should keep non-key-value parameters that follow the key-value pairs (only the first key/value pair is preceded by alias)", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make("-d", "key1=val1", "key2=val2", "key3=val3", "arg1", "arg2", "--verbose") + const result = yield* _(validation(defs, args, CliConfig.defaultConfig)) + expect(result).toEqual([ + ["arg1", "arg2", "--verbose"], + HashMap.make(["key1", "val1"], ["key2", "val2"], ["key3", "val3"]) + ]) + }).pipe(Effect.runPromise)) + + it("keyValueMap - validate should keep non-key-value parameters that follow the key-value pairs (with a 'mixed' style of proceeding -- name or alias)", () => + Effect.gen(function*(_) { + const args = ReadonlyArray.make( + "-d", + "key1=val1", + "key2=val2", + "--defs", + "key3=val3", + "key4=", + "arg1", + "arg2", + "--verbose" + ) + const result = yield* _(validation(defs, args, CliConfig.defaultConfig)) + expect(result).toEqual([ + ["key4=", "arg1", "arg2", "--verbose"], + HashMap.make(["key1", "val1"], ["key2", "val2"], ["key3", "val3"]) + ]) + }).pipe(Effect.runPromise)) + + it("choice", () => + Effect.gen(function*($) { + const option = Options.choice("animal", ["cat", "dog"]) + const args1 = ReadonlyArray.make("--animal", "cat") + const args2 = ReadonlyArray.make("--animal", "dog") + const result1 = yield* $(validation(option, args1, CliConfig.defaultConfig)) + const result2 = yield* $(validation(option, args2, CliConfig.defaultConfig)) + expect(result1).toEqual([[], "cat"]) + expect(result2).toEqual([[], "dog"]) + }).pipe(Effect.runPromise)) + + it("choiceWithValue", () => + Effect.gen(function*($) { + type Animal = Dog | Cat + class Dog extends Data.TaggedClass("Dog")<{}> {} + class Cat extends Data.TaggedClass("Dog")<{}> {} + const cat = new Cat() + const dog = new Dog() + const option: Options.Options = Options.choiceWithValue("animal", [ + ["dog", dog], + ["cat", cat] + ]) + const args1 = ReadonlyArray.make("--animal", "cat") + const args2 = ReadonlyArray.make("--animal", "dog") + const result1 = yield* $(validation(option, args1, CliConfig.defaultConfig)) + const result2 = yield* $(validation(option, args2, CliConfig.defaultConfig)) + expect(result1).toEqual([[], cat]) + expect(result2).toEqual([[], dog]) + }).pipe(Effect.runPromise)) +}) diff --git a/test/Options.ts b/test/Options.ts deleted file mode 100644 index 453ea41..0000000 --- a/test/Options.ts +++ /dev/null @@ -1,448 +0,0 @@ -import * as CliConfig from "@effect/cli/CliConfig" -import * as HelpDoc from "@effect/cli/HelpDoc" -import * as Span from "@effect/cli/HelpDoc/Span" -import * as Options from "@effect/cli/Options" -import * as it from "@effect/cli/test/utils/extend" -import * as ValidationError from "@effect/cli/ValidationError" -import * as Chunk from "effect/Chunk" -import * as Data from "effect/Data" -import * as Effect from "effect/Effect" -import * as Either from "effect/Either" -import { pipe } from "effect/Function" -import * as HashMap from "effect/HashMap" -import * as Option from "effect/Option" -import { describe, expect } from "vitest" - -describe.concurrent("Options", () => { - it.effect("validates a boolean option without a value", () => - Effect.gen(function*($) { - const args = ["--verbose"] - const config = CliConfig.defaultConfig - const result = yield* $(Options.validate(Options.boolean("verbose"), args, config)) - expect(result).toEqual([[], true]) - })) - - it.effect("validates a boolean option with a followup option", () => - Effect.gen(function*($) { - const options = Options.zip(Options.boolean("help"), Options.boolean("v")) - const config = CliConfig.defaultConfig - const args2 = ["--help"] - const args3 = ["--help", "-v"] - const result1 = yield* $(Options.validate(options, [], config)) - const result2 = yield* $(Options.validate(options, args2, config)) - const result3 = yield* $(Options.validate(options, args3, config)) - expect(result1).toEqual([[], [false, false]]) - expect(result2).toEqual([[], [true, false]]) - expect(result3).toEqual([[], [true, true]]) - })) - - it.effect("validates a boolean option with negation", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = pipe( - Options.boolean("verbose", { negationNames: ["silent", "s"] }), - Options.alias("v") - ) - const result1 = yield* $(Options.validate(option, [], config)) - const result2 = yield* $(Options.validate(option, ["--verbose"], config)) - const result3 = yield* $(Options.validate(option, ["-v"], config)) - const result4 = yield* $(Options.validate(option, ["--silent"], config)) - const result5 = yield* $(Options.validate(option, ["-s"], config)) - const result6 = yield* $(Effect.flip(Options.validate(option, ["--verbose", "--silent"], config))) - const result7 = yield* $(Effect.flip(Options.validate(option, ["-v", "-s"], config))) - expect(result1).toEqual([[], false]) - expect(result2).toEqual([[], true]) - expect(result3).toEqual([[], true]) - expect(result4).toEqual([[], false]) - expect(result5).toEqual([[], false]) - expect(result6).toEqual(ValidationError.invalidValue(HelpDoc.p(Span.error( - "Collision between two options detected." + - " You can only specify one of either: ['--verbose', '--silent']." - )))) - expect(result7).toEqual(ValidationError.invalidValue(HelpDoc.p(Span.error( - "Collision between two options detected." + - " You can only specify one of either: ['--verbose', '--silent']." - )))) - })) - - it.effect("validates a text option", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.text("firstName").pipe(Options.alias("f")) - const result = yield* $(Options.validate(option, ["--firstName", "John"], config)) - expect(result).toEqual([[], "John"]) - })) - - it.effect("validates a text option with an alternative format", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.text("firstName").pipe(Options.alias("f")) - const result = yield* $(Options.validate(option, ["--firstName=John"], config)) - expect(result).toEqual([[], "John"]) - })) - - it.effect("validates a text option with an alias", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.text("firstName").pipe(Options.alias("f")) - const result = yield* $(Options.validate(option, ["-f", "John"], config)) - expect(result).toEqual([[], "John"]) - })) - - it.effect("validates an integer option", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.integer("age") - const result = yield* $(Options.validate(option, ["--age", "100"], config)) - expect(result).toEqual([[], 100]) - })) - - it.effect("validates an option and returns the remainder", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.text("firstName").pipe(Options.alias("f")) - const args = ["--firstName", "John", "--lastName", "Doe"] - const result = yield* $(Options.validate(option, args, config)) - expect(result).toEqual([["--lastName", "Doe"], "John"]) - })) - - it.effect("validates an option and returns the remainder with different ordering", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.text("firstName").pipe(Options.alias("f")) - const args = ["--bar", "baz", "--firstName", "John", "--lastName", "Doe"] - const result = yield* $(Options.validate(option, args, config)) - expect(result).toEqual([["--bar", "baz", "--lastName", "Doe"], "John"]) - })) - - it.effect("does not validate when no valid values are passed", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.text("firstName").pipe(Options.alias("f")) - const args = ["--lastName", "Doe"] - const result = yield* $(Effect.either(Options.validate(option, args, config))) - expect(result).toEqual(Either.left(ValidationError.missingValue(HelpDoc.p(Span.error( - "Expected to find option: '--firstName'" - ))))) - })) - - it.effect("does not validate when an option is passed without a corresponding value", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.text("firstName").pipe(Options.alias("f")) - const args = ["--firstName"] - const result = yield* $(Effect.either(Options.validate(option, args, config))) - expect(result).toEqual(Either.left(ValidationError.invalidValue(HelpDoc.p( - "text options do not have a default value" - )))) - })) - - it.effect("does not validate an invalid option value", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.integer("t") - const args = ["-t", "abc"] - const result = yield* $(Effect.flip(Options.validate(option, args, config))) - expect(result).toEqual(ValidationError.invalidValue(HelpDoc.p("\"abc\" is not a integer"))) - })) - - it.effect("does not validate an invalid option value even when there is a default", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.withDefault(Options.integer("t"), 0) - const args = ["-t", "abc"] - const result = yield* $(Effect.flip(Options.validate(option, args, config))) - expect(result).toEqual(ValidationError.invalidValue(HelpDoc.p("\"abc\" is not a integer"))) - })) - - it.effect("does not validate a missing option", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.integer("t") - const result = yield* $(Effect.flip(Options.validate(option, [], config))) - expect(result).toEqual(ValidationError.missingValue(HelpDoc.p(Span.error("Expected to find option: '-t'")))) - })) - - it.effect("validates with case-insensitive configuration", () => - Effect.gen(function*($) { - const config = CliConfig.make(false, 2) - const option = Options.alias(Options.text("Firstname"), "F") - const args1 = ["--Firstname", "John"] - const args2 = ["--firstname", "John"] - const args3 = ["-F", "John"] - const args4 = ["-f", "John"] - const result1 = yield* $(Options.validate(option, args1, config)) - const result2 = yield* $(Options.validate(option, args2, config)) - const result3 = yield* $(Options.validate(option, args3, config)) - const result4 = yield* $(Options.validate(option, args4, config)) - expect(result1).toEqual([[], "John"]) - expect(result2).toEqual([[], "John"]) - expect(result3).toEqual([[], "John"]) - expect(result4).toEqual([[], "John"]) - })) - - it.effect("validates an unsupplied optional option", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.integer("age").pipe(Options.optional) - const result = yield* $(Options.validate(option, [], config)) - expect(result).toEqual([[], Option.none()]) - })) - - it.effect("validates an unsupplied optional option with remainder", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.integer("age").pipe(Options.optional) - const args = ["--bar", "baz"] - const result = yield* $(Options.validate(option, args, config)) - expect(result).toEqual([args, Option.none()]) - })) - - it.effect("validates a supplied optional option", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.integer("age").pipe(Options.optional) - const args = ["--age", "20"] - const result = yield* $(Options.validate(option, args, config)) - expect(result).toEqual([[], Option.some(20)]) - })) - - it.effect("validates a supplied optional option with remainder", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.integer("age").pipe(Options.optional) - const args = ["--firstName", "John", "--age", "20", "--lastName", "Doe"] - const result = yield* $(Options.validate(option, args, config)) - expect(result).toEqual([["--firstName", "John", "--lastName", "Doe"], Option.some(20)]) - })) - - it.effect("validates using all and returns the specified structure", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option1 = Options.all({ - firstName: Options.text("firstName"), - lastName: Options.text("lastName") - }) - const option2 = Options.all([Options.text("firstName"), Options.text("lastName")]) - const args = ["--firstName", "John", "--lastName", "Doe"] - const result1 = yield* $(Options.validate(option1, args, config)) - const result2 = yield* $(Options.validate(option2, args, config)) - expect(result1).toEqual([[], { firstName: "John", lastName: "Doe" }]) - expect(result2).toEqual([[], ["John", "Doe"]]) - })) - - it.effect("validate provides a suggestion if a provided option is close to a specified option", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.text("firstName") - const args = ["--firstme", "Alice"] - const result = yield* $(Effect.flip(Options.validate(option, args, config))) - expect(result).toEqual(ValidationError.invalidValue(HelpDoc.p(Span.error( - "The flag '--firstme' is not recognized. Did you mean '--firstName'?" - )))) - })) - - it.effect("validate provides a suggestion if a provided option with a default is close to a specified option", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.text("firstName").pipe(Options.withDefault("Jack")) - const args = ["--firstme", "Alice"] - const result = yield* $(Effect.flip(Options.validate(option, args, config))) - expect(result).toEqual(ValidationError.invalidValue(HelpDoc.p(Span.error( - "The flag '--firstme' is not recognized. Did you mean '--firstName'?" - )))) - })) - - it.effect("orElse - two options", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = pipe( - Options.text("string"), - Options.map(Either.left), - Options.orElse( - pipe( - Options.integer("integer"), - Options.map(Either.right) - ) - ) - ) - const args1 = ["--integer", "2"] - const args2 = ["--string", "two"] - const result1 = yield* $(Options.validate(option, args1, config)) - const result2 = yield* $(Options.validate(option, args2, config)) - expect(result1).toEqual([[], Either.right(2)]) - expect(result2).toEqual([[], Either.left("two")]) - })) - - it.effect("orElse - option collision", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.orElse(Options.text("string"), Options.integer("integer")) - const args = ["--integer", "2", "--string", "two"] - const result = yield* $(Effect.flip(Options.validate(option, args, config))) - expect(result).toEqual(ValidationError.invalidValue(HelpDoc.p(Span.error( - "Collision between two options detected. You can only specify one of either: ['--string', '--integer']." - )))) - })) - - it.effect("orElse - no options provided", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.orElse(Options.text("string"), Options.integer("integer")) - const result = yield* $(Effect.flip(Options.validate(option, [], config))) - const error = ValidationError.missingValue(HelpDoc.sequence( - HelpDoc.p(Span.error("Expected to find option: '--string'")), - HelpDoc.p(Span.error("Expected to find option: '--integer'")) - )) - expect(result).toEqual(error) - })) - - it.effect("orElse - invalid option provided with a default", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.integer("min").pipe( - Options.orElse(Options.integer("max")), - Options.withDefault(0) - ) - const args = ["--min", "abc"] - const result = yield* $(Effect.flip(Options.validate(option, args, config))) - const error = ValidationError.invalidValue(HelpDoc.sequence( - HelpDoc.p("\"abc\" is not a integer"), - HelpDoc.p(Span.error("Expected to find option: '--max'")) - )) - expect(result).toEqual(error) - })) - - it.effect("keyValueMap - validates a missing option", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.alias(Options.keyValueMap("defs"), "d") - const result = yield* $(Effect.flip(Options.validate(option, [], config))) - expect(result).toEqual(ValidationError.missingValue(HelpDoc.p(Span.error( - "Expected to find option: '--defs'" - )))) - })) - - it.effect("keyValueMap - validates repeated values", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.alias(Options.keyValueMap("defs"), "d") - const args = ["-d", "key1=v1", "-d", "key2=v2", "--verbose"] - const result = yield* $(Options.validate(option, args, config)) - expect(result).toEqual([["--verbose"], HashMap.make(["key1", "v1"], ["key2", "v2"])]) - })) - - it.effect("keyValueMap - validates different key/values", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.alias(Options.keyValueMap("defs"), "d") - const args = ["--defs", "key1=v1", "key2=v2", "--verbose"] - const result = yield* $(Options.validate(option, args, config)) - expect(result).toEqual([["--verbose"], HashMap.make(["key1", "v1"], ["key2", "v2"])]) - })) - - it.effect("keyValueMap - validates different key/values with alias", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.alias(Options.keyValueMap("defs"), "d") - const args = ["-d", "key1=v1", "key2=v2", "--verbose"] - const result = yield* $(Options.validate(option, args, config)) - expect(result).toEqual([["--verbose"], HashMap.make(["key1", "v1"], ["key2", "v2"])]) - })) - - it.effect("variadic - invalid integer", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.alias(Options.repeat1(Options.integer("defs")), "d") - const args = ["-d", "1", "-d", "v2", "-d", "3", "--verbose"] - const error = yield* $(Effect.flip(Options.validate(option, args, config))) - expect(error).toEqual(ValidationError.invalidValue(HelpDoc.p( - `"v2" is not a integer` - ))) - })) - - it.effect("variadic - missing value", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.alias(Options.repeat1(Options.integer("defs")), "d") - const args = ["--verbose"] - const error = yield* $(Effect.flip(Options.validate(option, args, config))) - expect(error).toEqual(ValidationError.missingValue(HelpDoc.p(Span.error( - "Expected at least 1 value(s) for option: '--defs'" - )))) - })) - - it.effect("variadic - extraneous value", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.alias(Options.atMost(Options.integer("defs"), 2), "d") - const args = ["-d", "1", "-d", "2", "-d", "3", "--verbose"] - const error = yield* $(Effect.flip(Options.validate(option, args, config))) - expect(error).toEqual(ValidationError.extraneousValue(HelpDoc.p(Span.error( - "Expected at most 2 value(s) for option: '--defs'" - )))) - })) - - it.effect("variadic - integers", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.alias(Options.repeat1(Options.integer("defs")), "d") - const args = ["-d", "1", "-d", "2", "-d", "3", "--verbose"] - const result = yield* $(Options.validate(option, args, config)) - expect(result).toEqual([["--verbose"], Chunk.make(1, 2, 3)]) - })) - - it.effect("variadic - integers atMost 2", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.alias(Options.atMost(Options.integer("defs"), 2), "d") - const args = ["-d", "1", "-d", "2", "--verbose"] - const result = yield* $(Options.validate(option, args, config)) - expect(result).toEqual([["--verbose"], Chunk.make(1, 2)]) - })) - - it.effect("choice", () => - Effect.gen(function*($) { - const config = CliConfig.defaultConfig - const option = Options.choice("animal", ["cat", "dog"]) - const args1 = ["--animal", "cat"] - const args2 = ["--animal", "dog"] - const result1 = yield* $(Options.validate(option, args1, config)) - const result2 = yield* $(Options.validate(option, args2, config)) - expect(result1).toEqual([[], "cat"]) - expect(result2).toEqual([[], "dog"]) - })) - - it.effect("choiceWithValue", () => - Effect.gen(function*($) { - type Animal = Dog | Cat - - interface Dog extends Data.Case { - readonly _tag: "Dog" - } - - const Dog = Data.tagged("Dog") - - interface Cat extends Data.Case { - readonly _tag: "Cat" - } - - const Cat = Data.tagged("Cat") - - const cat = Cat() - const dog = Dog() - - const option: Options.Options = Options.choiceWithValue("animal", [ - ["dog", dog], - ["cat", cat] - ]) - - const config = CliConfig.defaultConfig - const args1 = ["--animal", "cat"] - const args2 = ["--animal", "dog"] - const result1 = yield* $(Options.validate(option, args1, config)) - const result2 = yield* $(Options.validate(option, args2, config)) - expect(result1).toEqual([[], cat]) - expect(result2).toEqual([[], dog]) - })) -}) diff --git a/test/Primitive.test.ts b/test/Primitive.test.ts new file mode 100644 index 0000000..b52ab7b --- /dev/null +++ b/test/Primitive.test.ts @@ -0,0 +1,120 @@ +import * as CliConfig from "@effect/cli/CliConfig" +import * as Primitive from "@effect/cli/Primitive" +import { Effect, Equal, Function, Option, ReadonlyArray } from "effect" +import fc from "fast-check" +import { describe, expect, it } from "vitest" + +describe("Primitive", () => { + describe("Bool", () => { + it("validates that truthy text representations of a boolean return true", () => + fc.assert(fc.asyncProperty(trueValuesArb, (str) => + Effect.gen(function*(_) { + const bool = Primitive.boolean(Option.none()) + const result = yield* _(bool.validate(Option.some(str), CliConfig.defaultConfig)) + expect(result).toBe(true) + }).pipe(Effect.runPromise)))) + + it("validates that falsy text representations of a boolean return false", () => + fc.assert(fc.asyncProperty(falseValuesArb, (str) => + Effect.gen(function*(_) { + const bool = Primitive.boolean(Option.none()) + const result = yield* _(bool.validate(Option.some(str), CliConfig.defaultConfig)) + expect(result).toBe(false) + }).pipe(Effect.runPromise)))) + + it("validates that invalid boolean representations are rejected", () => + Effect.gen(function*(_) { + const bool = Primitive.boolean(Option.none()) + const result = yield* _(Effect.flip(bool.validate(Option.some("bad"), CliConfig.defaultConfig))) + expect(result).toBe("Unable to recognize 'bad' as a valid boolean") + }).pipe(Effect.runPromise)) + + it("validates that the default value will be used if a value is not provided", () => + fc.assert(fc.asyncProperty(fc.boolean(), (value) => + Effect.gen(function*(_) { + const bool = Primitive.boolean(Option.some(value)) + const result = yield* _(bool.validate(Option.none(), CliConfig.defaultConfig)) + expect(result).toBe(value) + }).pipe(Effect.runPromise)))) + }) + + describe("Choice", () => { + it("validates a choice that is one of the alternatives", () => + fc.assert(fc.asyncProperty(pairsArb, ([[selectedName, selectedValue], pairs]) => + Effect.gen(function*(_) { + const alternatives = Function.unsafeCoerce< + ReadonlyArray<[string, number]>, + ReadonlyArray.NonEmptyReadonlyArray<[string, number]> + >(pairs) + const choice = Primitive.choice(alternatives) + const result = yield* _(choice.validate(Option.some(selectedName), CliConfig.defaultConfig)) + expect(result).toEqual(selectedValue) + }).pipe(Effect.runPromise)))) + + it("does not validate a choice that is not one of the alternatives", () => + fc.assert(fc.asyncProperty(pairsArb, ([tuple, pairs]) => + Effect.gen(function*(_) { + const selectedName = tuple[0] + const alternatives = Function.unsafeCoerce< + ReadonlyArray<[string, number]>, + ReadonlyArray.NonEmptyReadonlyArray<[string, number]> + >(ReadonlyArray.filter(pairs, (pair) => !Equal.equals(tuple, pair))) + const choice = Primitive.choice(alternatives) + const result = yield* _(Effect.flip(choice.validate(Option.some(selectedName), CliConfig.defaultConfig))) + expect(result).toMatch(/^Expected one of the following cases:\s.*/) + }).pipe(Effect.runPromise)))) + }) + + simplePrimitiveTestSuite(Primitive.date, fc.date(), "Date") + + simplePrimitiveTestSuite(Primitive.float, fc.float({ noNaN: true }).filter((n) => n !== 0), "Float") + + simplePrimitiveTestSuite(Primitive.integer, fc.integer(), "Integer") + + describe("Text", () => { + it("validates all user-defined text", () => + fc.assert(fc.asyncProperty(fc.string(), (str) => + Effect.gen(function*(_) { + const result = yield* _(Primitive.text.validate(Option.some(str), CliConfig.defaultConfig)) + expect(result).toEqual(str) + }).pipe(Effect.runPromise)))) + }) +}) + +const simplePrimitiveTestSuite = ( + primitive: Primitive.Primitive, + arb: fc.Arbitrary, + primitiveTypeName: string +) => { + describe(`${primitiveTypeName}`, () => { + it(`validates that valid values are accepted`, () => + fc.assert(fc.asyncProperty(arb, (value) => + Effect.gen(function*(_) { + const str = value instanceof Date ? value.toISOString() : `${value}` + const result = yield* _(primitive.validate(Option.some(str), CliConfig.defaultConfig)) + expect(result).toEqual(value) + }).pipe(Effect.runPromise)))) + + it(`validates that invalid values are rejected`, () => + Effect.gen(function*(_) { + const result = yield* _(Effect.flip(primitive.validate(Option.some("bad"), CliConfig.defaultConfig))) + expect(result).toBe(`'bad' is not a ${primitive.typeName}`) + }).pipe(Effect.runPromise)) + }) +} + +const randomizeCharacterCases = (str: string): string => { + let result = "" + for (let i = 0; i < str.length; i++) { + const char = str[i] + result += Math.random() < 0.5 ? char.toLowerCase() : char.toUpperCase() + } + return result +} + +const trueValuesArb = fc.constantFrom("true", "1", "y", "yes", "on").map(randomizeCharacterCases) +const falseValuesArb = fc.constantFrom("false", "0", "n", "no", "off").map(randomizeCharacterCases) + +const pairsArb = fc.array(fc.tuple(fc.string(), fc.float()), { minLength: 2, maxLength: 100 }) + .map((pairs) => ReadonlyArray.dedupeWith(pairs, ([str1], [str2]) => str1 === str2)) + .chain((pairs) => fc.tuple(fc.constantFrom(...pairs), fc.constant(pairs))) diff --git a/test/utils/extend.ts b/test/utils/extend.ts deleted file mode 100644 index fe7b636..0000000 --- a/test/utils/extend.ts +++ /dev/null @@ -1,126 +0,0 @@ -import * as Terminal from "@effect/cli/Terminal" -import * as Duration from "effect/Duration" -import * as Effect from "effect/Effect" -import * as Layer from "effect/Layer" -import * as Schedule from "effect/Schedule" -import type * as Scope from "effect/Scope" -import * as TestEnvironment from "effect/TestContext" -import type * as TestServices from "effect/TestServices" -import type { TestAPI } from "vitest" -import * as V from "vitest" - -export type API = TestAPI<{}> - -export const it: API = V.it - -const testLayer = Layer.merge(TestEnvironment.TestContext, Terminal.layer) - -export const effect = (() => { - const f = ( - name: string, - self: () => Effect.Effect, - timeout = 5_000 - ) => { - return it( - name, - () => - Effect.suspend(self).pipe( - Effect.provide(testLayer), - Effect.runPromise - ), - timeout - ) - } - return Object.assign(f, { - skip: ( - name: string, - self: () => Effect.Effect, - timeout = 5_000 - ) => { - return it.skip( - name, - () => - Effect.suspend(self).pipe( - Effect.provide(testLayer), - Effect.runPromise - ), - timeout - ) - }, - only: ( - name: string, - self: () => Effect.Effect, - timeout = 5_000 - ) => { - return it.only( - name, - () => - Effect.suspend(self).pipe( - Effect.provide(testLayer), - Effect.runPromise - ), - timeout - ) - } - }) -})() - -export const live = ( - name: string, - self: () => Effect.Effect, - timeout = 5_000 -) => { - return it( - name, - () => Effect.suspend(self).pipe(Effect.runPromise), - timeout - ) -} - -export const flakyTest = ( - self: Effect.Effect, - timeout: Duration.Duration = Duration.seconds(30) -) => { - return Effect.catchAllDefect(self, Effect.fail).pipe( - Effect.retry( - Schedule.recurs(10).pipe( - Schedule.compose(Schedule.elapsed), - Schedule.whileOutput(Duration.lessThanOrEqualTo(timeout)) - ) - ), - Effect.orDie - ) -} - -export const scoped = ( - name: string, - self: () => Effect.Effect, - timeout = 5_000 -) => { - return it( - name, - () => - Effect.suspend(self).pipe( - Effect.scoped, - Effect.provide(testLayer), - Effect.runPromise - ), - timeout - ) -} - -export const scopedLive = ( - name: string, - self: () => Effect.Effect, - timeout = 5_000 -) => { - return it( - name, - () => - Effect.suspend(self).pipe( - Effect.scoped, - Effect.runPromise - ), - timeout - ) -} diff --git a/test/utils/grep.ts b/test/utils/grep.ts index a05acbf..44c6c23 100644 --- a/test/utils/grep.ts +++ b/test/utils/grep.ts @@ -2,9 +2,12 @@ import * as Args from "@effect/cli/Args" import * as Command from "@effect/cli/Command" import * as Options from "@effect/cli/Options" -const afterFlag = Options.alias(Options.integer("after"), "A") -const beforeFlag = Options.alias(Options.integer("before"), "B") -export const options: Options.Options = Options.zip(afterFlag, beforeFlag) +const afterFlag = Options.integer("after").pipe(Options.withAlias("A")) +const beforeFlag = Options.integer("before").pipe(Options.withAlias("B")) +export const options: Options.Options = Options.all([ + afterFlag, + beforeFlag +]) export const args: Args.Args = Args.text() @@ -12,4 +15,4 @@ export const command: Command.Command<{ readonly name: "grep" readonly options: readonly [number, number] readonly args: string -}> = Command.make("grep", { options, args }) +}> = Command.standard("grep", { options, args }) diff --git a/test/utils/tail.ts b/test/utils/tail.ts index d536960..dfb46d9 100644 --- a/test/utils/tail.ts +++ b/test/utils/tail.ts @@ -10,4 +10,4 @@ export const command: Command.Command<{ readonly name: "tail" readonly options: number readonly args: string -}> = Command.make("tail", { options, args }) +}> = Command.standard("tail", { options, args }) diff --git a/test/utils/wc.ts b/test/utils/wc.ts index 43d82a3..7193dee 100644 --- a/test/utils/wc.ts +++ b/test/utils/wc.ts @@ -1,24 +1,22 @@ import * as Args from "@effect/cli/Args" import * as Command from "@effect/cli/Command" import * as Options from "@effect/cli/Options" -import type { Chunk } from "effect/Chunk" -import { pipe } from "effect/Function" const bytesFlag = Options.boolean("c") const linesFlag = Options.boolean("l") const wordsFlag = Options.boolean("w") -const charFlag = Options.boolean("m") -export const options: Options.Options = pipe( +const charFlag = Options.boolean("m", { ifPresent: false }) +export const options: Options.Options = Options.all([ bytesFlag, - Options.zip(linesFlag), - Options.zipFlatten(wordsFlag), - Options.zipFlatten(charFlag) -) + linesFlag, + wordsFlag, + charFlag +]) -export const args: Args.Args> = Args.repeat(Args.text({ name: "files" })) +export const args: Args.Args> = Args.repeated(Args.text({ name: "files" })) export const command: Command.Command<{ readonly name: "wc" readonly options: readonly [boolean, boolean, boolean, boolean] - readonly args: Chunk -}> = Command.make("wc", { options, args }) + readonly args: ReadonlyArray +}> = Command.standard("wc", { options, args }) diff --git a/vitest.config.ts b/vitest.config.ts index 705313c..346f6fe 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,7 +5,7 @@ import { defineConfig } from "vite" export default defineConfig({ test: { - include: ["./test/**/*.ts"], + include: ["./test/**/*.test.ts"], exclude: ["./test/utils/**/*.ts", "./test/**/*.init.ts"], globals: true },