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
2,585 changes: 918 additions & 1,667 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "quartet",
"version": "11.0.2",
"version": "11.0.3",
"description": "functional and convenient validation library",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down Expand Up @@ -49,12 +49,13 @@
"size-limit": "^11.2.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.29.1",
"vitest": "^3.1.1"
"vitest": "^3.1.1",
"@eslint/compat": "^1.2.8"
},
"files": [
"lib/**/*"
],
"dependencies": {
"@eslint/compat": "^1.2.8"
"@standard-schema/spec": "^1.0.0"
}
}
56 changes: 56 additions & 0 deletions src/__tests__/e.standard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { StandardSchemaV1 } from "@standard-schema/spec";
import { describe, expect } from "vitest";
import { e } from "../e";

const util = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
equal: <a, b>(_value: a extends b ? (b extends a ? true : false) : false) => {
// just for typechecking
},
};

describe("standard schema compat", (test) => {
test("assignability", () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _s1: StandardSchemaV1 = e(e.string);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _s4: StandardSchemaV1<unknown, string> = e(e.string);
});

test("type inference", () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const stringToNumber = e(e.safeInteger);
type input = StandardSchemaV1.InferInput<typeof stringToNumber>;
util.equal<input, unknown>(true);
type output = StandardSchemaV1.InferOutput<typeof stringToNumber>;
util.equal<output, number>(true);
});

test("valid parse", () => {
const schema = e(e.string);
const result = schema["~standard"]["validate"]("hello");
if (result instanceof Promise) {
throw new Error("Expected sync result");
}
expect(result.issues).toEqual(undefined);
if (result.issues) {
throw new Error("Expected no issues");
} else {
expect(result.value).toEqual("hello");
}
});

test("invalid parse", () => {
const schema = e(e.string);
const result = schema["~standard"]["validate"](1234);
if (result instanceof Promise) {
throw new Error("Expected sync result");
}
expect(result.issues).toBeDefined();
if (!result.issues) {
throw new Error("Expected issues");
}
expect(result.issues.length).toEqual(1);
expect(result.issues[0].path).toEqual([]);
});
});
56 changes: 56 additions & 0 deletions src/__tests__/v.standard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { StandardSchemaV1 } from "@standard-schema/spec";
import { describe, expect } from "vitest";
import { v } from "../v";

const util = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
equal: <a, b>(_value: a extends b ? (b extends a ? true : false) : false) => {
// just for typechecking
},
};

describe("standard schema compat", (test) => {
test("assignability", () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _s1: StandardSchemaV1 = v(v.string);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _s4: StandardSchemaV1<unknown, string> = v(v.string);
});

test("type inference", () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const stringToNumber = v(v.safeInteger);
type input = StandardSchemaV1.InferInput<typeof stringToNumber>;
util.equal<input, unknown>(true);
type output = StandardSchemaV1.InferOutput<typeof stringToNumber>;
util.equal<output, number>(true);
});

test("valid parse", () => {
const schema = v(v.string);
const result = schema["~standard"]["validate"]("hello");
if (result instanceof Promise) {
throw new Error("Expected sync result");
}
expect(result.issues).toEqual(undefined);
if (result.issues) {
throw new Error("Expected no issues");
} else {
expect(result.value).toEqual("hello");
}
});

test("invalid parse", () => {
const schema = v(v.string);
const result = schema["~standard"]["validate"](1234);
if (result instanceof Promise) {
throw new Error("Expected sync result");
}
expect(result.issues).toBeDefined();
if (!result.issues) {
throw new Error("Expected issues");
}
expect(result.issues.length).toEqual(1);
expect(result.issues[0].path).toEqual([]);
});
});
149 changes: 146 additions & 3 deletions src/compilers/eCompiler/eCompileSchema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,136 @@
/* tslint:disable:object-literal-sort-keys */
import { IExplanation } from "../../explanations";
import { StandardSchemaV1 } from "@standard-schema/spec";
import {
ExplanationSchemaType,
IExplanation,
TExplanationSchema,
} from "../../explanations";
import { Z } from "../../types";
import { CompilationResult, TSchema, Validator } from "../../types";
import { implStandard } from "../implStandard";
import { getExplanator } from "./getExplanator";

