diff --git a/apps/website/components/SidebarSection.svelte b/apps/website/components/SidebarSection.svelte index 5db5dd99b..919717b0e 100644 --- a/apps/website/components/SidebarSection.svelte +++ b/apps/website/components/SidebarSection.svelte @@ -1,7 +1,7 @@
  • {section.title}
  • diff --git a/apps/website/static/master.css b/apps/website/static/master.css index f7ffd2702..e6ac71871 100644 --- a/apps/website/static/master.css +++ b/apps/website/static/master.css @@ -38,9 +38,9 @@ --h1: var(--prs-text-xl); --h2: var(--prs-text-l); --h3: var(--prs-text-n); - --h4: var(--prs-text-s); - --h5: var(--prs-text-xs); - --h6: var(--prs-text-xxs); + --h4: var(--prs-text-m); + --h5: var(--prs-text-m); + --h6: var(--prs-text-m); --a: var(--prs-link); --bg: #fff; @@ -133,6 +133,10 @@ h5 { font-size: var(--h5); } +h6 { + font-size: var(--h6); +} + ul { list-style: none; position: relative; @@ -430,6 +434,7 @@ pre { table { border-collapse: collapse; + margin-bottom: 2.5rem; } th, @@ -480,6 +485,10 @@ article a.deeplink { padding-left: 3rem; } +.sidebar .depth-4 { + padding-left: 4rem; +} + .sidebar li.active { border-left: 1px solid var(--sidebar-active); } diff --git a/docs/docs/validation.md b/docs/docs/validation.md index 5ce698fd5..aa2196f4a 100644 --- a/docs/docs/validation.md +++ b/docs/docs/validation.md @@ -4,14 +4,14 @@ title: Input validation # Validation -In Primate, validation refers to making sure that input passed into your routes -— typically from the frontend or API clients — is properly checked at runtime. -This ensures your backend logic never executes on malformed or malicious data. +Primate uses **Pema** (**P**rimate sch**ema**) for runtime data validation. Pema +is a schema validation library that provides runtime type checking with full +TypeScript type inference. Because the web is for the most part untyped, everything arrives as strings, -binary blobs, or loosely structured JSON. Primate uses its own validation -framework, Pema, to define schemas for these inputs. These schemas are applied -in your routes to guarantee that inputs match the shapes your logic expects. +binary blobs, or loosely structured JSON. Pema lets you define schemas for +these inputs and apply them in your routes to guarantee that inputs match the +shapes your logic expects. !!! TypeScript already provides compile-time validation during development, but @@ -19,9 +19,64 @@ only Pema can enforce correctness at runtime, when real clients interact with your app. !!! -Validation errors are surfaced as `ParseError`. Unlike regular errors, -Primate automatically serializes them into a `400 Bad Request` JSON response -and returns them to the client: +Pema can be used in two ways: +- **Within Primate**: Validate request bodies, query parameters, headers, and + path parameters in your routes +- **Standalone**: Use Pema anywhere in your application for general-purpose + validation + +## Installation + +Pema is included with Primate, but can also be installed standalone: + +```bash +npm install pema +``` +```bash +pnpm add pema +``` +```bash +yarn add pema +``` +```bash +bun add pema +``` + +## Quick Example + +```ts +import pema from "pema"; +import string from "pema/string"; +import number from "pema/number"; + +// Define a schema +const User = pema({ + name: string.min(1), + email: string.email(), + age: number.min(0), +}); + +// Parse and validate data +const user = User.parse({ + name: "John Doe", + email: "john@example.com", + age: 30, +}); +// user is typed as { name: string; email: string; age: number } + +// Invalid data throws ParseError +try { + User.parse({ name: "", email: "invalid", age: -1 }); +} catch (error) { + console.log(error.message); // Validation error details +} +``` + +## Using Pema in Primate + +In Primate, validation errors are surfaced as `ParseError`. Unlike regular +errors, Primate automatically serializes them into a `400 Bad Request` JSON +response and returns them to the client: ```json { @@ -35,7 +90,7 @@ and returns them to the client: You can override this by catching the error yourself and returning a custom response. -## Summary +### Summary | Input type | In Primate | Types | Use cases | | ------------------------------------- | --------------------- | ---------------- | ------------------------------------| @@ -46,7 +101,7 @@ response. | [Path parameters](#path-parameters) | `request.path` | `string` | REST resources, nested routes | | [Headers](#headers) | `request.headers` | `string` | Authentication, content negotiation | -## Web forms +### Web forms Form submissions are received as `request.body.form`, which optionally accepts a schema. Values arrive as strings (or `File` for file inputs). @@ -78,7 +133,8 @@ logic. `ParseError` that is intercepted and passed to the client. !!! -## JSON API calls +### JSON API calls + When clients send JSON data (e.g. via `fetch`), you access it with `request.body.json`. @@ -102,7 +158,8 @@ route.post(request => { }); ``` -## Binary uploads +### Binary uploads + Raw uploads (e.g. images) are available through `request.body.binary` as a `Blob`. @@ -129,7 +186,8 @@ route.post(request => { Use this for scenarios where the payload itself is the file, not just a form field. -## Query parameters +### Query parameters + Query parameters (e.g. `?page=2&filter=active`) are strings accessible at `request.query`. @@ -152,7 +210,8 @@ route.get(request => { }); ``` -## Path parameters +### Path parameters + Path parameters are extracted from the route definition and exposed on `request.path`. @@ -173,7 +232,8 @@ route.get(request => { This is common in REST-style routes. -## Headers +### Headers + Request headers are strings, available via `request.headers`. ```ts @@ -194,3 +254,1930 @@ route.get(request => { return `Validated token: ${bearer}`; }); ``` + +## Pema Reference + +Pema can be used anywhere you need validation, not just in Primate routes. +This section provides a complete reference for all Pema types, validators, and +patterns. + +### Core Concepts + +#### Creating Schemas + +The main `pema()` function creates a schema from an object definition: + +```ts +import pema from "pema"; +import string from "pema/string"; +import number from "pema/number"; + +const Schema = pema({ + name: string, + count: number, +}); +``` + +You can also use individual type validators directly: + +```ts +import string from "pema/string"; + +const validated = string.email().parse("user@example.com"); +``` + +#### Parsing Values + +Every schema has a `.parse()` method that validates input and returns the typed +value: + +```ts +const result = Schema.parse(input); +``` + +- If validation succeeds, returns the validated value with proper TypeScript + types +- If validation fails, throws a `ParseError` with detailed issue information + +#### Type Inference + +Pema provides full TypeScript type inference. The parsed result is +automatically typed based on your schema: + +```ts +import pema from "pema"; +import string from "pema/string"; +import number from "pema/number"; + +const User = pema({ + name: string, + age: number.optional(), +}); + +type User = typeof User.infer; +// Equivalent to: { name: string; age: number | undefined } +``` + +#### Schema Normalization + +Pema automatically normalizes JavaScript values into schema types: + +```ts +import pema from "pema"; +import string from "pema/string"; + +// Literal values become LiteralType +const Status = pema({ + type: "active", // LiteralType<"active"> + code: 200, // LiteralType<200> + enabled: true, // LiteralType +}); + +// Plain objects become ObjectType +const Nested = pema({ + config: { // ObjectType<{ host: StringType }> + host: string, + }, +}); + +// Arrays with single element become ArrayType +const Tags = pema({ + tags: [string], // ArrayType +}); + +// Arrays with multiple elements become TupleType +const Point = pema({ + coords: [number, number], // TupleType<[NumberType, NumberType]> +}); + +// Classes become ConstructorType +class CustomClass {} +const Custom = pema({ + instance: CustomClass, // ConstructorType +}); +``` + +### Primitive Types + +#### string + +Validates that a value is a string. + +```ts +import string from "pema/string"; + +// Basic validation +string.parse("hello"); // "hello" +string.parse(123); // throws ParseError +``` + +##### String Validators + +| Validator | Description | Example | +|-----------|-------------|---------| +| `.min(n)` | Minimum length | `string.min(5)` | +| `.max(n)` | Maximum length | `string.max(100)` | +| `.length(min, max)` | Length range | `string.length(5, 10)` | +| `.email()` | Valid email format | `string.email()` | +| `.uuid()` | Valid UUID format | `string.uuid()` | +| `.startsWith(prefix)` | Must start with prefix | `string.startsWith("/")` | +| `.endsWith(suffix)` | Must end with suffix | `string.endsWith(".js")` | +| `.regex(pattern)` | Match regex pattern | `string.regex(/^[a-z]+$/)` | +| `.isotime()` | ISO time format | `string.isotime()` | + +```ts +// Email validation +const email = string.email(); +email.parse("user@example.com"); // "user@example.com" +email.parse("invalid"); // throws: "invalid" is not a valid email + +// UUID validation +const uuid = string.uuid(); +uuid.parse("4d0996db-BDA9-4f95-ad7c-7075b10d4ba6"); // valid +uuid.parse("not-a-uuid"); // throws + +// Length constraints +const username = string.min(3).max(20); +username.parse("john"); // "john" +username.parse("ab"); // throws: min 3 characters + +// Prefix/suffix validation +const path = string.startsWith("/").endsWith(".html"); +path.parse("/index.html"); // "/index.html" +path.parse("index.html"); // throws: does not start with "/" + +// Chaining validators +const slug = string.min(1).max(50).regex(/^[a-z0-9-]+$/); +slug.parse("my-blog-post"); // "my-blog-post" +``` + +#### number + +Validates that a value is a number (64-bit float by default). + +```ts +import number from "pema/number"; + +// Basic validation +number.parse(42); // 42 +number.parse(3.14); // 3.14 +number.parse("42"); // throws ParseError +number.parse(42n); // throws ParseError (bigint not allowed) +``` + +##### Number Validators + +| Validator | Description | Example | +|-----------|-------------|---------| +| `.min(n)` | Minimum value | `number.min(0)` | +| `.max(n)` | Maximum value | `number.max(100)` | +| `.range(min, max)` | Value range | `number.range(0, 100)` | + +```ts +// With coercion (converts strings to numbers) +const coerced = number.coerce; +coerced.parse("42"); // 42 +coerced.parse("3.14"); // 3.14 +coerced.parse("-1.5"); // -1.5 + +// Range validation +const percentage = number.range(0, 100); +percentage.parse(50); // 50 +percentage.parse(150); // throws: out of range + +// Min/max validation +const positive = number.min(0); +positive.parse(10); // 10 +positive.parse(-5); // throws: -5 is lower than min (0) +``` + +#### boolean + +Validates that a value is a boolean. + +```ts +import boolean from "pema/boolean"; + +// Basic validation +boolean.parse(true); // true +boolean.parse(false); // false +boolean.parse("true"); // throws ParseError + +// With coercion +const coerced = boolean.coerce; +coerced.parse("true"); // true +coerced.parse("false"); // false +coerced.parse("1"); // throws (only "true"/"false" strings allowed) +``` + +#### bigint + +Validates that a value is a bigint (signed, 64-bit range by default). + +```ts +import bigint from "pema/bigint"; + +// Basic validation +bigint.parse(42n); // 42n +bigint.parse(0n); // 0n +bigint.parse(42); // throws ParseError (number not allowed) +bigint.parse("42"); // throws ParseError + +// With coercion +const coerced = bigint.coerce; +coerced.parse(42); // 42n +coerced.parse("42"); // 42n +coerced.parse("42.0"); // 42n (integer part only) +coerced.parse("0.5"); // throws (not an integer) +``` + +##### BigInt Validators + +| Validator | Description | Example | +|-----------|-------------|---------| +| `.min(n)` | Minimum value | `bigint.min(0n)` | +| `.max(n)` | Maximum value | `bigint.max(1000n)` | +| `.range(min, max)` | Value range | `bigint.range(0n, 100n)` | + +```ts +const positive = bigint.min(0n); +positive.parse(100n); // 100n +positive.parse(-1n); // throws: -1 is lower than min (0) +``` + +#### biguint + +Validates that a value is an unsigned bigint (>= 0). + +```ts +import biguint from "pema/biguint"; + +// Basic validation +biguint.parse(42n); // 42n +biguint.parse(0n); // 0n +biguint.parse(-1n); // throws: -1 is out of range + +// With coercion +const coerced = biguint.coerce; +coerced.parse(42); // 42n +coerced.parse("100"); // 100n +coerced.parse(-1); // throws: -1 is out of range +``` + +#### symbol + +Validates that a value is a symbol. + +```ts +import symbol from "pema/symbol"; + +const sym = Symbol("test"); +symbol.parse(sym); // sym +symbol.parse("symbol"); // throws ParseError +``` + +#### date + +Validates that a value is a Date object. + +```ts +import date from "pema/date"; + +// Basic validation +const d = new Date(); +date.parse(d); // d +date.parse("2024-01-01"); // throws ParseError + +// With coercion (converts timestamps to Date) +const coerced = date.coerce; +coerced.parse(1723718400000); // Date object +coerced.parse(new Date()); // Date object +``` + +#### unknown + +Accepts any value without validation. Useful as a placeholder or for dynamic +data. + +```ts +import unknown from "pema/unknown"; + +unknown.parse("anything"); // "anything" +unknown.parse(42); // 42 +unknown.parse({ foo: "bar" }); // { foo: "bar" } +unknown.parse(null); // null +``` + +### Binary Types + +#### blob + +Validates that a value is a Blob object. Useful for binary data and file +uploads. + +```ts +import blob from "pema/blob"; + +// Basic validation +const b = new Blob(["content"], { type: "text/plain" }); +blob.parse(b); // b +blob.parse("not a blob"); // throws ParseError + +// File extends Blob, so files pass blob validation +const f = new File(["content"], "test.txt"); +blob.parse(f); // f (File is a Blob subclass) +``` + +##### With Default Value + +```ts +const defaultBlob = new Blob(); +const blobWithDefault = blob.default(defaultBlob); + +blobWithDefault.parse(undefined); // defaultBlob +blobWithDefault.parse(new Blob()); // the provided Blob +``` + +#### file + +Validates that a value is a File object (more specific than Blob). + +```ts +import file from "pema/file"; + +// Basic validation +const f = new File(["content"], "document.txt"); +file.parse(f); // f +file.parse("not a file"); // throws ParseError + +// Blob is NOT a File +const b = new Blob(["content"]); +file.parse(b); // throws ParseError (Blob !== File) +``` + +##### With Default Value + +```ts +const defaultFile = new File([""], "default.txt"); +const fileWithDefault = file.default(defaultFile); + +fileWithDefault.parse(undefined); // defaultFile +fileWithDefault.parse(new File(["x"], "x.txt")); // the provided File +``` + +#### url + +Validates that a value is a URL object. + +```ts +import url from "pema/url"; + +// Basic validation - requires URL object, not string +const u = new URL("https://example.com"); +url.parse(u); // u +url.parse("https://example.com"); // throws ParseError (string not allowed) +``` + +##### With Default Value + +```ts +const defaultUrl = new URL("https://default.com"); +const urlWithDefault = url.default(defaultUrl); + +urlWithDefault.parse(undefined); // defaultUrl +urlWithDefault.parse(new URL("https://other.com")); // the provided URL +``` + +### Integer Types + +Pema provides precise integer types with compile-time and runtime range +validation. + +#### int / uint + +Generic integer types that map to `i32` and `u32` respectively. + +```ts +import int from "pema/int"; +import uint from "pema/uint"; + +// int: signed 32-bit integer (-2^31 to 2^31-1) +int.parse(42); // 42 +int.parse(-100); // -100 +int.parse(3.14); // throws: 3.14 is not an integer +int.parse("42"); // throws ParseError + +// uint: unsigned 32-bit integer (0 to 2^32-1) +uint.parse(42); // 42 +uint.parse(0); // 0 +uint.parse(-1); // throws: -1 is out of range +``` + +##### Integer Validators + +| Validator | Description | Example | +|-----------|-------------|---------| +| `.min(n)` | Minimum value | `int.min(-10)` | +| `.max(n)` | Maximum value | `int.max(100)` | +| `.range(min, max)` | Value range | `int.range(0, 100)` | + +```ts +// Range validation +const score = int.range(0, 100); +score.parse(85); // 85 +score.parse(150); // throws: 150 is out of range + +// Min/max validation +const age = uint.min(0).max(150); +age.parse(25); // 25 +age.parse(-1); // throws: -1 is out of range + +// With coercion +const coerced = int.coerce; +coerced.parse("42"); // 42 +coerced.parse("42.0"); // 42 +coerced.parse("42.5"); // throws: 42.5 is not an integer +``` + +#### Sized Integers + +Pema provides sized integer types for precise control over numeric ranges. + +##### Signed Integers + +| Type | Range | Use Case | +|------|-------|----------| +| `i8` | -128 to 127 | Byte-level data | +| `i16` | -32,768 to 32,767 | Short integers | +| `i32` | -2,147,483,648 to 2,147,483,647 | Standard integers | +| `i64` | -2^63 to 2^63-1 | Large integers (bigint) | +| `i128` | -2^127 to 2^127-1 | Very large integers (bigint) | + +```ts +import i8 from "pema/i8"; +import i16 from "pema/i16"; +import i32 from "pema/i32"; +import i64 from "pema/i64"; +import i128 from "pema/i128"; + +// i8: -128 to 127 +i8.parse(127); // 127 +i8.parse(128); // throws: 128 is out of range +i8.parse(-128); // -128 +i8.parse(-129); // throws: -129 is out of range + +// i16: -32768 to 32767 +i16.parse(32767); // 32767 +i16.parse(32768); // throws: out of range + +// i32: -2147483648 to 2147483647 +i32.parse(2147483647); // 2147483647 +i32.parse(2147483648); // throws: out of range + +// i64 and i128 use bigint +i64.parse(9223372036854775807n); // valid +i128.parse(0n); // valid +``` + +##### Unsigned Integers + +| Type | Range | Use Case | +|------|-------|----------| +| `u8` | 0 to 255 | Bytes, colors | +| `u16` | 0 to 65,535 | Ports, short counters | +| `u32` | 0 to 4,294,967,295 | IDs, timestamps | +| `u64` | 0 to 2^64-1 | Large IDs (bigint) | +| `u128` | 0 to 2^128-1 | UUIDs as integers (bigint) | + +```ts +import u8 from "pema/u8"; +import u16 from "pema/u16"; +import u32 from "pema/u32"; +import u64 from "pema/u64"; +import u128 from "pema/u128"; + +// u8: 0 to 255 +u8.parse(255); // 255 +u8.parse(256); // throws: 256 is out of range +u8.parse(-1); // throws: -1 is out of range + +// u16: 0 to 65535 (useful for ports) +u16.parse(8080); // 8080 +u16.parse(65536); // throws: out of range + +// u32: 0 to 4294967295 +u32.parse(4294967295); // 4294967295 + +// u64 and u128 use bigint +u64.parse(18446744073709551615n); // valid +u128.parse(0n); // valid +``` + +##### Coercion for Sized Integers + +All sized integer types support coercion from strings: + +```ts +import u8 from "pema/u8"; + +const coerced = u8.coerce; +coerced.parse("200"); // 200 +coerced.parse("200.0"); // 200 +coerced.parse("256"); // throws: 256 is out of range +``` + +#### Float Types + +For floating-point numbers with specific precision. + +```ts +import f32 from "pema/f32"; +import f64 from "pema/f64"; + +// f32: 32-bit float (single precision) +f32.parse(1.5); // 1.5 +f32.parse(123456.75); // 123456.75 +f32.parse(1.23456789012345); // throws: not a 32-bit float + +// f64: 64-bit float (double precision) - same as `number` +f64.parse(1.23456789012345); // 1.23456789012345 +``` + +### Collection Types + +#### array + +Validates arrays where all elements match a specific schema. + +```ts +import pema from "pema"; +import string from "pema/string"; +import number from "pema/number"; + +// Using the array function +import array from "pema/array"; +const Tags = array(string); +Tags.parse(["a", "b", "c"]); // ["a", "b", "c"] +Tags.parse([1, 2, 3]); // throws: expected string at index 0 + +// Shorthand: single-element array in schema definition +const Schema = pema({ + tags: [string], // equivalent to array(string) +}); + +// Nested arrays +const Matrix = array(array(number)); +Matrix.parse([[1, 2], [3, 4]]); // [[1, 2], [3, 4]] +``` + +##### Array Validators + +| Validator | Description | Example | +|-----------|-------------|---------| +| `.min(n)` | Minimum length | `array(string).min(1)` | +| `.max(n)` | Maximum length | `array(string).max(10)` | +| `.length(min, max)` | Length range | `array(string).length(1, 5)` | +| `.unique()` | No duplicate values | `array(string).unique()` | + +```ts +// Length constraints +const tags = array(string).min(1).max(5); +tags.parse(["a"]); // ["a"] +tags.parse([]); // throws: min 1 items +tags.parse(["a","b","c","d","e","f"]); // throws: max 5 items + +// Length range +const items = array(number).length(2, 4); +items.parse([1, 2]); // [1, 2] +items.parse([1]); // throws: length out of range + +// Unique values (only for primitive element types) +const uniqueTags = array(string).unique(); +uniqueTags.parse(["a", "b", "c"]); // ["a", "b", "c"] +uniqueTags.parse(["a", "b", "a"]); // throws: duplicate value at index 2 + +// With default value +const defaultTags = array(string).default(["default"]); +defaultTags.parse(undefined); // ["default"] +defaultTags.parse(["custom"]); // ["custom"] +``` + +#### object + +Validates objects with specific property schemas. + +```ts +import pema from "pema"; +import string from "pema/string"; +import number from "pema/number"; + +// Using pema() function (recommended) +const User = pema({ + name: string, + age: number, +}); + +User.parse({ name: "John", age: 30 }); // { name: "John", age: 30 } +User.parse({ name: "John" }); // throws: expected number for age + +// Using object() function directly +import object from "pema/object"; +const Config = object({ + host: string, + port: number, +}); +``` + +##### Nested Objects + +```ts +const Profile = pema({ + user: { + name: string, + email: string.email(), + }, + settings: { + theme: string.default("light"), + notifications: boolean.default(true), + }, +}); + +Profile.parse({ + user: { name: "John", email: "john@example.com" }, + settings: {}, // defaults applied +}); +// Result: { user: {...}, settings: { theme: "light", notifications: true } } +``` + +##### Optional and Default Properties + +```ts +const User = pema({ + name: string, + nickname: string.optional(), // string | undefined + role: string.default("user"), // defaults to "user" if undefined +}); + +User.parse({ name: "John" }); +// Result: { name: "John", role: "user" } +// Note: nickname is omitted (undefined) +``` + +#### tuple + +Validates fixed-length arrays with specific types at each position. + +```ts +import pema from "pema"; +import string from "pema/string"; +import number from "pema/number"; +import boolean from "pema/boolean"; + +// Shorthand: multi-element array in schema definition +const Point = pema({ + coords: [number, number], // TupleType<[NumberType, NumberType]> +}); +Point.parse({ coords: [10, 20] }); // { coords: [10, 20] } + +// Using tuple() function +import tuple from "pema/tuple"; +const Entry = tuple(string, number, boolean); + +Entry.parse(["name", 42, true]); // ["name", 42, true] +Entry.parse(["name", 42]); // throws: expected boolean at index 2 +Entry.parse(["name", 42, true, "extra"]); // throws: expected undefined at index 3 +``` + +##### Nested Tuples + +```ts +const NestedTuple = tuple(tuple(string)); +NestedTuple.parse([["hello"]]); // [["hello"]] + +// Tuples in arrays +const Points = array(tuple(number, number)); +Points.parse([[0, 0], [10, 20]]); // [[0, 0], [10, 20]] +``` + +#### record + +Validates key-value objects where keys and values match specific types. + +```ts +import record from "pema/record"; +import string from "pema/string"; +import number from "pema/number"; +import symbol from "pema/symbol"; + +// String keys, string values +const StringDict = record(string, string); +StringDict.parse({ foo: "bar", baz: "qux" }); // valid +StringDict.parse({ foo: 123 }); // throws: expected string value + +// Number keys, string values +const NumberKeyed = record(number, string); +NumberKeyed.parse({ 0: "first", 1: "second" }); // valid +NumberKeyed.parse({ foo: "bar" }); // throws: expected number key + +// Symbol keys +const SymbolKeyed = record(symbol, string); +const key = Symbol("myKey"); +SymbolKeyed.parse({ [key]: "value" }); // valid +``` + +##### Record with Validated Values + +```ts +const Scores = record(string, number.min(0).max(100)); +Scores.parse({ math: 95, english: 88 }); // valid +Scores.parse({ math: 150 }); // throws: 150 is out of range +``` + +### Union & Literal Types + +#### union + +Creates a schema that accepts any of the specified types. Requires at least two +members. + +```ts +import union from "pema/union"; +import string from "pema/string"; +import number from "pema/number"; +import boolean from "pema/boolean"; +import bigint from "pema/bigint"; + +// Basic union of primitive types +const StringOrNumber = union(string, number); +StringOrNumber.parse("hello"); // "hello" +StringOrNumber.parse(42); // 42 +StringOrNumber.parse(true); // throws: expected `string | number` + +// Union with boolean +const Primitive = union(string, number, boolean); +Primitive.parse("text"); // "text" +Primitive.parse(123); // 123 +Primitive.parse(true); // true +``` + +##### Union with Literals + +```ts +// String literal union (enum-like) +const Status = union("pending", "active", "completed"); +Status.parse("active"); // "active" +Status.parse("invalid"); // throws: expected `"pending" | "active" | "completed"` + +// Mixed literal union +const Value = union("auto", 0, true); +Value.parse("auto"); // "auto" +Value.parse(0); // 0 +Value.parse(true); // true +``` + +##### Union with Complex Types + +```ts +import pema from "pema"; + +// Union with objects +const Result = union( + string, + { status: "error", message: string } +); +Result.parse("success"); // "success" +Result.parse({ status: "error", message: "Failed" }); // valid + +// Union with classes +class CustomError {} +const ErrorOrString = union(string, CustomError); +ErrorOrString.parse("error message"); // "error message" +ErrorOrString.parse(new CustomError()); // CustomError instance +``` + +##### Union with Default + +```ts +const OptionalStatus = union(boolean, string).default("unknown"); +OptionalStatus.parse(undefined); // "unknown" +OptionalStatus.parse(true); // true +OptionalStatus.parse("active"); // "active" +``` + +#### Literals + +Literal types match exact values. They are created automatically when you use +primitive values in schemas. + +```ts +import pema from "pema"; + +// Implicit literal types in schemas +const Config = pema({ + type: "config", // LiteralType<"config"> + version: 1, // LiteralType<1> + enabled: true, // LiteralType +}); + +Config.parse({ type: "config", version: 1, enabled: true }); // valid +Config.parse({ type: "other", version: 1, enabled: true }); // throws +``` + +##### Explicit Literal Creation + +You can create literals explicitly using the normalize function behavior: + +```ts +import pema from "pema"; + +// String literals +const Method = pema({ + method: "GET", // only "GET" is valid +}); +Method.parse({ method: "GET" }); // valid +Method.parse({ method: "POST" }); // throws: expected "GET" + +// Number literals +const HttpOk = pema({ + status: 200, // only 200 is valid +}); +HttpOk.parse({ status: 200 }); // valid +HttpOk.parse({ status: 404 }); // throws: expected 200 + +// Boolean literals +const Enabled = pema({ + active: true, // only true is valid +}); +Enabled.parse({ active: true }); // valid +Enabled.parse({ active: false }); // throws: expected true +``` + +##### Discriminated Unions + +Combine literals with unions for type-safe discriminated unions: + +```ts +import pema from "pema"; +import union from "pema/union"; +import string from "pema/string"; +import number from "pema/number"; + +const SuccessResponse = pema({ + type: "success", + data: string, +}); + +const ErrorResponse = pema({ + type: "error", + code: number, + message: string, +}); + +const Response = union(SuccessResponse, ErrorResponse); + +// Valid success response +Response.parse({ type: "success", data: "Hello" }); + +// Valid error response +Response.parse({ type: "error", code: 404, message: "Not found" }); + +// Invalid - mixed types +Response.parse({ type: "success", code: 200 }); // throws +``` + +### Utility Types + +#### optional + +Makes any schema accept `undefined` in addition to its normal type. + +```ts +import optional from "pema/optional"; +import string from "pema/string"; +import number from "pema/number"; + +// Using optional() function +const OptionalString = optional(string); +OptionalString.parse("hello"); // "hello" +OptionalString.parse(undefined); // undefined +OptionalString.parse(null); // throws: null is not undefined + +// Using .optional() method (preferred) +const Name = string.optional(); +Name.parse("John"); // "John" +Name.parse(undefined); // undefined +``` + +##### Optional in Objects + +```ts +import pema from "pema"; +import string from "pema/string"; + +const User = pema({ + name: string, + nickname: string.optional(), // string | undefined +}); + +User.parse({ name: "John" }); +// Result: { name: "John" } +// Note: nickname key is omitted when undefined + +User.parse({ name: "John", nickname: "Johnny" }); +// Result: { name: "John", nickname: "Johnny" } + +User.parse({ name: "John", nickname: undefined }); +// Result: { name: "John" } +``` + +#### constructor + +Validates that a value is an instance of a specific class. + +```ts +import constructor from "pema/constructor"; + +class User { + constructor(public name: string) {} +} + +class Admin extends User { + constructor(name: string, public role: string) { + super(name); + } +} + +// Basic class validation +const UserType = constructor(User); +UserType.parse(new User("John")); // valid +UserType.parse(new Admin("Jane", "admin")); // valid (Admin extends User) +UserType.parse({ name: "John" }); // throws: not a User instance + +// With default value +const defaultUser = new User("Guest"); +const UserWithDefault = constructor(User).default(defaultUser); +UserWithDefault.parse(undefined); // defaultUser +``` + +##### Constructor in Schemas + +```ts +import pema from "pema"; +import string from "pema/string"; + +class CustomDate { + constructor(public value: Date) {} +} + +// Classes are automatically converted to ConstructorType +const Event = pema({ + name: string, + date: CustomDate, +}); + +Event.parse({ + name: "Meeting", + date: new CustomDate(new Date()), +}); // valid +``` + +#### pure + +A TypeScript-only type that performs no runtime validation. Useful for types +that cannot be validated at runtime or for integration with external systems. + +```ts +import pure from "pema/pure"; + +// Define a pure type with TypeScript type parameter +type CustomConfig = { + apiKey: string; + endpoint: string; +}; + +const Config = pure(); + +// No validation occurs - value passes through as-is +Config.parse({ apiKey: "key", endpoint: "url" }); // typed as CustomConfig +Config.parse(42); // 42, typed as CustomConfig (no validation!) +Config.parse("anything"); // "anything", typed as CustomConfig +``` + +##### When to Use Pure + +- Integration with external libraries that have their own validation +- Types that are impossible to validate at runtime (branded types, etc.) +- Performance-critical paths where validation has already occurred +- Gradual migration to Pema + +```ts +import pema from "pema"; +import string from "pema/string"; +import pure from "pema/pure"; + +// External library type +type ExternalLibraryType = { complex: "structure" }; + +const Schema = pema({ + name: string, + external: pure(), // Trust external validation +}); +``` + +#### primary + +An optional string type designed for database primary keys. Accepts +`string | undefined`. + +```ts +import primary from "pema/primary"; + +// Accepts string or undefined +primary.parse("abc-123"); // "abc-123" +primary.parse(undefined); // undefined +primary.parse(123); // throws: expected primary +``` + +##### Primary in Store Schemas + +```ts +import pema from "pema"; +import string from "pema/string"; +import primary from "pema/primary"; + +const UserStore = pema({ + id: primary, // auto-generated by database + name: string, +}); + +// Creating a new user (id is undefined) +UserStore.parse({ name: "John" }); // valid, id is undefined + +// Reading from database (id is string) +UserStore.parse({ id: "user-123", name: "John" }); // valid +``` + +#### omit + +Creates a new object schema with specified properties removed. + +```ts +import omit from "pema/omit"; +import object from "pema/object"; +import string from "pema/string"; +import number from "pema/number"; + +const User = object({ + id: string, + name: string, + age: number, + email: string, +}); + +// Remove single field +const CreateUser = omit(User, "id"); +CreateUser.parse({ name: "John", age: 30, email: "john@example.com" }); // valid +// Type: { name: string; age: number; email: string } + +// Remove multiple fields +const PublicUser = omit(User, "id", "email"); +PublicUser.parse({ name: "John", age: 30 }); // valid +// Type: { name: string; age: number } +``` + +##### Omit with Nested Objects + +```ts +const FullProfile = object({ + id: string, + user: { + name: string, + email: string, + }, + metadata: number, +}); + +const CreateProfile = omit(FullProfile, "id"); +CreateProfile.parse({ + user: { name: "John", email: "john@example.com" }, + metadata: 42, +}); // valid +``` + +##### Omit Preserves Validators + +```ts +const User = object({ + id: string, + email: string.email(), + age: number.min(0).max(150), +}); + +const CreateUser = omit(User, "id"); + +// Validators are preserved +CreateUser.parse({ email: "invalid", age: 25 }); // throws: invalid email +CreateUser.parse({ email: "john@example.com", age: 200 }); // throws: age out of range +``` + +#### partial + +Makes all properties in an object schema optional. Only validates properties +that are provided. + +```ts +import partial from "pema/partial"; +import string from "pema/string"; +import number from "pema/number"; + +const UserPartial = partial({ + name: string, + age: number, +}); + +// All properties are optional +UserPartial.parse({}); // {} +UserPartial.parse({ name: "John" }); // { name: "John" } +UserPartial.parse({ age: 30 }); // { age: 30 } +UserPartial.parse({ name: "John", age: 30 }); // { name: "John", age: 30 } + +// Validation still applies to provided values +UserPartial.parse({ name: 123 }); // throws: expected string +UserPartial.parse({ age: "thirty" }); // throws: expected number +``` + +##### Partial from Object Schema + +```ts +import object from "pema/object"; + +const User = object({ + name: string, + age: number, +}); + +const UserUpdate = partial(User); // Same as partial({ name: string, age: number }) +``` + +### Modifiers + +Modifiers are methods available on schema types that transform their behavior. + +#### .optional() Modifier + +Makes a schema accept `undefined` in addition to its normal type. + +```ts +import string from "pema/string"; +import number from "pema/number"; +import array from "pema/array"; + +// On primitives +const optionalString = string.optional(); +optionalString.parse("hello"); // "hello" +optionalString.parse(undefined); // undefined + +// On arrays +const optionalTags = array(string).optional(); +optionalTags.parse(["a", "b"]); // ["a", "b"] +optionalTags.parse(undefined); // undefined + +// Chaining with validators +const optionalEmail = string.email().optional(); +optionalEmail.parse("user@example.com"); // valid +optionalEmail.parse(undefined); // undefined +optionalEmail.parse("invalid"); // throws: not a valid email +``` + +#### .default() Modifier + +Provides a default value when the input is `undefined`. + +```ts +import string from "pema/string"; +import number from "pema/number"; +import array from "pema/array"; + +// Static default value +const role = string.default("user"); +role.parse("admin"); // "admin" +role.parse(undefined); // "user" + +// Function default (called each time) +const timestamp = number.default(() => Date.now()); +timestamp.parse(undefined); // current timestamp +timestamp.parse(12345); // 12345 + +// Array with default +const tags = array(string).default(["general"]); +tags.parse(undefined); // ["general"] +tags.parse(["custom"]); // ["custom"] + +// Chaining with validators +const port = number.min(1).max(65535).default(3000); +port.parse(undefined); // 3000 +port.parse(8080); // 8080 +port.parse(100000); // throws: out of range +``` + +##### Default in Objects + +```ts +import pema from "pema"; +import string from "pema/string"; +import number from "pema/number"; +import boolean from "pema/boolean"; + +const Config = pema({ + host: string.default("localhost"), + port: number.default(8080), + debug: boolean.default(false), +}); + +// All defaults applied +Config.parse({}); +// Result: { host: "localhost", port: 8080, debug: false } + +// Partial override +Config.parse({ host: "example.com" }); +// Result: { host: "example.com", port: 8080, debug: false } + +// Nested defaults +const AppConfig = pema({ + name: string, + server: { + host: string.default("0.0.0.0"), + port: number.default(3000), + }, +}); + +AppConfig.parse({ name: "MyApp" }); +// Result: { name: "MyApp", server: { host: "0.0.0.0", port: 3000 } } +``` + +#### .coerce Modifier + +Enables type coercion, converting compatible values to the target type. + +```ts +import string from "pema/string"; +import number from "pema/number"; +import boolean from "pema/boolean"; +import int from "pema/int"; +import uint from "pema/uint"; +import date from "pema/date"; +import bigint from "pema/bigint"; + +// Number coercion (from string) +const num = number.coerce; +num.parse("42"); // 42 +num.parse("3.14"); // 3.14 +num.parse("-1.5"); // -1.5 +num.parse(42); // 42 (already a number) + +// Integer coercion +const integer = int.coerce; +integer.parse("42"); // 42 +integer.parse("42.0"); // 42 +integer.parse("42.5"); // throws: not an integer + +// Unsigned integer coercion +const unsigned = uint.coerce; +unsigned.parse("100"); // 100 +unsigned.parse("-1"); // throws: out of range + +// Boolean coercion (only "true"/"false" strings) +const bool = boolean.coerce; +bool.parse("true"); // true +bool.parse("false"); // false +bool.parse("1"); // throws: invalid +bool.parse("yes"); // throws: invalid + +// Date coercion (from timestamp) +const d = date.coerce; +d.parse(1723718400000); // Date object +d.parse(new Date()); // Date object + +// BigInt coercion +const big = bigint.coerce; +big.parse(42); // 42n +big.parse("42"); // 42n +big.parse("42.0"); // 42n +``` + +##### Coercion in Schemas + +Coercion is particularly useful when parsing query parameters or form data +where everything arrives as strings: + +```ts +import pema from "pema"; +import uint from "pema/uint"; +import string from "pema/string"; + +const QueryParams = pema({ + page: uint.coerce.default(1), + limit: uint.coerce.default(20), + search: string.optional(), +}); + +// From URL query string (all values are strings) +QueryParams.parse({ page: "2", limit: "50" }); +// Result: { page: 2, limit: 50 } + +QueryParams.parse({}); +// Result: { page: 1, limit: 20 } +``` + +### Store Integration + +Pema provides types for database store integration, commonly used with +Primate's ORM. + +#### StoreType + +An extended object type with support for `.partial()` method, useful for update +operations. + +```ts +import { StoreType } from "pema"; +``` + +#### StoreSchema + +Type definition for store schemas, representing the structure of stored +entities. + +```ts +import { StoreSchema } from "pema"; +``` + +#### StoreId + +Type for store identifiers. + +```ts +import { StoreId } from "pema"; +``` + +#### InferStore / InferStoreOut + +Type utilities for inferring TypeScript types from store schemas. + +```ts +import { InferStore, InferStoreOut } from "pema"; +import pema from "pema"; +import string from "pema/string"; +import primary from "pema/primary"; + +const UserStore = pema({ + id: primary, + name: string, + email: string.email(), +}); + +// Infer the store type +type User = InferStore; +// { id: string | undefined; name: string; email: string } +``` + +#### Practical Store Example + +```ts +import pema from "pema"; +import string from "pema/string"; +import number from "pema/number"; +import primary from "pema/primary"; +import date from "pema/date"; + +// Define a store schema +const PostStore = pema({ + id: primary, + title: string.min(1).max(200), + content: string, + authorId: string, + views: number.default(0), + createdAt: date.default(() => new Date()), +}); + +// Creating a new post (id will be generated) +const newPost = PostStore.parse({ + title: "Hello World", + content: "My first post", + authorId: "user-123", +}); +// Result includes id: undefined, views: 0, createdAt: current date + +// Reading from database +const existingPost = PostStore.parse({ + id: "post-456", + title: "Hello World", + content: "My first post", + authorId: "user-123", + views: 42, + createdAt: new Date("2024-01-01"), +}); +``` + +### Error Handling + +#### ParseError + +When validation fails, Pema throws a `ParseError` containing detailed +information about what went wrong. + +```ts +import { ParseError } from "pema/ParseError"; +import pema from "pema"; +import string from "pema/string"; +import number from "pema/number"; + +const User = pema({ + name: string.min(1), + email: string.email(), + age: number.min(0), +}); + +try { + User.parse({ name: "", email: "invalid", age: -5 }); +} catch (error) { + if (error instanceof ParseError) { + console.log(error.message); // First error message + console.log(error.issues); // Array of all validation issues + } +} +``` + +#### Issue Structure + +Each issue in `ParseError.issues` contains: + +| Property | Type | Description | +|----------|------|-------------| +| `message` | `string` | Human-readable error message | +| `path` | `string` | JSON Pointer to the failing value | +| `input` | `unknown` | The actual value that failed validation | + +```ts +// Example issues array: +[ + { + message: "min 1 characters", + path: "/name", + input: "" + }, + { + message: "\"invalid\" is not a valid email", + path: "/email", + input: "invalid" + }, + { + message: "-5 is lower than min (0)", + path: "/age", + input: -5 + } +] +``` + +#### JSON Serialization + +`ParseError` implements `toJSON()` for easy serialization in API responses: + +```ts +try { + schema.parse(data); +} catch (error) { + if (error instanceof ParseError) { + // Returns structured JSON for API responses + const json = error.toJSON(); + + // For form errors (with paths): + // { + // "/email": { "message": "invalid email", "messages": ["invalid email"] }, + // "/age": { "message": "must be positive", "messages": ["must be positive"] } + // } + + // For scalar errors (no path): + // { "message": "expected string", "messages": ["expected string"] } + } +} +``` + +#### Error Paths + +Paths use JSON Pointer notation (RFC 6901): + +```ts +import pema from "pema"; +import string from "pema/string"; +import array from "pema/array"; + +const Schema = pema({ + users: [{ + profile: { + email: string.email(), + }, + }], +}); + +try { + Schema.parse({ + users: [ + { profile: { email: "invalid" } } + ] + }); +} catch (error) { + // error.issues[0].path === "/users/0/profile/email" +} +``` + +#### Catching Errors Gracefully + +```ts +import { ParseError } from "pema/ParseError"; + +function validateUser(data: unknown) { + try { + return { success: true, data: User.parse(data) }; + } catch (error) { + if (error instanceof ParseError) { + return { success: false, errors: error.toJSON() }; + } + throw error; // Re-throw unexpected errors + } +} + +const result = validateUser({ name: "", email: "bad" }); +if (!result.success) { + console.log(result.errors); +} +``` + +### Advanced Patterns + +#### Nested Schemas + +Build complex schemas by composing simpler ones: + +```ts +import pema from "pema"; +import string from "pema/string"; +import number from "pema/number"; +import array from "pema/array"; + +// Define reusable schemas +const Address = pema({ + street: string, + city: string, + zipCode: string.regex(/^\d{5}$/), +}); + +const ContactInfo = pema({ + email: string.email(), + phone: string.optional(), +}); + +// Compose into larger schema +const User = pema({ + name: string, + contact: ContactInfo, + addresses: array(Address), +}); + +User.parse({ + name: "John", + contact: { email: "john@example.com" }, + addresses: [ + { street: "123 Main St", city: "Boston", zipCode: "02101" } + ], +}); +``` + +#### Schema Composition + +Combine schemas using spread or programmatic composition: + +```ts +import pema from "pema"; +import string from "pema/string"; +import number from "pema/number"; +import union from "pema/union"; + +// Base schema properties +const baseUser = { + name: string, + email: string.email(), +}; + +// Extended schemas +const Customer = pema({ + ...baseUser, + customerId: string, + tier: union("free", "premium", "enterprise"), +}); + +const Employee = pema({ + ...baseUser, + employeeId: string, + department: string, + salary: number.min(0), +}); +``` + +#### Conditional Schemas with Unions + +Use discriminated unions for type-safe conditional parsing: + +```ts +import pema from "pema"; +import union from "pema/union"; +import string from "pema/string"; +import number from "pema/number"; + +// Payment method schemas +const CreditCard = pema({ + type: "credit_card", + cardNumber: string.length(16, 16), + expiry: string, + cvv: string.length(3, 4), +}); + +const BankTransfer = pema({ + type: "bank_transfer", + accountNumber: string, + routingNumber: string, +}); + +const PayPal = pema({ + type: "paypal", + email: string.email(), +}); + +// Combined payment schema +const Payment = union(CreditCard, BankTransfer, PayPal); + +// TypeScript knows the type based on the discriminant +const payment = Payment.parse({ + type: "credit_card", + cardNumber: "1234567890123456", + expiry: "12/25", + cvv: "123", +}); +``` + +#### Custom Validation with Regex + +Use `.regex()` for custom string validation: + +```ts +import string from "pema/string"; + +// Phone number validation +const phoneNumber = string.regex(/^\+?[\d\s-()]+$/); + +// Slug validation +const slug = string.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/); + +// Hex color validation +const hexColor = string.regex(/^#[0-9A-Fa-f]{6}$/); + +// IP address validation +const ipv4 = string.regex(/^(?:\d{1,3}\.){3}\d{1,3}$/); + +// Combine with other validators +const username = string + .min(3) + .max(20) + .regex(/^[a-zA-Z][a-zA-Z0-9_]*$/); // Must start with letter +``` + +#### Type Coercion Patterns + +Handle untyped input from forms and query strings: + +```ts +import pema from "pema"; +import string from "pema/string"; +import uint from "pema/uint"; +import boolean from "pema/boolean"; +import union from "pema/union"; + +// Form data processing +const RegistrationForm = pema({ + username: string.min(3).max(20), + email: string.email(), + age: uint.coerce.min(13), // Coerce string to number + newsletter: boolean.coerce.default(false), // "true"/"false" strings +}); + +// Query parameter processing +const SearchQuery = pema({ + q: string.optional(), + page: uint.coerce.default(1), + limit: uint.coerce.default(10).max(100), + sort: union("asc", "desc").default("desc"), +}); + +// Parse URL search params +const params = Object.fromEntries(new URLSearchParams("?q=test&page=2")); +const query = SearchQuery.parse(params); +// { q: "test", page: 2, limit: 10, sort: "desc" } +``` + +#### Recursive Schemas + +For tree-like structures, use schema references: + +```ts +import pema from "pema"; +import string from "pema/string"; +import array from "pema/array"; +import unknown from "pema/unknown"; + +// Category with subcategories +interface Category { + name: string; + children: Category[]; +} + +// Define the schema with explicit typing +const CategorySchema: any = pema({ + name: string, + children: array(/* lazy reference would go here */), +}); + +// For true recursion, use a factory pattern +function createCategorySchema(maxDepth: number) { + if (maxDepth === 0) { + return pema({ name: string, children: array(unknown) }); + } + return pema({ + name: string, + children: array(createCategorySchema(maxDepth - 1)), + }); +} + +const CategoryWithDepth = createCategorySchema(3); +``` + +#### TypeScript Integration + +Pema provides full TypeScript type inference: + +```ts +import pema from "pema"; +import string from "pema/string"; +import number from "pema/number"; + +const User = pema({ + name: string, + age: number.optional(), + role: string.default("user"), +}); + +// Infer the output type +type User = typeof User.infer; +// { name: string; age: number | undefined; role: string } + +// Infer the input type (what you can pass to parse) +type UserInput = typeof User.input; +// { name: string; age?: number; role?: string } + +// Use inferred types in functions +function createUser(data: UserInput): User { + return User.parse(data); +} + +// Type-safe access to parsed data +const user = User.parse({ name: "John" }); +user.name; // string +user.age; // number | undefined +user.role; // string (default applied) +``` + +### API Reference + +#### Schema Functions + +| Export | Import | Description | +|--------|--------|-------------| +| `pema` | `import pema from "pema"` | Main schema builder function | +| `array` | `import array from "pema/array"` | Create array schema | +| `object` | `import object from "pema/object"` | Create object schema | +| `tuple` | `import tuple from "pema/tuple"` | Create tuple schema | +| `record` | `import record from "pema/record"` | Create record schema | +| `union` | `import union from "pema/union"` | Create union schema | +| `optional` | `import optional from "pema/optional"` | Make schema optional | +| `constructor` | `import constructor from "pema/constructor"` | Validate class instances | +| `pure` | `import pure from "pema/pure"` | TypeScript-only type | +| `omit` | `import omit from "pema/omit"` | Remove object properties | +| `partial` | `import partial from "pema/partial"` | Make all properties optional | + +#### Primitive Types + +| Export | Import | Description | +|--------|--------|-------------| +| `string` | `import string from "pema/string"` | String validation | +| `number` | `import number from "pema/number"` | Number validation (f64) | +| `boolean` | `import boolean from "pema/boolean"` | Boolean validation | +| `bigint` | `import bigint from "pema/bigint"` | Signed bigint (i64) | +| `biguint` | `import biguint from "pema/biguint"` | Unsigned bigint (u64) | +| `symbol` | `import symbol from "pema/symbol"` | Symbol validation | +| `date` | `import date from "pema/date"` | Date object validation | +| `unknown` | `import unknown from "pema/unknown"` | Accept any value | + +#### Integer Types + +| Export | Import | Range | +|--------|--------|-------| +| `int` | `import int from "pema/int"` | -2^31 to 2^31-1 (i32) | +| `uint` | `import uint from "pema/uint"` | 0 to 2^32-1 (u32) | +| `i8` | `import i8 from "pema/i8"` | -128 to 127 | +| `i16` | `import i16 from "pema/i16"` | -32,768 to 32,767 | +| `i32` | `import i32 from "pema/i32"` | -2^31 to 2^31-1 | +| `i64` | `import i64 from "pema/i64"` | -2^63 to 2^63-1 (bigint) | +| `i128` | `import i128 from "pema/i128"` | -2^127 to 2^127-1 (bigint) | +| `u8` | `import u8 from "pema/u8"` | 0 to 255 | +| `u16` | `import u16 from "pema/u16"` | 0 to 65,535 | +| `u32` | `import u32 from "pema/u32"` | 0 to 4,294,967,295 | +| `u64` | `import u64 from "pema/u64"` | 0 to 2^64-1 (bigint) | +| `u128` | `import u128 from "pema/u128"` | 0 to 2^128-1 (bigint) | + +#### Float Types + +| Export | Import | Description | +|--------|--------|-------------| +| `f32` | `import f32 from "pema/f32"` | 32-bit float (single precision) | +| `f64` | `import f64 from "pema/f64"` | 64-bit float (double precision) | + +#### Binary Types + +| Export | Import | Description | +|--------|--------|-------------| +| `blob` | `import blob from "pema/blob"` | Blob validation | +| `file` | `import file from "pema/file"` | File validation | +| `url` | `import url from "pema/url"` | URL object validation | + +#### Special Types + +| Export | Import | Description | +|--------|--------|-------------| +| `primary` | `import primary from "pema/primary"` | Optional string (for database IDs) | + +#### Type Exports + +| Export | Import | Description | +|--------|--------|-------------| +| `ParseError` | `import { ParseError } from "pema/ParseError"` | Validation error class | +| `Issue` | `import { Issue } from "pema/Issue"` | Single validation issue type | +| `Schema` | `import { Schema } from "pema/Schema"` | Schema type definition | +| `StoreType` | `import { StoreType } from "pema/StoreType"` | Store schema type | +| `StoreSchema` | `import { StoreSchema } from "pema/StoreSchema"` | Store schema definition | +| `StoreId` | `import { StoreId } from "pema/StoreId"` | Store identifier type | +| `InferStore` | `import { InferStore } from "pema/InferStore"` | Infer type from store | +| `InferStoreOut` | `import { InferStoreOut } from "pema/InferStoreOut"` | Infer output type | +| `DataType` | `import { DataType } from "pema/DataType"` | Data type definition | +| `Serialized` | `import { Serialized } from "pema/Serialized"` | Serialized schema format | +| `JSONPayload` | `import { JSONPayload } from "pema/JSONPayload"` | JSON error payload type | +| `Id` | `import { Id } from "pema/Id"` | ID type | + +#### Validators by Type + +##### String Validators + +| Method | Description | Example | +|--------|-------------|---------| +| `.min(n)` | Minimum length | `string.min(1)` | +| `.max(n)` | Maximum length | `string.max(100)` | +| `.length(min, max)` | Length range | `string.length(5, 20)` | +| `.email()` | Email format | `string.email()` | +| `.uuid()` | UUID format | `string.uuid()` | +| `.startsWith(s)` | Prefix match | `string.startsWith("/")` | +| `.endsWith(s)` | Suffix match | `string.endsWith(".js")` | +| `.regex(r)` | Regex match | `string.regex(/^[a-z]+$/)` | +| `.isotime()` | ISO time format | `string.isotime()` | + +##### Number/Integer Validators + +| Method | Description | Example | +|--------|-------------|---------| +| `.min(n)` | Minimum value | `number.min(0)` | +| `.max(n)` | Maximum value | `number.max(100)` | +| `.range(min, max)` | Value range | `number.range(0, 100)` | + +##### Array Validators + +| Method | Description | Example | +|--------|-------------|---------| +| `.min(n)` | Minimum items | `array(string).min(1)` | +| `.max(n)` | Maximum items | `array(string).max(10)` | +| `.length(min, max)` | Item count range | `array(string).length(1, 5)` | +| `.unique()` | No duplicates | `array(string).unique()` | + +#### Common Modifiers + +| Modifier | Available On | Description | +|----------|--------------|-------------| +| `.optional()` | All types | Accept `undefined` | +| `.default(v)` | All types | Provide default value | +| `.coerce` | number, int, uint, boolean, date, bigint | Enable type coercion |