@craftguild/jscalendar 0.1.0
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/LICENSE.md +7 -0
- package/README.md +295 -0
- package/dist/__tests__/calendar-extra.test.d.ts +1 -0
- package/dist/__tests__/calendar-extra.test.js +185 -0
- package/dist/__tests__/calendar.test.d.ts +1 -0
- package/dist/__tests__/calendar.test.js +104 -0
- package/dist/__tests__/ical-extra.test.d.ts +1 -0
- package/dist/__tests__/ical-extra.test.js +87 -0
- package/dist/__tests__/ical.test.d.ts +1 -0
- package/dist/__tests__/ical.test.js +72 -0
- package/dist/__tests__/index.test.d.ts +1 -0
- package/dist/__tests__/index.test.js +9 -0
- package/dist/__tests__/patch.test.d.ts +1 -0
- package/dist/__tests__/patch.test.js +47 -0
- package/dist/__tests__/recurrence.test.d.ts +1 -0
- package/dist/__tests__/recurrence.test.js +498 -0
- package/dist/__tests__/search.test.d.ts +1 -0
- package/dist/__tests__/search.test.js +237 -0
- package/dist/__tests__/timezones.test.d.ts +1 -0
- package/dist/__tests__/timezones.test.js +12 -0
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +116 -0
- package/dist/__tests__/validation.test.d.ts +1 -0
- package/dist/__tests__/validation.test.js +91 -0
- package/dist/ical.d.ts +7 -0
- package/dist/ical.js +202 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/jscal.d.ts +129 -0
- package/dist/jscal.js +504 -0
- package/dist/patch.d.ts +5 -0
- package/dist/patch.js +91 -0
- package/dist/recurrence.d.ts +15 -0
- package/dist/recurrence.js +674 -0
- package/dist/search.d.ts +14 -0
- package/dist/search.js +208 -0
- package/dist/timezones.d.ts +4 -0
- package/dist/timezones.js +441 -0
- package/dist/types.d.ts +219 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +80 -0
- package/dist/validate.d.ts +6 -0
- package/dist/validate.js +745 -0
- package/package.json +33 -0
package/dist/validate.js
ADDED
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
import { TimeZones } from "./timezones.js";
|
|
2
|
+
export class ValidationError extends Error {
|
|
3
|
+
path;
|
|
4
|
+
constructor(path, message) {
|
|
5
|
+
super(`${path}: ${message}`);
|
|
6
|
+
this.name = "ValidationError";
|
|
7
|
+
this.path = path;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z)?$/;
|
|
11
|
+
const DURATION = /^-?P(?:(\d+)W(?:(\d+)D)?|(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)(?:\.(\d+))?S)?)?$/;
|
|
12
|
+
const DAY_OF_WEEK = new Set(["mo", "tu", "we", "th", "fr", "sa", "su"]);
|
|
13
|
+
const RECURRENCE_FREQUENCY = new Set([
|
|
14
|
+
"yearly",
|
|
15
|
+
"monthly",
|
|
16
|
+
"weekly",
|
|
17
|
+
"daily",
|
|
18
|
+
"hourly",
|
|
19
|
+
"minutely",
|
|
20
|
+
"secondly",
|
|
21
|
+
]);
|
|
22
|
+
const SKIP = new Set(["omit", "backward", "forward"]);
|
|
23
|
+
const ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
24
|
+
function utf8Length(value) {
|
|
25
|
+
if (typeof TextEncoder !== "undefined") {
|
|
26
|
+
return new TextEncoder().encode(value).length;
|
|
27
|
+
}
|
|
28
|
+
return value.length;
|
|
29
|
+
}
|
|
30
|
+
function fail(path, message) {
|
|
31
|
+
throw new ValidationError(path, message);
|
|
32
|
+
}
|
|
33
|
+
function isRecord(value) {
|
|
34
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
function assertString(value, path) {
|
|
37
|
+
if (value === undefined || value === null)
|
|
38
|
+
return;
|
|
39
|
+
if (typeof value !== "string")
|
|
40
|
+
fail(path, "must be a string");
|
|
41
|
+
}
|
|
42
|
+
function assertNonEmptyString(value, path) {
|
|
43
|
+
if (value === undefined)
|
|
44
|
+
return;
|
|
45
|
+
if (typeof value !== "string")
|
|
46
|
+
fail(path, "must be a string");
|
|
47
|
+
if (value.length === 0)
|
|
48
|
+
fail(path, "must not be empty");
|
|
49
|
+
}
|
|
50
|
+
function assertId(value, path) {
|
|
51
|
+
if (value === undefined)
|
|
52
|
+
return;
|
|
53
|
+
if (typeof value !== "string")
|
|
54
|
+
fail(path, "must be an Id");
|
|
55
|
+
const length = utf8Length(value);
|
|
56
|
+
if (length < 1 || length > 255)
|
|
57
|
+
fail(path, "must be between 1 and 255 octets");
|
|
58
|
+
if (!ID_PATTERN.test(value))
|
|
59
|
+
fail(path, "must use base64url characters");
|
|
60
|
+
}
|
|
61
|
+
function assertBoolean(value, path) {
|
|
62
|
+
if (value === undefined || value === null)
|
|
63
|
+
return;
|
|
64
|
+
if (typeof value !== "boolean")
|
|
65
|
+
fail(path, "must be a boolean");
|
|
66
|
+
}
|
|
67
|
+
function assertInteger(value, path) {
|
|
68
|
+
if (value === undefined || value === null)
|
|
69
|
+
return;
|
|
70
|
+
if (typeof value !== "number" || !Number.isInteger(value))
|
|
71
|
+
fail(path, "must be an integer");
|
|
72
|
+
}
|
|
73
|
+
function assertUnsignedInt(value, path) {
|
|
74
|
+
if (value === undefined || value === null)
|
|
75
|
+
return;
|
|
76
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
77
|
+
fail(path, "must be a non-negative integer");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function assertDateTime(value, path, requireZ) {
|
|
81
|
+
if (value === undefined)
|
|
82
|
+
return;
|
|
83
|
+
if (typeof value !== "string")
|
|
84
|
+
fail(path, "must be a date-time string");
|
|
85
|
+
const match = value.match(DATE_TIME);
|
|
86
|
+
if (!match) {
|
|
87
|
+
fail(path, requireZ ? "must be a UTCDateTime (YYYY-MM-DDTHH:mm:ssZ)" : "must be a LocalDateTime (YYYY-MM-DDTHH:mm:ss)");
|
|
88
|
+
}
|
|
89
|
+
const [, year, month, day, hour, minute, second, fraction, zFlag] = match;
|
|
90
|
+
if (requireZ && zFlag !== "Z")
|
|
91
|
+
fail(path, "must use Z suffix");
|
|
92
|
+
if (!requireZ && zFlag)
|
|
93
|
+
fail(path, "must not include time zone offset");
|
|
94
|
+
const monthNum = Number.parseInt(month ?? "0", 10);
|
|
95
|
+
const dayNum = Number.parseInt(day ?? "0", 10);
|
|
96
|
+
const hourNum = Number.parseInt(hour ?? "0", 10);
|
|
97
|
+
const minuteNum = Number.parseInt(minute ?? "0", 10);
|
|
98
|
+
const secondNum = Number.parseInt(second ?? "0", 10);
|
|
99
|
+
if (monthNum < 1 || monthNum > 12)
|
|
100
|
+
fail(path, "month must be 01-12");
|
|
101
|
+
if (dayNum < 1 || dayNum > 31)
|
|
102
|
+
fail(path, "day must be 01-31");
|
|
103
|
+
if (hourNum < 0 || hourNum > 23)
|
|
104
|
+
fail(path, "hour must be 00-23");
|
|
105
|
+
if (minuteNum < 0 || minuteNum > 59)
|
|
106
|
+
fail(path, "minute must be 00-59");
|
|
107
|
+
if (secondNum < 0 || secondNum > 59)
|
|
108
|
+
fail(path, "second must be 00-59");
|
|
109
|
+
if (fraction !== undefined) {
|
|
110
|
+
if (fraction.length > 9)
|
|
111
|
+
fail(path, "fractional seconds must be 1-9 digits");
|
|
112
|
+
if (/^0+$/.test(fraction))
|
|
113
|
+
fail(path, "fractional seconds must be non-zero");
|
|
114
|
+
if (fraction.endsWith("0"))
|
|
115
|
+
fail(path, "fractional seconds must not have trailing zeros");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function assertLocalDateTime(value, path) {
|
|
119
|
+
return assertDateTime(value, path, false);
|
|
120
|
+
}
|
|
121
|
+
function assertUtcDateTime(value, path) {
|
|
122
|
+
return assertDateTime(value, path, true);
|
|
123
|
+
}
|
|
124
|
+
function assertDurationLike(value, path, signed) {
|
|
125
|
+
if (value === undefined)
|
|
126
|
+
return;
|
|
127
|
+
if (typeof value !== "string")
|
|
128
|
+
fail(path, "must be a duration string");
|
|
129
|
+
if (!signed && value.startsWith("-"))
|
|
130
|
+
fail(path, "must not be negative");
|
|
131
|
+
const match = value.match(DURATION);
|
|
132
|
+
if (!match)
|
|
133
|
+
fail(path, "must be an ISO 8601 duration");
|
|
134
|
+
const week = match[1];
|
|
135
|
+
const dayFromWeek = match[2];
|
|
136
|
+
const day = match[3];
|
|
137
|
+
const hour = match[4];
|
|
138
|
+
const minute = match[5];
|
|
139
|
+
const second = match[6];
|
|
140
|
+
const fraction = match[7];
|
|
141
|
+
const hasDate = !!week || !!dayFromWeek || !!day;
|
|
142
|
+
const hasTime = !!hour || !!minute || !!second;
|
|
143
|
+
if (!hasDate && !hasTime)
|
|
144
|
+
fail(path, "must include at least one duration component");
|
|
145
|
+
if (fraction !== undefined) {
|
|
146
|
+
if (fraction.length > 9)
|
|
147
|
+
fail(path, "fractional seconds must be 1-9 digits");
|
|
148
|
+
if (/^0+$/.test(fraction))
|
|
149
|
+
fail(path, "fractional seconds must be non-zero");
|
|
150
|
+
if (fraction.endsWith("0"))
|
|
151
|
+
fail(path, "fractional seconds must not have trailing zeros");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function assertDuration(value, path) {
|
|
155
|
+
return assertDurationLike(value, path, false);
|
|
156
|
+
}
|
|
157
|
+
function assertSignedDuration(value, path) {
|
|
158
|
+
return assertDurationLike(value, path, true);
|
|
159
|
+
}
|
|
160
|
+
function assertBooleanMap(value, path) {
|
|
161
|
+
if (value === undefined)
|
|
162
|
+
return;
|
|
163
|
+
if (!isRecord(value))
|
|
164
|
+
fail(path, "must be a boolean map object");
|
|
165
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
166
|
+
if (entry !== true)
|
|
167
|
+
fail(`${path}.${key}`, "must be true");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function assertIdBooleanMap(value, path) {
|
|
171
|
+
if (value === undefined)
|
|
172
|
+
return;
|
|
173
|
+
if (!isRecord(value))
|
|
174
|
+
fail(path, "must be a boolean map object");
|
|
175
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
176
|
+
assertId(key, `${path}.${key}`);
|
|
177
|
+
if (entry !== true)
|
|
178
|
+
fail(`${path}.${key}`, "must be true");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function assertMediaType(value, path) {
|
|
182
|
+
if (value === undefined)
|
|
183
|
+
return;
|
|
184
|
+
if (typeof value !== "string")
|
|
185
|
+
fail(path, "must be a media type string");
|
|
186
|
+
const [typePart, ...params] = value.split(";");
|
|
187
|
+
const mediaType = (typePart ?? "").trim();
|
|
188
|
+
if (!/^[a-zA-Z0-9!#$&^_.+-]+\/[a-zA-Z0-9!#$&^_.+-]+$/.test(mediaType)) {
|
|
189
|
+
fail(path, "must be a valid media type");
|
|
190
|
+
}
|
|
191
|
+
for (const param of params) {
|
|
192
|
+
const [rawKey, rawValue] = param.split("=");
|
|
193
|
+
if (!rawKey || !rawValue)
|
|
194
|
+
continue;
|
|
195
|
+
const key = rawKey.trim().toLowerCase();
|
|
196
|
+
const valuePart = rawValue.trim().toLowerCase();
|
|
197
|
+
if (key === "charset" && valuePart !== "utf-8") {
|
|
198
|
+
fail(path, "charset parameter must be utf-8");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function assertTextContentType(value, path) {
|
|
203
|
+
if (value === undefined)
|
|
204
|
+
return;
|
|
205
|
+
if (typeof value !== "string")
|
|
206
|
+
fail(path, "must be a media type string");
|
|
207
|
+
const [typePart] = value.split(";");
|
|
208
|
+
const mediaType = (typePart ?? "").trim();
|
|
209
|
+
if (!mediaType.startsWith("text/"))
|
|
210
|
+
fail(path, "must be a text/* media type");
|
|
211
|
+
assertMediaType(value, path);
|
|
212
|
+
}
|
|
213
|
+
function assertContentId(value, path) {
|
|
214
|
+
if (value === undefined)
|
|
215
|
+
return;
|
|
216
|
+
if (typeof value !== "string")
|
|
217
|
+
fail(path, "must be a content-id");
|
|
218
|
+
const trimmed = value.trim();
|
|
219
|
+
const hasBrackets = trimmed.startsWith("<") || trimmed.endsWith(">");
|
|
220
|
+
if (hasBrackets && !(trimmed.startsWith("<") && trimmed.endsWith(">"))) {
|
|
221
|
+
fail(path, "must use matching angle brackets");
|
|
222
|
+
}
|
|
223
|
+
const raw = hasBrackets ? trimmed.slice(1, -1) : trimmed;
|
|
224
|
+
if (!/^[^\s<>@]+@[^\s<>@]+$/.test(raw)) {
|
|
225
|
+
fail(path, "must be a content-id");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function assertTimeZone(value, path) {
|
|
229
|
+
if (value === undefined || value === null)
|
|
230
|
+
return;
|
|
231
|
+
if (typeof value !== "string")
|
|
232
|
+
fail(path, "must be a time zone ID");
|
|
233
|
+
for (const tz of TimeZones) {
|
|
234
|
+
if (tz === value)
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
fail(path, "must be a supported time zone ID");
|
|
238
|
+
}
|
|
239
|
+
function assertJsonValue(value, path) {
|
|
240
|
+
if (value === undefined)
|
|
241
|
+
fail(path, "must be a JSON value");
|
|
242
|
+
if (value === null)
|
|
243
|
+
return;
|
|
244
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
245
|
+
return;
|
|
246
|
+
if (Array.isArray(value)) {
|
|
247
|
+
value.forEach((entry, index) => assertJsonValue(entry, `${path}[${index}]`));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (typeof value === "object") {
|
|
251
|
+
if (!isRecord(value))
|
|
252
|
+
fail(path, "must be a JSON object");
|
|
253
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
254
|
+
assertJsonValue(entry, `${path}.${key}`);
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
fail(path, "must be a JSON value");
|
|
259
|
+
}
|
|
260
|
+
function assertPatchObject(value, path) {
|
|
261
|
+
if (value === undefined)
|
|
262
|
+
return;
|
|
263
|
+
if (!isRecord(value))
|
|
264
|
+
fail(path, "must be a PatchObject");
|
|
265
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
266
|
+
if (entry === null)
|
|
267
|
+
continue;
|
|
268
|
+
assertJsonValue(entry, `${path}.${key}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function validateNDay(value, path) {
|
|
272
|
+
if (value["@type"] !== "NDay")
|
|
273
|
+
fail(path, "must have @type NDay");
|
|
274
|
+
if (!DAY_OF_WEEK.has(value.day))
|
|
275
|
+
fail(`${path}.day`, "must be a valid day of week");
|
|
276
|
+
assertInteger(value.nthOfPeriod, `${path}.nthOfPeriod`);
|
|
277
|
+
}
|
|
278
|
+
function validateRecurrenceRule(value, path) {
|
|
279
|
+
if (value["@type"] !== "RecurrenceRule")
|
|
280
|
+
fail(path, "must have @type RecurrenceRule");
|
|
281
|
+
if (!RECURRENCE_FREQUENCY.has(value.frequency))
|
|
282
|
+
fail(`${path}.frequency`, "must be a valid frequency");
|
|
283
|
+
assertUnsignedInt(value.interval, `${path}.interval`);
|
|
284
|
+
assertUnsignedInt(value.count, `${path}.count`);
|
|
285
|
+
if (value.rscale !== undefined && value.rscale !== "gregorian") {
|
|
286
|
+
fail(`${path}.rscale`, "only gregorian is supported");
|
|
287
|
+
}
|
|
288
|
+
if (value.skip !== undefined && !SKIP.has(value.skip))
|
|
289
|
+
fail(`${path}.skip`, "must be omit, backward, or forward");
|
|
290
|
+
if (value.firstDayOfWeek !== undefined && !DAY_OF_WEEK.has(value.firstDayOfWeek)) {
|
|
291
|
+
fail(`${path}.firstDayOfWeek`, "must be a valid day of week");
|
|
292
|
+
}
|
|
293
|
+
if (value.byDay) {
|
|
294
|
+
for (let i = 0; i < value.byDay.length; i += 1) {
|
|
295
|
+
const entry = value.byDay[i];
|
|
296
|
+
if (!entry)
|
|
297
|
+
continue;
|
|
298
|
+
validateNDay(entry, `${path}.byDay[${i}]`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (value.byMonthDay) {
|
|
302
|
+
for (let i = 0; i < value.byMonthDay.length; i += 1) {
|
|
303
|
+
const entry = value.byMonthDay[i];
|
|
304
|
+
if (typeof entry !== "number" || !Number.isInteger(entry) || entry === 0 || entry < -31 || entry > 31) {
|
|
305
|
+
fail(`${path}.byMonthDay[${i}]`, "must be an integer between -31 and 31, excluding 0");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (value.byMonth) {
|
|
310
|
+
for (let i = 0; i < value.byMonth.length; i += 1) {
|
|
311
|
+
const entry = value.byMonth[i];
|
|
312
|
+
if (typeof entry !== "string")
|
|
313
|
+
fail(`${path}.byMonth[${i}]`, "must be a string month");
|
|
314
|
+
const numeric = Number.parseInt(entry, 10);
|
|
315
|
+
if (!Number.isInteger(numeric) || numeric < 1 || numeric > 12) {
|
|
316
|
+
fail(`${path}.byMonth[${i}]`, "must be a month number between 1 and 12");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (value.byYearDay) {
|
|
321
|
+
for (let i = 0; i < value.byYearDay.length; i += 1) {
|
|
322
|
+
const entry = value.byYearDay[i];
|
|
323
|
+
if (typeof entry !== "number" || !Number.isInteger(entry) || entry === 0 || entry < -366 || entry > 366) {
|
|
324
|
+
fail(`${path}.byYearDay[${i}]`, "must be an integer between -366 and 366, excluding 0");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (value.byWeekNo) {
|
|
329
|
+
for (let i = 0; i < value.byWeekNo.length; i += 1) {
|
|
330
|
+
const entry = value.byWeekNo[i];
|
|
331
|
+
if (typeof entry !== "number" || !Number.isInteger(entry) || entry === 0 || entry < -53 || entry > 53) {
|
|
332
|
+
fail(`${path}.byWeekNo[${i}]`, "must be an integer between -53 and 53, excluding 0");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (value.byHour) {
|
|
337
|
+
for (let i = 0; i < value.byHour.length; i += 1) {
|
|
338
|
+
const entry = value.byHour[i];
|
|
339
|
+
if (typeof entry !== "number" || !Number.isInteger(entry) || entry < 0 || entry > 23) {
|
|
340
|
+
fail(`${path}.byHour[${i}]`, "must be an integer between 0 and 23");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (value.byMinute) {
|
|
345
|
+
for (let i = 0; i < value.byMinute.length; i += 1) {
|
|
346
|
+
const entry = value.byMinute[i];
|
|
347
|
+
if (typeof entry !== "number" || !Number.isInteger(entry) || entry < 0 || entry > 59) {
|
|
348
|
+
fail(`${path}.byMinute[${i}]`, "must be an integer between 0 and 59");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (value.bySecond) {
|
|
353
|
+
for (let i = 0; i < value.bySecond.length; i += 1) {
|
|
354
|
+
const entry = value.bySecond[i];
|
|
355
|
+
if (typeof entry !== "number" || !Number.isInteger(entry) || entry < 0 || entry > 59) {
|
|
356
|
+
fail(`${path}.bySecond[${i}]`, "must be an integer between 0 and 59");
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (value.bySetPosition) {
|
|
361
|
+
for (let i = 0; i < value.bySetPosition.length; i += 1) {
|
|
362
|
+
const entry = value.bySetPosition[i];
|
|
363
|
+
if (typeof entry !== "number" || !Number.isInteger(entry) || entry === 0) {
|
|
364
|
+
fail(`${path}.bySetPosition[${i}]`, "must be a non-zero integer");
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
assertLocalDateTime(value.until, `${path}.until`);
|
|
369
|
+
}
|
|
370
|
+
function validateAlert(value, path) {
|
|
371
|
+
if (value["@type"] !== "Alert")
|
|
372
|
+
fail(path, "must have @type Alert");
|
|
373
|
+
if (!value.trigger)
|
|
374
|
+
fail(`${path}.trigger`, "is required");
|
|
375
|
+
if (value.trigger["@type"] === "OffsetTrigger") {
|
|
376
|
+
const offset = value.trigger.offset;
|
|
377
|
+
if (typeof offset !== "string") {
|
|
378
|
+
fail(`${path}.trigger.offset`, "must be a duration string");
|
|
379
|
+
}
|
|
380
|
+
assertSignedDuration(offset, `${path}.trigger.offset`);
|
|
381
|
+
}
|
|
382
|
+
else if (value.trigger["@type"] === "AbsoluteTrigger") {
|
|
383
|
+
const when = value.trigger.when;
|
|
384
|
+
if (typeof when !== "string") {
|
|
385
|
+
fail(`${path}.trigger.when`, "must be a UTCDateTime string");
|
|
386
|
+
}
|
|
387
|
+
assertUtcDateTime(when, `${path}.trigger.when`);
|
|
388
|
+
}
|
|
389
|
+
assertUtcDateTime(value.acknowledged, `${path}.acknowledged`);
|
|
390
|
+
assertString(value.action, `${path}.action`);
|
|
391
|
+
if (value.relatedTo) {
|
|
392
|
+
if (typeof value.relatedTo !== "object" || value.relatedTo === null || Array.isArray(value.relatedTo)) {
|
|
393
|
+
fail(`${path}.relatedTo`, "must be an object");
|
|
394
|
+
}
|
|
395
|
+
for (const [key, entry] of Object.entries(value.relatedTo)) {
|
|
396
|
+
if (!entry || typeof entry !== "object") {
|
|
397
|
+
fail(`${path}.relatedTo.${key}`, "must be a relation object");
|
|
398
|
+
}
|
|
399
|
+
validateRelation(entry, `${path}.relatedTo.${key}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function validateRelation(value, path) {
|
|
404
|
+
if (value["@type"] !== "Relation")
|
|
405
|
+
fail(path, "must have @type Relation");
|
|
406
|
+
if (value.relation)
|
|
407
|
+
assertBooleanMap(value.relation, `${path}.relation`);
|
|
408
|
+
}
|
|
409
|
+
function validateLink(value, path) {
|
|
410
|
+
if (value["@type"] !== "Link")
|
|
411
|
+
fail(path, "must have @type Link");
|
|
412
|
+
assertString(value.href, `${path}.href`);
|
|
413
|
+
if (!value.href)
|
|
414
|
+
fail(`${path}.href`, "is required");
|
|
415
|
+
assertContentId(value.cid, `${path}.cid`);
|
|
416
|
+
assertMediaType(value.contentType, `${path}.contentType`);
|
|
417
|
+
assertUnsignedInt(value.size, `${path}.size`);
|
|
418
|
+
assertString(value.rel, `${path}.rel`);
|
|
419
|
+
assertString(value.display, `${path}.display`);
|
|
420
|
+
assertString(value.title, `${path}.title`);
|
|
421
|
+
}
|
|
422
|
+
function validateLocation(value, path) {
|
|
423
|
+
if (value["@type"] !== "Location")
|
|
424
|
+
fail(path, "must have @type Location");
|
|
425
|
+
assertId(value.relativeTo, `${path}.relativeTo`);
|
|
426
|
+
assertString(value.name, `${path}.name`);
|
|
427
|
+
assertString(value.description, `${path}.description`);
|
|
428
|
+
if (value.locationTypes)
|
|
429
|
+
assertBooleanMap(value.locationTypes, `${path}.locationTypes`);
|
|
430
|
+
assertString(value.relativeTo, `${path}.relativeTo`);
|
|
431
|
+
assertTimeZone(value.timeZone, `${path}.timeZone`);
|
|
432
|
+
assertString(value.coordinates, `${path}.coordinates`);
|
|
433
|
+
if (value.links) {
|
|
434
|
+
if (typeof value.links !== "object" || value.links === null || Array.isArray(value.links)) {
|
|
435
|
+
fail(`${path}.links`, "must be an object");
|
|
436
|
+
}
|
|
437
|
+
for (const [key, entry] of Object.entries(value.links)) {
|
|
438
|
+
assertId(key, `${path}.links.${key}`);
|
|
439
|
+
if (!entry || typeof entry !== "object") {
|
|
440
|
+
fail(`${path}.links.${key}`, "must be a link object");
|
|
441
|
+
}
|
|
442
|
+
validateLink(entry, `${path}.links.${key}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function validateVirtualLocation(value, path) {
|
|
447
|
+
if (value["@type"] !== "VirtualLocation")
|
|
448
|
+
fail(path, "must have @type VirtualLocation");
|
|
449
|
+
assertString(value.name, `${path}.name`);
|
|
450
|
+
assertString(value.description, `${path}.description`);
|
|
451
|
+
assertString(value.uri, `${path}.uri`);
|
|
452
|
+
if (!value.uri)
|
|
453
|
+
fail(`${path}.uri`, "is required");
|
|
454
|
+
if (value.features)
|
|
455
|
+
assertBooleanMap(value.features, `${path}.features`);
|
|
456
|
+
}
|
|
457
|
+
function validateTimeZoneRule(value, path) {
|
|
458
|
+
if (value["@type"] !== "TimeZoneRule")
|
|
459
|
+
fail(path, "must have @type TimeZoneRule");
|
|
460
|
+
assertLocalDateTime(value.start, `${path}.start`);
|
|
461
|
+
if (!value.start)
|
|
462
|
+
fail(`${path}.start`, "is required");
|
|
463
|
+
assertString(value.offsetFrom, `${path}.offsetFrom`);
|
|
464
|
+
if (!value.offsetFrom)
|
|
465
|
+
fail(`${path}.offsetFrom`, "is required");
|
|
466
|
+
assertString(value.offsetTo, `${path}.offsetTo`);
|
|
467
|
+
if (!value.offsetTo)
|
|
468
|
+
fail(`${path}.offsetTo`, "is required");
|
|
469
|
+
if (value.recurrenceRules) {
|
|
470
|
+
value.recurrenceRules.forEach((rule, index) => validateRecurrenceRule(rule, `${path}.recurrenceRules[${index}]`));
|
|
471
|
+
}
|
|
472
|
+
if (value.recurrenceOverrides) {
|
|
473
|
+
if (typeof value.recurrenceOverrides !== "object" || value.recurrenceOverrides === null || Array.isArray(value.recurrenceOverrides)) {
|
|
474
|
+
fail(`${path}.recurrenceOverrides`, "must be an object");
|
|
475
|
+
}
|
|
476
|
+
for (const [key, entry] of Object.entries(value.recurrenceOverrides)) {
|
|
477
|
+
assertLocalDateTime(key, `${path}.recurrenceOverrides.${key}`);
|
|
478
|
+
assertPatchObject(entry, `${path}.recurrenceOverrides.${key}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (value.names)
|
|
482
|
+
assertBooleanMap(value.names, `${path}.names`);
|
|
483
|
+
if (value.comments) {
|
|
484
|
+
value.comments.forEach((entry, index) => assertString(entry, `${path}.comments[${index}]`));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function validateTimeZoneObject(value, path) {
|
|
488
|
+
if (value["@type"] !== "TimeZone")
|
|
489
|
+
fail(path, "must have @type TimeZone");
|
|
490
|
+
assertTimeZone(value.tzId, `${path}.tzId`);
|
|
491
|
+
if (!value.tzId)
|
|
492
|
+
fail(`${path}.tzId`, "is required");
|
|
493
|
+
assertUtcDateTime(value.updated, `${path}.updated`);
|
|
494
|
+
assertString(value.url, `${path}.url`);
|
|
495
|
+
assertUtcDateTime(value.validUntil, `${path}.validUntil`);
|
|
496
|
+
if (value.aliases)
|
|
497
|
+
assertBooleanMap(value.aliases, `${path}.aliases`);
|
|
498
|
+
if (value.standard) {
|
|
499
|
+
value.standard.forEach((rule, index) => validateTimeZoneRule(rule, `${path}.standard[${index}]`));
|
|
500
|
+
}
|
|
501
|
+
if (value.daylight) {
|
|
502
|
+
value.daylight.forEach((rule, index) => validateTimeZoneRule(rule, `${path}.daylight[${index}]`));
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function validateParticipant(value, path) {
|
|
506
|
+
if (value["@type"] !== "Participant")
|
|
507
|
+
fail(path, "must have @type Participant");
|
|
508
|
+
assertString(value.name, `${path}.name`);
|
|
509
|
+
assertString(value.email, `${path}.email`);
|
|
510
|
+
assertString(value.description, `${path}.description`);
|
|
511
|
+
if (value.sendTo) {
|
|
512
|
+
if (typeof value.sendTo !== "object" || value.sendTo === null || Array.isArray(value.sendTo)) {
|
|
513
|
+
fail(`${path}.sendTo`, "must be an object");
|
|
514
|
+
}
|
|
515
|
+
for (const [key, entry] of Object.entries(value.sendTo)) {
|
|
516
|
+
assertString(entry, `${path}.sendTo.${key}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (value.roles)
|
|
520
|
+
assertBooleanMap(value.roles, `${path}.roles`);
|
|
521
|
+
assertId(value.locationId, `${path}.locationId`);
|
|
522
|
+
assertString(value.language, `${path}.language`);
|
|
523
|
+
assertString(value.participationStatus, `${path}.participationStatus`);
|
|
524
|
+
assertString(value.participationComment, `${path}.participationComment`);
|
|
525
|
+
assertBoolean(value.expectReply, `${path}.expectReply`);
|
|
526
|
+
assertString(value.scheduleAgent, `${path}.scheduleAgent`);
|
|
527
|
+
assertBoolean(value.scheduleForceSend, `${path}.scheduleForceSend`);
|
|
528
|
+
assertUnsignedInt(value.scheduleSequence, `${path}.scheduleSequence`);
|
|
529
|
+
if (value.scheduleStatus) {
|
|
530
|
+
value.scheduleStatus.forEach((entry, index) => assertString(entry, `${path}.scheduleStatus[${index}]`));
|
|
531
|
+
}
|
|
532
|
+
assertUtcDateTime(value.scheduleUpdated, `${path}.scheduleUpdated`);
|
|
533
|
+
assertString(value.sentBy, `${path}.sentBy`);
|
|
534
|
+
assertId(value.invitedBy, `${path}.invitedBy`);
|
|
535
|
+
if (value.delegatedTo)
|
|
536
|
+
assertIdBooleanMap(value.delegatedTo, `${path}.delegatedTo`);
|
|
537
|
+
if (value.delegatedFrom)
|
|
538
|
+
assertIdBooleanMap(value.delegatedFrom, `${path}.delegatedFrom`);
|
|
539
|
+
if (value.memberOf)
|
|
540
|
+
assertIdBooleanMap(value.memberOf, `${path}.memberOf`);
|
|
541
|
+
if (value.links) {
|
|
542
|
+
if (typeof value.links !== "object" || value.links === null || Array.isArray(value.links)) {
|
|
543
|
+
fail(`${path}.links`, "must be an object");
|
|
544
|
+
}
|
|
545
|
+
for (const [key, entry] of Object.entries(value.links)) {
|
|
546
|
+
assertId(key, `${path}.links.${key}`);
|
|
547
|
+
if (!entry || typeof entry !== "object") {
|
|
548
|
+
fail(`${path}.links.${key}`, "must be a link object");
|
|
549
|
+
}
|
|
550
|
+
validateLink(entry, `${path}.links.${key}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
assertString(value.progress, `${path}.progress`);
|
|
554
|
+
assertUtcDateTime(value.progressUpdated, `${path}.progressUpdated`);
|
|
555
|
+
assertUnsignedInt(value.percentComplete, `${path}.percentComplete`);
|
|
556
|
+
}
|
|
557
|
+
function validateCommon(value, path) {
|
|
558
|
+
assertNonEmptyString(value.uid, `${path}.uid`);
|
|
559
|
+
if (!value.uid)
|
|
560
|
+
fail(`${path}.uid`, "is required");
|
|
561
|
+
assertUtcDateTime(value.updated, `${path}.updated`);
|
|
562
|
+
assertUtcDateTime(value.created, `${path}.created`);
|
|
563
|
+
assertUnsignedInt(value.sequence, `${path}.sequence`);
|
|
564
|
+
assertString(value.method, `${path}.method`);
|
|
565
|
+
if (value.method && value.method !== value.method.toLowerCase()) {
|
|
566
|
+
fail(`${path}.method`, "must be lowercase");
|
|
567
|
+
}
|
|
568
|
+
assertString(value.title, `${path}.title`);
|
|
569
|
+
assertString(value.description, `${path}.description`);
|
|
570
|
+
assertTextContentType(value.descriptionContentType, `${path}.descriptionContentType`);
|
|
571
|
+
assertBoolean(value.showWithoutTime, `${path}.showWithoutTime`);
|
|
572
|
+
if (value.relatedTo) {
|
|
573
|
+
if (typeof value.relatedTo !== "object" || value.relatedTo === null || Array.isArray(value.relatedTo)) {
|
|
574
|
+
fail(`${path}.relatedTo`, "must be an object");
|
|
575
|
+
}
|
|
576
|
+
for (const [key, entry] of Object.entries(value.relatedTo)) {
|
|
577
|
+
assertNonEmptyString(key, `${path}.relatedTo.${key}`);
|
|
578
|
+
if (!entry || typeof entry !== "object") {
|
|
579
|
+
fail(`${path}.relatedTo.${key}`, "must be a relation object");
|
|
580
|
+
}
|
|
581
|
+
validateRelation(entry, `${path}.relatedTo.${key}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (value.keywords)
|
|
585
|
+
assertBooleanMap(value.keywords, `${path}.keywords`);
|
|
586
|
+
if (value.categories)
|
|
587
|
+
assertBooleanMap(value.categories, `${path}.categories`);
|
|
588
|
+
assertString(value.color, `${path}.color`);
|
|
589
|
+
assertLocalDateTime(value.recurrenceId, `${path}.recurrenceId`);
|
|
590
|
+
assertTimeZone(value.recurrenceIdTimeZone, `${path}.recurrenceIdTimeZone`);
|
|
591
|
+
if (value.recurrenceRules) {
|
|
592
|
+
value.recurrenceRules.forEach((rule, index) => validateRecurrenceRule(rule, `${path}.recurrenceRules[${index}]`));
|
|
593
|
+
}
|
|
594
|
+
if (value.excludedRecurrenceRules) {
|
|
595
|
+
value.excludedRecurrenceRules.forEach((rule, index) => validateRecurrenceRule(rule, `${path}.excludedRecurrenceRules[${index}]`));
|
|
596
|
+
}
|
|
597
|
+
if (value.recurrenceOverrides) {
|
|
598
|
+
if (typeof value.recurrenceOverrides !== "object" || value.recurrenceOverrides === null || Array.isArray(value.recurrenceOverrides)) {
|
|
599
|
+
fail(`${path}.recurrenceOverrides`, "must be an object");
|
|
600
|
+
}
|
|
601
|
+
for (const [key, entry] of Object.entries(value.recurrenceOverrides)) {
|
|
602
|
+
assertLocalDateTime(key, `${path}.recurrenceOverrides.${key}`);
|
|
603
|
+
assertPatchObject(entry, `${path}.recurrenceOverrides.${key}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
assertBoolean(value.excluded, `${path}.excluded`);
|
|
607
|
+
assertInteger(value.priority, `${path}.priority`);
|
|
608
|
+
assertString(value.freeBusyStatus, `${path}.freeBusyStatus`);
|
|
609
|
+
assertString(value.privacy, `${path}.privacy`);
|
|
610
|
+
if (value.replyTo) {
|
|
611
|
+
if (typeof value.replyTo !== "object" || value.replyTo === null || Array.isArray(value.replyTo)) {
|
|
612
|
+
fail(`${path}.replyTo`, "must be an object");
|
|
613
|
+
}
|
|
614
|
+
for (const [key, entry] of Object.entries(value.replyTo)) {
|
|
615
|
+
assertString(entry, `${path}.replyTo.${key}`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
assertString(value.sentBy, `${path}.sentBy`);
|
|
619
|
+
if (value.locations) {
|
|
620
|
+
if (typeof value.locations !== "object" || value.locations === null || Array.isArray(value.locations)) {
|
|
621
|
+
fail(`${path}.locations`, "must be an object");
|
|
622
|
+
}
|
|
623
|
+
for (const [key, entry] of Object.entries(value.locations)) {
|
|
624
|
+
assertId(key, `${path}.locations.${key}`);
|
|
625
|
+
if (!entry || typeof entry !== "object") {
|
|
626
|
+
fail(`${path}.locations.${key}`, "must be a location object");
|
|
627
|
+
}
|
|
628
|
+
validateLocation(entry, `${path}.locations.${key}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (value.virtualLocations) {
|
|
632
|
+
if (typeof value.virtualLocations !== "object" || value.virtualLocations === null || Array.isArray(value.virtualLocations)) {
|
|
633
|
+
fail(`${path}.virtualLocations`, "must be an object");
|
|
634
|
+
}
|
|
635
|
+
for (const [key, entry] of Object.entries(value.virtualLocations)) {
|
|
636
|
+
assertId(key, `${path}.virtualLocations.${key}`);
|
|
637
|
+
if (!entry || typeof entry !== "object") {
|
|
638
|
+
fail(`${path}.virtualLocations.${key}`, "must be a virtual location object");
|
|
639
|
+
}
|
|
640
|
+
validateVirtualLocation(entry, `${path}.virtualLocations.${key}`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (value.links) {
|
|
644
|
+
if (typeof value.links !== "object" || value.links === null || Array.isArray(value.links)) {
|
|
645
|
+
fail(`${path}.links`, "must be an object");
|
|
646
|
+
}
|
|
647
|
+
for (const [key, entry] of Object.entries(value.links)) {
|
|
648
|
+
assertId(key, `${path}.links.${key}`);
|
|
649
|
+
if (!entry || typeof entry !== "object") {
|
|
650
|
+
fail(`${path}.links.${key}`, "must be a link object");
|
|
651
|
+
}
|
|
652
|
+
validateLink(entry, `${path}.links.${key}`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (value.participants) {
|
|
656
|
+
if (typeof value.participants !== "object" || value.participants === null || Array.isArray(value.participants)) {
|
|
657
|
+
fail(`${path}.participants`, "must be an object");
|
|
658
|
+
}
|
|
659
|
+
for (const [key, entry] of Object.entries(value.participants)) {
|
|
660
|
+
assertId(key, `${path}.participants.${key}`);
|
|
661
|
+
if (!entry || typeof entry !== "object") {
|
|
662
|
+
fail(`${path}.participants.${key}`, "must be a participant object");
|
|
663
|
+
}
|
|
664
|
+
validateParticipant(entry, `${path}.participants.${key}`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
assertString(value.requestStatus, `${path}.requestStatus`);
|
|
668
|
+
assertBoolean(value.useDefaultAlerts, `${path}.useDefaultAlerts`);
|
|
669
|
+
if (value.alerts) {
|
|
670
|
+
if (typeof value.alerts !== "object" || value.alerts === null || Array.isArray(value.alerts)) {
|
|
671
|
+
fail(`${path}.alerts`, "must be an object");
|
|
672
|
+
}
|
|
673
|
+
for (const [key, entry] of Object.entries(value.alerts)) {
|
|
674
|
+
assertId(key, `${path}.alerts.${key}`);
|
|
675
|
+
if (!entry || typeof entry !== "object") {
|
|
676
|
+
fail(`${path}.alerts.${key}`, "must be an alert object");
|
|
677
|
+
}
|
|
678
|
+
validateAlert(entry, `${path}.alerts.${key}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (value.localizations) {
|
|
682
|
+
if (!isRecord(value.localizations))
|
|
683
|
+
fail(`${path}.localizations`, "must be an object");
|
|
684
|
+
for (const [key, entry] of Object.entries(value.localizations)) {
|
|
685
|
+
assertPatchObject(entry, `${path}.localizations.${key}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
assertTimeZone(value.timeZone, `${path}.timeZone`);
|
|
689
|
+
if (value.timeZones) {
|
|
690
|
+
const timeZones = value.timeZones;
|
|
691
|
+
if (typeof timeZones !== "object" || timeZones === null || Array.isArray(timeZones)) {
|
|
692
|
+
fail(`${path}.timeZones`, "must be an object");
|
|
693
|
+
}
|
|
694
|
+
for (const [key] of Object.entries(timeZones)) {
|
|
695
|
+
assertTimeZone(key, `${path}.timeZones.${key}`);
|
|
696
|
+
}
|
|
697
|
+
for (const [key, entry] of Object.entries(timeZones)) {
|
|
698
|
+
if (!entry || typeof entry !== "object") {
|
|
699
|
+
fail(`${path}.timeZones.${key}`, "must be a time zone object");
|
|
700
|
+
}
|
|
701
|
+
validateTimeZoneObject(entry, `${path}.timeZones.${key}`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function validateEvent(value, path) {
|
|
706
|
+
if (value["@type"] !== "Event")
|
|
707
|
+
fail(path, "must have @type Event");
|
|
708
|
+
validateCommon(value, path);
|
|
709
|
+
assertLocalDateTime(value.start, `${path}.start`);
|
|
710
|
+
if (!value.start)
|
|
711
|
+
fail(`${path}.start`, "is required");
|
|
712
|
+
assertDuration(value.duration, `${path}.duration`);
|
|
713
|
+
assertString(value.status, `${path}.status`);
|
|
714
|
+
}
|
|
715
|
+
function validateTask(value, path) {
|
|
716
|
+
if (value["@type"] !== "Task")
|
|
717
|
+
fail(path, "must have @type Task");
|
|
718
|
+
validateCommon(value, path);
|
|
719
|
+
assertLocalDateTime(value.start, `${path}.start`);
|
|
720
|
+
assertLocalDateTime(value.due, `${path}.due`);
|
|
721
|
+
assertDuration(value.estimatedDuration, `${path}.estimatedDuration`);
|
|
722
|
+
assertUnsignedInt(value.percentComplete, `${path}.percentComplete`);
|
|
723
|
+
assertString(value.progress, `${path}.progress`);
|
|
724
|
+
assertUtcDateTime(value.progressUpdated, `${path}.progressUpdated`);
|
|
725
|
+
}
|
|
726
|
+
function validateGroup(value, path) {
|
|
727
|
+
if (value["@type"] !== "Group")
|
|
728
|
+
fail(path, "must have @type Group");
|
|
729
|
+
validateCommon(value, path);
|
|
730
|
+
if (!Array.isArray(value.entries))
|
|
731
|
+
fail(`${path}.entries`, "must be an array");
|
|
732
|
+
value.entries.forEach((entry, index) => validateJsCalendarObject(entry, `${path}.entries[${index}]`));
|
|
733
|
+
assertString(value.source, `${path}.source`);
|
|
734
|
+
}
|
|
735
|
+
export function validateJsCalendarObject(value, path = "object") {
|
|
736
|
+
if (!value || typeof value !== "object")
|
|
737
|
+
fail(path, "must be an object");
|
|
738
|
+
if (value["@type"] === "Event")
|
|
739
|
+
return validateEvent(value, path);
|
|
740
|
+
if (value["@type"] === "Task")
|
|
741
|
+
return validateTask(value, path);
|
|
742
|
+
if (value["@type"] === "Group")
|
|
743
|
+
return validateGroup(value, path);
|
|
744
|
+
fail(`${path}["@type"]`, "must be Event, Task, or Group");
|
|
745
|
+
}
|