function getExpectedTypeName(schema: TExplanationSchema): string {
if (schema === undefined) return `undefined`;
if (schema === null) return `null`;
if (
typeof schema === "boolean" ||
typeof schema === "number" ||
typeof schema === "string"
)
return `${JSON.stringify(schema)}`;
if (typeof schema === "symbol") {
return `${schema.toString()}`;
}
if (typeof schema === "bigint") {
return `${schema}n`;
}
if (schema.type === ExplanationSchemaType.And) {
return `and<${schema.schemas.map((t) => getExpectedTypeName(t)).join(",")}>`;
}
if (schema.type === ExplanationSchemaType.Any) {
return `any`;
}
if (schema.type === ExplanationSchemaType.Array) {
return `Array<any>`;
}
if (schema.type === ExplanationSchemaType.ArrayOf) {
return `Array<${getExpectedTypeName(schema.elementSchema)}>`;
}
if (schema.type === ExplanationSchemaType.Boolean) {
return `boolean`;
}
if (schema.type === ExplanationSchemaType.Finite) {
return `finite`;
}
if (schema.type === ExplanationSchemaType.Function) {
return `function`;
}
if (schema.type === ExplanationSchemaType.Max) {
if (schema.isExclusive) {
return `lt<${schema.maxValue}>`;
} else {
return `le<${schema.maxValue}>`;
}
}
if (schema.type === ExplanationSchemaType.MaxLength) {
if (schema.isExclusive) {
return `lengthLt<${schema.maxLength}>`;
}
return `lengthLe<${schema.maxLength}>`;
}
if (schema.type === ExplanationSchemaType.Min) {
if (schema.isExclusive) {
return `gt<${schema.minValue}>`;
} else {
return `ge<${schema.minValue}>`;
}
}
if (schema.type === ExplanationSchemaType.MinLength) {
if (schema.isExclusive) {
return `lengthGt<${schema.minLength}>`;
}
return `lengthGe<${schema.minLength}>`;
}
if (schema.type === ExplanationSchemaType.Negative) {
return `ge<0>`;
}
if (schema.type === ExplanationSchemaType.Never) {
return `never`;
}
if (schema.type === ExplanationSchemaType.Not) {
const inner = getExpectedTypeName(schema.schema);
return `not<${inner}>`;
}
if (schema.type === ExplanationSchemaType.NotANumber) {
return `NaN`;
}
if (schema.type === ExplanationSchemaType.Number) {
return `number`;
}
if (schema.type === ExplanationSchemaType.Object) {
return `{ ${Object.entries(schema.propsSchemas).map((x) => `${x[0]}: ${getExpectedTypeName(x[1])}`)} }`;
}
if (schema.type === ExplanationSchemaType.Pair) {
return `pair<${getExpectedTypeName(schema.keyValueSchema)}>`;
}
if (schema.type === ExplanationSchemaType.Positive) {
return `gt<0>`;
}
if (schema.type === ExplanationSchemaType.SafeInteger) {
return `safeInteger`;
}
if (schema.type === ExplanationSchemaType.String) {
return `string`;
}
if (schema.type === ExplanationSchemaType.Symbol) {
return `symbol`;
}
if (schema.type === ExplanationSchemaType.Test) {
return `test<${schema.description}>`;
}
if (schema.type === ExplanationSchemaType.Variant) {
return `oneOf<${schema.variants.map((x) => getExpectedTypeName(x)).join(", ")}>`;
}
if (schema.type === ExplanationSchemaType.Custom) {
return `custom<${schema.description}>`;
}

return JSON.stringify(schema);
}

function getMessage(explanation: IExplanation): string {
const { schema } = explanation;

return `expected type: ${getExpectedTypeName(schema)}`;
}

function getPath(
explanation: IExplanation,
): ReadonlyArray<PropertyKey | StandardSchemaV1.PathSegment> | undefined {
return [...explanation.path];
}

export function eCompileSchema<T = Z>(
schema: TSchema,
): CompilationResult<T, Z> {
Expand All @@ -25,11 +152,27 @@ export function eCompileSchema<T = Z>(
}
}

return Object.assign(validator as Validator<T>, {
const res = Object.assign(validator as Validator<T>, {
explanations,
schema,
cast() {
return this as Z;
},
});
}) as Z;
res["~standard"] = implStandard(
res as CompilationResult<T, IExplanation>,
(explanations) => {
return explanations.map((explanation) => {
const message = getMessage(explanation);
const path = getPath(explanation);
return {
/** The error message of the issue. */
message,
/** The path of the issue, if any. */
path,
};
});
},
);
return res;
}
21 changes: 21 additions & 0 deletions src/compilers/implStandard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { StandardSchemaV1 } from "@standard-schema/spec";
import { CompilationResult } from "../types";

export function implStandard<const T, const E>(
v: CompilationResult<T, E>,
explanationsToIssues: (explanation: readonly E[]) => StandardSchemaV1.Issue[],
): StandardSchemaV1<unknown, T>["~standard"] {
return {
validate: (value) =>
v(value)
? {
value: value as T,
issues: undefined,
}
: {
issues: explanationsToIssues(v.explanations),
},
vendor: "quartet",
version: 1,
};
}
12 changes: 10 additions & 2 deletions src/compilers/vCompiler/vCompileSchema.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { Z } from "../../types";
import { CompilationResult, TSchema, Validator } from "../../types";
import { getValidatorFromSchema } from "./getValidatorFromSchema";
import { implStandard } from "../implStandard";

export function vCompileSchema<T = Z>(
schema: TSchema,
): CompilationResult<T, Z> {
const explanations: Z[] = [];
const validator = getValidatorFromSchema(schema, undefined) as Validator<T>;
return Object.assign(validator, {
const res = Object.assign(validator, {
explanations,
schema,
cast() {
return this as Z;
},
});
}) as Z;
res["~standard"] = implStandard(res, () => [
{
message: "invalid value",
path: [],
},
]);
return res;
}
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { StandardSchemaV1 } from "@standard-schema/spec";
import { IfAny } from "./IfAny";
import { SchemaType } from "./schemas/SchemaType";

Expand Down Expand Up @@ -177,7 +178,7 @@ export type CompilationResult<T = Z, Explanation = Z> = Validator<T> &
ICompilationResultProps<Explanation> & {
readonly schema: TSchema;
cast<U>(): CompilationResult<U, Explanation>;
};
} & StandardSchemaV1<unknown, T>;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Z = any;