Skip to content

Commit

Permalink
refactor(validators)!: remove error field (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
PierreDemailly authored Jul 11, 2024
1 parent eb1dd7a commit b70e090
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 22 deletions.
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;
}

0 comments on commit b70e090

Please sign in to comment.