Skip to content

Commit

Permalink
Merge 3978970 into 1fbac90
Browse files Browse the repository at this point in the history
  • Loading branch information
shimataro committed Apr 1, 2021
2 parents 1fbac90 + 3978970 commit ea90a3f
Show file tree
Hide file tree
Showing 13 changed files with 252 additions and 6 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

* `union()`

### Fixed

* TypeScript example in README
Expand Down
64 changes: 59 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ supports [Node.js](https://nodejs.org/), [TypeScript](https://www.typescriptlang
* [email](#email)
* [array](#array)
* [object](#object)
* [union](#union)
* [Changelog](#changelog)

- - -
Expand Down Expand Up @@ -219,11 +220,10 @@ The `ValueSchemaError` object represents an error.
```typescript
export interface ValueSchemaError extends Error
{
name: string
message: string
cause: string
value: any
keyStack: (string | number)[]
readonly cause: string;
readonly value: unknown;
readonly keyStack: (string | number)[];
readonly unionErrors: ValueSchemaError[];

/**
* check whether error is instance of ValueSchemaError or not
Expand All @@ -243,6 +243,7 @@ export interface ValueSchemaError extends Error
|`cause`|cause of error; see [`CAUSE`](#cause)|
|`value`|value to apply|
|`keyStack`|array consists of path to key name(for object) or index(for array) that caused error; for nested object or array|
|`unionErrors`|array of `ValueSchemaError` instances from `union()`; used only in `union()` error|

See below example.
For detail about schema / `value-schema`, see [basic usage](#basic-usage)
Expand Down Expand Up @@ -2194,6 +2195,59 @@ assert.throws(
{name: "ValueSchemaError", cause: vs.CAUSE.CONVERTER});
```

### union

This schema creates a new schema **from other schemas**.
The new schema matches any one of old schemas.

It might be useful for login form, such as "Input email or username".

#### ambient declarations

```typescript
export function object<T>(...schemas: BaseSchema<T>): UnionSchema<T>;

type ErrorHandler<T> = (err: ValueSchemaError) => T | null | never;
interface UnionSchema<T> {
applyTo(value: unknown, onError?: ErrorHandler<T>): T | null
}
```

#### `applyTo(value[, onError])`

Applies schema to `value`.

If an error occurs, this method calls `onError` (if specified) or throw `ValueSchemaError` (otherwise).

```javascript
// should be OK
assert.strictEqual(
vs.union(vs.number(), vs.string()).applyTo(1),
1);
assert.strictEqual(
vs.union(vs.number(), vs.string()).applyTo("a"),
"a");
assert.strictEqual(
vs.union(vs.boolean(), vs.number(), vs.string()).applyTo(true),
true);
assert.strictEqual(
vs.union(vs.email(), vs.string({pattern: /^\w+$/})).applyTo("user@example.com"),
"user@example.com");

// should be adjusted
assert.strictEqual(
vs.union(vs.number(), vs.string()).applyTo("1"),
1);
assert.strictEqual(
vs.union(vs.string(), vs.number()).applyTo("1"), // this won't be adjusted. be careful of schemas order!
"1");

// should cause error
assert.throws(
() => vs.union(vs.number(), vs.string()).applyTo({}),
{name: "ValueSchemaError", cause: vs.CAUSE.UNION});
```

## Changelog

See [CHANGELOG.md](CHANGELOG.md).
Expand Down
34 changes: 34 additions & 0 deletions dist-deno/appliers/union/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Key, Values } from "../../libs/types.ts";
import { CAUSE, ValueSchemaError } from "../../libs/ValueSchemaError.ts";
import { BaseSchema } from "../../schemaClasses/BaseSchema.ts";
export interface Options<T> {
schemas?: BaseSchema<T>[];
}
/**
* apply schema
* @param values input/output values
* @param options options
* @param keyStack key stack for error handling
* @returns applied value
*/
export function applyTo<T>(values: Values, options: Options<T>, keyStack: Key[]): values is Values<T> {
const normalizedOptions: Required<Options<T>> = {
schemas: [],
...options
};
const err = new ValueSchemaError(CAUSE.UNION, values.input, keyStack);
for (const schema of normalizedOptions.schemas) {
try {
values.output = schema.applyTo(values.output);
return true;
}
catch (thrownError) {
// istanbul ignore next
if (!ValueSchemaError.is(thrownError)) {
throw thrownError;
}
err.unionErrors.push(thrownError);
}
}
throw err;
}
1 change: 1 addition & 0 deletions dist-deno/exporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export * from "./schemas/email.ts";
export * from "./schemas/number.ts";
export * from "./schemas/numericString.ts";
export * from "./schemas/object.ts";
export * from "./schemas/union.ts";
export * from "./schemas/string.ts";
5 changes: 4 additions & 1 deletion dist-deno/libs/ValueSchemaError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export enum CAUSE {
MIN_LENGTH = "min-length",
MAX_LENGTH = "max-length",
PATTERN = "pattern",
CHECKSUM = "checksum"
CHECKSUM = "checksum",
UNION = "union"
}
/**
* Value-Schema Error
Expand All @@ -20,6 +21,7 @@ export class ValueSchemaError extends Error {
public readonly cause: CAUSE;
public readonly value: unknown;
public readonly keyStack: Key[];
public readonly unionErrors: ValueSchemaError[];
/**
* throw an error
* @param cause cause of error
Expand Down Expand Up @@ -50,5 +52,6 @@ export class ValueSchemaError extends Error {
this.cause = cause;
this.value = value;
this.keyStack = [...keyStack];
this.unionErrors = [];
}
}
7 changes: 7 additions & 0 deletions dist-deno/schemaClasses/UnionSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as type from "../appliers/union/type.ts";
import { BaseSchema } from "../schemaClasses/BaseSchema.ts";
export class UnionSchema<T> extends BaseSchema<T> {
constructor(options: type.Options<T>) {
super(options, [type.applyTo]);
}
}
17 changes: 17 additions & 0 deletions dist-deno/schemas/union.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { BaseSchema } from "../schemaClasses/BaseSchema.ts";
import { UnionSchema } from "../schemaClasses/UnionSchema.ts";
type Union<T extends BaseSchema[]> = Tuple<T>[number];
type Tuple<T extends BaseSchema[]> = {
[U in keyof T]: Inferred<T[U]>;
};
type Inferred<T> = T extends BaseSchema<infer U> ? U : never;
/**
* create schema
* @param schemas schemas to unify
* @returns schema
*/
export function union<T extends BaseSchema[]>(...schemas: T): UnionSchema<Union<T>> {
return new UnionSchema({
schemas: schemas as BaseSchema<Union<T>>[]
});
}
45 changes: 45 additions & 0 deletions src/appliers/union/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {Key, Values} from "../../libs/types";
import {CAUSE, ValueSchemaError} from "../../libs/ValueSchemaError";
import {BaseSchema} from "../../schemaClasses/BaseSchema";

export interface Options<T>
{
schemas?: BaseSchema<T>[];
}

/**
* apply schema
* @param values input/output values
* @param options options
* @param keyStack key stack for error handling
* @returns applied value
*/
export function applyTo<T>(values: Values, options: Options<T>, keyStack: Key[]): values is Values<T>
{
const normalizedOptions: Required<Options<T>> = {
schemas: [],
...options,
};

const err = new ValueSchemaError(CAUSE.UNION, values.input, keyStack);
for(const schema of normalizedOptions.schemas)
{
try
{
values.output = schema.applyTo(values.output);
return true;
}
catch(thrownError)
{
// istanbul ignore next
if(!ValueSchemaError.is(thrownError))
{
throw thrownError;
}

err.unionErrors.push(thrownError);
}
}

throw err;
}
1 change: 1 addition & 0 deletions src/exporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export * from "./schemas/email";
export * from "./schemas/number";
export * from "./schemas/numericString";
export * from "./schemas/object";
export * from "./schemas/union";
export * from "./schemas/string";
4 changes: 4 additions & 0 deletions src/libs/ValueSchemaError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export enum CAUSE
PATTERN = "pattern",

CHECKSUM = "checksum",

UNION = "union",
}

/**
Expand All @@ -27,6 +29,7 @@ export class ValueSchemaError extends Error
public readonly cause: CAUSE;
public readonly value: unknown;
public readonly keyStack: Key[];
public readonly unionErrors: ValueSchemaError[];

/**
* throw an error
Expand Down Expand Up @@ -64,5 +67,6 @@ export class ValueSchemaError extends Error
this.cause = cause;
this.value = value;
this.keyStack = [...keyStack];
this.unionErrors = [];
}
}
10 changes: 10 additions & 0 deletions src/schemaClasses/UnionSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as type from "../appliers/union/type";
import {BaseSchema} from "../schemaClasses/BaseSchema";

export class UnionSchema<T> extends BaseSchema<T>
{
constructor(options: type.Options<T>)
{
super(options, [type.applyTo]);
}
}
18 changes: 18 additions & 0 deletions src/schemas/union.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {BaseSchema} from "../schemaClasses/BaseSchema";
import {UnionSchema} from "../schemaClasses/UnionSchema";

type Union<T extends BaseSchema[]> = Tuple<T>[number];
type Tuple<T extends BaseSchema[]> = {[U in keyof T]: Inferred<T[U]>};
type Inferred<T> = T extends BaseSchema<infer U> ? U : never;

/**
* create schema
* @param schemas schemas to unify
* @returns schema
*/
export function union<T extends BaseSchema[]>(...schemas: T): UnionSchema<Union<T>>
{
return new UnionSchema({
schemas: schemas as BaseSchema<Union<T>>[],
});
}
48 changes: 48 additions & 0 deletions test/schemas/union.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import vs from "value-schema";

{
describe("type", testType);
}

/**
* type
*/
function testType(): void
{
it("should be OK", () =>
{
// two schemas
expect(vs.union(vs.number(), vs.string()).applyTo(1)).toEqual(1);
expect(vs.union(vs.number(), vs.string()).applyTo("a")).toEqual("a");

// three schemas
expect(vs.union(vs.boolean(), vs.number(), vs.string()).applyTo(true)).toEqual(true);
expect(vs.union(vs.boolean(), vs.number(), vs.string()).applyTo(1)).toEqual(true);

// email or username
expect(vs.union(vs.email(), vs.string({pattern: /^\w+$/})).applyTo("user@example.com")).toEqual("user@example.com");
expect(vs.union(vs.email(), vs.string({pattern: /^\w+$/})).applyTo("username")).toEqual("username");
});
it("should be adjusted", () =>
{
// two schemas
expect(vs.union(vs.number(), vs.string()).applyTo("1")).toEqual(1);
expect(vs.union(vs.string(), vs.number()).applyTo(1)).toEqual("1");

// three schemas
expect(vs.union(vs.number(), vs.boolean(), vs.string()).applyTo(true)).toEqual(1);
});
it("should cause error(s)", () =>
{
expect(() =>
{
vs.union(vs.number(), vs.string()).applyTo({});
}).toThrow(vs.CAUSE.UNION);

// email or username
expect(() =>
{
vs.union(vs.email(), vs.string({pattern: /^\w+$/})).applyTo("!abcxyz");
}).toThrow(vs.CAUSE.UNION);
});
}

0 comments on commit ea90a3f

Please sign in to comment.