-
Notifications
You must be signed in to change notification settings - Fork 221
/
struct.ts
212 lines (181 loc) · 4.98 KB
/
struct.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
import { toFailures } from './utils'
/**
* `Struct` objects encapsulate the schema for a specific data type (with
* optional coercion). You can then use the `assert`, `is` or `validate` helpers
* to validate unknown data against a struct.
*/
export class Struct<T, S = any> {
type: string
schema: S
coercer: (value: unknown) => unknown
validator: (value: unknown, context: StructContext) => StructResult
refiner: (value: T, context: StructContext) => StructResult
constructor(props: {
type: Struct<T>['type']
schema: S
coercer?: Struct<T>['coercer']
validator?: Struct<T>['validator']
refiner?: Struct<T>['refiner']
}) {
const {
type,
schema,
coercer = (value: unknown) => value,
validator = () => [],
refiner = () => [],
} = props
this.type = type
this.schema = schema
this.coercer = coercer
this.validator = validator
this.refiner = refiner
}
}
/**
* `StructError` objects are thrown (or returned) by Superstruct when its
* validation fails. The error represents the first error encountered during
* validation. But they also have an `error.failures` property that holds
* information for all of the failures encountered.
*/
export class StructError extends TypeError {
value: any
type: string
path: Array<number | string>
branch: Array<any>
failures: () => Iterable<StructFailure>;
[key: string]: any
constructor(failure: StructFailure, iterable: Iterable<StructFailure>) {
const { path, value, type, branch, ...rest } = failure
const message = `Expected a value of type \`${type}\`${
path.length ? ` for \`${path.join('.')}\`` : ''
} but received \`${JSON.stringify(value)}\`.`
function* failures(): Iterable<StructFailure> {
yield failure
yield* iterable
}
super(message)
this.value = value
Object.assign(this, rest)
this.type = type
this.path = path
this.branch = branch
this.failures = failures
this.stack = new Error().stack
;(this as any).__proto__ = StructError.prototype
}
}
/**
* A `StructContext` contains information about the current value being
* validated as well as helper functions for failures and recursive validating.
*/
export type StructContext = {
value: any
type: string
branch: Array<any>
path: Array<string | number>
fail: (props?: Partial<StructFailure>) => StructFailure
check: (
value: any,
struct: Struct<any> | Struct<never>,
parent?: any,
key?: string | number
) => Iterable<StructFailure>
}
/**
* A `StructFailure` represents a single specific failure in validation.
*/
export type StructFailure = {
value: StructContext['value']
type: StructContext['type']
branch: StructContext['branch']
path: StructContext['path']
[key: string]: any
}
/**
* A `StructResult` is returned from validation functions.
*/
export type StructResult = boolean | Iterable<StructFailure>
/**
* A type utility to extract the type from a `Struct` class.
*/
export type StructType<T extends Struct<any>> = Parameters<T['refiner']>[0]
/**
* Assert that a value passes a `Struct`, throwing if it doesn't.
*/
export function assert<T>(
value: unknown,
struct: Struct<T>
): asserts value is T {
const result = validate(value, struct)
if (result[0]) {
throw result[0]
}
}
/**
* Coerce a value with the coercion logic of `Struct` and validate it.
*/
export function coerce<T>(value: unknown, struct: Struct<T>): T {
const ret = struct.coercer(value)
assert(ret, struct)
return ret
}
/**
* Check if a value passes a `Struct`.
*/
export function is<T>(value: unknown, struct: Struct<T>): value is T {
const result = validate(value, struct)
return !result[0]
}
/**
* Validate a value against a `Struct`, returning an error if invalid.
*/
export function validate<T>(
value: unknown,
struct: Struct<T>,
coercing: boolean = false
): [StructError, undefined] | [undefined, T] {
if (coercing) {
value = struct.coercer(value)
}
const iterable = check(value, struct)
const [failure] = iterable
if (failure) {
const error = new StructError(failure, iterable)
return [error, undefined]
} else {
return [undefined, value as T]
}
}
/**
* Check a value against a `Struct`, returning an iterable of failures.
*/
function* check<T>(
value: unknown,
struct: Struct<T>,
path: any[] = [],
branch: any[] = []
): Iterable<StructFailure> {
const { type } = struct
const ctx: StructContext = {
value,
type,
branch,
path,
fail(props = {}) {
return { value, type, path, branch: [...branch, value], ...props }
},
check(v, s, parent, key) {
const p = parent !== undefined ? [...path, key] : path
const b = parent !== undefined ? [...branch, parent] : branch
return check(v, s, p, b)
},
}
const failures = toFailures(struct.validator(value, ctx), ctx)
const [failure] = failures
if (failure) {
yield failure
yield* failures
} else {
yield* toFailures(struct.refiner(value as T, ctx), ctx)
}
}