@nerd-bible/valio 0.1.13 → 0.1.15

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/src/containers.ts CHANGED
@@ -1,44 +1,52 @@
1
1
  import type { Input, Output, Result } from "./pipe.ts";
2
- import { Context, HalfPipe, Pipe } from "./pipe.ts";
2
+ import { Context, Pipe } from "./pipe.ts";
3
3
  import * as p from "./primitives.ts";
4
4
 
5
5
  export class ValioArray<T> extends p.Arrayish<any[], T[]> {
6
6
  element: Pipe<any, T>;
7
7
 
8
+ constructor(element: Pipe<any, T>) {
9
+ super();
10
+ this.element = element;
11
+ }
12
+
13
+ get inputName() {
14
+ return "array";
15
+ }
16
+
8
17
  static typeCheck(v: any): v is any[] {
9
18
  return Array.isArray(v);
10
19
  }
11
20
 
12
- constructor(element: Pipe<any, T>) {
13
- super(
14
- new HalfPipe("array", ValioArray.typeCheck, function parseAnyArr(
15
- input: any[],
16
- ctx: Context,
17
- ): Result<T[]> {
18
- const output = new Array<T>(input.length);
19
- let success = true;
20
-
21
- const length = ctx.jsonPath.length;
22
- for (let i = 0; i < input.length; i++) {
23
- ctx.jsonPath[length] = i.toString();
24
- const decoded = element.decode(input[i], ctx);
25
- if (decoded.success) output[i] = decoded.output;
26
- else success = false;
27
- }
28
- ctx.jsonPath.length = length;
29
-
30
- if (!success) return { success, errors: ctx.errors };
31
- return { success, output };
32
- }),
33
- new HalfPipe(`array<${element.o.name}>`, function isArrT(
34
- v: any,
35
- ): v is T[] {
36
- if (!ValioArray.typeCheck(v)) return false;
37
- for (const e of v) if (!element.o.typeCheck(e)) return false;
38
- return true;
39
- }),
40
- );
41
- this.element = element;
21
+ inputTypeCheck(v: any): v is any[] {
22
+ return ValioArray.typeCheck(v);
23
+ }
24
+
25
+ inputTransform(input: any[], ctx: Context): Result<T[]> {
26
+ const output = new Array<T>(input.length);
27
+ let success = true;
28
+
29
+ const length = ctx.jsonPath.length;
30
+ for (let i = 0; i < input.length; i++) {
31
+ ctx.jsonPath[length] = i.toString();
32
+ const decoded = this.element.decode(input[i], ctx);
33
+ if (decoded.success) output[i] = decoded.output;
34
+ else success = false;
35
+ }
36
+ ctx.jsonPath.length = length;
37
+
38
+ if (!success) return { success, errors: ctx.errors };
39
+ return { success, output };
40
+ }
41
+
42
+ get outputName() {
43
+ return `array<${this.element.outputName}>`;
44
+ }
45
+
46
+ outputTypeCheck(v: any): v is T[] {
47
+ if (!ValioArray.typeCheck(v)) return false;
48
+ for (const e of v) if (!this.element.outputTypeCheck(e)) return false;
49
+ return true;
42
50
  }
43
51
  }
44
52
  export function array<T>(element: Pipe<any, T>): ValioArray<T> {
@@ -52,54 +60,61 @@ export class ValioRecord<K extends PropertyKey, V> extends Pipe<
52
60
  keyPipe: Pipe<any, K>;
53
61
  valPipe: Pipe<any, V>;
54
62
 
63
+ constructor(keyPipe: Pipe<any, K>, valPipe: Pipe<any, V>) {
64
+ super();
65
+ this.keyPipe = keyPipe;
66
+ this.valPipe = valPipe;
67
+ }
68
+
69
+ get inputName() {
70
+ return "object";
71
+ }
72
+
55
73
  static typeCheck(v: any): v is Record<any, any> {
56
- return Object.prototype.toString.call(v) === "[object Object]";
74
+ return v && typeof v === "object";
57
75
  }
58
76
 
59
- constructor(keyPipe: Pipe<any, K>, valPipe: Pipe<any, V>) {
60
- super(
61
- new HalfPipe("object", ValioRecord.typeCheck, function anyToRecordKV(
62
- input: Record<any, any>,
63
- ctx: Context,
64
- ): Result<Record<K, V>> {
65
- const output = {} as Record<K, V>;
66
-
67
- let success = true;
68
- const length = ctx.jsonPath.length;
69
- for (const key in input) {
70
- ctx.jsonPath[length] = key;
71
- const decodedKey = keyPipe.decode(key, ctx);
72
- if (decodedKey.success) {
73
- const decodedVal = valPipe.decode((input as any)[key], ctx);
74
- if (decodedVal.success) {
75
- output[decodedKey.output] = decodedVal.output;
76
- } else {
77
- success = false;
78
- }
79
- } else {
80
- success = false;
81
- }
77
+ inputTypeCheck(v: any): v is Record<any, any> {
78
+ return ValioRecord.typeCheck(v);
79
+ }
80
+
81
+ inputTransform(input: Record<any, any>, ctx: Context): Result<Record<K, V>> {
82
+ const output = {} as Record<K, V>;
83
+
84
+ let success = true;
85
+ const length = ctx.jsonPath.length;
86
+ for (const key in input) {
87
+ ctx.jsonPath[length] = key;
88
+ const decodedKey = this.keyPipe.decode(key, ctx);
89
+ if (decodedKey.success) {
90
+ const decodedVal = this.valPipe.decode((input as any)[key], ctx);
91
+ if (decodedVal.success) {
92
+ output[decodedKey.output] = decodedVal.output;
93
+ } else {
94
+ success = false;
82
95
  }
83
- ctx.jsonPath.length = length;
84
-
85
- if (!success) return { success, errors: ctx.errors };
86
- return { success, output };
87
- }),
88
- new HalfPipe(
89
- `record<${keyPipe.o.name},${valPipe.o.name}>`,
90
- function recordCheckV(v): v is Record<K, V> {
91
- if (!ValioRecord.typeCheck(v)) return false;
92
- for (const k in v) {
93
- // Keys will always be strings.
94
- // if (!keyPipe.o.typeCheck(k)) return false;
95
- if (!valPipe.o.typeCheck(v[k])) return false;
96
- }
97
- return true;
98
- },
99
- ),
100
- );
101
- this.keyPipe = keyPipe;
102
- this.valPipe = valPipe;
96
+ } else {
97
+ success = false;
98
+ }
99
+ }
100
+ ctx.jsonPath.length = length;
101
+
102
+ if (!success) return { success, errors: ctx.errors };
103
+ return { success, output };
104
+ }
105
+
106
+ get outputName() {
107
+ return `record<${this.keyPipe.inputName},${this.valPipe.outputName}>`;
108
+ }
109
+
110
+ outputTypeCheck(v: any): v is Record<K, V> {
111
+ if (!ValioRecord.typeCheck(v)) return false;
112
+ for (const k in v) {
113
+ // Keys will always be strings.
114
+ // if (!keyPipe.o.typeCheck(k)) return false;
115
+ if (!this.valPipe.outputTypeCheck(v[k])) return false;
116
+ }
117
+ return true;
103
118
  }
104
119
  }
105
120
  export function record<K extends PropertyKey, V>(
@@ -116,37 +131,45 @@ export class Union<T extends Readonly<Pipe[]>> extends Pipe<
116
131
  options: T;
117
132
 
118
133
  constructor(options: T) {
119
- const name = options.map((o) => o.o.name).join("|");
120
- type O = Output<T[number]>;
121
- super(
122
- new HalfPipe(
123
- name,
124
- function isUnionType(v: any): v is O {
125
- for (const f of options) if (f.i.typeCheck(v)) return true;
126
- return false;
127
- },
128
- (data: O, ctx: Context): Result<O> => {
129
- // Throw away errors since we expect them.
130
- const newCtx = new Context();
131
- newCtx.pushErrorFmt = () => {};
132
- newCtx.pushError = () => {};
133
- for (const f of options) {
134
- const decoded = f.decode(data, newCtx);
135
- if (decoded.success) return decoded;
136
- }
137
-
138
- // Sad path -- do again with real ctx to gather errors.
139
- for (const f of options) f.decode(data, ctx);
140
- return { success: false, errors: ctx.errors };
141
- },
142
- ),
143
- new HalfPipe(name, function isUnionType2(v: any): v is O {
144
- for (const f of options) if (f.o.typeCheck(v)) return true;
145
- return false;
146
- }),
147
- );
134
+ super();
148
135
  this.options = options;
149
136
  }
137
+
138
+ get inputName() {
139
+ return this.options.map((o) => o.outputName).join("|");
140
+ }
141
+
142
+ inputTypeCheck(v: any): v is Output<T[number]> {
143
+ for (const f of this.options) if (f.inputTypeCheck(v)) return true;
144
+ return false;
145
+ }
146
+
147
+ inputTransform(
148
+ data: Output<T[number]>,
149
+ ctx: Context,
150
+ ): Result<Output<T[number]>> {
151
+ // Throw away errors since we expect them.
152
+ const newCtx = new Context();
153
+ newCtx.pushErrorFmt = () => {};
154
+ newCtx.pushError = () => {};
155
+ for (const f of this.options) {
156
+ const decoded = f.decode(data, newCtx);
157
+ if (decoded.success) return decoded;
158
+ }
159
+
160
+ // Sad path -- do again with real ctx to gather errors.
161
+ for (const f of this.options) f.decode(data, ctx);
162
+ return { success: false, errors: ctx.errors };
163
+ }
164
+
165
+ get outputName() {
166
+ return this.inputName;
167
+ }
168
+
169
+ outputTypeCheck(v: any): v is Output<T[number]> {
170
+ for (const f of this.options) if (f.outputTypeCheck(v)) return true;
171
+ return false;
172
+ }
150
173
  }
151
174
  export function union<T extends Readonly<Pipe[]>>(options: T): Union<T> {
152
175
  return new Union(options);
@@ -188,30 +211,26 @@ export class ValioObject<
188
211
  }
189
212
 
190
213
  constructor(shape: Shape, isLoose: boolean) {
191
- super(
192
- new HalfPipe("object", ValioObject.typeCheck, (data, ctx) =>
193
- this.transformInput(data, ctx),
194
- ),
195
- new HalfPipe(
196
- `{${Object.entries(shape)
197
- .map(([k, v]) => `${k}: ${v.o.name}`)
198
- .join(",")}}`,
199
- (v) => this.typeCheckOutput(v),
200
- ),
201
- );
202
-
214
+ super();
203
215
  this.shape = shape;
204
216
  this.isLoose = isLoose;
205
217
  }
206
218
 
207
- clone(): this {
208
- return new ValioObject(this.shape, this.isLoose) as any;
219
+ get inputName() {
220
+ return "object";
209
221
  }
210
222
 
211
- protected transformInput(
212
- data: object,
213
- ctx: Context,
214
- ): Result<ObjectOutput<Shape>> {
223
+ get outputName() {
224
+ return `{${Object.entries(this.shape)
225
+ .map(([k, v]) => `${k}: ${v.outputName}`)
226
+ .join(",")}}`;
227
+ }
228
+
229
+ inputTypeCheck(v: any): v is Record<any, any> {
230
+ return ValioObject.typeCheck(v);
231
+ }
232
+
233
+ inputTransform(data: object, ctx: Context): Result<ObjectOutput<Shape>> {
215
234
  const output: Record<PropertyKey, any> = this.isLoose ? data : {};
216
235
  let success = true;
217
236
 
@@ -232,10 +251,10 @@ export class ValioObject<
232
251
  return { success, output: output as ObjectOutput<Shape> };
233
252
  }
234
253
 
235
- protected typeCheckOutput(v: any): v is ObjectOutput<Shape> {
254
+ outputTypeCheck(v: any): v is ObjectOutput<Shape> {
236
255
  if (!ValioObject.typeCheck(v)) return false;
237
256
  for (const s in this.shape)
238
- if (!this.shape[s]!.o.typeCheck(v[s])) return false;
257
+ if (!this.shape[s]!.outputTypeCheck(v[s])) return false;
239
258
  return true;
240
259
  }
241
260
 
package/src/pipe.ts CHANGED
@@ -5,49 +5,12 @@ export type Errors = { [inputPath: string]: Error[] };
5
5
  export type Result<T> =
6
6
  | { success: true; output: T }
7
7
  | { success: false; errors: any };
8
-
9
- function clone<T>(obj: T): T {
10
- return Object.create(
11
- Object.getPrototypeOf(obj),
12
- Object.getOwnPropertyDescriptors(obj),
13
- );
14
- }
15
-
16
- interface Check<T> {
8
+ export interface Check<T> {
17
9
  valid(data: T, ctx: Context): boolean;
18
10
  name: string;
19
11
  props: Record<any, any>;
20
12
  }
21
13
 
22
- export class HalfPipe<I, O = never> {
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 */
27
- checks: Check<I>[] = [];
28
-
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
- );
48
- }
49
- }
50
-
51
14
  /** During encoding, decoding, or validation. */
52
15
  export class Context {
53
16
  jsonPath: (string | number)[] = [];
@@ -75,13 +38,18 @@ export class Context {
75
38
  this.pushError({ input, message });
76
39
  }
77
40
 
78
- run<I, O>(input: any, halfPipe: HalfPipe<I, O>): Result<I> {
79
- if (!halfPipe.typeCheck(input)) {
80
- this.pushErrorFmt("type", input, { expected: halfPipe.name });
41
+ run<I>(
42
+ input: any,
43
+ name: () => string,
44
+ typeCheck: (v: any) => v is I,
45
+ checks?: Check<I>[],
46
+ ): Result<I> {
47
+ if (!typeCheck(input)) {
48
+ this.pushErrorFmt("type", input, { expected: name() });
81
49
  return { success: false, errors: this.errors };
82
50
  }
83
51
  let success = true;
84
- for (const c of halfPipe.checks ?? []) {
52
+ for (const c of checks ?? []) {
85
53
  if (!c.valid(input, this)) {
86
54
  this.pushErrorFmt(c.name, input, c.props);
87
55
  success = false;
@@ -92,35 +60,47 @@ export class Context {
92
60
  }
93
61
  }
94
62
 
95
- export class Pipe<I = any, O = any> {
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
- }
63
+ export function cloneObject(obj: any) {
64
+ const res =
65
+ Object.getPrototypeOf(obj) == Object.prototype ? {} : Object.create(obj);
66
+ for (const p in obj) {
67
+ const v = obj[p];
68
+ if (Array.isArray(v)) res[p] = v.slice();
69
+ else if (v && v.clone) res[p] = v.clone();
70
+ else if (v && typeof v === "object") res[p] = cloneObject(v);
71
+ else res[p] = v;
72
+ }
73
+ return res;
74
+ }
103
75
 
76
+ export abstract class Pipe<I = any, O = any> {
104
77
  pipes: Pipe<any, any>[] = [];
105
78
  registry: Record<PropertyKey, any> = {};
106
- debug = false;
79
+
80
+ abstract inputName: string;
81
+ abstract inputTypeCheck(v: any): v is I;
82
+ /** Checks to run after type check */
83
+ inputChecks?: Check<I>[];
84
+ inputTransform?(v: I, ctx: Context): Result<O>;
85
+
86
+ abstract outputName: string;
87
+ abstract outputTypeCheck(v: any): v is O;
88
+ /** Checks to run after type check */
89
+ outputChecks?: Check<O>[];
90
+ outputTransform?(v: O, ctx: Context): Result<I>;
107
91
 
108
92
  clone(): this {
109
- const res = clone(this);
110
- res.i = res.i.clone();
111
- res.o = res.o.clone();
112
- res.pipes = res.pipes.slice();
113
- res.registry = { ...res.registry };
114
- return res;
93
+ return cloneObject(this);
115
94
  }
116
95
 
117
96
  refine(
118
97
  valid: (data: O, ctx: Context) => boolean,
119
98
  name: string,
120
99
  props: Record<any, any> = {},
121
- ): this {
100
+ ) {
122
101
  const res = this.clone();
123
- res.o.checks.push({ valid, name, props });
102
+ res.outputChecks ??= [];
103
+ res.outputChecks.push({ valid, name, props });
124
104
  return res;
125
105
  }
126
106
 
@@ -132,15 +112,25 @@ export class Pipe<I = any, O = any> {
132
112
 
133
113
  decodeAny(input: any, ctx = new Context()): Result<O> {
134
114
  // 1. Verify input
135
- let res: Result<any> = ctx.run(input, this.i);
115
+ let res: Result<any> = ctx.run(
116
+ input,
117
+ () => this.inputName,
118
+ this.inputTypeCheck.bind(this),
119
+ this.inputChecks,
120
+ );
136
121
  if (!res.success) return res;
137
122
  // 2. Transform input to output
138
- if (this.i.transform) {
139
- res = this.i.transform(res.output, ctx);
123
+ if (this.inputTransform) {
124
+ res = this.inputTransform(res.output, ctx);
140
125
  if (!res.success) return res;
141
126
  }
142
127
  // 3. Verify output
143
- res = ctx.run(res.output, this.o);
128
+ res = ctx.run(
129
+ res.output,
130
+ () => this.outputName,
131
+ this.outputTypeCheck.bind(this),
132
+ this.outputChecks,
133
+ );
144
134
  if (!res.success) return res;
145
135
  // 4. Next
146
136
  for (const p of this.pipes) {
@@ -161,21 +151,31 @@ export class Pipe<I = any, O = any> {
161
151
  if (!res.success) return res;
162
152
  }
163
153
  // 2. Verify output
164
- res = ctx.run(res.output, this.o);
154
+ res = ctx.run(
155
+ res.output,
156
+ () => this.outputName,
157
+ this.outputTypeCheck.bind(this),
158
+ this.outputChecks,
159
+ );
165
160
  if (!res.success) return res;
166
161
  // 3. Transform output to input
167
- if (this.o.transform) {
168
- res = this.o.transform(res.output, ctx);
162
+ if (this.outputTransform) {
163
+ res = this.outputTransform(res.output, ctx);
169
164
  if (!res.success) return res;
170
165
  }
171
166
  // 4. Verify input
172
- return ctx.run(res.output, this.i);
167
+ return ctx.run(
168
+ res.output,
169
+ () => this.inputName,
170
+ this.inputTypeCheck.bind(this),
171
+ this.inputChecks,
172
+ );
173
173
  }
174
174
  encode(output: O, ctx = new Context()): Result<I> {
175
175
  return this.encodeAny(output, ctx);
176
176
  }
177
177
 
178
- register(key: PropertyKey, value: any): this {
178
+ register(key: PropertyKey, value: any): Pipe<I, O> {
179
179
  const res = this.clone();
180
180
  res.registry[key] = value;
181
181
  return res;