From 031f76eaba588f4803a3a52068497b43f24e57cc Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Fri, 2 Aug 2024 17:14:45 -0700 Subject: [PATCH] chore: readme docs --- deno.json | 1 + mod.ts | 292 +++++++++++++++++++++++++++++------------------------- readme.md | 124 +++++++++++++++++++++++ 3 files changed, 281 insertions(+), 136 deletions(-) create mode 100644 readme.md diff --git a/deno.json b/deno.json index 2157689..4b6ecd3 100644 --- a/deno.json +++ b/deno.json @@ -21,6 +21,7 @@ }, "exclude": [ "coverage", + "readme.md", "npm" ] } diff --git a/mod.ts b/mod.ts index 5cccb51..2ea22f5 100644 --- a/mod.ts +++ b/mod.ts @@ -1,3 +1,8 @@ +/** + * JSON Schema builders and type definitions. + * @module + */ + type OmitNever = { [K in keyof T as T[K] extends never ? never : K]: T[K]; }; @@ -103,20 +108,22 @@ export type Field = | _string | _tuple; -// MODIFIERS -// --- - const OPTIONAL: unique symbol = Symbol.for('optional'); /** - * Mark a field as optional. - * - * NOTE: Only has an effect within {@link object} definitions. + * @internal + * Marks an object property as optional. */ type _optional = T & { [OPTIONAL]: true; }; +/** + * Mark an object property field as optional. + * + * > [!NOTE] + * > Only has an effect within {@link object} definitions. + */ function _optional< F extends Field, >(field: F): _optional { @@ -126,6 +133,14 @@ function _optional< }; } +/** + * @internal + * Marks an {@link object}, {@link array}, or {@link tuple} field as readonly. + */ +type _readonly = T & { + readOnly: true; +}; + /** * Mark a field as readonly. * @@ -155,10 +170,6 @@ function _optional< * //-> readonly string[] * ``` */ -type _readonly = T & { - readOnly: true; -}; - function _readonly(field: T): _readonly { return { ...field, @@ -166,31 +177,36 @@ function _readonly(field: T): _readonly { }; } -// NULL -// --- - /** - * Defines a `null` type. + * The `null` type. * - * @see [Reference](https://json-schema.org/understanding-json-schema/reference/null) + * [Reference](https://json-schema.org/understanding-json-schema/reference/null) */ type _null = Annotations & { type: 'null'; }; +/** + * Defines a `null` type. + */ function _null(options?: Omit<_null, 'type'>): _null { return { ...options, type: 'null' }; } -// ENUM -// --- - /** - * Defines an `enum` type which accepts a list of possible values. + * The `enum` type. * - * Because no `type` attribute is defined, the enumerated values may include different data types. + * [Reference](https://json-schema.org/understanding-json-schema/reference/enum) + */ +type _enum = Annotations & { + enum: T[]; +}; + +/** + * Defines an `enum` type to accept a list of possible values. * - * @see [Reference](https://json-schema.org/understanding-json-schema/reference/enum) + * > [!NOTE] + * > Because no `type` attribute is defined, the enumerated values may include different data types. * * ```ts * // Define possible literal values of different types: @@ -202,10 +218,6 @@ function _null(options?: Omit<_null, 'type'>): _null { * }); * ``` */ -type _enum = Annotations & { - enum: T[]; -}; - function _enum< const V extends (boolean | null | number | string), >( @@ -218,54 +230,59 @@ function _enum< }; } -// BOOLEAN -// --- - /** - * Defines a `boolean` type. + * The `boolean` type. * - * @see [Reference](https://json-schema.org/understanding-json-schema/reference/boolean) + * [Reference](https://json-schema.org/understanding-json-schema/reference/boolean) */ type _boolean = Annotations & { type: 'boolean'; }; +/** + * Defines a `boolean` type. + */ function _boolean(options?: Omit<_boolean, 'type'>): _boolean { return { ...options, type: 'boolean' }; } -// STRING -// --- - /** - * https://json-schema.org/understanding-json-schema/reference/string#built-in-formats + * A `string` type. + * + * [Reference](https://json-schema.org/understanding-json-schema/reference/string) */ -type Format = - | 'date-time' - | 'time' - | 'date' - | 'duration' - | 'email' - | 'idn-email' - | 'hostname' - | 'idn-hostname' - | 'ipv4' - | 'ipv6' - | 'uuid' - | 'uri' - | 'uri-reference' - | 'iri' - | 'iri-reference' - | 'uri-template' - | 'json-pointer' - | 'relative-json-pointer' - | 'regex'; +type _string = Annotations & { + type: 'string'; + minLength?: number; + maxLength?: number; + pattern?: string; + enum?: E[]; + /** https://json-schema.org/understanding-json-schema/reference/string#built-in-formats */ + format?: + | 'date-time' + | 'time' + | 'date' + | 'duration' + | 'email' + | 'idn-email' + | 'hostname' + | 'idn-hostname' + | 'ipv4' + | 'ipv6' + | 'uuid' + | 'uri' + | 'uri-reference' + | 'iri' + | 'iri-reference' + | 'uri-template' + | 'json-pointer' + | 'relative-json-pointer' + | 'regex'; +}; /** * Defines a `string` type. * - * @see [Reference](https://json-schema.org/understanding-json-schema/reference/string) - * * When `enum` attribute is defined, then {@link Infer} narrows this type to only those values. * * ```ts @@ -278,15 +295,6 @@ type Format = * //-> "foo" | "bar" * ``` */ -type _string = Annotations & { - type: 'string'; - minLength?: number; - maxLength?: number; - pattern?: string; - format?: Format; - enum?: E[]; -}; - function _string< const V extends string, F extends _string, @@ -302,15 +310,26 @@ function _string( return { ...options, type: 'string' }; } -// NUMERIC -// --- +/** + * A `number` type. + * + * [Reference](https://json-schema.org/understanding-json-schema/reference/numeric#number) + */ +type _number = Annotations & { + type: 'number'; + enum?: E[]; + multipleOf?: number; + minimum?: number; + exclusiveMinimum?: number; + maximum?: number; + exclusiveMaximum?: number; +}; /** * Defines a `number` type. * - * NOTE: A {@link number} is used for integers **or** floating point numbers. See {@link integer} to exclude floats. - * - * @see [Reference](https://json-schema.org/understanding-json-schema/reference/numeric#number) + * > [!NOTE] + * > A {@link number} is used for integers **or** floating point numbers. See {@link integer} to exclude floats. * * When `enum` attribute is defined, then {@link Infer} narrows this type to only those values. * @@ -324,16 +343,6 @@ function _string( * //-> 0 | 1 | 12.8 | 101.3 * ``` */ -type _number = Annotations & { - type: 'number'; - enum?: E[]; - multipleOf?: number; - minimum?: number; - exclusiveMinimum?: number; - maximum?: number; - exclusiveMaximum?: number; -}; - function _number< const V extends number, F extends _number, @@ -350,11 +359,20 @@ function _number( } /** - * Defines an `integer` type. + * The `integer` type. * - * NOTE: An {@link integer} is used to **only** accept integer numbers. See {@link number} to accept any numeric. + * [Reference](https://json-schema.org/understanding-json-schema/reference/numeric#integer) + */ +type _integer = Omit<_number, 'type'> & { + type: 'integer'; +}; + +/** + * Defines an `integer` type. * - * @see [Reference](https://json-schema.org/understanding-json-schema/reference/numeric#integer) + * > [!NOTE] + * > An {@link integer} is used to **only** accept integer numbers. + * > See {@link number} to accept any numeric. * * When `enum` attribute is defined, then {@link Infer} narrows this type to only those values. * @@ -368,10 +386,6 @@ function _number( * //-> 0 | 1 | 2 | 3 * ``` */ -type _integer = Omit<_number, 'type'> & { - type: 'integer'; -}; - function _integer< const V extends number, F extends _integer, @@ -387,22 +401,10 @@ function _integer( return { ...options, type: 'integer' }; } -// LISTS -// --- - /** - * Define an `array` type. + * The `array` type. * - * NOTE: The {@link array} is used to define a sequence of arbitrary length where each item matches the same schema. - * Consider {@link tuple} to define a sequence of fixed length where each item may have a different schema. - * - * @see [Reference](https://json-schema.org/understanding-json-schema/reference/array) - * - * ```ts - * let hobbies = t.array(t.string()); - * type Hobbies = t.Infer; - * //-> string[] - * ``` + * [Reference](https://json-schema.org/understanding-json-schema/reference/array) */ type _array = Annotations & { type: 'array'; @@ -416,6 +418,19 @@ type _array = Annotations & { prefixItems?: Field[]; }; +/** + * Define an `array` type. + * + * > [!NOTE] + * > The {@link array} is used to define a sequence of arbitrary length where each item matches the same schema. + * > Consider {@link tuple} to define a sequence of fixed length where each item may have a different schema. + * + * ```ts + * let hobbies = t.array(t.string()); + * type Hobbies = t.Infer; + * //-> string[] + * ``` + */ function _array< const I extends Field, F extends _array, @@ -431,21 +446,9 @@ function _array< } /** - * Define a `tuple` type. - * - * NOTE: A {@link tuple} is used to define a sequence of fixed length where each item may have a different schema. - * Consider {@link array} to define a sequence of arbitrary length where each item matches the same schema. + * The `tuple` type. * - * @see [Reference](https://json-schema.org/understanding-json-schema/reference/array#tupleValidation) - * - * ```ts - * let item = t.tuple([ - * t.integer(), // quantity - * t.string(), // item name - * ]); - * type InvoiceItem = t.Infer; - * //-> [number, string] - * ``` + * [Reference](https://json-schema.org/understanding-json-schema/reference/array#tupleValidation) */ type _tuple = Annotations & { type: 'array'; @@ -459,6 +462,22 @@ type _tuple = Annotations & { maxContains?: number; }; +/** + * Define a `tuple` type. + * + * > [!NOTE] + * > A {@link tuple} is used to define a sequence of fixed length where each item may have a different schema. + * > Consider {@link array} to define a sequence of arbitrary length where each item matches the same schema. + * + * ```ts + * let item = t.tuple([ + * t.integer(), // quantity + * t.string(), // item name + * ]); + * type InvoiceItem = t.Infer; + * //-> [number, string] + * ``` + */ function _tuple< const M extends Field[], >( @@ -472,9 +491,6 @@ function _tuple< } as _tuple; } -// OBJECT -// --- - type Properties = { [name: string]: Field & { [OPTIONAL]?: true; @@ -482,13 +498,30 @@ type Properties = { }; /** - * Define an `object` type. + * The `object` type. * - * @see [Reference](https://json-schema.org/understanding-json-schema/reference/object) + * [Reference](https://json-schema.org/understanding-json-schema/reference/object) + */ +type _object = Annotations & { + type: 'object'; + properties?: { + [K in keyof T]: T[K]; + }; + required?: (keyof T)[]; + patternProperties?: Field; + additionalProperties?: Field | boolean; + propertyNames?: Partial<_string>; + minProperties?: number; + maxProperties?: number; +}; + +/** + * Define an `object` type. * - * NOTE: By default, all properties are marked as required. - * Use {@link optional} to mark individual properties as optional, or supply - * your own `required` attribute. + * > [!IMPORTANT] + * > By default, all properties are marked as required. + * > Use {@link optional} to mark individual properties as optional, or supply + * > your own `required` attribute. * * @example Basic Usage * ```ts @@ -518,19 +551,6 @@ type Properties = { * }); * ``` */ -type _object = Annotations & { - type: 'object'; - properties?: { - [K in keyof T]: T[K]; - }; - required?: (keyof T)[]; - patternProperties?: Field; - additionalProperties?: Field | boolean; - propertyNames?: Partial<_string>; - minProperties?: number; - maxProperties?: number; -}; - function _object< P extends Properties, F extends _object

, diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..89d1ff9 --- /dev/null +++ b/readme.md @@ -0,0 +1,124 @@ +# tschema [![CI](https://github.com/lukeed/tschema/workflows/CI/badge.svg)](https://github.com/lukeed/tschema/actions?query=workflow%3ACI) [![licenses](https://licenses.dev/b/npm/tschema)](https://licenses.dev/npm/tschema) + +> A tiny (347b) utility to build [JSON schema](https://json-schema.org/understanding-json-schema/reference) types. + +## Install + +This package is compatible with all JavaScript runtimes and is available on multiple registries: + +- **npm** — available as [`tschema`](https://www.npmjs.com/package/tschema) +- **JSR** — available as [`@lukeed/tschema`](https://jsr.io/@lukeed/tschema) + +## Usage + +```ts +import * as t from 'tschema'; + +// Define JSON object schema +let User = t.object({ + uid: t.integer(), + name: t.string({ + description: 'full name', + examples: ['Alex Johnson'], + }), + isActive: t.boolean(), + avatar: t.optional( + t.string({ format: 'uri' }), + ), + achievements: t.tuple([ + t.number({ minimum: 0 }), // points + t.enum(['novice', 'pro', 'expert', 'master']), + ]), + interests: t.array( + t.string({ + minLength: 4, + maxLength: 36, + }), + ), + last_updated: t.integer({ + minimum: 0, + examples: [1722642982], + description: 'unix seconds', + }), +}, { + additionalProperties: false, +}); + +// Infer its TypeScript definition: +// NOTE: names do not have to match~! +type User = t.Infer; +//-> { +//-> uid: number; +//-> name: string; +//-> isActive: boolean; +//-> avatar?: string | undefined; +//-> achievements: [number, "novice" | "pro" | "expert" | "master"]; +//-> interests: string[]; +//-> last_updated: number; +//-> } + +console.log(User); +//-> { +//-> type: "object", +//-> additionalProperties: false, +//-> properties: { +//-> uid: { +//-> type: "integer" +//-> }, +//-> name: { +//-> type: "string", +//-> description: "full name", +//-> examples: ["Alex Johnson"] +//-> }, +//-> isActive: { +//-> type: "boolean" +//-> }, +//-> avatar: { +//-> type: "string", +//-> format: "uri" +//-> }, +//-> achievements: { +//-> type: "array", +//-> prefixItems: [{ +//-> type: "number", +//-> minimum: 0, +//-> }, { +//-> enum: ["novice", "pro", "expert", "master"] +//-> }] +//-> }, +//-> interests: { +//-> type: "array", +//-> items: { +//-> type: "string", +//-> minLength: 4, +//-> maxLength: 36, +//-> } +//-> }, +//-> last_updated: { +//-> type: "integer", +//-> minimum: 0, +//-> examples: [1722642982], +//-> description: "unix seconds", +//-> } +//-> }, +//-> required: [ +//-> "uid", +//-> "name", +//-> "isActive", +//-> "achievements", +//-> "interests", +//-> "last_updated" +//-> ] +//-> } +``` + +## API + +Please refer to the [generated API documentation](https://jsr.io/@lukeed/tschema/doc), as it's +always kept up-to-date. + +> **Note:** The API is the same across all JavaScript runtimes, regardless of [installation](#install) source. Only the package's name changes. + +## License + +MIT © [Luke Edwards](https://lukeed.com)