Skip to content

Commit 3b9af20

Browse files
committed
Add schema package
1 parent 0e555f3 commit 3b9af20

File tree

10 files changed

+334
-16
lines changed

10 files changed

+334
-16
lines changed

.npmrc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@jsr:registry=https://npm.jsr.io

bun.lock

+19
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,32 @@
1212
},
1313
"packages/formdata": {
1414
"name": "@razr/formdata",
15+
"dependencies": {
16+
"@razr/utils": "workspace:*",
17+
},
18+
},
19+
"packages/schema": {
20+
"name": "@razr/schema",
21+
"dependencies": {
22+
"@razr/utils": "workspace:*",
23+
"@standard-schema/spec": "npm:@jsr/standard-schema__spec",
24+
},
25+
},
26+
"packages/utils": {
27+
"name": "@razr/utils",
1528
},
1629
},
1730
"packages": {
1831
"@razr/crypto": ["@razr/crypto@workspace:packages/crypto"],
1932

2033
"@razr/formdata": ["@razr/formdata@workspace:packages/formdata"],
2134

35+
"@razr/schema": ["@razr/schema@workspace:packages/schema"],
36+
37+
"@razr/utils": ["@razr/utils@workspace:packages/utils"],
38+
39+
"@standard-schema/spec": ["@jsr/[email protected]", "https://npm.jsr.io/~/11/@jsr/standard-schema__spec/1.0.0.tgz", {}, "sha512-eBw0t0tzF8I/72aM7CFIseRSAAYX4awSiKUf/1DHy0msgJpnYM/8/zSD09QEncfLz8Y+DKDGTP86zaT/dk+zOg=="],
40+
2241
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
2342
}
2443
}

packages/formdata/index.ts

+4-15
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,4 @@
1-
/**
2-
* Checks if the given input is an object (but not `null` or an array).
3-
* Ensures the input has a valid prototype chain for plain objects.
4-
*
5-
* @param {unknown} input - The value to check.
6-
* @returns {input is { [key: string]: unknown }} - True if the input is a plain object, false otherwise.
7-
*/
8-
function isObject(input: unknown): input is { [key: string]: unknown } {
9-
if (input === null || input === undefined) return false; // Early exit for null/undefined.
10-
const proto = Object.getPrototypeOf(input); // Get the prototype.
11-
return proto === null || proto === Object.prototype; // Check if it's a plain object.
12-
}
1+
import { isObject } from "@razr/utils";
132

