Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/union #438

Draft
wants to merge 31 commits into
base: v3
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9f24c1d
* add union()
shimataro Feb 19, 2020
5bcf2a6
* update README
shimataro Feb 19, 2020
ee46a00
* remove OptionsForUnion
shimataro Feb 19, 2020
3c14fd1
* update CHANGELOG
shimataro Feb 19, 2020
7c35f5f
* update README
shimataro Feb 19, 2020
2f6936a
* use Mapped Tuple Type
shimataro Feb 19, 2020
ab87a18
* update about union() description
shimataro Feb 19, 2020
fbf27a4
* any -> unknown
shimataro Feb 19, 2020
a09a671
* remove "any" type
shimataro Feb 20, 2020
d6407fc
* update README
shimataro Feb 20, 2020
b03ad34
* update README example
shimataro Feb 20, 2020
992e7fc
* add tests
shimataro Feb 20, 2020
1bdb06e
* update README
shimataro Feb 20, 2020
c879e13
* add test and examples
shimataro Feb 20, 2020
96dc4a3
* update types
shimataro Feb 21, 2020
764434a
Merge branch 'develop' into feature/union
shimataro Feb 26, 2020
37ccdc0
Merge branch 'develop' into feature/union
shimataro Feb 26, 2020
5934f90
Merge branch 'develop' into feature/union
shimataro Feb 26, 2020
4a11033
Merge branch 'develop' into feature/union
shimataro Feb 26, 2020
38a351b
Merge branch 'develop' into feature/union
shimataro Feb 27, 2020
652329d
Merge branch 'develop' into feature/union
shimataro Feb 27, 2020
af57dba
Merge branch 'develop' into feature/union
shimataro Feb 28, 2020
5440c8b
Merge branch 'develop' into feature/union
shimataro Feb 28, 2020
9eb3174
* add unionErrors property
shimataro Feb 29, 2020
b993b00
* update README
shimataro Feb 29, 2020
3d8ae51
* update CHANGELOG
shimataro Feb 29, 2020
783eb1d
* update README
shimataro Feb 29, 2020
876d151
* check thrown error is instance of ValueSchemaError or not
shimataro Mar 1, 2020
130c9b2
* re-throw if not ValueSchemaError
shimataro Mar 1, 2020
85894ca
Merge branch 'develop' into feature/union
shimataro Sep 4, 2020
3978970
Merge branch 'develop' into feature/union
shimataro Apr 1, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
});
}