skir-codemirror-plugin 0.9.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/README.md +126 -0
- package/dist/codemirror/create_editor_state.d.ts +20 -0
- package/dist/codemirror/create_editor_state.d.ts.map +1 -0
- package/dist/codemirror/create_editor_state.js +252 -0
- package/dist/codemirror/create_editor_state.js.map +1 -0
- package/dist/codemirror/enter_key_handler.d.ts +8 -0
- package/dist/codemirror/enter_key_handler.d.ts.map +1 -0
- package/dist/codemirror/enter_key_handler.js +181 -0
- package/dist/codemirror/enter_key_handler.js.map +1 -0
- package/dist/codemirror/json_completion.d.ts +4 -0
- package/dist/codemirror/json_completion.d.ts.map +1 -0
- package/dist/codemirror/json_completion.js +150 -0
- package/dist/codemirror/json_completion.js.map +1 -0
- package/dist/codemirror/json_linter.d.ts +4 -0
- package/dist/codemirror/json_linter.d.ts.map +1 -0
- package/dist/codemirror/json_linter.js +277 -0
- package/dist/codemirror/json_linter.js.map +1 -0
- package/dist/codemirror/json_state.d.ts +16 -0
- package/dist/codemirror/json_state.d.ts.map +1 -0
- package/dist/codemirror/json_state.js +124 -0
- package/dist/codemirror/json_state.js.map +1 -0
- package/dist/codemirror/status_bar.d.ts +3 -0
- package/dist/codemirror/status_bar.d.ts.map +1 -0
- package/dist/codemirror/status_bar.js +123 -0
- package/dist/codemirror/status_bar.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/json/json_parser.d.ts +3 -0
- package/dist/json/json_parser.d.ts.map +1 -0
- package/dist/json/json_parser.js +414 -0
- package/dist/json/json_parser.js.map +1 -0
- package/dist/json/json_parser.test.d.ts +2 -0
- package/dist/json/json_parser.test.d.ts.map +1 -0
- package/dist/json/json_parser.test.js +337 -0
- package/dist/json/json_parser.test.js.map +1 -0
- package/dist/json/schema_validator.d.ts +3 -0
- package/dist/json/schema_validator.d.ts.map +1 -0
- package/dist/json/schema_validator.js +525 -0
- package/dist/json/schema_validator.js.map +1 -0
- package/dist/json/schema_validator.test.d.ts +2 -0
- package/dist/json/schema_validator.test.d.ts.map +1 -0
- package/dist/json/schema_validator.test.js +212 -0
- package/dist/json/schema_validator.test.js.map +1 -0
- package/dist/json/to_json.d.ts +6 -0
- package/dist/json/to_json.d.ts.map +1 -0
- package/dist/json/to_json.js +61 -0
- package/dist/json/to_json.js.map +1 -0
- package/dist/json/to_json.test.d.ts +2 -0
- package/dist/json/to_json.test.d.ts.map +1 -0
- package/dist/json/to_json.test.js +128 -0
- package/dist/json/to_json.test.js.map +1 -0
- package/dist/json/types.d.ts +170 -0
- package/dist/json/types.d.ts.map +1 -0
- package/dist/json/types.js +2 -0
- package/dist/json/types.js.map +1 -0
- package/package.json +85 -0
- package/src/codemirror/create_editor_state.ts +278 -0
- package/src/codemirror/enter_key_handler.ts +232 -0
- package/src/codemirror/json_completion.ts +182 -0
- package/src/codemirror/json_linter.ts +358 -0
- package/src/codemirror/json_state.ts +170 -0
- package/src/codemirror/status_bar.ts +137 -0
- package/src/index.ts +6 -0
- package/src/json/json_parser.test.ts +360 -0
- package/src/json/json_parser.ts +461 -0
- package/src/json/schema_validator.test.ts +230 -0
- package/src/json/schema_validator.ts +558 -0
- package/src/json/to_json.test.ts +150 -0
- package/src/json/to_json.ts +70 -0
- package/src/json/types.ts +254 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { primitiveSerializer } from "skir-client";
|
|
2
|
+
import { toJson } from "./to_json";
|
|
3
|
+
import type {
|
|
4
|
+
FieldDefinition,
|
|
5
|
+
Hint,
|
|
6
|
+
JsonError,
|
|
7
|
+
JsonObject,
|
|
8
|
+
JsonValue,
|
|
9
|
+
MutableTypeHint,
|
|
10
|
+
Path,
|
|
11
|
+
PrimitiveType,
|
|
12
|
+
RecordDefinition,
|
|
13
|
+
TypeDefinition,
|
|
14
|
+
TypeHint,
|
|
15
|
+
TypeSignature,
|
|
16
|
+
ValidationResult,
|
|
17
|
+
VariantDefinition,
|
|
18
|
+
} from "./types";
|
|
19
|
+
|
|
20
|
+
export function validateSchema(
|
|
21
|
+
value: JsonValue,
|
|
22
|
+
schema: TypeDefinition,
|
|
23
|
+
): ValidationResult {
|
|
24
|
+
const idToRecordDef: { [id: string]: RecordDefinition } = {};
|
|
25
|
+
for (const record of schema.records) {
|
|
26
|
+
idToRecordDef[record.id] = record;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const validator = new SchemaValidator(idToRecordDef);
|
|
30
|
+
const root: Path = { kind: "root" };
|
|
31
|
+
validator.validate(value, root, schema.type);
|
|
32
|
+
const pathToTypeHint = new Map<Path, TypeHint>();
|
|
33
|
+
for (const hint of validator.hints) {
|
|
34
|
+
const { valueContext } = hint;
|
|
35
|
+
if (valueContext) {
|
|
36
|
+
pathToTypeHint.set(valueContext.path, hint);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
errors: validator.errors,
|
|
41
|
+
hints: validator.hints,
|
|
42
|
+
rootTypeHint: validator.rootTypeHint,
|
|
43
|
+
pathToTypeHint: pathToTypeHint,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class SchemaValidator {
|
|
48
|
+
constructor(readonly idToRecordDef: { [id: string]: RecordDefinition }) {}
|
|
49
|
+
readonly errors: JsonError[] = [];
|
|
50
|
+
readonly hints: Hint[] = [];
|
|
51
|
+
private readonly typeHintStack: MutableTypeHint[] = [];
|
|
52
|
+
rootTypeHint: TypeHint | undefined;
|
|
53
|
+
|
|
54
|
+
validate(
|
|
55
|
+
value: JsonValue,
|
|
56
|
+
path: Path,
|
|
57
|
+
schema: TypeSignature,
|
|
58
|
+
optionalSchema?: TypeSignature,
|
|
59
|
+
): void {
|
|
60
|
+
const { idToRecordDef, typeHintStack } = this;
|
|
61
|
+
value.expectedType = schema;
|
|
62
|
+
// For every call to pushTypeHint() there emust be one call to typeHintStack.pop()
|
|
63
|
+
const pushTypeHint = (): void => {
|
|
64
|
+
const typeDesc = getTypeDesc(optionalSchema ?? schema);
|
|
65
|
+
const typeDoc = getTypeDoc(schema, idToRecordDef);
|
|
66
|
+
const message = [typeDesc];
|
|
67
|
+
if (typeDoc) {
|
|
68
|
+
message.push(typeDoc);
|
|
69
|
+
}
|
|
70
|
+
if (value.kind === "array") {
|
|
71
|
+
const { length } = value.values;
|
|
72
|
+
if (length) {
|
|
73
|
+
message.push(`Length: ${length}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const hint: MutableTypeHint = {
|
|
77
|
+
segment: value.firstToken,
|
|
78
|
+
message: message.length === 1 ? message[0] : message,
|
|
79
|
+
valueContext: { value, path },
|
|
80
|
+
childHints: [],
|
|
81
|
+
};
|
|
82
|
+
if (typeHintStack.length) {
|
|
83
|
+
const topOfStack = typeHintStack[typeHintStack.length - 1];
|
|
84
|
+
topOfStack.childHints.push(hint);
|
|
85
|
+
} else {
|
|
86
|
+
this.rootTypeHint = hint;
|
|
87
|
+
}
|
|
88
|
+
this.hints.push(hint);
|
|
89
|
+
typeHintStack.push(hint);
|
|
90
|
+
};
|
|
91
|
+
switch (schema.kind) {
|
|
92
|
+
case "array": {
|
|
93
|
+
if (value.kind === "array") {
|
|
94
|
+
pushTypeHint();
|
|
95
|
+
for (const [index, item] of value.values.entries()) {
|
|
96
|
+
const { key_extractor: keyExtractor, item: itemSchema } =
|
|
97
|
+
schema.value;
|
|
98
|
+
const itemKey = extractItemKey(item, keyExtractor);
|
|
99
|
+
const itemPath: Path = {
|
|
100
|
+
kind: "array-item",
|
|
101
|
+
index: index,
|
|
102
|
+
key: itemKey,
|
|
103
|
+
arrayPath: path,
|
|
104
|
+
};
|
|
105
|
+
this.validate(item, itemPath, itemSchema);
|
|
106
|
+
}
|
|
107
|
+
typeHintStack.pop();
|
|
108
|
+
} else {
|
|
109
|
+
this.errors.push({
|
|
110
|
+
kind: "error",
|
|
111
|
+
segment: value.segment,
|
|
112
|
+
message: "Expected: array",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "optional": {
|
|
118
|
+
if (value.kind === "literal" && value.jsonCode === "null") {
|
|
119
|
+
pushTypeHint();
|
|
120
|
+
typeHintStack.pop();
|
|
121
|
+
} else {
|
|
122
|
+
this.validate(value, path, schema.value, schema);
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case "primitive": {
|
|
127
|
+
const primitiveType = schema.value;
|
|
128
|
+
if (primitiveType === "timestamp") {
|
|
129
|
+
if (this.validateTimestamp(value)) {
|
|
130
|
+
pushTypeHint();
|
|
131
|
+
typeHintStack.pop();
|
|
132
|
+
}
|
|
133
|
+
} else if (hasPrimitiveType(value, primitiveType)) {
|
|
134
|
+
pushTypeHint();
|
|
135
|
+
typeHintStack.pop();
|
|
136
|
+
} else {
|
|
137
|
+
this.errors.push({
|
|
138
|
+
kind: "error",
|
|
139
|
+
segment: value.firstToken,
|
|
140
|
+
message: `Expected: ${primitiveType}`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case "record": {
|
|
146
|
+
const recordDef = idToRecordDef[schema.value];
|
|
147
|
+
if (recordDef.kind === "struct") {
|
|
148
|
+
const nameToFieldDef: { [name: string]: FieldDefinition } = {};
|
|
149
|
+
recordDef.fields.forEach((field) => {
|
|
150
|
+
nameToFieldDef[field.name] = field;
|
|
151
|
+
});
|
|
152
|
+
if (value.kind === "object") {
|
|
153
|
+
pushTypeHint();
|
|
154
|
+
for (const keyValue of Object.values(value.keyValues)) {
|
|
155
|
+
const { key, value } = keyValue;
|
|
156
|
+
const fieldDef = nameToFieldDef[key];
|
|
157
|
+
if (fieldDef) {
|
|
158
|
+
if (fieldDef.doc) {
|
|
159
|
+
this.hints.push({
|
|
160
|
+
segment: keyValue.keySegment,
|
|
161
|
+
message: fieldDef.doc,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
const valuePath: Path = {
|
|
165
|
+
kind: "field-value",
|
|
166
|
+
fieldName: key,
|
|
167
|
+
structPath: path,
|
|
168
|
+
};
|
|
169
|
+
this.validate(value, valuePath, fieldDef.type!);
|
|
170
|
+
} else {
|
|
171
|
+
this.errors.push({
|
|
172
|
+
kind: "error",
|
|
173
|
+
segment: keyValue.keySegment,
|
|
174
|
+
message: "Unknown field",
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
typeHintStack.pop();
|
|
179
|
+
} else {
|
|
180
|
+
this.errors.push({
|
|
181
|
+
kind: "error",
|
|
182
|
+
segment: value.firstToken,
|
|
183
|
+
message: "Expected: object",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
// Enum
|
|
188
|
+
const nameToVariantDef: { [name: string]: VariantDefinition } = {};
|
|
189
|
+
recordDef.variants.forEach((variant) => {
|
|
190
|
+
nameToVariantDef[variant.name] = variant;
|
|
191
|
+
});
|
|
192
|
+
if (value.kind === "object") {
|
|
193
|
+
pushTypeHint();
|
|
194
|
+
this.validateEnumObject(value, path, nameToVariantDef);
|
|
195
|
+
typeHintStack.pop();
|
|
196
|
+
} else if (value.kind === "literal" && value.type === "string") {
|
|
197
|
+
const name = JSON.parse(value.jsonCode);
|
|
198
|
+
const fieldDef = nameToVariantDef[name];
|
|
199
|
+
if (name === "UNKNOWN" || fieldDef) {
|
|
200
|
+
pushTypeHint();
|
|
201
|
+
typeHintStack.pop();
|
|
202
|
+
} else {
|
|
203
|
+
this.errors.push({
|
|
204
|
+
kind: "error",
|
|
205
|
+
segment: value.segment,
|
|
206
|
+
message: "Unknown variant",
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
this.errors.push({
|
|
211
|
+
kind: "error",
|
|
212
|
+
segment: value.firstToken,
|
|
213
|
+
message: "Expected: object or string",
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
default: {
|
|
220
|
+
const _: never = schema;
|
|
221
|
+
throw new Error(_);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
validateEnumObject(
|
|
227
|
+
object: JsonObject,
|
|
228
|
+
enumPath: Path,
|
|
229
|
+
nameToVariantDef: { [name: string]: VariantDefinition },
|
|
230
|
+
): void {
|
|
231
|
+
const kindKv = object.keyValues["kind"];
|
|
232
|
+
if (!kindKv) {
|
|
233
|
+
this.errors.push({
|
|
234
|
+
kind: "error",
|
|
235
|
+
segment: object.segment,
|
|
236
|
+
message: "Missing: 'kind'",
|
|
237
|
+
});
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (kindKv.value.kind !== "literal" || kindKv.value.type !== "string") {
|
|
241
|
+
this.errors.push({
|
|
242
|
+
kind: "error",
|
|
243
|
+
segment: kindKv.value.firstToken,
|
|
244
|
+
message: "Expected: string",
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const kind: string = JSON.parse(kindKv.value.jsonCode);
|
|
249
|
+
if (kind !== kind.toLowerCase()) {
|
|
250
|
+
this.errors.push({
|
|
251
|
+
kind: "error",
|
|
252
|
+
segment: kindKv.value.segment,
|
|
253
|
+
message: "Expected: lowercase variant name",
|
|
254
|
+
});
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const variantDef = nameToVariantDef[kind];
|
|
258
|
+
if (!variantDef) {
|
|
259
|
+
this.errors.push({
|
|
260
|
+
kind: "error",
|
|
261
|
+
segment: kindKv.value.segment,
|
|
262
|
+
message: "Unknown variant",
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const valueKv = object.keyValues["value"];
|
|
267
|
+
if (!valueKv) {
|
|
268
|
+
this.errors.push({
|
|
269
|
+
kind: "error",
|
|
270
|
+
segment: object.segment,
|
|
271
|
+
message: "Missing: 'value'",
|
|
272
|
+
});
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
for (const key of object.allKeys) {
|
|
276
|
+
if (key.key !== "kind" && key.key !== "value") {
|
|
277
|
+
this.errors.push({
|
|
278
|
+
kind: "error",
|
|
279
|
+
segment: key.keySegment,
|
|
280
|
+
message: "Unexpected entry",
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const path: Path = {
|
|
285
|
+
kind: "variant-value",
|
|
286
|
+
variantName: variantDef.name,
|
|
287
|
+
enumPath: enumPath,
|
|
288
|
+
};
|
|
289
|
+
this.validate(valueKv.value, path, variantDef.type!);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private validateTimestamp(value: JsonValue): boolean {
|
|
293
|
+
if (value.kind !== "object") {
|
|
294
|
+
this.errors.push({
|
|
295
|
+
kind: "error",
|
|
296
|
+
segment: value.firstToken,
|
|
297
|
+
message: "Expected: timestamp",
|
|
298
|
+
});
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
const unixMillisKv = value.keyValues["unix_millis"];
|
|
302
|
+
if (!unixMillisKv) {
|
|
303
|
+
this.errors.push({
|
|
304
|
+
kind: "error",
|
|
305
|
+
segment: value.firstToken,
|
|
306
|
+
message: "Missing: 'unix_millis'",
|
|
307
|
+
});
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
if (
|
|
311
|
+
unixMillisKv.value.kind !== "literal" ||
|
|
312
|
+
unixMillisKv.value.type !== "number"
|
|
313
|
+
) {
|
|
314
|
+
this.errors.push({
|
|
315
|
+
kind: "error",
|
|
316
|
+
segment: unixMillisKv.value.firstToken,
|
|
317
|
+
message: "Expected: number",
|
|
318
|
+
});
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
const unixMillis = toJson(unixMillisKv.value) as number;
|
|
322
|
+
if (unixMillis < -8640000000000000 || 8640000000000000 < unixMillis) {
|
|
323
|
+
this.errors.push({
|
|
324
|
+
kind: "error",
|
|
325
|
+
segment: unixMillisKv.value.firstToken,
|
|
326
|
+
message: "Timestamp out of range",
|
|
327
|
+
});
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
// At this point: the timestamp is technically valid.
|
|
331
|
+
const formatted = value.keyValues["formatted"];
|
|
332
|
+
if (!formatted) {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
if (
|
|
336
|
+
formatted.value.kind !== "literal" ||
|
|
337
|
+
formatted.value.type !== "string"
|
|
338
|
+
) {
|
|
339
|
+
this.errors.push({
|
|
340
|
+
kind: "error",
|
|
341
|
+
segment: formatted.value.firstToken,
|
|
342
|
+
message: "Expected: string",
|
|
343
|
+
});
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
const formattedStr = toJson(formatted.value) as string;
|
|
347
|
+
if (formattedStr === "n/a") {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
const parsedMillis = Date.parse(formattedStr);
|
|
351
|
+
if (Number.isNaN(parsedMillis)) {
|
|
352
|
+
this.errors.push({
|
|
353
|
+
kind: "error",
|
|
354
|
+
segment: formatted.value.firstToken,
|
|
355
|
+
message: "Invalid ISO 8601 date string",
|
|
356
|
+
});
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
if (parsedMillis !== unixMillis) {
|
|
360
|
+
this.errors.push({
|
|
361
|
+
kind: "error",
|
|
362
|
+
segment: formatted.value.firstToken,
|
|
363
|
+
message: "Does not match 'unix_millis'",
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
for (const key of value.allKeys) {
|
|
367
|
+
if (key.key !== "unix_millis" && key.key !== "formatted") {
|
|
368
|
+
this.errors.push({
|
|
369
|
+
kind: "error",
|
|
370
|
+
segment: key.keySegment,
|
|
371
|
+
message: "Unexpected entry",
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function extractItemKey(
|
|
380
|
+
item: JsonValue,
|
|
381
|
+
keyExtractor: string | undefined,
|
|
382
|
+
): string | null {
|
|
383
|
+
if (keyExtractor === undefined) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
const pieces = keyExtractor.split(".");
|
|
387
|
+
let value = item;
|
|
388
|
+
for (const piece of pieces) {
|
|
389
|
+
if (value.kind !== "object") {
|
|
390
|
+
// Error
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
const kv = value.keyValues[piece];
|
|
394
|
+
if (!kv) {
|
|
395
|
+
// Error
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
value = kv.value;
|
|
399
|
+
}
|
|
400
|
+
switch (value.kind) {
|
|
401
|
+
case "literal": {
|
|
402
|
+
switch (value.type) {
|
|
403
|
+
case "string":
|
|
404
|
+
case "number":
|
|
405
|
+
case "boolean":
|
|
406
|
+
return value.jsonCode;
|
|
407
|
+
}
|
|
408
|
+
// Error
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
case "object": {
|
|
412
|
+
const kv = value.keyValues["unix_millis"];
|
|
413
|
+
if (kv && kv.value.kind === "literal" && kv.value.type === "number") {
|
|
414
|
+
return kv.value.jsonCode;
|
|
415
|
+
} else {
|
|
416
|
+
// Error
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
case "array": {
|
|
421
|
+
// Error
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function hasPrimitiveType(
|
|
428
|
+
value: JsonValue,
|
|
429
|
+
expectedType: PrimitiveType,
|
|
430
|
+
): boolean {
|
|
431
|
+
switch (expectedType) {
|
|
432
|
+
case "bool":
|
|
433
|
+
return value.kind === "literal" && value.type === "boolean";
|
|
434
|
+
case "int32": {
|
|
435
|
+
return isInteger(value, -2147483648n, 2147483647n);
|
|
436
|
+
}
|
|
437
|
+
case "int64": {
|
|
438
|
+
return isInteger(value, -9223372036854775808n, 9223372036854775807n);
|
|
439
|
+
}
|
|
440
|
+
case "hash64": {
|
|
441
|
+
return isInteger(value, 0n, 18446744073709551615n);
|
|
442
|
+
}
|
|
443
|
+
case "float32":
|
|
444
|
+
case "float64": {
|
|
445
|
+
return isFloat(value);
|
|
446
|
+
}
|
|
447
|
+
case "string": {
|
|
448
|
+
return value.kind === "literal" && value.type === "string";
|
|
449
|
+
}
|
|
450
|
+
case "bytes": {
|
|
451
|
+
if (value.kind !== "literal") {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
primitiveSerializer("bytes").fromJsonCode(value.jsonCode);
|
|
456
|
+
return true;
|
|
457
|
+
} catch {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
case "timestamp": {
|
|
462
|
+
// Case handled separately in validate()
|
|
463
|
+
throw new Error();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function isInteger(value: JsonValue, min: bigint, max: bigint): boolean {
|
|
469
|
+
if (value.kind !== "literal") {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
let decimalRep: string;
|
|
473
|
+
if (value.type === "string") {
|
|
474
|
+
decimalRep = JSON.parse(value.jsonCode);
|
|
475
|
+
} else if (value.type === "number") {
|
|
476
|
+
decimalRep = value.jsonCode;
|
|
477
|
+
} else {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
let int: bigint;
|
|
481
|
+
try {
|
|
482
|
+
int = BigInt(decimalRep);
|
|
483
|
+
} catch {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
return min <= int && int <= max;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function isFloat(value: JsonValue): boolean {
|
|
490
|
+
if (value.kind !== "literal") {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
if (value.type === "number") {
|
|
494
|
+
return true;
|
|
495
|
+
} else if (value.type === "string") {
|
|
496
|
+
try {
|
|
497
|
+
primitiveSerializer("float64").fromJsonCode(value.jsonCode);
|
|
498
|
+
return true;
|
|
499
|
+
} catch {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
} else {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function getTypeDesc(type: TypeSignature): string {
|
|
508
|
+
function getTypePart(type: TypeSignature): string {
|
|
509
|
+
switch (type.kind) {
|
|
510
|
+
case "primitive":
|
|
511
|
+
return type.value;
|
|
512
|
+
case "array":
|
|
513
|
+
return `[${getTypePart(type.value.item)}]`;
|
|
514
|
+
case "optional":
|
|
515
|
+
return `${getTypePart(type.value)}?`;
|
|
516
|
+
case "record": {
|
|
517
|
+
const recordId = type.value;
|
|
518
|
+
return recordId.split(":")[1];
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function getModulePart(type: TypeSignature): string | null {
|
|
523
|
+
switch (type.kind) {
|
|
524
|
+
case "primitive":
|
|
525
|
+
return null;
|
|
526
|
+
case "array":
|
|
527
|
+
return getModulePart(type.value.item);
|
|
528
|
+
case "optional":
|
|
529
|
+
return getModulePart(type.value);
|
|
530
|
+
case "record": {
|
|
531
|
+
const recordId = type.value;
|
|
532
|
+
return recordId.split(":")[0];
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const typePart = getTypePart(type);
|
|
537
|
+
const modulePart = getModulePart(type);
|
|
538
|
+
return modulePart ? `${typePart} (${modulePart})` : typePart;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function getTypeDoc(
|
|
542
|
+
type: TypeSignature,
|
|
543
|
+
idToRecordDef: Readonly<{ [id: string]: RecordDefinition }>,
|
|
544
|
+
): string | undefined {
|
|
545
|
+
switch (type.kind) {
|
|
546
|
+
case "primitive":
|
|
547
|
+
return undefined;
|
|
548
|
+
case "array":
|
|
549
|
+
return getTypeDoc(type.value.item, idToRecordDef);
|
|
550
|
+
case "optional":
|
|
551
|
+
return getTypeDoc(type.value, idToRecordDef);
|
|
552
|
+
case "record": {
|
|
553
|
+
const recordId = type.value;
|
|
554
|
+
const recordDef = idToRecordDef[recordId]!;
|
|
555
|
+
return recordDef.doc;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { expect } from "buckwheat";
|
|
2
|
+
import { describe, it } from "mocha";
|
|
3
|
+
import { parseJsonValue } from "./json_parser.js";
|
|
4
|
+
import { makeJsonTemplate, toJson } from "./to_json.js";
|
|
5
|
+
import { JsonValue, RecordDefinition, TypeSignature } from "./types.js";
|
|
6
|
+
|
|
7
|
+
function parse(json: string): JsonValue {
|
|
8
|
+
const result = parseJsonValue(json);
|
|
9
|
+
if (!result.value) {
|
|
10
|
+
throw new Error(`JSON parse error: ${JSON.stringify(result.errors)}`);
|
|
11
|
+
}
|
|
12
|
+
return result.value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("to_json", () => {
|
|
16
|
+
describe("toJson", () => {
|
|
17
|
+
it("converts primitives", () => {
|
|
18
|
+
expect(toJson(parse("true"))).toBe(true);
|
|
19
|
+
expect(toJson(parse("false"))).toBe(false);
|
|
20
|
+
expect(toJson(parse("null"))).toBe(null);
|
|
21
|
+
expect(toJson(parse("123"))).toBe(123);
|
|
22
|
+
expect(toJson(parse('"hello"'))).toBe("hello");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("converts array", () => {
|
|
26
|
+
expect(toJson(parse("[1, 2, 3]"))).toMatch([1, 2, 3]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("converts object", () => {
|
|
30
|
+
expect(toJson(parse('{"a": 1, "b": "2"}'))).toMatch({ a: 1, b: "2" });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("converts nested structure", () => {
|
|
34
|
+
const input = '{"arr": [1, {"x": "y"}], "obj": {"z": null}}';
|
|
35
|
+
expect(toJson(parse(input))).toMatch({
|
|
36
|
+
arr: [1, { x: "y" }],
|
|
37
|
+
obj: { z: null },
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("makeJsonTemplate", () => {
|
|
43
|
+
const emptyRecords: { [id: string]: RecordDefinition } = {};
|
|
44
|
+
|
|
45
|
+
it("creates template for array", () => {
|
|
46
|
+
const type: TypeSignature = {
|
|
47
|
+
kind: "array",
|
|
48
|
+
value: { item: { kind: "primitive", value: "int32" } },
|
|
49
|
+
};
|
|
50
|
+
expect(makeJsonTemplate(type, emptyRecords)).toMatch([]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("creates template for optional", () => {
|
|
54
|
+
const type: TypeSignature = {
|
|
55
|
+
kind: "optional",
|
|
56
|
+
value: { kind: "primitive", value: "int32" },
|
|
57
|
+
};
|
|
58
|
+
expect(makeJsonTemplate(type, emptyRecords)).toBe(null);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("creates template for primitives", () => {
|
|
62
|
+
expect(
|
|
63
|
+
makeJsonTemplate({ kind: "primitive", value: "bool" }, emptyRecords),
|
|
64
|
+
).toBe(false);
|
|
65
|
+
expect(
|
|
66
|
+
makeJsonTemplate({ kind: "primitive", value: "int32" }, emptyRecords),
|
|
67
|
+
).toBe(0);
|
|
68
|
+
expect(
|
|
69
|
+
makeJsonTemplate({ kind: "primitive", value: "float64" }, emptyRecords),
|
|
70
|
+
).toBe(0);
|
|
71
|
+
expect(
|
|
72
|
+
makeJsonTemplate({ kind: "primitive", value: "int64" }, emptyRecords),
|
|
73
|
+
).toBe("0");
|
|
74
|
+
expect(
|
|
75
|
+
makeJsonTemplate({ kind: "primitive", value: "string" }, emptyRecords),
|
|
76
|
+
).toBe("");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("creates template for struct", () => {
|
|
80
|
+
const records: { [id: string]: RecordDefinition } = {
|
|
81
|
+
MyStruct: {
|
|
82
|
+
kind: "struct",
|
|
83
|
+
id: "MyStruct",
|
|
84
|
+
fields: [
|
|
85
|
+
{
|
|
86
|
+
name: "foo",
|
|
87
|
+
number: 1,
|
|
88
|
+
type: { kind: "primitive", value: "string" },
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "bar",
|
|
92
|
+
number: 2,
|
|
93
|
+
type: { kind: "primitive", value: "int32" },
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
const type: TypeSignature = { kind: "record", value: "MyStruct" };
|
|
99
|
+
expect(makeJsonTemplate(type, records)).toMatch({
|
|
100
|
+
foo: "",
|
|
101
|
+
bar: 0,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("creates template for nested struct (depth limit)", () => {
|
|
106
|
+
const records: { [id: string]: RecordDefinition } = {
|
|
107
|
+
Parent: {
|
|
108
|
+
kind: "struct",
|
|
109
|
+
id: "Parent",
|
|
110
|
+
fields: [
|
|
111
|
+
{
|
|
112
|
+
name: "child",
|
|
113
|
+
number: 1,
|
|
114
|
+
type: { kind: "record", value: "Child" },
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
Child: {
|
|
119
|
+
kind: "struct",
|
|
120
|
+
id: "Child",
|
|
121
|
+
fields: [
|
|
122
|
+
{
|
|
123
|
+
name: "val",
|
|
124
|
+
number: 1,
|
|
125
|
+
type: { kind: "primitive", value: "int32" },
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
const type: TypeSignature = { kind: "record", value: "Parent" };
|
|
131
|
+
// Expect child to be {}, because depth logic in makeJsonTemplate
|
|
132
|
+
// passes "depth" to recursive call for struct fields.
|
|
133
|
+
expect(makeJsonTemplate(type, records)).toMatch({
|
|
134
|
+
child: {},
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("creates template for enum", () => {
|
|
139
|
+
const records: { [id: string]: RecordDefinition } = {
|
|
140
|
+
MyEnum: {
|
|
141
|
+
kind: "enum",
|
|
142
|
+
id: "MyEnum",
|
|
143
|
+
variants: [],
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
const type: TypeSignature = { kind: "record", value: "MyEnum" };
|
|
147
|
+
expect(makeJsonTemplate(type, records)).toBe("UNKNOWN");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|