Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(validators)!: remove error field #117

Merged
merged 1 commit into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,21 @@ Simple prompt, similar to `rl.question()` with an improved UI.

Use `options.secure` if you need to hide both input and answer.

Use `options.validators` to handle user input.

Use `options.signal` to set an `AbortSignal` (throws a [AbortError](#aborterror)).

Use `options.validators` to handle user input.

**Example**

```js
const packageName = await question('Package name', {
validators: [
{
validate: (value) => !existsSync(join(process.cwd(), value)),
error: (value) => `Folder ${value} already exists`
validate: (value) => {
if (!existsSync(join(process.cwd(), value))) {
return `Folder ${value} already exists`
}
}
}
]
});
Expand Down
9 changes: 5 additions & 4 deletions src/prompts/multiselect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import wcwidth from "@topcli/wcwidth";
import { AbstractPrompt, AbstractPromptOptions } from "./abstract.js";
import { stripAnsi } from "../utils.js";
import { SYMBOLS } from "../constants.js";
import { PromptValidator } from "../validators.js";
import { isValid, PromptValidator, resultError } from "../validators.js";
import { Choice } from "../types.js";

// CONSTANTS
Expand Down Expand Up @@ -247,9 +247,10 @@ export class MultiselectPrompt extends AbstractPrompt<string | string[]> {
});

for (const validator of this.#validators) {
if (!validator.validate(values)) {
const error = validator.error(values);
render({ error });
const validationResult = validator.validate(values);

if (isValid(validationResult) === false) {
render({ error: resultError(validationResult) });

return;
}
Expand Down
9 changes: 5 additions & 4 deletions src/prompts/question.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import wcwidth from "@topcli/wcwidth";
import { AbstractPrompt, AbstractPromptOptions } from "./abstract.js";
import { stripAnsi } from "../utils.js";
import { SYMBOLS } from "../constants.js";
import { PromptValidator } from "../validators.js";
import { isValid, PromptValidator, resultError } from "../validators.js";

export interface QuestionOptions extends AbstractPromptOptions {
defaultValue?: string;
Expand Down Expand Up @@ -86,9 +86,10 @@ export class QuestionPrompt extends AbstractPrompt<string> {
this.stdout.clearScreenDown();

for (const validator of this.#validators) {
if (!validator.validate(this.answer!)) {
const error = validator.error(this.answer!);
this.#setQuestionSuffixError(error);
const validationResult = validator.validate(this.answer!);

if (isValid(validationResult) === false) {
this.#setQuestionSuffixError(resultError(validationResult));
this.answerBuffer = this.#question();

return;
Expand Down
9 changes: 5 additions & 4 deletions src/prompts/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import wcwidth from "@topcli/wcwidth";
import { AbstractPrompt, AbstractPromptOptions } from "./abstract.js";
import { stripAnsi } from "../utils.js";
import { SYMBOLS } from "../constants.js";
import { PromptValidator } from "../validators.js";
import { isValid, PromptValidator, resultError } from "../validators.js";
import { Choice } from "../types.js";

// CONSTANTS
Expand Down Expand Up @@ -199,9 +199,10 @@ export class SelectPrompt extends AbstractPrompt<string> {
const value = typeof choice === "string" ? choice : choice.value;

for (const validator of this.#validators) {
if (!validator.validate(value)) {
const error = validator.error(value);
render({ error });
const validationResult = validator.validate(value);

if (isValid(validationResult) === false) {
render({ error: resultError(validationResult) });

return;
}
Expand Down
42 changes: 38 additions & 4 deletions src/validators.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,45 @@
export type ValidResponseObject = {
isValid?: true;
}
export type InvalidResponseObject = {
isValid: false;
error: string;
}
export type ValidationResponseObject = ValidResponseObject | InvalidResponseObject;
export type ValidationResponse = InvalidResponse | ValidResponse;
export type InvalidResponse = string | InvalidResponseObject;
export type ValidResponse = null | undefined | true | ValidResponseObject;

export interface PromptValidator<T = string | string[] | boolean> {
validate: (input: T) => boolean;
error: (input: T) => string;
validate: (input: T) => ValidationResponse;
}

export function required<T = string | string[] | boolean>(): PromptValidator<T> {
return {
validate: (input) => (Array.isArray(input) ? input.length > 0 : Boolean(input)),
error: () => "required"
validate: (input) => {
const isValid = (Array.isArray(input) ? input.length > 0 : Boolean(input));

return isValid ? null : { isValid, error: "required" };
}
};
}

export function isValid(result: ValidationResponse): result is ValidResponse {
if (typeof result === "object") {
return result?.isValid !== false;
}

if (typeof result === "string") {
return result.length > 0;
}

return true;
}

export function resultError(result: InvalidResponse) {
if (typeof result === "object") {
return result.error;
}

return result;
}
10 changes: 8 additions & 2 deletions test/question-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,14 @@ describe("QuestionPrompt", () => {
const questionPrompt = await TestingPrompt.QuestionPrompt("What's your name?", {
input: ["test1", "test10", "test2"],
validators: [{
validate: (input) => !(input as string).startsWith("test1"),
error: (input) => `Value cannot start with 'test1', given ${input}.`
validate: (input) => {
const isValid = !(input as string).startsWith("test1");
if (!isValid) {
return { isValid, error: `Value cannot start with 'test1', given ${input}.` };
}

return void 0;
}
}],
onStdoutWrite: (log) => logs.push(log)
});
Expand Down
65 changes: 65 additions & 0 deletions test/validators.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Import Node.js Dependencies
import assert from "node:assert";
import { describe, it } from "node:test";

import { InvalidResponseObject, isValid, resultError, ValidationResponseObject } from "../src/validators.js";

describe("Validators", () => {
describe("isValid", () => {
const testCases = [
{
input: "test",
expected: true
},
{
input: "",
expected: false
},
{
input: null,
expected: true
},
{
input: undefined,
expected: true
},
{
input: { isValid: true } as ValidationResponseObject,
expected: true
},
{
input: { isValid: false, error: "boo" } as ValidationResponseObject,
expected: false
}
];

for (const testCase of testCases) {
it(`given '${formatInput(testCase.input)}', it should return ${testCase.expected}`, () => {
assert.strictEqual(isValid(testCase.input), testCase.expected);
});
}
});

describe("resultError", () => {
it("should return the error message given an object", () => {
const error = "required";
const result: InvalidResponseObject = { isValid: false, error };

assert.strictEqual(resultError(result), error);
});

it("should return the error message given a string", () => {
const error = "required";

assert.strictEqual(resultError(error), error);
});
});
});

function formatInput(input) {
if (typeof input === "object") {
return JSON.stringify(input);
}

return input;
}