@nerd-bible/valio 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pipe.js ADDED
@@ -0,0 +1,149 @@
1
+ import enFormat from "./locales/en";
2
+ function clone(obj) {
3
+ return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
4
+ }
5
+ export class HalfPipe {
6
+ name;
7
+ typeCheck;
8
+ transform;
9
+ constructor(
10
+ /** The type name */
11
+ name,
12
+ /** The first check to run */
13
+ typeCheck,
14
+ /** Optional transform for pipe to run at end. Useful for containers */
15
+ transform) {
16
+ this.name = name;
17
+ this.typeCheck = typeCheck;
18
+ this.transform = transform;
19
+ }
20
+ /** The second checks to run */
21
+ checks = [];
22
+ clone() {
23
+ const res = clone(this);
24
+ res.checks = res.checks.slice();
25
+ return res;
26
+ }
27
+ }
28
+ /** During encoding, decoding, or validation. */
29
+ export class Context {
30
+ jsonPath = [];
31
+ errors = {};
32
+ errorFmt(name, props) {
33
+ return enFormat(name, props);
34
+ }
35
+ clone() {
36
+ const res = clone(this);
37
+ res.jsonPath = res.jsonPath.slice();
38
+ res.errors = { ...res.errors };
39
+ return res;
40
+ }
41
+ pushError(error) {
42
+ const key = "." + this.jsonPath.join(".");
43
+ this.errors[key] ??= [];
44
+ this.errors[key].push(error);
45
+ }
46
+ pushErrorFmt(name, input, props) {
47
+ const message = this.errorFmt(name, props);
48
+ this.pushError({ input, message });
49
+ }
50
+ run(input, halfPipe) {
51
+ if (!halfPipe.typeCheck(input)) {
52
+ this.pushErrorFmt("type", input, { expected: halfPipe.name });
53
+ return { success: false, errors: this.errors };
54
+ }
55
+ let success = true;
56
+ for (const c of halfPipe.checks ?? []) {
57
+ if (!c.valid(input, this)) {
58
+ this.pushErrorFmt(c.name, input, c.props);
59
+ success = false;
60
+ }
61
+ }
62
+ if (!success)
63
+ return { success, errors: this.errors };
64
+ return { success, output: input };
65
+ }
66
+ }
67
+ export class Pipe {
68
+ i;
69
+ o;
70
+ constructor(i, o) {
71
+ this.i = i;
72
+ this.o = o;
73
+ }
74
+ pipes = [];
75
+ registry = {};
76
+ clone() {
77
+ const res = clone(this);
78
+ res.i = res.i.clone();
79
+ res.o = res.o.clone();
80
+ res.pipes = res.pipes.slice();
81
+ res.registry = { ...res.registry };
82
+ return res;
83
+ }
84
+ refine(valid, name, props) {
85
+ const res = this.clone();
86
+ res.o.checks.push({ valid, name, props });
87
+ return res;
88
+ }
89
+ pipe(pipe) {
90
+ const res = this.clone();
91
+ res.pipes.push(pipe);
92
+ return res;
93
+ }
94
+ decodeAny(input, ctx = new Context()) {
95
+ // 1. Verify input
96
+ let res = ctx.run(input, this.i);
97
+ if (!res.success)
98
+ return res;
99
+ // 2. Transform input to output
100
+ if (this.i.transform) {
101
+ res = this.i.transform(res.output, ctx);
102
+ if (!res.success)
103
+ return res;
104
+ }
105
+ // 3. Verify output
106
+ res = ctx.run(res.output, this.o);
107
+ if (!res.success)
108
+ return res;
109
+ // 4. Next
110
+ for (const p of this.pipes) {
111
+ res = p.decode(res.output, ctx);
112
+ if (!res.success)
113
+ return res;
114
+ }
115
+ return res;
116
+ }
117
+ decode(input, ctx = new Context()) {
118
+ return this.decodeAny(input, ctx);
119
+ }
120
+ encodeAny(output, ctx = new Context()) {
121
+ // 1. Next
122
+ let res = { success: true, output };
123
+ for (let i = this.pipes.length - 1; i >= 0; i--) {
124
+ res = this.pipes[i].encodeAny(res.output, ctx);
125
+ if (!res.success)
126
+ return res;
127
+ }
128
+ // 2. Verify output
129
+ res = ctx.run(res.output, this.o);
130
+ if (!res.success)
131
+ return res;
132
+ // 3. Transform output to input
133
+ if (this.o.transform) {
134
+ res = this.o.transform(res.output, ctx);
135
+ if (!res.success)
136
+ return res;
137
+ }
138
+ // 4. Verify input
139
+ return ctx.run(res.output, this.i);
140
+ }
141
+ encode(output, ctx = new Context()) {
142
+ return this.encodeAny(output, ctx);
143
+ }
144
+ register(key, value) {
145
+ const res = this.clone();
146
+ res.registry[key] = value;
147
+ return res;
148
+ }
149
+ }
@@ -0,0 +1,41 @@
1
+ import { Pipe } from "./pipe";
2
+ export declare function boolean(): Pipe<boolean, boolean>;
3
+ export declare function undefined(): Pipe<undefined, undefined>;
4
+ export declare function any(): Pipe<any, any>;
5
+ declare function null_(): Pipe<null, null>;
6
+ export { null_ as null };
7
+ export declare class Comparable<I, O> extends Pipe<I, O> {
8
+ gt(n: O): this;
9
+ gte(n: O): this;
10
+ lt(n: O): this;
11
+ lte(n: O): this;
12
+ eq(n: O): this;
13
+ }
14
+ declare class ValioNumber extends Comparable<number, number> {
15
+ constructor();
16
+ }
17
+ export declare function number(): ValioNumber;
18
+ export declare class Arrayish<I, O extends {
19
+ length: number;
20
+ }> extends Pipe<I, O> {
21
+ minLength(n: number): this;
22
+ maxLength(n: number): this;
23
+ }
24
+ declare class ValioString extends Pipe<string, string> {
25
+ constructor();
26
+ regex(re: RegExp): this;
27
+ }
28
+ export declare function string(): ValioString;
29
+ export type Lit = string | number | bigint | boolean | null | undefined;
30
+ declare class ValioLiteral<T extends Lit> extends Pipe<T, T> {
31
+ literal: T;
32
+ constructor(literal: T);
33
+ }
34
+ export declare function literal<T extends Lit>(literal: T): ValioLiteral<T>;
35
+ declare class ValioEnum<T extends Lit> extends Pipe<T, T> {
36
+ literals: T[];
37
+ constructor(literals: T[]);
38
+ }
39
+ declare function enum_<T extends Lit>(literals: T[]): ValioEnum<T>;
40
+ export { enum_ as enum };
41
+ //# sourceMappingURL=primitives.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../src/primitives.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,IAAI,EAAE,MAAM,QAAQ,CAAC;AAOxC,wBAAgB,OAAO,2BAKtB;AAED,wBAAgB,SAAS,+BAKxB;AAED,wBAAgB,GAAG,mBAElB;AAED,iBAAS,KAAK,qBAEb;AACD,OAAO,EAAE,KAAK,IAAI,IAAI,EAAE,CAAC;AAEzB,qBAAa,UAAU,CAAC,CAAC,EAAE,CAAC,CAAE,SAAQ,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;IAC/C,EAAE,CAAC,CAAC,EAAE,CAAC;IAGP,GAAG,CAAC,CAAC,EAAE,CAAC;IAGR,EAAE,CAAC,CAAC,EAAE,CAAC;IAGP,GAAG,CAAC,CAAC,EAAE,CAAC;IAGR,EAAE,CAAC,CAAC,EAAE,CAAC;CAGP;AAED,cAAM,WAAY,SAAQ,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC;;CAKnD;AACD,wBAAgB,MAAM,gBAErB;AAED,qBAAa,QAAQ,CACpB,CAAC,EACD,CAAC,SAAS;IACT,MAAM,EAAE,MAAM,CAAC;CACf,CACA,SAAQ,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;IACnB,SAAS,CAAC,CAAC,EAAE,MAAM;IAGnB,SAAS,CAAC,CAAC,EAAE,MAAM;CAGnB;AAED,cAAM,WAAY,SAAQ,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;;IAM7C,KAAK,CAAC,EAAE,EAAE,MAAM;CAGhB;AACD,wBAAgB,MAAM,IAAI,WAAW,CAEpC;AAED,MAAM,MAAM,GAAG,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC;AAExE,cAAM,YAAY,CAAC,CAAC,SAAS,GAAG,CAAE,SAAQ,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;IAChC,OAAO,EAAE,CAAC;gBAAV,OAAO,EAAE,CAAC;CAI7B;AACD,wBAAgB,OAAO,CAAC,CAAC,SAAS,GAAG,EAAE,OAAO,EAAE,CAAC,mBAEhD;AAED,cAAM,SAAS,CAAC,CAAC,SAAS,GAAG,CAAE,SAAQ,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;IAC7B,QAAQ,EAAE,CAAC,EAAE;gBAAb,QAAQ,EAAE,CAAC,EAAE;CAMhC;AACD,iBAAS,KAAK,CAAC,CAAC,SAAS,GAAG,EAAE,QAAQ,EAAE,CAAC,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,CAEzD;AACD,OAAO,EAAE,KAAK,IAAI,IAAI,EAAE,CAAC"}
@@ -0,0 +1,87 @@
1
+ import { HalfPipe, Pipe } from "./pipe";
2
+ function primitive(name, typeCheck) {
3
+ const half = new HalfPipe(name, typeCheck);
4
+ return new Pipe(half, half);
5
+ }
6
+ export function boolean() {
7
+ return primitive("boolean", (v) => typeof v == "boolean");
8
+ }
9
+ export function undefined() {
10
+ return primitive("undefined", (v) => typeof v == "undefined");
11
+ }
12
+ export function any() {
13
+ return primitive("any", (v) => true);
14
+ }
15
+ function null_() {
16
+ return primitive("null", (v) => v === null);
17
+ }
18
+ export { null_ as null };
19
+ export class Comparable extends Pipe {
20
+ gt(n) {
21
+ return this.refine((v) => v > n, "gt", { n });
22
+ }
23
+ gte(n) {
24
+ return this.refine((v) => v >= n, "gte", { n });
25
+ }
26
+ lt(n) {
27
+ return this.refine((v) => v < n, "lt", { n });
28
+ }
29
+ lte(n) {
30
+ return this.refine((v) => v <= n, "lte", { n });
31
+ }
32
+ eq(n) {
33
+ return this.refine((v) => v == n, "eq", { n });
34
+ }
35
+ }
36
+ class ValioNumber extends Comparable {
37
+ constructor() {
38
+ const half = new HalfPipe("number", (v) => typeof v == "number");
39
+ super(half, half);
40
+ }
41
+ }
42
+ export function number() {
43
+ return new ValioNumber();
44
+ }
45
+ export class Arrayish extends Pipe {
46
+ minLength(n) {
47
+ return this.refine((v) => v.length >= n, "minLength", { n });
48
+ }
49
+ maxLength(n) {
50
+ return this.refine((v) => v.length <= n, "maxLength", { n });
51
+ }
52
+ }
53
+ class ValioString extends Pipe {
54
+ constructor() {
55
+ const half = new HalfPipe("string", (v) => typeof v == "string");
56
+ super(half, half);
57
+ }
58
+ regex(re) {
59
+ return this.refine((v) => !!v.match(re), "regex", { regex: re.source });
60
+ }
61
+ }
62
+ export function string() {
63
+ return new ValioString();
64
+ }
65
+ class ValioLiteral extends Pipe {
66
+ literal;
67
+ constructor(literal) {
68
+ const half = new HalfPipe(`${literal}`, (v) => v == literal);
69
+ super(half, half);
70
+ this.literal = literal;
71
+ }
72
+ }
73
+ export function literal(literal) {
74
+ return new ValioLiteral(literal);
75
+ }
76
+ class ValioEnum extends Pipe {
77
+ literals;
78
+ constructor(literals) {
79
+ const half = new HalfPipe(`${literals.join(",")}`, (v) => literals.includes(v));
80
+ super(half, half);
81
+ this.literals = literals;
82
+ }
83
+ }
84
+ function enum_(literals) {
85
+ return new ValioEnum(literals);
86
+ }
87
+ export { enum_ as enum };
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@nerd-bible/valio",
3
+ "version": "0.0.1",
4
+ "module": "index.ts",
5
+ "devDependencies": {
6
+ "@biomejs/biome": "^2.3.0",
7
+ "@types/bun": "latest"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "fmt": "biome check",
12
+ "fmt-fix": "biome check --write --unsafe"
13
+ },
14
+ "peerDependencies": {
15
+ "typescript": "^5"
16
+ },
17
+ "type": "module"
18
+ }
@@ -0,0 +1,88 @@
1
+ import { expect, test } from "bun:test";
2
+ import * as v from "./index";
3
+
4
+ test("number codec", () => {
5
+ const schema = v.codecs.number();
6
+
7
+ expect(schema.decode("13")).toEqual({ success: true, output: 13 });
8
+ expect(schema.lt(12).decode("13")).toEqual({
9
+ success: false,
10
+ errors: { ".": [{ input: 13, message: "must be < 12" }] },
11
+ });
12
+ expect(schema.decode("-1.3")).toEqual({ success: true, output: -1.3 });
13
+ expect(schema.decode("asdf")).toEqual({
14
+ success: false,
15
+ errors: {
16
+ ".": [{ input: "asdf", message: "could not coerce to number" }],
17
+ },
18
+ });
19
+ expect(schema.decode("Infinity")).toEqual({
20
+ success: true,
21
+ output: Number.POSITIVE_INFINITY,
22
+ });
23
+ expect(schema.decode("-Infinity")).toEqual({
24
+ success: true,
25
+ output: Number.NEGATIVE_INFINITY,
26
+ });
27
+ expect(schema.encode(1.3)).toEqual({ success: true, output: 1.3 });
28
+ });
29
+
30
+ test("2 pipes", () => {
31
+ const schema = v.codecs.number();
32
+ const next = schema.pipe(v.number().gt(5));
33
+
34
+ expect(next.decode("3")).toEqual({
35
+ success: false,
36
+ errors: {
37
+ ".": [{ input: 3, message: "must be > 5" }],
38
+ },
39
+ });
40
+ expect(schema.decode(undefined)).toEqual({
41
+ success: true,
42
+ output: Number.NaN,
43
+ });
44
+ expect(schema.decode(null)).toEqual({ success: true, output: Number.NaN });
45
+ });
46
+
47
+ test("array number codec", () => {
48
+ const schema = v.array(v.codecs.number().gt(4).gt(5));
49
+
50
+ expect(schema.decode(["10a", "11b"])).toEqual({
51
+ success: true,
52
+ output: [10, 11],
53
+ });
54
+ expect(schema.decode(["NaN", "5", 5])).toEqual({
55
+ errors: {
56
+ ".0": [
57
+ { input: Number.NaN, message: "must be > 4" },
58
+ { input: Number.NaN, message: "must be > 5" },
59
+ ],
60
+ ".1": [{ input: 5, message: "must be > 5" }],
61
+ ".2": [{ input: 5, message: "must be > 5" }],
62
+ },
63
+ success: false,
64
+ });
65
+ });
66
+
67
+ test("boolean codec", () => {
68
+ const schema = v.codecs.boolean({ true: ["yes"], false: ["no"] });
69
+
70
+ expect(schema.decode("yes")).toEqual({ success: true, output: true });
71
+ expect(schema.decode("no")).toEqual({ success: true, output: false });
72
+ expect(schema.decode("")).toEqual({ success: true, output: false });
73
+ expect(schema.decode("1")).toEqual({ success: true, output: true });
74
+ expect(schema.decode(false)).toEqual({ success: true, output: false });
75
+ expect(schema.decode(0)).toEqual({ success: true, output: false });
76
+ });
77
+
78
+ // test("string codec", () => {
79
+ // const schema = v.codecs.string();
80
+ //
81
+ // expect(schema.decode(13)).toEqual({ success: true, output: "13" });
82
+ // expect(schema.decode(null)).toEqual({ success: true, output: "null" });
83
+ // expect(schema.encode(null as any)).toEqual({
84
+ // success: false,
85
+ // errors: { ".": [ { input: null, message: "not type string" } ] },
86
+ // });
87
+ // expect(schema.encode("asdf")).toEqual({ success: true, output: "asdf" });
88
+ // });
package/src/codecs.ts ADDED
@@ -0,0 +1,56 @@
1
+ import * as c from "./containers";
2
+ import type { Context, Pipe, Result } from "./pipe";
3
+ import * as p from "./primitives";
4
+
5
+ export function custom<I, O>(
6
+ input: Pipe<I, any>,
7
+ output: Pipe<any, O>,
8
+ codec: {
9
+ encode?(input: O, ctx: Context): Result<I>;
10
+ decode?(output: I, ctx: Context): Result<O>;
11
+ },
12
+ ): Pipe<I, O> {
13
+ const res = output.clone() as any;
14
+ res.i = input.i.clone();
15
+ res.i.transform = codec.decode;
16
+ res.o.transform = codec.encode;
17
+ return res;
18
+ }
19
+
20
+ export function number(
21
+ parser = Number.parseFloat,
22
+ ): p.Comparable<string | number | null | undefined, number> {
23
+ return custom(
24
+ c.union([p.string(), p.number(), p.null(), p.undefined()]),
25
+ p.number(),
26
+ {
27
+ decode(input, ctx) {
28
+ if (typeof input == "number") return { success: true, output: input };
29
+ if (input == null || input.toLowerCase() == "nan")
30
+ return { success: true, output: Number.NaN };
31
+
32
+ const output = parser(input);
33
+ if (!Number.isNaN(output)) return { success: true, output };
34
+
35
+ ctx.pushErrorFmt("coerce", input, { expected: "number" });
36
+ return { success: false, errors: ctx.errors };
37
+ },
38
+ },
39
+ ) as ReturnType<typeof number>;
40
+ }
41
+
42
+ export function boolean(opts: {
43
+ true?: string[];
44
+ false?: string[];
45
+ }): Pipe<any, boolean> {
46
+ return custom(p.any(), p.boolean(), {
47
+ decode(input) {
48
+ if (typeof input === "string") {
49
+ if (opts.true?.includes(input)) return { success: true, output: true };
50
+ if (opts.false?.includes(input))
51
+ return { success: true, output: false };
52
+ }
53
+ return { success: true, output: Boolean(input) };
54
+ },
55
+ });
56
+ }
@@ -0,0 +1,203 @@
1
+ import { expect, test } from "bun:test";
2
+ import * as v from "./index";
3
+
4
+ test("array", () => {
5
+ const schema = v.array(v.number());
6
+
7
+ expect(schema.decode([54])).toEqual({ success: true, output: [54] });
8
+ expect(schema.decode(["54"])).toEqual({
9
+ success: false,
10
+ errors: { ".0": [{ input: "54", message: "not type number" }] },
11
+ });
12
+ expect(schema.decodeAny(54)).toEqual({
13
+ success: false,
14
+ errors: { ".": [{ input: 54, message: "not type array" }] },
15
+ });
16
+ });
17
+
18
+ test("array failed element", () => {
19
+ const schema = v.array(v.number().gt(4).gt(5));
20
+
21
+ expect(schema.decode(["5", 5])).toEqual({
22
+ success: false,
23
+ errors: {
24
+ ".0": [{ input: "5", message: "not type number" }],
25
+ ".1": [{ input: 5, message: "must be > 5" }],
26
+ },
27
+ });
28
+ });
29
+
30
+ test("object", () => {
31
+ const o = v.object({ foo: v.number().gt(4) });
32
+ type O = v.Output<typeof o>;
33
+
34
+ expect(o.decode({ foo: 10 })).toEqual({
35
+ success: true,
36
+ output: { foo: 10 } as O,
37
+ });
38
+ expect(o.decode({ foo: 10, bar: 10 })).toEqual({
39
+ success: true,
40
+ output: { foo: 10 } as O,
41
+ });
42
+ expect(o.decode({ bar: 10 })).toEqual({
43
+ success: false,
44
+ errors: {
45
+ ".foo": [
46
+ {
47
+ input: undefined,
48
+ message: "not type number",
49
+ },
50
+ ],
51
+ },
52
+ });
53
+ });
54
+
55
+ test("loose object", () => {
56
+ const o = v.object({ foo: v.number().gt(4) }).loose<number>();
57
+ type O = v.Output<typeof o>;
58
+
59
+ expect(o.isLoose).toBe(true);
60
+ expect(o.decode({ foo: 10, bar: 10 })).toEqual({
61
+ success: true,
62
+ output: { foo: 10, bar: 10 } as O,
63
+ });
64
+ });
65
+
66
+ test("nested object", () => {
67
+ const o = v.object({
68
+ foo: v.object({ bar: v.number().gt(4) }),
69
+ });
70
+
71
+ expect(o.decode({ foo: { bar: 10 } })).toEqual({
72
+ success: true,
73
+ output: { foo: { bar: 10 } },
74
+ });
75
+ expect(o.decode({ bar: 10 })).toEqual({
76
+ success: false,
77
+ errors: {
78
+ ".foo": [
79
+ {
80
+ input: undefined,
81
+ message: "not type object",
82
+ },
83
+ ],
84
+ },
85
+ });
86
+ });
87
+
88
+ test("partial object", () => {
89
+ const o = v.object({ foo: v.number().gt(4) }).partial({ foo: true });
90
+ type O = v.Output<typeof o>;
91
+
92
+ expect(o.decode({ foo: 10 })).toEqual({
93
+ success: true,
94
+ output: { foo: 10 } as O,
95
+ });
96
+ expect(o.decode({ foo: undefined })).toEqual({
97
+ success: true,
98
+ output: { foo: undefined } as O,
99
+ });
100
+ expect(o.decode({})).toEqual({
101
+ success: true,
102
+ output: {} as O,
103
+ });
104
+ });
105
+
106
+ test("pick object", () => {
107
+ const o = v
108
+ .object({ foo: v.number().gt(4), bar: v.number() })
109
+ .pick({ foo: true });
110
+ type O = v.Output<typeof o>;
111
+
112
+ expect(o.decode({ bar: 10 })).toEqual({
113
+ success: false,
114
+ errors: {
115
+ ".foo": [
116
+ {
117
+ input: undefined,
118
+ message: "not type number",
119
+ },
120
+ ],
121
+ },
122
+ });
123
+ expect(o.decode({ foo: 10, bar: 10 } as O)).toEqual({
124
+ success: true,
125
+ output: { foo: 10 },
126
+ });
127
+ expect(o.decode({})).toEqual({
128
+ success: false,
129
+ errors: { ".foo": [{ input: undefined, message: "not type number" }] },
130
+ });
131
+ });
132
+
133
+ test("omit object", () => {
134
+ const o = v
135
+ .object({ foo: v.number().gt(4), bar: v.number() })
136
+ .omit({ foo: true });
137
+ type O = v.Output<typeof o>;
138
+
139
+ expect(o.decode({ bar: 10 })).toEqual({
140
+ success: true,
141
+ output: { bar: 10 } as O,
142
+ });
143
+ expect(o.decode({ foo: undefined, bar: 10 })).toEqual({
144
+ success: true,
145
+ output: { bar: 10 },
146
+ });
147
+ expect(o.decode({})).toEqual({
148
+ success: false,
149
+ errors: { ".bar": [{ input: undefined, message: "not type number" }] },
150
+ });
151
+ });
152
+
153
+ test("record", () => {
154
+ const o = v.record(v.string(), v.number());
155
+
156
+ expect(o.decode({ bar: 10 })).toEqual({
157
+ success: true,
158
+ output: { bar: 10 },
159
+ });
160
+ expect(o.decode({ foo: { bar: 10 } })).toEqual({
161
+ success: false,
162
+ errors: { ".foo": [{ input: { bar: 10 }, message: "not type number" }] },
163
+ });
164
+ });
165
+
166
+ test("extend object", () => {
167
+ const o = v.object({ foo: v.number().gt(4) }).extend({ bar: v.number() });
168
+ type O = v.Output<typeof o>;
169
+
170
+ expect(o.decode({ foo: 10, bar: 5 })).toEqual({
171
+ success: true,
172
+ output: { foo: 10, bar: 5 } as O,
173
+ });
174
+ expect(o.decode({})).toEqual({
175
+ success: false,
176
+ errors: {
177
+ ".bar": [
178
+ {
179
+ input: undefined,
180
+ message: "not type number",
181
+ },
182
+ ],
183
+ ".foo": [
184
+ {
185
+ input: undefined,
186
+ message: "not type number",
187
+ },
188
+ ],
189
+ },
190
+ });
191
+ });
192
+
193
+ test("union", () => {
194
+ const schema = v.union([v.string(), v.number()]);
195
+ type O = v.Output<typeof schema>;
196
+
197
+ expect(schema.decode(42 as O)).toEqual({ success: true, output: 42 });
198
+ expect(schema.decode("asdf")).toEqual({ success: true, output: "asdf" });
199
+ expect(schema.decodeAny({})).toEqual({
200
+ success: false,
201
+ errors: { ".": [{ input: {}, message: "not type string|number" }] },
202
+ });
203
+ });