/
refinements.ts
163 lines (146 loc) · 4.29 KB
/
refinements.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
import { Struct, Refiner } from '../struct'
import { toFailures } from '../utils'
/**
* Ensure that a string, array, map, or set is empty.
*/
export function empty<
T extends string | any[] | Map<any, any> | Set<any>,
S extends any
>(struct: Struct<T, S>): Struct<T, S> {
return refine(struct, 'empty', (value) => {
const size = getSize(value)
return (
size === 0 ||
`Expected an empty ${struct.type} but received one with a size of \`${size}\``
)
})
}
function getSize(value: string | any[] | Map<any, any> | Set<any>): number {
if (value instanceof Map || value instanceof Set) {
return value.size
} else {
return value.length
}
}
/**
* Ensure that a number or date is below a threshold.
*/
export function max<T extends number | Date, S extends any>(
struct: Struct<T, S>,
threshold: T,
options: {
exclusive?: boolean
} = {}
): Struct<T, S> {
const { exclusive } = options
return refine(struct, 'max', (value) => {
return exclusive
? value < threshold
: value <= threshold ||
`Expected a ${struct.type} less than ${
exclusive ? '' : 'or equal to '
}${threshold} but received \`${value}\``
})
}
/**
* Ensure that a number or date is above a threshold.
*/
export function min<T extends number | Date, S extends any>(
struct: Struct<T, S>,
threshold: T,
options: {
exclusive?: boolean
} = {}
): Struct<T, S> {
const { exclusive } = options
return refine(struct, 'min', (value) => {
return exclusive
? value > threshold
: value >= threshold ||
`Expected a ${struct.type} greater than ${
exclusive ? '' : 'or equal to '
}${threshold} but received \`${value}\``
})
}
/**
* Ensure that a string, array, map or set is not empty.
*/
export function nonempty<
T extends string | any[] | Map<any, any> | Set<any>,
S extends any
>(struct: Struct<T, S>): Struct<T, S> {
return refine(struct, 'nonempty', (value) => {
const size = getSize(value)
return (
size > 0 || `Expected a nonempty ${struct.type} but received an empty one`
)
})
}
/**
* Ensure that a string matches a regular expression.
*/
export function pattern<T extends string, S extends any>(
struct: Struct<T, S>,
regexp: RegExp
): Struct<T, S> {
return refine(struct, 'pattern', (value) => {
return (
regexp.test(value) ||
`Expected a ${struct.type} matching \`/${regexp.source}/\` but received "${value}"`
)
})
}
/**
* Ensure that a string, array, number, date, map, or set has a size (or length, or time) between `min` and `max`.
*/
export function size<
T extends string | number | Date | any[] | Map<any, any> | Set<any>,
S extends any
>(struct: Struct<T, S>, min: number, max: number = min): Struct<T, S> {
const expected = `Expected a ${struct.type}`
const of = min === max ? `of \`${min}\`` : `between \`${min}\` and \`${max}\``
return refine(struct, 'size', (value) => {
if (typeof value === 'number' || value instanceof Date) {
return (
(min <= value && value <= max) ||
`${expected} ${of} but received \`${value}\``
)
} else if (value instanceof Map || value instanceof Set) {
const { size } = value
return (
(min <= size && size <= max) ||
`${expected} with a size ${of} but received one with a size of \`${size}\``
)
} else {
const { length } = value as string | any[]
return (
(min <= length && length <= max) ||
`${expected} with a length ${of} but received one with a length of \`${length}\``
)
}
})
}
/**
* Augment a `Struct` to add an additional refinement to the validation.
*
* The refiner function is guaranteed to receive a value of the struct's type,
* because the struct's existing validation will already have passed. This
* allows you to layer additional validation on top of existing structs.
*/
export function refine<T, S>(
struct: Struct<T, S>,
name: string,
refiner: Refiner<T>
): Struct<T, S> {
return new Struct({
...struct,
*refiner(value, ctx) {
yield* struct.refiner(value, ctx)
const result = refiner(value, ctx)
const failures = toFailures(result, ctx, struct, value)
for (const failure of failures) {
yield { ...failure, refinement: name }
}
},
})
}