143
/**
154
* Encodes an object into a `FormData` object for use in HTTP requests.
@@ -20,7 +9,7 @@ function isObject(input: unknown): input is { [key: string]: unknown } {
209
* @returns {FormData} - A `FormData` instance with the serialized data.
2110
*/
2211
export function encode<T extends { [key: string]: unknown }>(
23-
data: T,
12+
data: T
2413
): FormData {
2514
if (!isObject(data)) {
2615
throw new Error("The provided data must be a plain object.");
@@ -116,7 +105,7 @@ export type DecodeArray = DecodeValue[];
116105
*/
117106
export function decode(
118107
formData: FormData,
119-
options: { emptyString?: "set null" | "set undefined" | "preserve" } = {},
108+
options: { emptyString?: "set null" | "set undefined" | "preserve" } = {}
120109
): DecodeObject {
121110
const { emptyString = "preserve" } = options; // Default behavior for empty strings.
122111
const result = Object.create(null) as DecodeObject; // Root object for decoding.
@@ -131,7 +120,7 @@ export function decode(
131120
function setValue(
132121
target: Record<string, unknown>,
133122
keys: readonly string[],
134-
value: FormDataEntryValue,
123+
value: FormDataEntryValue
135124
): void {
136125
const len = keys.length;
137126
let current = target; // Pointer to the current level in the nested object.

packages/formdata/package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
{
22
"name": "@razr/formdata",
33
"module": "index.ts",
4-
"type": "module"
4+
"type": "module",
5+
"dependencies": {
6+
"@razr/utils": "workspace:*"
7+
}
58
}

packages/schema/README.md

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# schema
2+
3+
To install dependencies:
4+
5+
```bash
6+
bun install
7+
```
8+
9+
To run:
10+
11+
```bash
12+
bun run index.ts
13+
```
14+
15+
This project was created using `bun init` in bun v1.2.2. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

packages/schema/index.ts

+250
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import { isObject } from "@razr/utils";
2+
import type { StandardSchemaV1 } from "@standard-schema/spec";
3+
4+
/**
5+
* Represents the result of a schema validation.
6+
* @template T - The type of the value if validation is successful.
7+
*/
8+
export type Result<T> = StandardSchemaV1.Result<T>;
9+
10+
/**
11+
* Represents an issue encountered during schema validation.
12+
*/
13+
export type Issue = StandardSchemaV1.Issue;
14+
15+
/**
16+
* Infers the input type of a schema.
17+
* @template T - The schema type.
18+
*/
19+
export type InferInput<T extends Schema> = StandardSchemaV1.InferInput<T>;
20+
21+
/**
22+
* Infers the output type of a schema.
23+
* @template T - The schema type.
24+
*/
25+
export type InferOutput<T extends Schema> = StandardSchemaV1.InferOutput<T>;
26+
27+
/**
28+
* Defines a schema for validating and transforming input data.
29+
* @template TOutput - The type of the output after successful validation.
30+
* @template TInput - The type of the input data.
31+
*/
32+
export interface Schema<TOutput = unknown, TInput = unknown>
33+
extends StandardSchemaV1<TInput, TOutput> {
34+
/**
35+
* Parses the input data and returns the validated output.
36+
* @param input - The input data to be validated.
37+
* @returns The validated output.
38+
* @throws {SchemaError} If the input data is invalid.
39+
*/
40+
parse(input: TInput): TOutput;
41+
42+
/**
43+
* Safely parses the input data and returns a result object.
44+
* @param input - The input data to be validated.
45+
* @returns A result object containing either the validated output or a list of issues.
46+
*/
47+
safeParse(input: TInput): Result<TOutput>;
48+
}
49+
50+
/**
51+
* Represents an error that occurs during schema validation.
52+
*/
53+
export class SchemaError extends Error {
54+
/**
55+
* Creates a new SchemaError instance.
56+
* @param issues - A list of issues encountered during validation.
57+
*/
58+
constructor(readonly issues: readonly Issue[]) {
59+
super();
60+
}
61+
}
62+
63+
/**
64+
* Creates a new schema with the given safeParse function.
65+
* @template TOutput - The type of the output after successful validation.
66+
* @template TInput - The type of the input data.
67+
* @param safeParse - A function that safely parses the input data.
68+
* @returns A new schema instance.
69+
*/
70+
function createSchema<TOutput = unknown, TInput = unknown>(
71+
safeParse: (input: TInput) => Result<TOutput>
72+
): Schema<TOutput, TInput> {
73+
/**
74+
* Parses the input data and returns the validated output.
75+
* @param input - The input data to be validated.
76+
* @returns The validated output.
77+
* @throws {SchemaError} If the input data is invalid.
78+
*/
79+
const parse = (input: TInput): TOutput => {
80+
const result = safeParse(input);
81+
if (result.issues) {
82+
throw new SchemaError(result.issues);
83+
}
84+
return result.value;
85+
};
86+
87+
return {
88+
parse,
89+
safeParse,
90+
"~standard": {
91+
validate: (value) => safeParse(value as TInput),
92+
vendor: "razr",
93+
version: 1,
94+
},
95+
};
96+
}
97+
98+
/**
99+
* Prepends a key (e.g., array index) to the path of each issue in the list.
100+
* @param key - The key to prepend (e.g., array index).
101+
* @param issues - The list of issues to modify.
102+
* @returns A new list of issues with the key prepended to their paths.
103+
*/
104+
function prependKeyToIssues(
105+
key: PropertyKey,
106+
issues: readonly Issue[]
107+
): Issue[] {
108+
return issues.map((issue) => ({
109+
...issue,
110+
path: Array.isArray(issue.path) ? [key, ...issue.path] : undefined,
111+
}));
112+
}
113+
114+
/**
115+
* Creates a schema that validates if the input is a string.
116+
* @param message - The error message to return if validation fails.
117+
* @returns A schema that validates string inputs.
118+
*/
119+
export function str(message = "Expected string") {
120+
return createSchema<string>((value) => {
121+
if ("string" === typeof value) return { value };
122+
return { issues: [{ message }] };
123+
});
124+
}
125+
126+
/**
127+
* Creates a schema that validates if the input is a number.
128+
* @param message - The error message to return if validation fails.
129+
* @returns A schema that validates number inputs.
130+
*/
131+
export function num(message = "Expected number") {
132+
return createSchema<number>((value) => {
133+
if ("number" === typeof value && Number.isFinite(value)) return { value };
134+
return { issues: [{ message }] };
135+
});
136+
}
137+
138+
/**
139+
* Creates a schema that validates if the input is a boolean.
140+
* @param message - The error message to return if validation fails.
141+
* @returns A schema that validates boolean inputs.
142+
*/
143+
export function bool(message = "Expected boolean") {
144+
return createSchema<boolean>((value) => {
145+
if ("boolean" === typeof value) return { value };
146+
return { issues: [{ message }] };
147+
});
148+
}
149+
150+
/**
151+
* Creates a schema that validates if the input is an array and validates each element using the provided schema.
152+
* @template T - The schema type for validating array elements.
153+
* @param schema - The schema used to validate each element of the array.
154+
* @param message - The error message to return if validation fails.
155+
* @returns A schema that validates array inputs.
156+
*/
157+
export function list<T extends Schema>(schema: T, message = "Expected array") {
158+
return createSchema<InferOutput<T>[]>((input) => {
159+
if (!Array.isArray(input)) return { issues: [{ message }] };
160+
const len = input.length;
161+
const value = new Array(len) as InferOutput<T>[];
162+
for (let i = 0; i < len; i++) {
163+
const result = schema.safeParse(input[i]);
164+
if (result.issues) {
165+
return { issues: prependKeyToIssues(i, result.issues) };
166+
}
167+
value[i] = result.value;
168+
}
169+
return { value };
170+
});
171+
}
172+
173+
/**
174+
* Represents a raw object shape where keys are property keys and values are unknown.
175+
*/
176+
type RawShape = { [key: PropertyKey]: unknown };
177+
178+
/**
179+
* Represents an object shape where each key is mapped to a schema.
180+
* @template T - The raw object shape.
181+
*/
182+
type ObjectShape<T extends RawShape> = { [K in keyof T]: Schema<T[K]> };
183+
184+
/**
185+
* Represents a schema for validating and transforming objects.
186+
* @template TOutput - The type of the output object after successful validation.
187+
* @template TInput - The type of the input data.
188+
*/
189+
export interface ObjectSchema<TOutput extends RawShape, TInput = unknown>
190+
extends Schema<TOutput, TInput> {
191+
/**
192+
* The shape of the object, where each key is mapped to a schema.
193+
*/
194+
shape: ObjectShape<TOutput>;
195+
}
196+
197+
/**
198+
* Creates a schema that validates if the input is an object and validates each property using the provided shape.
199+
* @template T - The raw object shape.
200+
* @param shape - The shape of the object, where each key is mapped to a schema.
201+
* @param message - The error message to return if validation fails.
202+
* @returns A schema that validates object inputs.
203+
*/
204+
export function obj<T extends RawShape>(
205+
shape: ObjectShape<T>,
206+
message = "Expected object"
207+
): ObjectSchema<T> {
208+
return {
209+
shape,
210+
...createSchema<T>((input) => {
211+
if (!isObject(input)) return { issues: [{ message }] };
212+
const value = Object.create(null) as T;
213+
for (const key in shape) {
214+
const result = shape[key].safeParse(input[key]);
215+
if (result.issues) {
216+
return { issues: prependKeyToIssues(key, result.issues) };
217+
}
218+
value[key] = result.value;
219+
}
220+
return { value };
221+
}),
222+
};
223+
}
224+
225+
/**
226+
* Creates a schema that allows the input to be `null` or `undefined`, and validates it using the provided schema if it is not.
227+
* @template T - The schema type.
228+
* @param schema - The schema used to validate the input if it is not `null` or `undefined`.
229+
* @returns A schema that validates inputs that can be `null`, `undefined`, or match the provided schema.
230+
*/
231+
export function maybe<T extends Schema>(schema: T) {
232+
return createSchema<InferOutput<T> | undefined>((value) => {
233+
if (null === value || undefined === value) return { value: undefined };
234+
return schema.safeParse(value);
235+
});
236+
}
237+
238+
/**
239+
* Creates a schema that provides a default value if the input is `null` or `undefined`, and validates it using the provided schema if it is not.
240+
* @template T - The schema type.
241+
* @param schema - The schema used to validate the input if it is not `null` or `undefined`.
242+
* @param defaultValue - The default value to use if the input is `null` or `undefined`.
243+
* @returns A schema that validates inputs and provides a default value if necessary.
244+
*/
245+
export function def<T extends Schema>(schema: T, defaultValue: InferOutput<T>) {
246+
return createSchema<InferOutput<T>>((value) => {
247+
if (null === value || undefined === value) return { value: defaultValue };
248+
return schema.safeParse(value);
249+
});
250+
}

packages/schema/package.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "@razr/schema",
3+
"module": "index.ts",
4+
"type": "module",
5+
"dependencies": {
6+
"@razr/utils": "workspace:*",
7+
"@standard-schema/spec": "npm:@jsr/standard-schema__spec"
8+
}
9+
}

packages/utils/README.md

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# utils
2+
3+
To install dependencies:
4+
5+
```bash
6+
bun install
7+
```
8+
9+
To run:
10+
11+
```bash
12+
bun run index.ts
13+
```
14+
15+
This project was created using `bun init` in bun v1.2.2. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

packages/utils/index.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Checks if the given input is an object (but not `null` or an array).
3+
* Ensures the input has a valid prototype chain for plain objects.
4+
*
5+
* @param {unknown} input - The value to check.
6+
* @returns {input is { [key: string]: unknown }} - True if the input is a plain object, false otherwise.
7+
*/
8+
export function isObject(input: unknown): input is { [key: string]: unknown } {
9+
if (input === null || input === undefined) return false; // Early exit for null/undefined.
10+
const proto = Object.getPrototypeOf(input); // Get the prototype.
11+
return proto === null || proto === Object.prototype; // Check if it's a plain object.
12+
}

0 commit comments

Comments
 (0)