-
Notifications
You must be signed in to change notification settings - Fork 64
/
Copy pathbasic.ts
235 lines (216 loc) · 6.24 KB
/
basic.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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
import * as t from 'io-ts';
import type { Either } from 'fp-ts/lib/Either';
import { chain, fold, isRight, left, right } from 'fp-ts/lib/Either';
import { pipe, flow } from 'fp-ts/lib/function';
/**
* A short string is a non null string between
* 0 and 255 characters
*/
export const ShortString = new t.Type<string, string>(
'ShortString',
(i: unknown): i is string => t.string.is(i) && i.length <= 255,
(i, c) =>
pipe(
t.string.validate(i, c),
chain((s) =>
s.length <= 255
? t.success(s)
: t.failure(s, c, 'must be at most 255 chars long'),
),
),
t.identity,
);
// Note: assigning this type to a string will not throw compilation errorrs.
//
// e.g. the following will compile without issues.
// ```
// const x: ShortString = 'a'.repeat(300);
// ```
export type ShortString = t.TypeOf<typeof ShortString>;
/**
* A string identifier is a string that encodes a
* positive integer (an id to be used as a database id)
*
* e.g.
* Invalid decimal strings: 'aaa', '0xaaa'
* Valid decimal strings: '0', '123'
*/
export const StringIdentifier = new t.Type<string, string>(
'StringIdentifier',
(i: unknown): i is string =>
t.string.is(i) && !isNaN(+i) && +i === parseInt(i, 10) && +i >= 0,
(i, c) =>
pipe(
t.string.validate(i, c),
chain((s) =>
!isNaN(+s) && +s === parseInt(s, 10) && +s >= 0
? t.success(s)
: t.failure(s, c, 'must be be an positive integer'),
),
),
String,
);
export type StringIdentifier = t.TypeOf<typeof StringIdentifier>;
export const StringOrNumber = t.union([t.number, t.string]);
export type StringOrNumber = t.TypeOf<typeof StringOrNumber>;
/**
* A numeric identifier is any valid identifier encoded as a string or number
*/
export const NumericIdentifier = new t.Type<number, StringOrNumber>(
'NumericIdentifier',
(i): i is number =>
StringOrNumber.is(i) &&
!isNaN(+i) &&
+i === parseInt(String(i), 10) &&
+i >= 0,
(i, c) =>
pipe(
StringOrNumber.validate(i, c),
chain((n) =>
!isNaN(+n) && +n === parseInt(String(n), 10) && +n >= 0
? t.success(+n)
: t.failure(n, c, 'must be be an positive integer'),
),
),
Number,
);
export type NumericIdentifier = t.TypeOf<typeof NumericIdentifier>;
/**
* Valid variable names are between 0 and 255 characters
* and match /^[a-zA-Z_][a-zA-Z0-9_]*$/
*/
const VAR_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
/**
* Config vars also allow a colon in the name
*/
const CONFIG_VAR_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_:]*$/;
export const shortStringWithRegex = (
name: string,
regex: RegExp,
message: string,
) =>
new t.Type<string, string>(
name,
(s: unknown): s is string => ShortString.is(s) && regex.test(s),
(i, c) =>
pipe(
ShortString.validate(i, c),
chain((s) => (regex.test(s) ? t.success(s) : t.failure(s, c, message))),
),
t.identity,
);
/**
* Valid label names are between 0 and 255 characters
* and each character is any printable ASCII character except space (0x20),
* double and single quotes ( " 0x22, ' 0x27) and backtick ( ` 0x60). Rationale
* is to accept a character unless likely not useful and error prone.
*/
const LABEL_NAME_REGEX = /^[!#-&(-_a-~]+$/;
export const LabelName = shortStringWithRegex(
'LabelName',
LABEL_NAME_REGEX,
'may contain printable ASCII characters except space, single/double quotes and backtick',
);
export type LabelName = t.TypeOf<typeof LabelName>;
/**
* An env var object is a dictionary with valid variables as keys
*/
export const ConfigVarObject = t.record(
shortStringWithRegex(
'ConfigVarName',
CONFIG_VAR_NAME_REGEX,
"needs to start with a letter and may only contain alphanumeric characters plus '_' or ':'",
),
t.string,
);
export type ConfigVarObject = t.TypeOf<typeof ConfigVarObject>;
/**
* An env var object is a dictionary with valid variables as keys
*/
export const EnvVarObject = t.record(
shortStringWithRegex(
'EnvVarName',
VAR_NAME_REGEX,
"needs to start with a letter and may only contain alphanumeric characters plus '_'",
),
t.string,
);
export type EnvVarObject = t.TypeOf<typeof EnvVarObject>;
/**
* An env var object is a dictionary with valid labels as keys
*/
export const LabelObject = t.record(LabelName, t.string);
export type LabelObject = t.TypeOf<typeof LabelObject>;
// Valid docker container and volume name according to
// https://github.com/moby/moby/blob/04c6f09fbdf60c7765cc4cb78883faaa9d971fa5/daemon/daemon.go#L56
// [a-zA-Z0-9][a-zA-Z0-9_.-]
const DOCKER_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
export const DockerName = shortStringWithRegex(
'LabelName',
DOCKER_NAME_REGEX,
'only "[a-zA-Z0-9][a-zA-Z0-9_.-]" are allowed',
);
export type DockerName = t.TypeOf<typeof DockerName>;
/**
* Device name can have any characters except '\n'
*/
export const DeviceName = new t.Type<string, string>(
'DeviceName',
(i: unknown): i is string => ShortString.is(i) && i.indexOf('\n') === -1,
(i, c) =>
pipe(
ShortString.validate(i, c),
chain((s) =>
s.indexOf('\n') === -1
? t.success(s)
: t.failure(s, c, 'must not contain newline chars'),
),
),
t.identity,
);
export type DeviceName = t.TypeOf<typeof DeviceName>;
/**
* Creates a record type that checks for constraints on the record elements
*/
const restrictedRecord = <
K extends t.Mixed,
V extends t.Mixed,
R extends { [key in t.TypeOf<K>]: t.TypeOf<V> },
>(
k: K,
v: V,
test: (i: R) => Either<string, R>,
name = 'RestrictedRecord',
) => {
return new t.Type<R>(
name,
(i): i is R => t.record(k, v).is(i) && isRight(test(i as R)),
(i, c) =>
pipe(
// pipe takes the first result and passes it through rest of the function arguments
t.record(k, v).validate(i, c), // validate that the element is a proper record first (returns an Either)
chain(
// chain takes a function (a) => Either and converts it into a function (Either) => (Either)
flow(
// flow creates a function composition
test, // receives a record and returns Either<string,R>
fold((m) => t.failure(i, c, m), t.success), // fold converts Either<string,R> to an Either<Errors, R>
),
),
),
t.identity,
);
};
export const nonEmptyRecord = <K extends t.Mixed, V extends t.Mixed>(
k: K,
v: V,
) =>
restrictedRecord(
k,
v,
(o) =>
Object.keys(o).length > 0
? right(o)
: left('must have at least 1 element'),
'NonEmptyRecord',
);