Skip to content

Commit

Permalink
feat: extends the issue API (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
adwher authored Jan 28, 2024
1 parent 98eab83 commit 5921b5f
Show file tree
Hide file tree
Showing 96 changed files with 1,001 additions and 1,095 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 Andres Celis
Copyright (c) 2024 Andres Celis

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
14 changes: 6 additions & 8 deletions context.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
export interface SchemaContext {
/** Exact path of the current context. */
path: Array<string | number>;
}

/** Create an empty `SchemaContext` ready to use. */
export function createContext(): SchemaContext {
return { path: [] };
export interface Context {
/**
* Skip the accumulation of issues during execution-time.
* @default false
*/
verbose: boolean;
}
87 changes: 37 additions & 50 deletions errors.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,46 @@
import { SchemaContext } from "./context.ts";

/** Describes one single issue during the process of validation. */
export interface SchemaIssue extends SchemaContext {
message?: string;
issues?: SchemaIssue[];
interface IssueBase {
reason: string;
/** Stack of issues with more details. */
issues?: Issue[];
/** Current `index` or `key` where the issue was generated. */
position?: unknown;
}

/** Used to gather all the issues during the validation phase. */
export class SchemaError extends Error {
readonly issues: SchemaIssue[];

constructor(issue: SchemaIssue) {
super(issue.message);

this.issues = [issue];
}

/**
* Flatten the stack of issues by returning the non-nested ones.
* @returns Summarized array of issues.
*/
flatten() {
return flatten(this.issues);
}

/**
* Flatten the stack of issues and returns the first one.
* @returns First issue in the flatten stack.
*/
first() {
const [first] = this.flatten();
return first;
}
/** Received type is not the expected. */
export const ISSUE_TYPE_REASON = "TYPE";

/** Received type is not the expected. */
export interface IssueType extends IssueBase {
reason: typeof ISSUE_TYPE_REASON;
expected: unknown;
received?: unknown;
}

/**
* Creates a new `SchemaError` using the current `context` and the `descriptor`.
* @returns Instance of `SchemaError`.
*/
export function createError(
context: SchemaContext,
descriptor?: Omit<SchemaIssue, keyof SchemaContext>,
) {
return new SchemaError({ ...context, ...descriptor });
export const ISSUE_VALIDATION_REASON = "VALIDATION";

/** Received value does not fulfill the constraint. */
export interface IssueValidation extends IssueBase {
reason: typeof ISSUE_VALIDATION_REASON;
expected?: unknown;
received?: unknown;
}

/** Flatten a stack of issues by returning the non-nested ones. */
function flatten(issues: SchemaIssue[]): SchemaIssue[] {
return issues.flatMap((issue) => {
const stack = issue.issues ?? [];
/** Expect one value but no one was received. */
export const ISSUE_MISSING_REASON = "MISSING";

if (stack.length > 0) {
return flatten(stack);
}
/** Expect one value but no one was received. */
export interface IssueMissing extends IssueBase {
reason: typeof ISSUE_MISSING_REASON;
}

/** Expect no value but one was received. */
export const ISSUE_PRESENT_REASON = "PRESENT";

return issue;
});
/** Expect no value but one was received. */
export interface IssuePresent extends IssueBase {
reason: typeof ISSUE_PRESENT_REASON;
}

export type Issue = Readonly<
IssueType | IssueValidation | IssueMissing | IssuePresent
>;
6 changes: 5 additions & 1 deletion mod.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
// Copyright (c) 2023 Andres Celis. MIT license.
// Copyright (c) 2024 Andres Celis. MIT license.

/**
* An awesome, tiny and extensible runtime types validation library.
* @module
*/

export * from "./schema.ts";
export * from "./context.ts";
export * from "./errors.ts";
export * from "./types.ts";
export * from "./pipes/mod.ts";
export * from "./schemas/mod.ts";
export * from "./utils/mod.ts";
16 changes: 8 additions & 8 deletions pipes/isEmail.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { assertEquals, assertIsError } from "assert/mod.ts";
import { createContext } from "../context.ts";
import { assertObjectMatch } from "assert/mod.ts";
import { pipe, string } from "../schemas/mod.ts";
import { isEmail } from "./isEmail.ts";
import { safeParse } from "../utils/mod.ts";

const context = createContext();

Deno.test("should assert formatted emails", () => {
const schema = pipe(string(), isEmail());
const schema = pipe(string(), isEmail());

Deno.test("assert formatted emails", () => {
const correct = [
`[email protected]`,
`[email protected]`,
Expand All @@ -34,10 +32,12 @@ Deno.test("should assert formatted emails", () => {
];

for (const received of correct) {
assertEquals(schema.check(received, context), received);
const commit = safeParse(received, schema);
assertObjectMatch(commit, { success: true, value: received });
}

for (const received of incorrect) {
assertIsError(schema.check(received, context));
const commit = safeParse(received, schema);
assertObjectMatch(commit, { success: false });
}
});
5 changes: 2 additions & 3 deletions pipes/isEmail.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { isMatch } from "./isMatch.ts";

const REGEX = /^[A-Z0-9._%+-]+@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i;
const ERROR_MESSAGE = "Must be a valid email";

/**
* Check the `value` as an email.
*/
export function isEmail(message = ERROR_MESSAGE) {
return isMatch(REGEX, message);
export function isEmail() {
return isMatch(REGEX);
}
13 changes: 7 additions & 6 deletions pipes/isInteger.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { assertEquals, assertIsError } from "assert/mod.ts";
import { createContext } from "../context.ts";
import { assertObjectMatch } from "assert/mod.ts";
import { number, pipe } from "../schemas/mod.ts";
import { isInteger } from "./isInteger.ts";
import { safeParse } from "../utils/mod.ts";

const context = createContext();
const schema = pipe(number(), isInteger());

Deno.test("should assert integer numbers", () => {
Deno.test("assert integer numbers", () => {
const correct = [1, -1, 100_000];
const incorrect = [1.23, 0.12, 0.1 + 0.2];

for (const received of correct) {
assertEquals(schema.check(received, context), received);
const commit = safeParse(received, schema);
assertObjectMatch(commit, { success: true, value: received });
}

for (const received of incorrect) {
assertIsError(schema.check(received, context));
const commit = safeParse(received, schema);
assertObjectMatch(commit, { success: false });
}
});
15 changes: 8 additions & 7 deletions pipes/isInteger.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { SchemaContext } from "../context.ts";
import { createError } from "../errors.ts";

const ERROR_MESSAGE = "Must be a integer number";
import { failure } from "../schema.ts";

/**
* Check the `value` as a integer number using the `Number.isInteger` function.
*/
export function isInteger(message = ERROR_MESSAGE) {
return function (value: number, context: SchemaContext) {
export function isInteger() {
return function (value: number) {
if (Number.isInteger(value)) {
return value;
}

return createError(context, { message });
return failure({
reason: "VALIDATION",
expected: "integer",
received: value,
});
};
}
13 changes: 7 additions & 6 deletions pipes/isMatch.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { assertEquals, assertIsError } from "assert/mod.ts";
import { createContext } from "../context.ts";
import { assertObjectMatch } from "assert/mod.ts";
import { pipe, string } from "../schemas/mod.ts";
import { isMatch } from "./isMatch.ts";
import { safeParse } from "../utils/mod.ts";

const context = createContext();
const schema = pipe(string(), isMatch(/[A-Z]{3}-\d{1,}/i));

Deno.test("should pass accepted values on the regular-expression", () => {
Deno.test("pass accepted values on the regular-expression", () => {
const correct = ["ABC-123", "XYZ-456"];
const incorrect = ["ABCD", "AB-1234"];

for (const received of correct) {
assertEquals(schema.check(received, context), received);
const commit = safeParse(received, schema);
assertObjectMatch(commit, { success: true, value: received });
}

for (const received of incorrect) {
assertIsError(schema.check(received, context));
const commit = safeParse(received, schema);
assertObjectMatch(commit, { success: false });
}
});
16 changes: 8 additions & 8 deletions pipes/isMatch.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { SchemaContext } from "../context.ts";
import { createError } from "../errors.ts";
import { failure } from "../schema.ts";

/**
* Create a pipe that validates the `value` as the specified regular expression.
*/
export function isMatch(
regex: RegExp,
message = `Must match with "${regex}" expression`,
) {
return function (value: string, context: SchemaContext) {
export function isMatch(regex: RegExp) {
return function (value: string) {
if (regex.test(value)) {
return value;
}

return createError(context, { message });
return failure({
reason: "VALIDATION",
expected: regex,
received: value,
});
};
}
13 changes: 7 additions & 6 deletions pipes/isNegative.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { assertEquals, assertIsError } from "assert/mod.ts";
import { createContext } from "../context.ts";
import { assertObjectMatch } from "assert/mod.ts";
import { number, pipe } from "../schemas/mod.ts";
import { isNegative } from "./isNegative.ts";
import { safeParse } from "../utils/mod.ts";

const context = createContext();
const schema = pipe(number(), isNegative());

Deno.test("should assert negative numbers", () => {
Deno.test("assert negative numbers", () => {
const correct = [-1, -10, -100_000];
const incorrect = [1, 10, 100_000];

for (const received of correct) {
assertEquals(schema.check(received, context), received);
const commit = safeParse(received, schema);
assertObjectMatch(commit, { success: true, value: received });
}

for (const received of incorrect) {
assertIsError(schema.check(received, context));
const commit = safeParse(received, schema);
assertObjectMatch(commit, { success: false });
}
});
15 changes: 8 additions & 7 deletions pipes/isNegative.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { SchemaContext } from "../context.ts";
import { createError } from "../errors.ts";

const ERROR_MESSAGE = "Must be a negative number";
import { failure } from "../schema.ts";

/**
* Check the `value` as a negative number.
*/
export function isNegative(message = ERROR_MESSAGE) {
return function (value: number, context: SchemaContext) {
export function isNegative() {
return function (value: number) {
if (value < 0) {
return value;
}

return createError(context, { message });
return failure({
reason: "VALIDATION",
expected: "negative",
received: value,
});
};
}
13 changes: 7 additions & 6 deletions pipes/isPositive.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { assertEquals, assertIsError } from "assert/mod.ts";
import { createContext } from "../context.ts";
import { assertObjectMatch } from "assert/mod.ts";
import { number, pipe } from "../schemas/mod.ts";
import { isPositive } from "./isPositive.ts";
import { safeParse } from "../utils/mod.ts";

const context = createContext();
const schema = pipe(number(), isPositive());

Deno.test("should assert negative numbers", () => {
Deno.test("assert negative numbers", () => {
const correct = [1, 10, 100_000];
const incorrect = [-1, -10, -100_000];

for (const received of correct) {
assertEquals(schema.check(received, context), received);
const commit = safeParse(received, schema);
assertObjectMatch(commit, { success: true, value: received });
}

for (const received of incorrect) {
assertIsError(schema.check(received, context));
const commit = safeParse(received, schema);
assertObjectMatch(commit, { success: false });
}
});
Loading

0 comments on commit 5921b5f

Please sign in to comment.