@nerd-bible/valio 0.0.12 → 0.1.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/.github/workflows/publish.yml +27 -0
- package/.github/workflows/test.yml +14 -0
- package/README.md +2 -2
- package/package.json +16 -17
- package/publish.ts +49 -0
- package/src/codecs.test.ts +4 -3
- package/src/codecs.ts +23 -24
- package/src/containers.test.ts +3 -2
- package/src/containers.ts +68 -39
- package/src/index.ts +4 -4
- package/src/pipe.ts +36 -22
- package/src/primitives.test.ts +5 -4
- package/src/primitives.ts +18 -12
- package/test/conllu.ts +226 -0
- package/test/gum.conllu +66 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +22 -0
- package/dist/codecs.d.ts +0 -12
- package/dist/codecs.d.ts.map +0 -1
- package/dist/codecs.js +0 -38
- package/dist/codecs.js.map +0 -1
- package/dist/containers.d.ts +0 -54
- package/dist/containers.d.ts.map +0 -1
- package/dist/containers.js +0 -193
- package/dist/containers.js.map +0 -1
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -5
- package/dist/index.js.map +0 -1
- package/dist/locales/en.d.ts +0 -2
- package/dist/locales/en.d.ts.map +0 -1
- package/dist/locales/en.js +0 -22
- package/dist/locales/en.js.map +0 -1
- package/dist/pipe.d.ts +0 -66
- package/dist/pipe.d.ts.map +0 -1
- package/dist/pipe.js +0 -150
- package/dist/pipe.js.map +0 -1
- package/dist/primitives.d.ts +0 -41
- package/dist/primitives.d.ts.map +0 -1
- package/dist/primitives.js +0 -89
- package/dist/primitives.js.map +0 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags:
|
|
5
|
+
- v[0-9]+.[0-9]+.[0-9]+
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
with:
|
|
19
|
+
fetch-depth: 0
|
|
20
|
+
- uses: actions/setup-node@v6
|
|
21
|
+
with:
|
|
22
|
+
node-version: '24'
|
|
23
|
+
registry-url: 'https://registry.npmjs.org'
|
|
24
|
+
- run: npm install
|
|
25
|
+
- run: npm test
|
|
26
|
+
- run: node ./publish.ts "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
|
|
27
|
+
- run: npm publish --verbose
|
package/README.md
CHANGED
|
@@ -35,8 +35,8 @@ const schema = v.number(); // Pipe<number, number>
|
|
|
35
35
|
|
|
36
36
|
expect(schema.decode(5)).toEqual({ success: true, output: 5 });
|
|
37
37
|
expect(schema.decodeAny("5")).toEqual({
|
|
38
|
-
success: false,
|
|
39
|
-
errors: { ".": [{ input: "5", message: "not type number" }] },
|
|
38
|
+
success: false,
|
|
39
|
+
errors: { ".": [{ input: "5", message: "not type number" }] },
|
|
40
40
|
});
|
|
41
41
|
```
|
|
42
42
|
|
package/package.json
CHANGED
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nerd-bible/valio",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
"exports": {
|
|
5
|
+
".": {
|
|
6
|
+
"import": {
|
|
7
|
+
"nbsource": "./src/index.ts",
|
|
8
|
+
"node": "./dist/index.js"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"./package.json": "./package.json"
|
|
9
12
|
},
|
|
10
13
|
"scripts": {
|
|
11
|
-
"build": "tsc",
|
|
12
|
-
"
|
|
13
|
-
"fmt-fix": "biome check --write --unsafe"
|
|
14
|
+
"build": "tsc -p ./tsconfig.build.json",
|
|
15
|
+
"test": "node --test"
|
|
14
16
|
},
|
|
15
|
-
"
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^25.3.3",
|
|
19
|
+
"expect": "^30.2.0",
|
|
16
20
|
"typescript": "^5.9.3"
|
|
17
21
|
},
|
|
18
|
-
"
|
|
19
|
-
"README.md",
|
|
20
|
-
"src",
|
|
21
|
-
"dist"
|
|
22
|
-
],
|
|
23
|
-
"version": "0.0.12",
|
|
22
|
+
"version": "0.1.1",
|
|
24
23
|
"repository": {
|
|
25
|
-
"url": "
|
|
24
|
+
"url": "https://github.com/nerd-bible/valio"
|
|
26
25
|
}
|
|
27
|
-
}
|
|
26
|
+
}
|
package/publish.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// This script allows using Git tags as the source of truth for versions rather
|
|
2
|
+
// than package.json.
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
const repoUrl = process.argv.slice(2).find((a) => !a.startsWith("--"));
|
|
7
|
+
if (!repoUrl) throw Error("Required arg: repo URL");
|
|
8
|
+
const dry = process.argv.includes("--dry");
|
|
9
|
+
|
|
10
|
+
function execGit(args: string, allowFailure = false) {
|
|
11
|
+
try {
|
|
12
|
+
return execSync(`git ${args}`, {
|
|
13
|
+
encoding: "utf8",
|
|
14
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
15
|
+
}).trim();
|
|
16
|
+
} catch (err) {
|
|
17
|
+
if (!allowFailure) throw err;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
execGit("fetch --tags");
|
|
22
|
+
const tagCmd = "describe --tags --abbrev=0 --match='v[0-9]*.[0-9]*.[0-9]*'";
|
|
23
|
+
let version = execGit(`${tagCmd} --exact-match`, true);
|
|
24
|
+
|
|
25
|
+
if (!version) {
|
|
26
|
+
const lastVersion = execGit(tagCmd, true) ?? "v0.0.0";
|
|
27
|
+
console.log("No manual tag, bumping", lastVersion);
|
|
28
|
+
|
|
29
|
+
const split = lastVersion.split(".");
|
|
30
|
+
// Tags should only be missing for minor bumps.
|
|
31
|
+
const last = parseInt(split.pop()!) + 1;
|
|
32
|
+
version = [...split, last].join(".");
|
|
33
|
+
|
|
34
|
+
if (!dry) {
|
|
35
|
+
console.log("Tagging + pushing", version);
|
|
36
|
+
execGit(`tag ${version}`);
|
|
37
|
+
execGit("push --tags origin master");
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
console.log("Using manual tag", version);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!dry) {
|
|
44
|
+
console.log("Writing temporary package.json");
|
|
45
|
+
const pkg = JSON.parse(readFileSync("package.json", "utf8"));
|
|
46
|
+
pkg.version = version.substring(1);
|
|
47
|
+
pkg.repository = { url: repoUrl };
|
|
48
|
+
writeFileSync("package.json", JSON.stringify(pkg, null, 2));
|
|
49
|
+
}
|
package/src/codecs.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import expect from "expect";
|
|
3
|
+
import * as v from "./index.ts";
|
|
3
4
|
|
|
4
5
|
test("number codec", () => {
|
|
5
6
|
const schema = v.codecs.number();
|
|
@@ -65,7 +66,7 @@ test("array number codec", () => {
|
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
test("boolean codec", () => {
|
|
68
|
-
const schema = v.codecs.boolean({ true: [
|
|
69
|
+
const schema = v.codecs.boolean({ true: /yes|^[1-9]/, false: /no|^0?$/ });
|
|
69
70
|
|
|
70
71
|
expect(schema.decode("yes")).toEqual({ success: true, output: true });
|
|
71
72
|
expect(schema.decode("no")).toEqual({ success: true, output: false });
|
package/src/codecs.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import * as p from "./primitives";
|
|
1
|
+
import type { Context, Pipe, Result } from "./pipe.ts";
|
|
2
|
+
import * as p from "./primitives.ts";
|
|
4
3
|
|
|
5
4
|
export function custom<I, O>(
|
|
6
5
|
input: Pipe<I, any>,
|
|
@@ -20,37 +19,37 @@ export function custom<I, O>(
|
|
|
20
19
|
export function number(
|
|
21
20
|
parser = Number.parseFloat,
|
|
22
21
|
): p.Comparable<string | number | null | undefined, number> {
|
|
23
|
-
return custom(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (typeof input == "number") return { success: true, output: input };
|
|
29
|
-
if (input == null || input.toLowerCase() == "nan")
|
|
30
|
-
return { success: true, output: Number.NaN };
|
|
22
|
+
return custom(p.any(), p.number(), {
|
|
23
|
+
decode(input, ctx) {
|
|
24
|
+
if (typeof input === "number") return { success: true, output: input };
|
|
25
|
+
if (!input || input.toLowerCase() === "nan")
|
|
26
|
+
return { success: true, output: Number.NaN };
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
const output = parser(input);
|
|
29
|
+
if (!Number.isNaN(output)) return { success: true, output };
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
},
|
|
31
|
+
ctx.pushErrorFmt("coerce", input, { expected: "number" });
|
|
32
|
+
return { success: false, errors: ctx.errors };
|
|
38
33
|
},
|
|
39
|
-
) as ReturnType<typeof number>;
|
|
34
|
+
}) as ReturnType<typeof number>;
|
|
40
35
|
}
|
|
41
36
|
|
|
42
37
|
export function boolean(opts: {
|
|
43
|
-
true?:
|
|
44
|
-
false?:
|
|
38
|
+
true?: RegExp;
|
|
39
|
+
false?: RegExp;
|
|
45
40
|
}): Pipe<any, boolean> {
|
|
46
41
|
return custom(p.any(), p.boolean(), {
|
|
47
|
-
decode(input) {
|
|
42
|
+
decode(input, ctx) {
|
|
43
|
+
if (typeof input === "boolean") return { success: true, output: input };
|
|
48
44
|
if (typeof input === "string") {
|
|
49
|
-
if (opts.true?.
|
|
50
|
-
if (opts.false?.
|
|
51
|
-
return { success: true, output: false };
|
|
45
|
+
if (opts.true?.test(input)) return { success: true, output: true };
|
|
46
|
+
if (opts.false?.test(input)) return { success: true, output: false };
|
|
52
47
|
}
|
|
53
|
-
|
|
48
|
+
if (typeof input === "number")
|
|
49
|
+
return { success: true, output: Boolean(input) };
|
|
50
|
+
|
|
51
|
+
ctx.pushErrorFmt("coerce", input, { expected: "boolean" });
|
|
52
|
+
return { success: false, errors: ctx.errors };
|
|
54
53
|
},
|
|
55
54
|
});
|
|
56
55
|
}
|
package/src/containers.test.ts
CHANGED
package/src/containers.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
import type { Input, Output, Result } from "./pipe";
|
|
2
|
-
import {
|
|
3
|
-
import * as p from "./primitives";
|
|
1
|
+
import type { Input, Output, Result } from "./pipe.ts";
|
|
2
|
+
import { Context, HalfPipe, Pipe } from "./pipe.ts";
|
|
3
|
+
import * as p from "./primitives.ts";
|
|
4
4
|
|
|
5
5
|
class ValioArray<T> extends p.Arrayish<any[], T[]> {
|
|
6
|
-
|
|
6
|
+
element: Pipe<any, T>;
|
|
7
|
+
|
|
8
|
+
static typeCheck(v: any): v is any[] {
|
|
9
|
+
return Array.isArray(v);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
constructor(element: Pipe<any, T>) {
|
|
7
13
|
super(
|
|
8
14
|
new HalfPipe(
|
|
9
15
|
"array",
|
|
10
|
-
|
|
11
|
-
(input: any[], ctx: Context): Result<T[]>
|
|
16
|
+
ValioArray.typeCheck,
|
|
17
|
+
function parseAnyArr(input: any[], ctx: Context): Result<T[]> {
|
|
12
18
|
const output = new Array<T>(input.length);
|
|
13
19
|
let success = true;
|
|
14
20
|
|
|
@@ -25,12 +31,16 @@ class ValioArray<T> extends p.Arrayish<any[], T[]> {
|
|
|
25
31
|
return { success, output };
|
|
26
32
|
},
|
|
27
33
|
),
|
|
28
|
-
new HalfPipe(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
new HalfPipe(
|
|
35
|
+
`array<${element.o.name}>`,
|
|
36
|
+
function isArrT(v: any): v is T[] {
|
|
37
|
+
if (!ValioArray.typeCheck(v)) return false;
|
|
38
|
+
for (const e of v) if (!element.o.typeCheck(e)) return false;
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
),
|
|
33
42
|
);
|
|
43
|
+
this.element = element;
|
|
34
44
|
}
|
|
35
45
|
}
|
|
36
46
|
export function array<T>(element: Pipe<any, T>): ValioArray<T> {
|
|
@@ -41,16 +51,22 @@ class ValioRecord<K extends PropertyKey, V> extends Pipe<
|
|
|
41
51
|
Record<any, any>,
|
|
42
52
|
Record<K, V>
|
|
43
53
|
> {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
) {
|
|
54
|
+
keyPipe: Pipe<any, K>;
|
|
55
|
+
valPipe: Pipe<any, V>;
|
|
56
|
+
|
|
57
|
+
static typeCheck(v: any): v is Record<any, any> {
|
|
58
|
+
return Object.prototype.toString.call(v) === "[object Object]";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
constructor(keyPipe: Pipe<any, K>, valPipe: Pipe<any, V>) {
|
|
48
62
|
super(
|
|
49
63
|
new HalfPipe(
|
|
50
64
|
"object",
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
ValioRecord.typeCheck,
|
|
66
|
+
function anyToRecordKV(
|
|
67
|
+
input: Record<any, any>,
|
|
68
|
+
ctx: Context,
|
|
69
|
+
): Result<Record<K, V>> {
|
|
54
70
|
const output = {} as Record<K, V>;
|
|
55
71
|
|
|
56
72
|
let success = true;
|
|
@@ -77,9 +93,8 @@ class ValioRecord<K extends PropertyKey, V> extends Pipe<
|
|
|
77
93
|
),
|
|
78
94
|
new HalfPipe(
|
|
79
95
|
`record<${keyPipe.o.name},${valPipe.o.name}>`,
|
|
80
|
-
(v): v is Record<K, V>
|
|
81
|
-
if (
|
|
82
|
-
return false;
|
|
96
|
+
function recordCheckV(v): v is Record<K, V> {
|
|
97
|
+
if (!ValioRecord.typeCheck(v)) return false;
|
|
83
98
|
for (const k in v) {
|
|
84
99
|
// Keys will always be strings.
|
|
85
100
|
// if (!keyPipe.o.typeCheck(k)) return false;
|
|
@@ -89,6 +104,8 @@ class ValioRecord<K extends PropertyKey, V> extends Pipe<
|
|
|
89
104
|
},
|
|
90
105
|
),
|
|
91
106
|
);
|
|
107
|
+
this.keyPipe = keyPipe;
|
|
108
|
+
this.valPipe = valPipe;
|
|
92
109
|
}
|
|
93
110
|
}
|
|
94
111
|
export function record<K extends PropertyKey, V>(
|
|
@@ -102,32 +119,39 @@ class Union<T extends Readonly<Pipe[]>> extends Pipe<
|
|
|
102
119
|
Output<T[number]>,
|
|
103
120
|
Output<T[number]>
|
|
104
121
|
> {
|
|
105
|
-
|
|
122
|
+
options: T;
|
|
123
|
+
|
|
124
|
+
constructor(options: T) {
|
|
106
125
|
const name = options.map((o) => o.o.name).join("|");
|
|
107
126
|
type O = Output<T[number]>;
|
|
108
127
|
super(
|
|
109
128
|
new HalfPipe(
|
|
110
129
|
name,
|
|
111
|
-
(v: any): v is O
|
|
130
|
+
function isUnionType(v: any): v is O {
|
|
112
131
|
for (const f of options) if (f.i.typeCheck(v)) return true;
|
|
113
132
|
return false;
|
|
114
133
|
},
|
|
115
134
|
(data: O, ctx: Context): Result<O> => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
135
|
+
// Throw away errors since we expect them.
|
|
136
|
+
const newCtx = new Context();
|
|
137
|
+
newCtx.pushErrorFmt = () => {};
|
|
138
|
+
newCtx.pushError = () => {};
|
|
139
|
+
for (const f of options) {
|
|
140
|
+
const decoded = f.decode(data, newCtx);
|
|
119
141
|
if (decoded.success) return decoded;
|
|
120
142
|
}
|
|
121
143
|
|
|
122
|
-
|
|
144
|
+
// Sad path -- do again with real ctx to gather errors.
|
|
145
|
+
for (const f of options) f.decode(data, ctx);
|
|
123
146
|
return { success: false, errors: ctx.errors };
|
|
124
147
|
},
|
|
125
148
|
),
|
|
126
|
-
new HalfPipe(name, (v: any): v is O
|
|
149
|
+
new HalfPipe(name, function isUnionType2(v: any): v is O {
|
|
127
150
|
for (const f of options) if (f.o.typeCheck(v)) return true;
|
|
128
151
|
return false;
|
|
129
152
|
}),
|
|
130
153
|
);
|
|
154
|
+
this.options = options;
|
|
131
155
|
}
|
|
132
156
|
}
|
|
133
157
|
export function union<T extends Readonly<Pipe[]>>(options: T): Union<T> {
|
|
@@ -151,19 +175,21 @@ type Extend<A extends Record<any, any>, B extends Record<any, any>> = Flatten<
|
|
|
151
175
|
}
|
|
152
176
|
>;
|
|
153
177
|
|
|
154
|
-
class ValioObject<
|
|
155
|
-
Record<any, any
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
178
|
+
export class ValioObject<
|
|
179
|
+
Shape extends Record<any, Pipe<any, any>>,
|
|
180
|
+
> extends Pipe<Record<any, any>, ObjectOutput<Shape>> {
|
|
181
|
+
shape: Shape;
|
|
182
|
+
isLoose: boolean;
|
|
183
|
+
|
|
184
|
+
static typeCheck(v: any): v is Record<any, any> {
|
|
185
|
+
return typeof v === "object";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
constructor(shape: Shape, isLoose: boolean) {
|
|
162
189
|
super(
|
|
163
190
|
new HalfPipe(
|
|
164
191
|
"object",
|
|
165
|
-
|
|
166
|
-
Object.prototype.toString.call(v) == "[object Object]",
|
|
192
|
+
ValioObject.typeCheck,
|
|
167
193
|
(data, ctx) => this.transformInput(data, ctx),
|
|
168
194
|
),
|
|
169
195
|
new HalfPipe(
|
|
@@ -173,6 +199,9 @@ class ValioObject<Shape extends Record<any, Pipe<any, any>>> extends Pipe<
|
|
|
173
199
|
(v) => this.typeCheckOutput(v),
|
|
174
200
|
),
|
|
175
201
|
);
|
|
202
|
+
|
|
203
|
+
this.shape = shape;
|
|
204
|
+
this.isLoose = isLoose;
|
|
176
205
|
}
|
|
177
206
|
|
|
178
207
|
clone(): this {
|
|
@@ -204,7 +233,7 @@ class ValioObject<Shape extends Record<any, Pipe<any, any>>> extends Pipe<
|
|
|
204
233
|
}
|
|
205
234
|
|
|
206
235
|
protected typeCheckOutput(v: any): v is ObjectOutput<Shape> {
|
|
207
|
-
if (
|
|
236
|
+
if (!ValioObject.typeCheck(v)) return false;
|
|
208
237
|
for (const s in this.shape)
|
|
209
238
|
if (!this.shape[s]!.o.typeCheck(v[s])) return false;
|
|
210
239
|
return true;
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export * as codecs from "./codecs";
|
|
2
|
-
export * from "./containers";
|
|
3
|
-
export * from "./pipe";
|
|
4
|
-
export * from "./primitives";
|
|
1
|
+
export * as codecs from "./codecs.ts";
|
|
2
|
+
export * from "./containers.ts";
|
|
3
|
+
export * from "./pipe.ts";
|
|
4
|
+
export * from "./primitives.ts";
|
package/src/pipe.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import enFormat from "./locales/en";
|
|
1
|
+
import enFormat from "./locales/en.ts";
|
|
2
2
|
|
|
3
3
|
export type Error = { input: any; message: string };
|
|
4
4
|
export type Errors = { [inputPath: string]: Error[] };
|
|
@@ -20,21 +20,31 @@ interface Check<T> {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export class HalfPipe<I, O = never> {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
public typeCheck: (v: any) => v is I,
|
|
28
|
-
/** Optional transform for pipe to run at end. Useful for containers */
|
|
29
|
-
public transform?: (v: I, ctx: Context) => Result<O>,
|
|
30
|
-
) {}
|
|
31
|
-
/** The second checks to run */
|
|
23
|
+
name: string;
|
|
24
|
+
typeCheck: (v: any) => v is I;
|
|
25
|
+
transform?: (v: I, ctx: Context) => Result<O>;
|
|
26
|
+
/** Checks to run after type check */
|
|
32
27
|
checks: Check<I>[] = [];
|
|
33
28
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
constructor(
|
|
30
|
+
name: string,
|
|
31
|
+
typeCheck: (v: any) => v is I,
|
|
32
|
+
transform?: (v: I, ctx: Context) => Result<O>,
|
|
33
|
+
checks: Check<I>[] = [],
|
|
34
|
+
) {
|
|
35
|
+
this.name = name;
|
|
36
|
+
this.typeCheck = typeCheck;
|
|
37
|
+
this.transform = transform;
|
|
38
|
+
this.checks = checks;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
clone(): HalfPipe<I, O> {
|
|
42
|
+
return new HalfPipe(
|
|
43
|
+
this.name,
|
|
44
|
+
this.typeCheck,
|
|
45
|
+
this.transform,
|
|
46
|
+
this.checks.slice(),
|
|
47
|
+
);
|
|
38
48
|
}
|
|
39
49
|
}
|
|
40
50
|
|
|
@@ -48,9 +58,9 @@ export class Context {
|
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
clone(): Context {
|
|
51
|
-
const res =
|
|
52
|
-
res.jsonPath =
|
|
53
|
-
res.errors = { ...
|
|
61
|
+
const res = new Context();
|
|
62
|
+
res.jsonPath = this.jsonPath.slice();
|
|
63
|
+
res.errors = { ...this.errors };
|
|
54
64
|
return res;
|
|
55
65
|
}
|
|
56
66
|
|
|
@@ -83,13 +93,17 @@ export class Context {
|
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
export class Pipe<I = any, O = any> {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
) {
|
|
96
|
+
i: HalfPipe<I, O>;
|
|
97
|
+
o: HalfPipe<O, I>;
|
|
98
|
+
|
|
99
|
+
constructor(i: HalfPipe<I, O>, o: HalfPipe<O, I>) {
|
|
100
|
+
this.i = i;
|
|
101
|
+
this.o = o;
|
|
102
|
+
}
|
|
90
103
|
|
|
91
104
|
pipes: Pipe<any, any>[] = [];
|
|
92
105
|
registry: Record<PropertyKey, any> = {};
|
|
106
|
+
debug = false;
|
|
93
107
|
|
|
94
108
|
clone(): this {
|
|
95
109
|
const res = clone(this);
|
|
@@ -103,7 +117,7 @@ export class Pipe<I = any, O = any> {
|
|
|
103
117
|
refine(
|
|
104
118
|
valid: (data: O, ctx: Context) => boolean,
|
|
105
119
|
name: string,
|
|
106
|
-
props: Record<any, any
|
|
120
|
+
props: Record<any, any> = {},
|
|
107
121
|
): this {
|
|
108
122
|
const res = this.clone();
|
|
109
123
|
res.o.checks.push({ valid, name, props });
|
package/src/primitives.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import expect from "expect";
|
|
3
|
+
import * as v from "./index.ts";
|
|
3
4
|
|
|
4
5
|
test("number", () => {
|
|
5
6
|
const schema = v.number();
|
|
@@ -12,7 +13,7 @@ test("number", () => {
|
|
|
12
13
|
});
|
|
13
14
|
|
|
14
15
|
test("custom validator", () => {
|
|
15
|
-
const schema = v.number().refine((n) => n
|
|
16
|
+
const schema = v.number().refine((n) => n === 5, "eq", { n: 5 });
|
|
16
17
|
|
|
17
18
|
expect(schema.decode(5)).toEqual({ success: true, output: 5 });
|
|
18
19
|
expect(schema.encode(3)).toEqual({
|
|
@@ -22,7 +23,7 @@ test("custom validator", () => {
|
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
test("custom context", () => {
|
|
25
|
-
const schema = v.number().refine((n) => n
|
|
26
|
+
const schema = v.number().refine((n) => n === 5, "eq", { n: 5 });
|
|
26
27
|
class MyContext extends v.Context {
|
|
27
28
|
errorFmt() {
|
|
28
29
|
return "You done messed up";
|
package/src/primitives.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HalfPipe, Pipe } from "./pipe";
|
|
1
|
+
import { HalfPipe, Pipe } from "./pipe.ts";
|
|
2
2
|
|
|
3
3
|
function primitive<T>(name: string, typeCheck: (v: T) => v is T) {
|
|
4
4
|
const half = new HalfPipe(name, typeCheck);
|
|
@@ -8,7 +8,7 @@ function primitive<T>(name: string, typeCheck: (v: T) => v is T) {
|
|
|
8
8
|
export function boolean() {
|
|
9
9
|
return primitive<boolean>(
|
|
10
10
|
"boolean",
|
|
11
|
-
(v): v is boolean => typeof v
|
|
11
|
+
(v): v is boolean => typeof v === "boolean",
|
|
12
12
|
);
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -16,12 +16,12 @@ export function boolean() {
|
|
|
16
16
|
export function undefined() {
|
|
17
17
|
return primitive<undefined>(
|
|
18
18
|
"undefined",
|
|
19
|
-
(v): v is undefined => typeof v
|
|
19
|
+
(v): v is undefined => typeof v === "undefined",
|
|
20
20
|
);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export function any() {
|
|
24
|
-
return primitive<any>("any", (
|
|
24
|
+
return primitive<any>("any", (_v): _v is any => true);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function null_() {
|
|
@@ -43,13 +43,13 @@ export class Comparable<I, O> extends Pipe<I, O> {
|
|
|
43
43
|
return this.refine((v) => v <= n, "lte", { n });
|
|
44
44
|
}
|
|
45
45
|
eq(n: O) {
|
|
46
|
-
return this.refine((v) => v
|
|
46
|
+
return this.refine((v) => v === n, "eq", { n });
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
class ValioNumber extends Comparable<number, number> {
|
|
51
51
|
constructor() {
|
|
52
|
-
const half = new HalfPipe("number", (v) => typeof v
|
|
52
|
+
const half = new HalfPipe("number", (v) => typeof v === "number");
|
|
53
53
|
super(half, half);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -71,9 +71,9 @@ export class Arrayish<
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
class ValioString extends
|
|
74
|
+
class ValioString extends Arrayish<string, string> {
|
|
75
75
|
constructor() {
|
|
76
|
-
const half = new HalfPipe("string", (v) => typeof v
|
|
76
|
+
const half = new HalfPipe("string", (v) => typeof v === "string");
|
|
77
77
|
super(half, half);
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -88,9 +88,12 @@ export function string(): ValioString {
|
|
|
88
88
|
export type Lit = string | number | bigint | boolean | null | undefined;
|
|
89
89
|
|
|
90
90
|
class ValioLiteral<T extends Lit> extends Pipe<T, T> {
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
literal: T;
|
|
92
|
+
|
|
93
|
+
constructor(literal: T) {
|
|
94
|
+
const half = new HalfPipe(`${literal}`, (v): v is T => v === literal);
|
|
93
95
|
super(half, half);
|
|
96
|
+
this.literal = literal;
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
99
|
export function literal<T extends Lit>(literal: T) {
|
|
@@ -98,14 +101,17 @@ export function literal<T extends Lit>(literal: T) {
|
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
class ValioEnum<T extends Lit> extends Pipe<T, T> {
|
|
101
|
-
|
|
104
|
+
literals: readonly T[];
|
|
105
|
+
|
|
106
|
+
constructor(literals: readonly T[]) {
|
|
102
107
|
const half = new HalfPipe(`${literals.join(",")}`, (v: any): v is T =>
|
|
103
108
|
literals.includes(v),
|
|
104
109
|
);
|
|
105
110
|
super(half, half);
|
|
111
|
+
this.literals = literals;
|
|
106
112
|
}
|
|
107
113
|
}
|
|
108
|
-
function enum_<T extends Lit>(literals: T[]): ValioEnum<T> {
|
|
114
|
+
function enum_<T extends Lit>(literals: readonly T[]): ValioEnum<T> {
|
|
109
115
|
return new ValioEnum(literals);
|
|
110
116
|
}
|
|
111
117
|
export { enum_ as enum };
|