skir-codemirror-plugin 1.0.2 → 1.0.4
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 +15 -6
- package/dist/codemirror/create_editor_state.d.ts +4 -2
- package/dist/codemirror/create_editor_state.d.ts.map +1 -1
- package/dist/codemirror/create_editor_state.js +178 -31
- package/dist/codemirror/create_editor_state.js.map +1 -1
- package/dist/codemirror/json_linter.d.ts.map +1 -1
- package/dist/codemirror/json_linter.js +67 -3
- package/dist/codemirror/json_linter.js.map +1 -1
- package/dist/codemirror/json_state.d.ts +5 -1
- package/dist/codemirror/json_state.d.ts.map +1 -1
- package/dist/codemirror/json_state.js +26 -1
- package/dist/codemirror/json_state.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/json/json_parser.js +35 -1
- package/dist/json/json_parser.js.map +1 -1
- package/dist/json/json_parser.test.js +48 -0
- package/dist/json/json_parser.test.js.map +1 -1
- package/dist/json/schema_validator.d.ts.map +1 -1
- package/dist/json/schema_validator.js +22 -14
- package/dist/json/schema_validator.js.map +1 -1
- package/dist/json/schema_validator.test.js +146 -5
- package/dist/json/schema_validator.test.js.map +1 -1
- package/dist/json/to_json.d.ts +1 -1
- package/dist/json/to_json.d.ts.map +1 -1
- package/dist/json/to_json.js +3 -3
- package/dist/json/to_json.js.map +1 -1
- package/dist/json/types.d.ts +4 -0
- package/dist/json/types.d.ts.map +1 -1
- package/dist/json/types.js.map +1 -1
- package/package.json +4 -6
- package/src/codemirror/create_editor_state.ts +272 -31
- package/src/codemirror/json_linter.ts +89 -4
- package/src/codemirror/json_state.ts +44 -1
- package/src/index.ts +1 -0
- package/src/json/json_parser.test.ts +51 -0
- package/src/json/json_parser.ts +37 -1
- package/src/json/schema_validator.test.ts +166 -5
- package/src/json/schema_validator.ts +26 -13
- package/src/json/to_json.ts +3 -3
- package/src/json/types.ts +7 -0
|
@@ -9,6 +9,7 @@ import { parseJsonValue } from "../json/json_parser";
|
|
|
9
9
|
import { validateSchema } from "../json/schema_validator";
|
|
10
10
|
import type {
|
|
11
11
|
JsonParseResult,
|
|
12
|
+
RecordDefinition,
|
|
12
13
|
TypeDefinition,
|
|
13
14
|
ValidationResult,
|
|
14
15
|
} from "../json/types";
|
|
@@ -17,6 +18,8 @@ export interface JsonState {
|
|
|
17
18
|
readonly parseResult: JsonParseResult;
|
|
18
19
|
readonly validationResult?: ValidationResult;
|
|
19
20
|
readonly source: string;
|
|
21
|
+
readonly schema: TypeDefinition;
|
|
22
|
+
readonly recordIdToDefinition: { [id: string]: RecordDefinition };
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
const updateJsonState = StateEffect.define<JsonState>();
|
|
@@ -58,7 +61,15 @@ export function ensureJsonState(
|
|
|
58
61
|
validationResult = validateSchema(parseResult.value, schema);
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
const
|
|
64
|
+
const recordIdToDefinition = indexRecordDefinitions(schema, currentState);
|
|
65
|
+
|
|
66
|
+
const newState: JsonState = {
|
|
67
|
+
parseResult,
|
|
68
|
+
validationResult,
|
|
69
|
+
source,
|
|
70
|
+
schema,
|
|
71
|
+
recordIdToDefinition,
|
|
72
|
+
};
|
|
62
73
|
|
|
63
74
|
// Update the state if it's different
|
|
64
75
|
if (!currentState || currentState !== newState) {
|
|
@@ -139,21 +150,37 @@ export function debouncedJsonParser(schema: TypeDefinition): Extension[] {
|
|
|
139
150
|
insert: edit.replacement,
|
|
140
151
|
}));
|
|
141
152
|
|
|
153
|
+
const oldState = this.view.state.field(jsonStateField, false);
|
|
154
|
+
const recordIdToDefinition = indexRecordDefinitions(
|
|
155
|
+
schema,
|
|
156
|
+
oldState,
|
|
157
|
+
);
|
|
158
|
+
|
|
142
159
|
this.view.dispatch({
|
|
143
160
|
changes,
|
|
144
161
|
effects: updateJsonState.of({
|
|
145
162
|
parseResult,
|
|
146
163
|
validationResult,
|
|
147
164
|
source,
|
|
165
|
+
schema,
|
|
166
|
+
recordIdToDefinition,
|
|
148
167
|
}),
|
|
149
168
|
scrollIntoView: true,
|
|
150
169
|
});
|
|
151
170
|
} else {
|
|
171
|
+
const oldState = this.view.state.field(jsonStateField, false);
|
|
172
|
+
const recordIdToDefinition = indexRecordDefinitions(
|
|
173
|
+
schema,
|
|
174
|
+
oldState,
|
|
175
|
+
);
|
|
176
|
+
|
|
152
177
|
this.view.dispatch({
|
|
153
178
|
effects: updateJsonState.of({
|
|
154
179
|
parseResult,
|
|
155
180
|
validationResult,
|
|
156
181
|
source,
|
|
182
|
+
schema,
|
|
183
|
+
recordIdToDefinition,
|
|
157
184
|
}),
|
|
158
185
|
});
|
|
159
186
|
}
|
|
@@ -168,3 +195,19 @@ export function debouncedJsonParser(schema: TypeDefinition): Extension[] {
|
|
|
168
195
|
),
|
|
169
196
|
];
|
|
170
197
|
}
|
|
198
|
+
|
|
199
|
+
function indexRecordDefinitions(
|
|
200
|
+
schema: TypeDefinition,
|
|
201
|
+
oldState: JsonState | null | undefined,
|
|
202
|
+
): {
|
|
203
|
+
[id: string]: RecordDefinition;
|
|
204
|
+
} {
|
|
205
|
+
if (schema === oldState?.schema) {
|
|
206
|
+
return oldState.recordIdToDefinition;
|
|
207
|
+
}
|
|
208
|
+
const idToRecordDef: { [id: string]: RecordDefinition } = {};
|
|
209
|
+
for (const recordDef of schema.records) {
|
|
210
|
+
idToRecordDef[recordDef.id] = recordDef;
|
|
211
|
+
}
|
|
212
|
+
return idToRecordDef;
|
|
213
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -63,6 +63,18 @@ describe("json_parser", () => {
|
|
|
63
63
|
});
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
it("tracks indent for top-level literal", () => {
|
|
67
|
+
expect(parseJsonValue(" true")).toMatch({
|
|
68
|
+
value: {
|
|
69
|
+
kind: "literal",
|
|
70
|
+
jsonCode: "true",
|
|
71
|
+
type: "boolean",
|
|
72
|
+
indent: 3,
|
|
73
|
+
},
|
|
74
|
+
errors: [],
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
66
78
|
it("parses empty array", () => {
|
|
67
79
|
expect(parseJsonValue("[]")).toMatch({
|
|
68
80
|
value: { kind: "array", values: [] },
|
|
@@ -119,6 +131,45 @@ describe("json_parser", () => {
|
|
|
119
131
|
});
|
|
120
132
|
});
|
|
121
133
|
|
|
134
|
+
it("tracks indent from line leading spaces for field values", () => {
|
|
135
|
+
const result = parseJsonValue(`{
|
|
136
|
+
"a": 1,
|
|
137
|
+
"b": [
|
|
138
|
+
2
|
|
139
|
+
]
|
|
140
|
+
}`);
|
|
141
|
+
|
|
142
|
+
expect(result).toMatch({
|
|
143
|
+
value: {
|
|
144
|
+
kind: "object",
|
|
145
|
+
indent: 0,
|
|
146
|
+
keyValues: {
|
|
147
|
+
a: {
|
|
148
|
+
value: {
|
|
149
|
+
kind: "literal",
|
|
150
|
+
jsonCode: "1",
|
|
151
|
+
indent: 2,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
b: {
|
|
155
|
+
value: {
|
|
156
|
+
kind: "array",
|
|
157
|
+
indent: 2,
|
|
158
|
+
values: [
|
|
159
|
+
{
|
|
160
|
+
kind: "literal",
|
|
161
|
+
jsonCode: "2",
|
|
162
|
+
indent: 4,
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
errors: [],
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
122
173
|
it("parses arrays with whitespace", () => {
|
|
123
174
|
expect(parseJsonValue(" [ 1 ] ")).toMatch({
|
|
124
175
|
value: {
|
package/src/json/json_parser.ts
CHANGED
|
@@ -33,6 +33,7 @@ export function parseJsonValue(input: string): JsonParseResult {
|
|
|
33
33
|
interface JsonToken {
|
|
34
34
|
segment: Segment;
|
|
35
35
|
jsonCode: string;
|
|
36
|
+
indent: number;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
interface JsonTokens {
|
|
@@ -43,16 +44,29 @@ interface JsonTokens {
|
|
|
43
44
|
function tokenize(input: string): JsonTokens | JsonError {
|
|
44
45
|
const tokens: JsonToken[] = [];
|
|
45
46
|
let pos = 0;
|
|
47
|
+
let lineIndent = 0;
|
|
48
|
+
let lineHasContent = false;
|
|
46
49
|
|
|
47
50
|
const whitespaceRegex = /[ \t\r\n]*/y;
|
|
48
51
|
const tokenRegex =
|
|
49
52
|
/([[\]{}:,]|(-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)|false|true|null|("(((?=\\)\\(["\\/ bfnrt]|u[0-9a-fA-F]{4}))|[^"\\\0-\x1F\x7F]+)*")|$)/y;
|
|
50
53
|
|
|
51
54
|
while (true) {
|
|
55
|
+
const beforeWhitespacePos = pos;
|
|
52
56
|
whitespaceRegex.lastIndex = pos;
|
|
53
57
|
whitespaceRegex.exec(input);
|
|
54
58
|
pos = whitespaceRegex.lastIndex;
|
|
55
59
|
|
|
60
|
+
for (let i = beforeWhitespacePos; i < pos; ++i) {
|
|
61
|
+
const ch = input[i];
|
|
62
|
+
if (ch === "\n") {
|
|
63
|
+
lineIndent = 0;
|
|
64
|
+
lineHasContent = false;
|
|
65
|
+
} else if (!lineHasContent && (ch === " " || ch === "\t")) {
|
|
66
|
+
lineIndent++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
tokenRegex.lastIndex = pos;
|
|
57
71
|
const tokenMatch = tokenRegex.exec(input);
|
|
58
72
|
if (tokenMatch) {
|
|
@@ -61,9 +75,16 @@ function tokenize(input: string): JsonTokens | JsonError {
|
|
|
61
75
|
start: pos,
|
|
62
76
|
end: pos + tokenText.length,
|
|
63
77
|
};
|
|
64
|
-
const token: JsonToken = {
|
|
78
|
+
const token: JsonToken = {
|
|
79
|
+
segment,
|
|
80
|
+
jsonCode: tokenText,
|
|
81
|
+
indent: lineIndent,
|
|
82
|
+
};
|
|
65
83
|
pos = tokenRegex.lastIndex;
|
|
66
84
|
tokens.push(token);
|
|
85
|
+
if (tokenText !== "") {
|
|
86
|
+
lineHasContent = true;
|
|
87
|
+
}
|
|
67
88
|
if (tokenText === "") {
|
|
68
89
|
return {
|
|
69
90
|
kind: "tokens",
|
|
@@ -96,6 +117,8 @@ class JsonParser {
|
|
|
96
117
|
const token = this.peekToken();
|
|
97
118
|
const firstChar = token.jsonCode ? token.jsonCode[0] : "";
|
|
98
119
|
|
|
120
|
+
const { indent } = token;
|
|
121
|
+
|
|
99
122
|
switch (firstChar) {
|
|
100
123
|
case "[":
|
|
101
124
|
return this.parseArray();
|
|
@@ -109,6 +132,7 @@ class JsonParser {
|
|
|
109
132
|
segment: token.segment,
|
|
110
133
|
jsonCode: "null",
|
|
111
134
|
type: "null",
|
|
135
|
+
indent,
|
|
112
136
|
};
|
|
113
137
|
case "f":
|
|
114
138
|
this.nextToken();
|
|
@@ -118,6 +142,7 @@ class JsonParser {
|
|
|
118
142
|
segment: token.segment,
|
|
119
143
|
jsonCode: "false",
|
|
120
144
|
type: "boolean",
|
|
145
|
+
indent,
|
|
121
146
|
};
|
|
122
147
|
case "t":
|
|
123
148
|
this.nextToken();
|
|
@@ -127,6 +152,7 @@ class JsonParser {
|
|
|
127
152
|
segment: token.segment,
|
|
128
153
|
jsonCode: "true",
|
|
129
154
|
type: "boolean",
|
|
155
|
+
indent,
|
|
130
156
|
};
|
|
131
157
|
case '"':
|
|
132
158
|
this.nextToken();
|
|
@@ -136,6 +162,7 @@ class JsonParser {
|
|
|
136
162
|
segment: token.segment,
|
|
137
163
|
jsonCode: token.jsonCode,
|
|
138
164
|
type: "string",
|
|
165
|
+
indent,
|
|
139
166
|
};
|
|
140
167
|
case "0":
|
|
141
168
|
case "1":
|
|
@@ -155,6 +182,7 @@ class JsonParser {
|
|
|
155
182
|
segment: token.segment,
|
|
156
183
|
jsonCode: token.jsonCode,
|
|
157
184
|
type: "number",
|
|
185
|
+
indent,
|
|
158
186
|
};
|
|
159
187
|
}
|
|
160
188
|
|
|
@@ -169,6 +197,7 @@ class JsonParser {
|
|
|
169
197
|
|
|
170
198
|
private parseArray(): JsonArray {
|
|
171
199
|
const leftBracket = this.nextToken();
|
|
200
|
+
const { indent } = leftBracket;
|
|
172
201
|
const values: JsonValue[] = [];
|
|
173
202
|
while (true) {
|
|
174
203
|
if (this.peekToken().jsonCode === "]") {
|
|
@@ -181,6 +210,7 @@ class JsonParser {
|
|
|
181
210
|
end: rightBracket.segment.end,
|
|
182
211
|
},
|
|
183
212
|
values,
|
|
213
|
+
indent,
|
|
184
214
|
};
|
|
185
215
|
}
|
|
186
216
|
if (this.peekToken().jsonCode === "}") {
|
|
@@ -198,6 +228,7 @@ class JsonParser {
|
|
|
198
228
|
end: wrongBracket.segment.end,
|
|
199
229
|
},
|
|
200
230
|
values,
|
|
231
|
+
indent,
|
|
201
232
|
};
|
|
202
233
|
}
|
|
203
234
|
if (this.peekToken().jsonCode === "") {
|
|
@@ -211,6 +242,7 @@ class JsonParser {
|
|
|
211
242
|
end: this.peekToken().segment.start,
|
|
212
243
|
},
|
|
213
244
|
values,
|
|
245
|
+
indent,
|
|
214
246
|
};
|
|
215
247
|
}
|
|
216
248
|
const value = this.parseValueOrSkip();
|
|
@@ -233,6 +265,7 @@ class JsonParser {
|
|
|
233
265
|
|
|
234
266
|
private parseObject(): JsonObject {
|
|
235
267
|
const leftBracket = this.nextToken();
|
|
268
|
+
const { indent } = leftBracket;
|
|
236
269
|
const keyValues: { [key: string]: JsonKeyValue } = {};
|
|
237
270
|
const allKeys: JsonKey[] = [];
|
|
238
271
|
while (true) {
|
|
@@ -247,6 +280,7 @@ class JsonParser {
|
|
|
247
280
|
},
|
|
248
281
|
keyValues,
|
|
249
282
|
allKeys,
|
|
283
|
+
indent,
|
|
250
284
|
};
|
|
251
285
|
}
|
|
252
286
|
if (this.peekToken().jsonCode === "]") {
|
|
@@ -265,6 +299,7 @@ class JsonParser {
|
|
|
265
299
|
},
|
|
266
300
|
keyValues,
|
|
267
301
|
allKeys,
|
|
302
|
+
indent,
|
|
268
303
|
};
|
|
269
304
|
}
|
|
270
305
|
if (this.peekToken().jsonCode === "") {
|
|
@@ -279,6 +314,7 @@ class JsonParser {
|
|
|
279
314
|
},
|
|
280
315
|
keyValues,
|
|
281
316
|
allKeys,
|
|
317
|
+
indent,
|
|
282
318
|
};
|
|
283
319
|
}
|
|
284
320
|
const keyToken = this.peekToken();
|
|
@@ -152,7 +152,7 @@ describe("schema_validator", () => {
|
|
|
152
152
|
]);
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
-
it("validates enum (string literal)", () => {
|
|
155
|
+
it("validates enum (string literal accepts lower and upper)", () => {
|
|
156
156
|
const schema: TypeDefinition = {
|
|
157
157
|
type: { kind: "record", value: "MyEnum" },
|
|
158
158
|
records: [
|
|
@@ -160,14 +160,100 @@ describe("schema_validator", () => {
|
|
|
160
160
|
kind: "enum",
|
|
161
161
|
id: "MyEnum",
|
|
162
162
|
variants: [
|
|
163
|
-
{ name: "
|
|
164
|
-
{ name: "
|
|
163
|
+
{ name: "FOO", number: 1 },
|
|
164
|
+
{ name: "BAR", number: 2 },
|
|
165
165
|
],
|
|
166
166
|
},
|
|
167
167
|
],
|
|
168
168
|
};
|
|
169
|
-
const
|
|
170
|
-
expect(
|
|
169
|
+
const lowerResult = validateSchema(parse('"foo"'), schema);
|
|
170
|
+
expect(lowerResult).toMatch({ errors: [] });
|
|
171
|
+
|
|
172
|
+
const upperResult = validateSchema(parse('"FOO"'), schema);
|
|
173
|
+
expect(upperResult).toMatch({ errors: [] });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("rejects mixed-case enum string literal", () => {
|
|
177
|
+
const schema: TypeDefinition = {
|
|
178
|
+
type: { kind: "record", value: "MyEnum" },
|
|
179
|
+
records: [
|
|
180
|
+
{
|
|
181
|
+
kind: "enum",
|
|
182
|
+
id: "MyEnum",
|
|
183
|
+
variants: [{ name: "FOO", number: 1 }],
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const result = validateSchema(parse('"Foo"'), schema);
|
|
189
|
+
expect(result.errors).toMatch([
|
|
190
|
+
{
|
|
191
|
+
message: "Unknown variant",
|
|
192
|
+
},
|
|
193
|
+
]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("accepts UNKNOWN and unknown enum literals", () => {
|
|
197
|
+
const schema: TypeDefinition = {
|
|
198
|
+
type: { kind: "record", value: "MyEnum" },
|
|
199
|
+
records: [
|
|
200
|
+
{
|
|
201
|
+
kind: "enum",
|
|
202
|
+
id: "MyEnum",
|
|
203
|
+
variants: [{ name: "FOO", number: 1 }],
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
expect(validateSchema(parse('"UNKNOWN"'), schema)).toMatch({ errors: [] });
|
|
209
|
+
expect(validateSchema(parse('"unknown"'), schema)).toMatch({ errors: [] });
|
|
210
|
+
|
|
211
|
+
const mixedCaseResult = validateSchema(parse('"Unknown"'), schema);
|
|
212
|
+
expect(mixedCaseResult.errors).toMatch([
|
|
213
|
+
{
|
|
214
|
+
message: "Unknown variant",
|
|
215
|
+
},
|
|
216
|
+
]);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("validates enum object kind in lower and upper", () => {
|
|
220
|
+
const schema: TypeDefinition = {
|
|
221
|
+
type: { kind: "record", value: "MyEnum" },
|
|
222
|
+
records: [
|
|
223
|
+
{
|
|
224
|
+
kind: "enum",
|
|
225
|
+
id: "MyEnum",
|
|
226
|
+
variants: [
|
|
227
|
+
{
|
|
228
|
+
name: "COMPLEX",
|
|
229
|
+
number: 1,
|
|
230
|
+
type: { kind: "primitive", value: "int32" },
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
expect(
|
|
238
|
+
validateSchema(parse('{"kind": "complex", "value": 123}'), schema),
|
|
239
|
+
).toMatch({
|
|
240
|
+
errors: [],
|
|
241
|
+
});
|
|
242
|
+
expect(
|
|
243
|
+
validateSchema(parse('{"kind": "COMPLEX", "value": 123}'), schema),
|
|
244
|
+
).toMatch({
|
|
245
|
+
errors: [],
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const mixedCaseResult = validateSchema(
|
|
249
|
+
parse('{"kind": "Complex", "value": 123}'),
|
|
250
|
+
schema,
|
|
251
|
+
);
|
|
252
|
+
expect(mixedCaseResult.errors).toMatch([
|
|
253
|
+
{
|
|
254
|
+
message: "Expected: lowercase or uppercase variant name",
|
|
255
|
+
},
|
|
256
|
+
]);
|
|
171
257
|
});
|
|
172
258
|
|
|
173
259
|
it("reports unknown enum variant", () => {
|
|
@@ -189,6 +275,81 @@ describe("schema_validator", () => {
|
|
|
189
275
|
]);
|
|
190
276
|
});
|
|
191
277
|
|
|
278
|
+
it("stores enum definition in type hint for UNKNOWN enum literal", () => {
|
|
279
|
+
const schema: TypeDefinition = {
|
|
280
|
+
type: { kind: "record", value: "MyEnum" },
|
|
281
|
+
records: [
|
|
282
|
+
{
|
|
283
|
+
kind: "enum",
|
|
284
|
+
id: "MyEnum",
|
|
285
|
+
variants: [
|
|
286
|
+
{ name: "First", number: 1 },
|
|
287
|
+
{ name: "Second", number: 2 },
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const result = validateSchema(parse('"UNKNOWN"'), schema);
|
|
294
|
+
expect(result.errors).toMatch([]);
|
|
295
|
+
if (!result.rootTypeHint) {
|
|
296
|
+
throw new Error("Expected root type hint");
|
|
297
|
+
}
|
|
298
|
+
expect(result.rootTypeHint.enumDefinition).toMatch({
|
|
299
|
+
kind: "enum",
|
|
300
|
+
id: "MyEnum",
|
|
301
|
+
variants: [
|
|
302
|
+
{ name: "First", number: 1 },
|
|
303
|
+
{ name: "Second", number: 2 },
|
|
304
|
+
],
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("stores enum definition in nested UNKNOWN enum field hint", () => {
|
|
309
|
+
const schema: TypeDefinition = {
|
|
310
|
+
type: { kind: "record", value: "MyStruct" },
|
|
311
|
+
records: [
|
|
312
|
+
{
|
|
313
|
+
kind: "struct",
|
|
314
|
+
id: "MyStruct",
|
|
315
|
+
fields: [
|
|
316
|
+
{
|
|
317
|
+
name: "status",
|
|
318
|
+
number: 1,
|
|
319
|
+
type: { kind: "record", value: "MyEnum" },
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
kind: "enum",
|
|
325
|
+
id: "MyEnum",
|
|
326
|
+
variants: [
|
|
327
|
+
{ name: "First", number: 1 },
|
|
328
|
+
{ name: "Second", number: 2 },
|
|
329
|
+
],
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const result = validateSchema(parse('{"status": "UNKNOWN"}'), schema);
|
|
335
|
+
expect(result.errors).toMatch([]);
|
|
336
|
+
if (!result.rootTypeHint) {
|
|
337
|
+
throw new Error("Expected root type hint");
|
|
338
|
+
}
|
|
339
|
+
if (result.rootTypeHint.childHints.length !== 1) {
|
|
340
|
+
throw new Error("Expected one child type hint");
|
|
341
|
+
}
|
|
342
|
+
const statusHint = result.rootTypeHint.childHints[0];
|
|
343
|
+
expect(statusHint.enumDefinition).toMatch({
|
|
344
|
+
kind: "enum",
|
|
345
|
+
id: "MyEnum",
|
|
346
|
+
variants: [
|
|
347
|
+
{ name: "First", number: 1 },
|
|
348
|
+
{ name: "Second", number: 2 },
|
|
349
|
+
],
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
192
353
|
it("validates enum (object with kind)", () => {
|
|
193
354
|
const schema: TypeDefinition = {
|
|
194
355
|
type: { kind: "record", value: "MyEnum" },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { primitiveSerializer } from "skir-client";
|
|
2
2
|
import { toJson } from "./to_json";
|
|
3
3
|
import type {
|
|
4
|
+
EnumDefinition,
|
|
4
5
|
FieldDefinition,
|
|
5
6
|
Hint,
|
|
6
7
|
JsonError,
|
|
@@ -60,7 +61,9 @@ class SchemaValidator {
|
|
|
60
61
|
const { idToRecordDef, typeHintStack } = this;
|
|
61
62
|
value.expectedType = schema;
|
|
62
63
|
// For every call to pushTypeHint() there emust be one call to typeHintStack.pop()
|
|
63
|
-
const pushTypeHint = (
|
|
64
|
+
const pushTypeHint = (options?: {
|
|
65
|
+
enumDefinition?: EnumDefinition;
|
|
66
|
+
}): void => {
|
|
64
67
|
const typeDesc = getTypeDesc(optionalSchema ?? schema);
|
|
65
68
|
const typeDoc = getTypeDoc(schema, idToRecordDef);
|
|
66
69
|
const message = [typeDesc];
|
|
@@ -78,6 +81,7 @@ class SchemaValidator {
|
|
|
78
81
|
message: message.length === 1 ? message[0] : message,
|
|
79
82
|
valueContext: { value, path },
|
|
80
83
|
childHints: [],
|
|
84
|
+
enumDefinition: options?.enumDefinition,
|
|
81
85
|
};
|
|
82
86
|
if (typeHintStack.length) {
|
|
83
87
|
const topOfStack = typeHintStack[typeHintStack.length - 1];
|
|
@@ -187,7 +191,8 @@ class SchemaValidator {
|
|
|
187
191
|
// Enum
|
|
188
192
|
const nameToVariantDef: { [name: string]: VariantDefinition } = {};
|
|
189
193
|
recordDef.variants.forEach((variant) => {
|
|
190
|
-
nameToVariantDef[variant.name] = variant;
|
|
194
|
+
nameToVariantDef[variant.name.toLowerCase()] = variant;
|
|
195
|
+
nameToVariantDef[variant.name.toUpperCase()] = variant;
|
|
191
196
|
});
|
|
192
197
|
if (value.kind === "object") {
|
|
193
198
|
pushTypeHint();
|
|
@@ -196,8 +201,14 @@ class SchemaValidator {
|
|
|
196
201
|
} else if (value.kind === "literal" && value.type === "string") {
|
|
197
202
|
const name = JSON.parse(value.jsonCode);
|
|
198
203
|
const fieldDef = nameToVariantDef[name];
|
|
199
|
-
if (
|
|
200
|
-
|
|
204
|
+
if (fieldDef && fieldDef.type) {
|
|
205
|
+
this.errors.push({
|
|
206
|
+
kind: "error",
|
|
207
|
+
segment: value.segment,
|
|
208
|
+
message: "Expected: uppercase variant name",
|
|
209
|
+
});
|
|
210
|
+
} else if (name === "unknown" || name === "UNKNOWN" || fieldDef) {
|
|
211
|
+
pushTypeHint({ enumDefinition: recordDef });
|
|
201
212
|
typeHintStack.pop();
|
|
202
213
|
} else {
|
|
203
214
|
this.errors.push({
|
|
@@ -246,11 +257,13 @@ class SchemaValidator {
|
|
|
246
257
|
return;
|
|
247
258
|
}
|
|
248
259
|
const kind: string = JSON.parse(kindKv.value.jsonCode);
|
|
249
|
-
|
|
260
|
+
const isLowercase = kind === kind.toLowerCase();
|
|
261
|
+
const isUppercase = kind === kind.toUpperCase();
|
|
262
|
+
if (!isLowercase && !isUppercase) {
|
|
250
263
|
this.errors.push({
|
|
251
264
|
kind: "error",
|
|
252
265
|
segment: kindKv.value.segment,
|
|
253
|
-
message: "Expected: lowercase variant name",
|
|
266
|
+
message: "Expected: lowercase or uppercase variant name",
|
|
254
267
|
});
|
|
255
268
|
return;
|
|
256
269
|
}
|
|
@@ -492,13 +505,13 @@ function isFloat(value: JsonValue): boolean {
|
|
|
492
505
|
}
|
|
493
506
|
if (value.type === "number") {
|
|
494
507
|
return true;
|
|
495
|
-
} else if (
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
508
|
+
} else if (
|
|
509
|
+
value.type === "string" &&
|
|
510
|
+
(value.jsonCode === '"NaN"' ||
|
|
511
|
+
value.jsonCode === '"Infinity"' ||
|
|
512
|
+
value.jsonCode === '"-Infinity"')
|
|
513
|
+
) {
|
|
514
|
+
return true;
|
|
502
515
|
} else {
|
|
503
516
|
return false;
|
|
504
517
|
}
|
package/src/json/to_json.ts
CHANGED
|
@@ -22,7 +22,7 @@ export function toJson(value: JsonValue): Json {
|
|
|
22
22
|
export function makeJsonTemplate(
|
|
23
23
|
type: TypeSignature,
|
|
24
24
|
idToRecordDef: { [id: string]: RecordDefinition },
|
|
25
|
-
|
|
25
|
+
nested?: "nested",
|
|
26
26
|
): Json {
|
|
27
27
|
switch (type.kind) {
|
|
28
28
|
case "array": {
|
|
@@ -34,13 +34,13 @@ export function makeJsonTemplate(
|
|
|
34
34
|
case "record": {
|
|
35
35
|
const recordDef = idToRecordDef[type.value]!;
|
|
36
36
|
if (recordDef.kind === "struct") {
|
|
37
|
-
if (
|
|
37
|
+
if (nested) {
|
|
38
38
|
return {};
|
|
39
39
|
}
|
|
40
40
|
return Object.fromEntries(
|
|
41
41
|
recordDef.fields.map((field) => [
|
|
42
42
|
field.name,
|
|
43
|
-
makeJsonTemplate(field.type!, idToRecordDef, "
|
|
43
|
+
makeJsonTemplate(field.type!, idToRecordDef, "nested"),
|
|
44
44
|
]),
|
|
45
45
|
);
|
|
46
46
|
} else {
|
package/src/json/types.ts
CHANGED
|
@@ -39,6 +39,7 @@ export interface JsonArray {
|
|
|
39
39
|
/// From '[' to ']' included.
|
|
40
40
|
readonly segment: Segment;
|
|
41
41
|
readonly values: JsonValue[];
|
|
42
|
+
readonly indent: number;
|
|
42
43
|
expectedType?: TypeSignature;
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -63,6 +64,7 @@ export interface JsonObject {
|
|
|
63
64
|
readonly keyValues: { [key: string]: JsonKeyValue };
|
|
64
65
|
/// Includes "broken" keys which produced a parsing error.
|
|
65
66
|
readonly allKeys: readonly JsonKey[];
|
|
67
|
+
readonly indent: number;
|
|
66
68
|
expectedType?: TypeSignature;
|
|
67
69
|
}
|
|
68
70
|
|
|
@@ -73,6 +75,7 @@ export interface JsonLiteral {
|
|
|
73
75
|
readonly segment: Segment;
|
|
74
76
|
readonly jsonCode: string;
|
|
75
77
|
readonly type: "boolean" | "null" | "number" | "string";
|
|
78
|
+
readonly indent: number;
|
|
76
79
|
expectedType?: TypeSignature;
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -223,6 +226,10 @@ export interface TypeHint {
|
|
|
223
226
|
readonly valueContext: JsonValueContext;
|
|
224
227
|
/// In order. All are included in 'valueContext.value.segment'.
|
|
225
228
|
readonly childHints: readonly TypeHint[];
|
|
229
|
+
/// Set if the expected type is an enum and the value is a literal string.
|
|
230
|
+
/// In that case, the editor can show a dropdown with the possible variants
|
|
231
|
+
/// of the enum.
|
|
232
|
+
readonly enumDefinition?: EnumDefinition;
|
|
226
233
|
}
|
|
227
234
|
|
|
228
235
|
export type Hint =
|