Skip to content

Commit

Permalink
feat: add schema composition utils (#6)
Browse files Browse the repository at this point in the history
* feat: add schema composition utils

* chore: update module size
  • Loading branch information
lukeed committed Aug 6, 2024
1 parent 08bae75 commit 04b22b5
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 5 deletions.
32 changes: 32 additions & 0 deletions infer.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,3 +623,35 @@ assert<[string, boolean]>(T2);

// @ts-expect-error; STRING is not in ENUM
assert<typeof T2>([STRING, true]);

// ---
// COMPOSITIONS
// ---

let not1 = t.not(t.string());
type NOT1 = t.Infer<typeof not1>;
declare let NOT1: NOT1;
assert<unknown>(NOT1);

let any1 = t.any(t.string(), t.number());
type ANY1 = t.Infer<typeof any1>;
declare let ANY1: ANY1;
assert<string | number>(ANY1);
assert<ANY1>(STRING);
assert<ANY1>(NUMBER);

// @ts-expect-error; must all be string
let all1 = t.all(t.string(), t.number());
type ALL1 = t.Infer<typeof all1>;
declare let ALL1: ALL1;
assert<string>(ALL1);
assert<ALL1>(STRING);

let one1 = t.one(t.string(), t.number(), t.boolean());
type ONE1 = t.Infer<typeof one1>;
declare let ONE1: ONE1;
assert<string | number | boolean>(ONE1);
assert<ONE1>(STRING);
assert<ONE1>(NUMBER);
assert<ONE1>(false);
assert<ONE1>(true);
98 changes: 98 additions & 0 deletions mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,3 +519,101 @@ describe('Readonly', () => {
});
});
});

// ---

describe('t.not', () => {
it('should be a function', () => {
assert(typeof t.not === 'function');
});

it('should be JSON schema', () => {
let output = t.not(
t.string(),
);

assertEquals(output, {
not: {
type: 'string',
},
});
});
});

describe('t.one', () => {
it('should be a function', () => {
assert(typeof t.one === 'function');
});

it('should be JSON schema', () => {
let output = t.one(
t.number({ multipleOf: 5 }),
t.number({ multipleOf: 3 }),
);

assertEquals(output, {
oneOf: [
{
type: 'number',
multipleOf: 5,
},
{
type: 'number',
multipleOf: 3,
},
],
});
});
});

describe('t.any', () => {
it('should be a function', () => {
assert(typeof t.any === 'function');
});

it('should be JSON schema', () => {
let output = t.any(
t.string({ maxLength: 5 }),
t.number({ minimum: 0 }),
);

assertEquals(output, {
anyOf: [
{
type: 'string',
maxLength: 5,
},
{
type: 'number',
minimum: 0,
},
],
});
});
});

describe('t.all', () => {
it('should be a function', () => {
assert(typeof t.all === 'function');
});

it('should be JSON schema', () => {
let output = t.all(
t.number({ maximum: 5 }),
t.number({ minimum: 0 }),
);

assertEquals(output, {
allOf: [
{
type: 'number',
maximum: 5,
},
{
type: 'number',
minimum: 0,
},
],
});
});
});
110 changes: 107 additions & 3 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export type Infer<T> =
: T extends _enum<infer E> ? E
: T extends _tuple<infer I> ? Infer<I>
: T extends _array<infer I> ? Infer<I>[]
// compositions
: T extends _not<infer _> ? unknown
: T extends _any<infer A> | _one<infer A> ? Infer<A>
: T extends _all<infer A> ? Infer<A>
// read out values
: T extends [infer A, ...infer B]
? [Infer<A>, ...Infer<B>]
Expand Down Expand Up @@ -173,10 +177,10 @@ function _null(options?: Omit<_null, 'type'>): _null {
}

/**
* A constant literal value.
* A constant literal value.
* [Reference](https://json-schema.org/understanding-json-schema/reference/const)
*/
* [Reference](https://json-schema.org/understanding-json-schema/reference/const)
*/
type _constant<V extends Value> = Annotations & {
const: V;
default?: V;
Expand Down Expand Up @@ -603,8 +607,108 @@ function _object<
return o;
}

// compositions

/**
* An `anyOf` (OR) composition.
*
* [Reference](https://json-schema.org/understanding-json-schema/reference/combining#anyOf)
*/
type _any<T> = {
anyOf: T[];
};

/**
* Defines an `anyOf` (OR) composition.
*
* Declares that the field may be valid against *any* of the subschemas.
*
* > [!IMPORTANT]
* > This is NOT the TypeScript `any` keyword!
*
* ```ts
* let _ = t.any(
* t.string({ maxLength: 5 }),
* t.number({ minimum: 0 }),
* );
* //-> PASS: `"short"` or `12`
* //-> FAIL: `"too long"` or `-5`
* ```
*/
function _any<T extends Type[]>(...anyOf: T): _any<T[number]> {
return { anyOf };
}

/**
* An `oneOf` (XOR) composition.
*
* [Reference](https://json-schema.org/understanding-json-schema/reference/combining#oneOf)
*/
type _one<T> = {
oneOf: T[];
};

/**
* Defines an `oneOf` composition.
*
* Declares that a field must be valid against *exactly one* of the subschemas.
*
* ```ts
* let _ = t.one(
* t.number({ multipleOf: 5 }),
* t.number({ multipleOf: 3 }),
* );
* //-> PASS: `10` or `9`
* //-> FAIL: `15` or `2`
* ```
*/
function _one<T extends Type[]>(...oneOf: T): _one<T[number]> {
return { oneOf };
}

/**
* A `not` composition.
*
* [Reference](https://json-schema.org/understanding-json-schema/reference/combining#not)
*/
type _not<T> = {
not: T;
};

/**
* Defines a `not` composition (NOT).
*
* Declares that a field must *not* be valid against the schema.
*/
function _not<T extends Type>(not: T): _not<T> {
return { not };
}

/**
* An `allOf` (AND) composition.
*
* [Reference](https://json-schema.org/understanding-json-schema/reference/combining#allOf)
*/
type _all<T> = {
allOf: T[];
};

/**
* Defines an `allOf` composition (AND).
*
* Declares that a field must be valid against **all** of the subschemas.
*/
function _all<T extends Type>(...allOf: T[]): _all<T> {
return { allOf };
}

// deno-fmt-ignore
export {
// composites
_all as all,
_any as any,
_one as one,
_not as not,
// modifiers
_optional as optional,
_readonly as readonly,
Expand Down
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 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 (359b) utility to build [JSON schema](https://json-schema.org/understanding-json-schema/reference) types.
> A tiny (408b) utility to build [JSON schema](https://json-schema.org/understanding-json-schema/reference) types.
## Install

Expand Down
4 changes: 3 additions & 1 deletion scripts/build.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { join, resolve } from 'jsr:@std/path@^1.0';
import oxc from 'npm:oxc-transform@^0.23';

const bytes = 408;

const version = Deno.args[0];
console.log('? version:', version);

Expand Down Expand Up @@ -82,7 +84,7 @@ let pkg = {
name: 'tschema',
version: version,
repository: 'lukeed/tschema',
description: 'A tiny (359b) utility to build JSON schema types.',
description: `A tiny (${bytes}b) utility to build JSON schema types.`,
module: 'index.mjs',
types: 'index.d.ts',
type: 'module',
Expand Down

0 comments on commit 04b22b5

Please sign in to comment.