Skip to content
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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

Nothing yet.
### Fixed

- Empty strings are now valid in every rule validating strings, except `required`. This is technically a _breaking change_, but is not _change_ per say because it should have been done since the beginning.

## [1.0.2] - 2025-04-28

Expand Down
6 changes: 6 additions & 0 deletions src/rules/__tests__/allowedCharacters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ describe("allowedCharacters", () => {
expect(outcome.message).toBe("{{name}} contains the following prohibited characters: !. Only the following characters are allowed: {{allowedCharacters}}");
});

it.concurrent("should return value when then value is an empty string", () => {
const outcome = rule("", allowedCharacters) as RuleExecutionOutcome;
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});

it.concurrent("should return valid when the value only contains allowed characters", () => {
const outcome = rule("valid", allowedCharacters) as RuleExecutionOutcome;
expect(outcome.severity).toBe("information");
Expand Down
8 changes: 7 additions & 1 deletion src/rules/__tests__/containsDigits.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { RuleExecutionOutcome } from "../../types";

describe("containsDigits", () => {
test.each([undefined, null, {}, [], true, 0, 0n])("should return invalid when the value is not a string", (value) => {
const outcome = containsDigits(value) as RuleExecutionOutcome;
const outcome = containsDigits(value, 10) as RuleExecutionOutcome;
expect(outcome.severity).toBe("error");
expect(outcome.message).toBe("{{name}} must be a string.");
});
Expand Down Expand Up @@ -36,4 +36,10 @@ describe("containsDigits", () => {
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});

it.concurrent("should return valid when the value is an empty string", () => {
const outcome = containsDigits("", 10) as RuleExecutionOutcome;
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});
});
8 changes: 7 additions & 1 deletion src/rules/__tests__/containsLowercase.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { RuleExecutionOutcome } from "../../types";

describe("containsLowercase", () => {
test.each([undefined, null, {}, [], true, 0, 0n])("should return invalid when the value is not a string", (value) => {
const outcome = containsLowercase(value) as RuleExecutionOutcome;
const outcome = containsLowercase(value, 10) as RuleExecutionOutcome;
expect(outcome.severity).toBe("error");
expect(outcome.message).toBe("{{name}} must be a string.");
});
Expand Down Expand Up @@ -36,4 +36,10 @@ describe("containsLowercase", () => {
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});

it.concurrent("should return valid when the value is an empty string", () => {
const outcome = containsLowercase("", 10) as RuleExecutionOutcome;
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});
});
8 changes: 7 additions & 1 deletion src/rules/__tests__/containsNonAlphanumeric.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { RuleExecutionOutcome } from "../../types";

describe("containsNonAlphanumerics", () => {
test.each([undefined, null, {}, [], true, 0, 0n])("should return invalid when the value is not a string", (value) => {
const outcome = containsNonAlphanumeric(value) as RuleExecutionOutcome;
const outcome = containsNonAlphanumeric(value, 10) as RuleExecutionOutcome;
expect(outcome.severity).toBe("error");
expect(outcome.message).toBe("{{name}} must be a string.");
});
Expand Down Expand Up @@ -36,4 +36,10 @@ describe("containsNonAlphanumerics", () => {
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});

it.concurrent("should return valid when the value is an empty string", () => {
const outcome = containsNonAlphanumeric("", 10) as RuleExecutionOutcome;
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});
});
8 changes: 7 additions & 1 deletion src/rules/__tests__/containsUppercase.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { RuleExecutionOutcome } from "../../types";

describe("containsUppercase", () => {
test.each([undefined, null, {}, [], true, 0, 0n])("should return invalid when the value is not a string", (value) => {
const outcome = containsUppercase(value) as RuleExecutionOutcome;
const outcome = containsUppercase(value, 10) as RuleExecutionOutcome;
expect(outcome.severity).toBe("error");
expect(outcome.message).toBe("{{name}} must be a string.");
});
Expand Down Expand Up @@ -36,4 +36,10 @@ describe("containsUppercase", () => {
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});

it.concurrent("should return valid when the value is an empty string", () => {
const outcome = containsUppercase("", 10) as RuleExecutionOutcome;
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});
});
10 changes: 8 additions & 2 deletions src/rules/__tests__/email.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,14 @@ describe("email", () => {
expect(outcome.message).toBe("{{name}} must be a valid email address.");
});

test.each(["", "test@example.com"])("should return valid when the value is a valid email address", (value) => {
const outcome = email(value) as RuleExecutionOutcome;
it.concurrent("should return valid when the value is a valid email address", () => {
const outcome = email("test@example.com") as RuleExecutionOutcome;
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});

it.concurrent("should return valid when the value is an empty string", () => {
const outcome = email("") as RuleExecutionOutcome;
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});
Expand Down
12 changes: 6 additions & 6 deletions src/rules/__tests__/identifier.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ describe("identifier", () => {
expect(outcome.message).toBe("{{name}} must be a string.");
});

it.concurrent("should return invalid when the value is an empty string", () => {
const outcome = identifier("") as RuleExecutionOutcome;
expect(outcome.severity).toBe("error");
expect(outcome.message).toBe("{{name}} cannot be an empty string.");
});

it.concurrent("should return invalid when the value starts with a digit", () => {
const outcome = identifier("123") as RuleExecutionOutcome;
expect(outcome.severity).toBe("error");
Expand All @@ -33,4 +27,10 @@ describe("identifier", () => {
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});

it.concurrent("should return valid when the value is an empty string", () => {
const outcome = identifier("") as RuleExecutionOutcome;
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});
});
12 changes: 12 additions & 0 deletions src/rules/__tests__/maximumLength.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,16 @@ describe("maximumLength", () => {
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});

it.concurrent("should return valid when the value is an empty array", () => {
const outcome = maximumLength([], 10) as RuleExecutionOutcome;
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});

it.concurrent("should return valid when the value is an empty string", () => {
const outcome = maximumLength("", 10) as RuleExecutionOutcome;
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});
});
14 changes: 13 additions & 1 deletion src/rules/__tests__/minimumLength.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe("minimumLength", () => {
});

it.concurrent("should return invalid when the value is a string that is too short", () => {
const outcome = minimumLength("", true) as RuleExecutionOutcome;
const outcome = minimumLength(" ", 10) as RuleExecutionOutcome;
expect(outcome.severity).toBe("error");
expect(outcome.message).toBe("{{name}} must be at least {{minimumLength}} character(s) long.");
});
Expand Down Expand Up @@ -39,4 +39,16 @@ describe("minimumLength", () => {
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});

it.concurrent("should return valid when the value is an empty array", () => {
const outcome = minimumLength([], 10) as RuleExecutionOutcome;
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});

it.concurrent("should return valid when the value is an empty string", () => {
const outcome = minimumLength("", 10) as RuleExecutionOutcome;
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});
});
8 changes: 7 additions & 1 deletion src/rules/__tests__/pattern.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const pattern = /^[ABCEGHJ-NPRSTVXY]\d[ABCEGHJ-NPRSTV-Z][ -]?\d[ABCEGHJ-NPRSTV-Z

describe("pattern", () => {
test.each([undefined, null, {}, [], true, 0, 0n])("should return invalid when the value is not a string", (value) => {
const outcome = rule(value) as RuleExecutionOutcome;
const outcome = rule(value, pattern) as RuleExecutionOutcome;
expect(outcome.severity).toBe("error");
expect(outcome.message).toBe("{{name}} must be a string.");
});
Expand All @@ -24,6 +24,12 @@ describe("pattern", () => {
expect(outcome.message).toBe("{{name}} must match the pattern {{pattern}}.");
});

it.concurrent("should return valid when the value is an empty string", () => {
const outcome = rule("", pattern) as RuleExecutionOutcome;
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});

test.each(["H2X3Y2", "H2X 3Y2", "H2X-3Y2"])("should return valid when the value matches the pattern", (value) => {
const outcome = rule(value, pattern) as RuleExecutionOutcome;
expect(outcome.severity).toBe("information");
Expand Down
6 changes: 6 additions & 0 deletions src/rules/__tests__/slug.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,10 @@ describe("slug", () => {
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});

it.concurrent("should return valid when the value is an empty string", () => {
const outcome = slug("") as RuleExecutionOutcome;
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});
});
8 changes: 7 additions & 1 deletion src/rules/__tests__/uniqueCharacters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { RuleExecutionOutcome } from "../../types";

describe("uniqueCharacters", () => {
test.each([undefined, null, {}, [], true, 0, 0n])("should return invalid when the value is not a string", (value) => {
const outcome = uniqueCharacters(value) as RuleExecutionOutcome;
const outcome = uniqueCharacters(value, 10) as RuleExecutionOutcome;
expect(outcome.severity).toBe("error");
expect(outcome.message).toBe("{{name}} must be a string.");
});
Expand All @@ -27,4 +27,10 @@ describe("uniqueCharacters", () => {
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});

it.concurrent("should return valid when the value is an empty string", () => {
const outcome = uniqueCharacters("", 10) as RuleExecutionOutcome;
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});
});
13 changes: 10 additions & 3 deletions src/rules/__tests__/url.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ describe("url", () => {
});

test.each([
["", undefined],
["", false],
["http://example.com", undefined],
["http://example.com", true],
["http://example.com", "http,https"],
Expand All @@ -45,5 +43,14 @@ describe("url", () => {
expect(outcome.message).toBeUndefined();
});

test.each([])("should return valid when the value is a valid URL with an allowed protocol", (value) => {});
test.each([])("should return valid when the value is a valid URL with an allowed protocol", () => {});

test.each([
["", undefined],
["", false],
])("should return valid when the value is an empty string", (value, args) => {
const outcome = url(value, args) as RuleExecutionOutcome;
expect(outcome.severity).toBe("information");
expect(outcome.message).toBeUndefined();
});
});
14 changes: 7 additions & 7 deletions src/rules/containsDigits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ const { isDigit } = stringUtils;
* @returns The result of the validation rule execution.
*/
const containsDigits: ValidationRule = (value: unknown, args: unknown): RuleExecutionOutcome => {
if (typeof value !== "string") {
return { severity: "error", message: "{{name}} must be a string." };
}

const requiredDigits: number = Number(args);
if (isNaN(requiredDigits) || requiredDigits <= 0) {
return { severity: "warning", message: "The arguments should be a positive number." };
}

const digits: number = [...value].filter(isDigit).length;
if (digits < requiredDigits) {
return { severity: "error", message: "{{name}} must contain at least {{containsDigits}} digit(s)." };
if (typeof value !== "string") {
return { severity: "error", message: "{{name}} must be a string." };
} else if (value.length > 0) {
const digits: number = [...value].filter(isDigit).length;
if (digits < requiredDigits) {
return { severity: "error", message: "{{name}} must contain at least {{containsDigits}} digit(s)." };
}
}

return { severity: "information" };
Expand Down
14 changes: 7 additions & 7 deletions src/rules/containsLowercase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ const { isLetter } = stringUtils;
* @returns The result of the validation rule execution.
*/
const containsLowercase: ValidationRule = (value: unknown, args: unknown): RuleExecutionOutcome => {
if (typeof value !== "string") {
return { severity: "error", message: "{{name}} must be a string." };
}

const requiredLowercase: number = Number(args);
if (isNaN(requiredLowercase) || requiredLowercase <= 0) {
return { severity: "warning", message: "The arguments should be a positive number." };
}

const lowercase: number = [...value].filter((c) => isLetter(c) && c.toLowerCase() === c).length;
if (lowercase < requiredLowercase) {
return { severity: "error", message: "{{name}} must contain at least {{containsLowercase}} lowercase letter(s)." };
if (typeof value !== "string") {
return { severity: "error", message: "{{name}} must be a string." };
} else if (value.length > 0) {
const lowercase: number = [...value].filter((c) => isLetter(c) && c.toLowerCase() === c).length;
if (lowercase < requiredLowercase) {
return { severity: "error", message: "{{name}} must contain at least {{containsLowercase}} lowercase letter(s)." };
}
}

return { severity: "information" };
Expand Down
14 changes: 7 additions & 7 deletions src/rules/containsNonAlphanumeric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ const { isLetterOrDigit } = stringUtils;
* @returns The result of the validation rule execution.
*/
const containsNonAlphanumeric: ValidationRule = (value: unknown, args: unknown): RuleExecutionOutcome => {
if (typeof value !== "string") {
return { severity: "error", message: "{{name}} must be a string." };
}

const requiredNonAlphanumeric: number = Number(args);
if (isNaN(requiredNonAlphanumeric) || requiredNonAlphanumeric <= 0) {
return { severity: "warning", message: "The arguments should be a positive number." };
}

const nonAlphanumeric: number = [...value].filter((c) => !isLetterOrDigit(c)).length;
if (nonAlphanumeric < requiredNonAlphanumeric) {
return { severity: "error", message: "{{name}} must contain at least {{containsNonAlphanumeric}} non-alphanumeric character(s)." };
if (typeof value !== "string") {
return { severity: "error", message: "{{name}} must be a string." };
} else if (value.length > 0) {
const nonAlphanumeric: number = [...value].filter((c) => !isLetterOrDigit(c)).length;
if (nonAlphanumeric < requiredNonAlphanumeric) {
return { severity: "error", message: "{{name}} must contain at least {{containsNonAlphanumeric}} non-alphanumeric character(s)." };
}
}

return { severity: "information" };
Expand Down
14 changes: 7 additions & 7 deletions src/rules/containsUppercase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ const { isLetter } = stringUtils;
* @returns The result of the validation rule execution.
*/
const containsUppercase: ValidationRule = (value: unknown, args: unknown): RuleExecutionOutcome => {
if (typeof value !== "string") {
return { severity: "error", message: "{{name}} must be a string." };
}

const requiredUppercase: number = Number(args);
if (isNaN(requiredUppercase) || requiredUppercase <= 0) {
return { severity: "warning", message: "The arguments should be a positive number." };
}

const uppercase: number = [...value].filter((c) => isLetter(c) && c.toUpperCase() === c).length;
if (uppercase < requiredUppercase) {
return { severity: "error", message: "{{name}} must contain at least {{containsUppercase}} uppercase letter(s)." };
if (typeof value !== "string") {
return { severity: "error", message: "{{name}} must be a string." };
} else if (value.length > 0) {
const uppercase: number = [...value].filter((c) => isLetter(c) && c.toUpperCase() === c).length;
if (uppercase < requiredUppercase) {
return { severity: "error", message: "{{name}} must contain at least {{containsUppercase}} uppercase letter(s)." };
}
}

return { severity: "information" };
Expand Down
12 changes: 6 additions & 6 deletions src/rules/identifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ const { isDigit, isLetterOrDigit, isNullOrEmpty } = stringUtils;
const identifier: ValidationRule = (value: unknown): RuleExecutionOutcome => {
if (typeof value !== "string") {
return { severity: "error", message: "{{name}} must be a string." };
} else if (isNullOrEmpty(value)) {
return { severity: "error", message: "{{name}} cannot be an empty string." };
} else if (isDigit(value[0])) {
return { severity: "error", message: "{{name}} cannot start with a digit." };
} else if ([...value].some((c) => !isLetterOrDigit(c) && c !== "_")) {
return { severity: "error", message: "{{name}} may only contain letters, digits and underscores (_)." };
} else if (value.length > 0) {
if (isDigit(value[0])) {
return { severity: "error", message: "{{name}} cannot start with a digit." };
} else if ([...value].some((c) => !isLetterOrDigit(c) && c !== "_")) {
return { severity: "error", message: "{{name}} may only contain letters, digits and underscores (_)." };
}
}
return { severity: "information" };
};
Expand Down
Loading