skir-codemirror-plugin 1.0.2 → 1.0.3

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.
Files changed (42) hide show
  1. package/README.md +15 -6
  2. package/dist/codemirror/create_editor_state.d.ts +4 -2
  3. package/dist/codemirror/create_editor_state.d.ts.map +1 -1
  4. package/dist/codemirror/create_editor_state.js +178 -31
  5. package/dist/codemirror/create_editor_state.js.map +1 -1
  6. package/dist/codemirror/json_linter.d.ts.map +1 -1
  7. package/dist/codemirror/json_linter.js +67 -3
  8. package/dist/codemirror/json_linter.js.map +1 -1
  9. package/dist/codemirror/json_state.d.ts +5 -1
  10. package/dist/codemirror/json_state.d.ts.map +1 -1
  11. package/dist/codemirror/json_state.js +26 -1
  12. package/dist/codemirror/json_state.js.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/json/json_parser.js +35 -1
  17. package/dist/json/json_parser.js.map +1 -1
  18. package/dist/json/json_parser.test.js +48 -0
  19. package/dist/json/json_parser.test.js.map +1 -1
  20. package/dist/json/schema_validator.d.ts.map +1 -1
  21. package/dist/json/schema_validator.js +16 -11
  22. package/dist/json/schema_validator.js.map +1 -1
  23. package/dist/json/schema_validator.test.js +71 -0
  24. package/dist/json/schema_validator.test.js.map +1 -1
  25. package/dist/json/to_json.d.ts +1 -1
  26. package/dist/json/to_json.d.ts.map +1 -1
  27. package/dist/json/to_json.js +3 -3
  28. package/dist/json/to_json.js.map +1 -1
  29. package/dist/json/types.d.ts +4 -0
  30. package/dist/json/types.d.ts.map +1 -1
  31. package/dist/json/types.js.map +1 -1
  32. package/package.json +2 -3
  33. package/src/codemirror/create_editor_state.ts +272 -31
  34. package/src/codemirror/json_linter.ts +89 -4
  35. package/src/codemirror/json_state.ts +44 -1
  36. package/src/index.ts +1 -0
  37. package/src/json/json_parser.test.ts +51 -0
  38. package/src/json/json_parser.ts +37 -1
  39. package/src/json/schema_validator.test.ts +75 -0
  40. package/src/json/schema_validator.ts +20 -10
  41. package/src/json/to_json.ts +3 -3
  42. package/src/json/types.ts +7 -0
@@ -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 = { segment, jsonCode: tokenText };
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();
@@ -189,6 +189,81 @@ describe("schema_validator", () => {
189
189
  ]);
190
190
  });
191
191
 
192
+ it("stores enum definition in type hint for UNKNOWN enum literal", () => {
193
+ const schema: TypeDefinition = {
194
+ type: { kind: "record", value: "MyEnum" },
195
+ records: [
196
+ {
197
+ kind: "enum",
198
+ id: "MyEnum",
199
+ variants: [
200
+ { name: "First", number: 1 },
201
+ { name: "Second", number: 2 },
202
+ ],
203
+ },
204
+ ],
205
+ };
206
+
207
+ const result = validateSchema(parse('"UNKNOWN"'), schema);
208
+ expect(result.errors).toMatch([]);
209
+ if (!result.rootTypeHint) {
210
+ throw new Error("Expected root type hint");
211
+ }
212
+ expect(result.rootTypeHint.enumDefinition).toMatch({
213
+ kind: "enum",
214
+ id: "MyEnum",
215
+ variants: [
216
+ { name: "First", number: 1 },
217
+ { name: "Second", number: 2 },
218
+ ],
219
+ });
220
+ });
221
+
222
+ it("stores enum definition in nested UNKNOWN enum field hint", () => {
223
+ const schema: TypeDefinition = {
224
+ type: { kind: "record", value: "MyStruct" },
225
+ records: [
226
+ {
227
+ kind: "struct",
228
+ id: "MyStruct",
229
+ fields: [
230
+ {
231
+ name: "status",
232
+ number: 1,
233
+ type: { kind: "record", value: "MyEnum" },
234
+ },
235
+ ],
236
+ },
237
+ {
238
+ kind: "enum",
239
+ id: "MyEnum",
240
+ variants: [
241
+ { name: "First", number: 1 },
242
+ { name: "Second", number: 2 },
243
+ ],
244
+ },
245
+ ],
246
+ };
247
+
248
+ const result = validateSchema(parse('{"status": "UNKNOWN"}'), schema);
249
+ expect(result.errors).toMatch([]);
250
+ if (!result.rootTypeHint) {
251
+ throw new Error("Expected root type hint");
252
+ }
253
+ if (result.rootTypeHint.childHints.length !== 1) {
254
+ throw new Error("Expected one child type hint");
255
+ }
256
+ const statusHint = result.rootTypeHint.childHints[0];
257
+ expect(statusHint.enumDefinition).toMatch({
258
+ kind: "enum",
259
+ id: "MyEnum",
260
+ variants: [
261
+ { name: "First", number: 1 },
262
+ { name: "Second", number: 2 },
263
+ ],
264
+ });
265
+ });
266
+
192
267
  it("validates enum (object with kind)", () => {
193
268
  const schema: TypeDefinition = {
194
269
  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 = (): void => {
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];
@@ -196,8 +200,14 @@ class SchemaValidator {
196
200
  } else if (value.kind === "literal" && value.type === "string") {
197
201
  const name = JSON.parse(value.jsonCode);
198
202
  const fieldDef = nameToVariantDef[name];
199
- if (name === "UNKNOWN" || fieldDef) {
200
- pushTypeHint();
203
+ if (fieldDef && fieldDef.type) {
204
+ this.errors.push({
205
+ kind: "error",
206
+ segment: value.segment,
207
+ message: "Expected: uppercase variant name",
208
+ });
209
+ } else if (name === "UNKNOWN" || fieldDef) {
210
+ pushTypeHint({ enumDefinition: recordDef });
201
211
  typeHintStack.pop();
202
212
  } else {
203
213
  this.errors.push({
@@ -492,13 +502,13 @@ function isFloat(value: JsonValue): boolean {
492
502
  }
493
503
  if (value.type === "number") {
494
504
  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
- }
505
+ } else if (
506
+ value.type === "string" &&
507
+ (value.jsonCode === '"NaN"' ||
508
+ value.jsonCode === '"Infinity"' ||
509
+ value.jsonCode === '"-Infinity"')
510
+ ) {
511
+ return true;
502
512
  } else {
503
513
  return false;
504
514
  }
@@ -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
- depth?: "depth",
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 (depth) {
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, "depth"),
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 =