Skip to content

Commit aa27ad7

Browse files
thomasjahodaacao
andauthored
Much more robust auto-completion suggestions for conditional types (even when current data is invalid) (#138)
I continued the work on properly supporting conditional types. Conditional types were first supported via #133, quite recently. A lot of cases were not working at all, specifically whenever the data was not valid for the current schema and whenever a new property was being added in a dynamic schema, where the parent does not have enough info for the types. I added some tests for the scenarios I needed to support. And after verifying the old tests are working, I added `additionalProperties: false` to some existing test-data schemas, as that complicates matters. --------- Co-authored-by: Rikki Schulte <[email protected]>
1 parent 07c8b3a commit aa27ad7

File tree

11 files changed

+1828
-611
lines changed

11 files changed

+1828
-611
lines changed

.changeset/cuddly-toes-drop.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"codemirror-json-schema": minor
3+
---
4+
5+
More robust conditional types support (thanks @thomasjahoda!)

dev/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ const persistEditorStateOnChange = (key: string) => {
8080
if (v.docChanged) {
8181
ls.setItem(key, v.state.doc.toString());
8282
}
83-
}, 300)
83+
}, 300),
8484
);
8585
};
8686

@@ -161,7 +161,7 @@ const getSchema = async (val: string) => {
161161
const schemaSelect = document.getElementById("schema-selection");
162162
const schemaValue = localStorage.getItem("selectedSchema")!;
163163

164-
const setFileName = (value) => {
164+
const setFileName = (value: any) => {
165165
document.querySelectorAll("h2 code span").forEach((el) => {
166166
el.textContent = value;
167167
});

package.json

+8-6
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,19 @@
9696
"@types/json-schema": "^7.0.12",
9797
"@types/markdown-it": "^13.0.7",
9898
"@types/node": "^20.4.2",
99-
"@vitest/coverage-v8": "^0.34.6",
99+
"@vitest/coverage-v8": "^2.0.5",
100100
"codemirror-json5": "^1.0.3",
101101
"happy-dom": "^10.3.2",
102+
"jsdom": "^24.1.1",
102103
"json5": "^2.2.3",
104+
"prettier": "^3.3.3",
103105
"typedoc": "^0.24.8",
104106
"typedoc-plugin-markdown": "^3.15.3",
105-
"typescript": "^5.1.6",
106-
"vite": "^5.2.12",
107-
"vite-tsconfig-paths": "^4.3.1",
108-
"vitest": "0.34.6",
109-
"vitest-dom": "^0.1.0"
107+
"typescript": "^5.5.2",
108+
"vite": "^5.3.1",
109+
"vitest": "^1.6.0",
110+
"vitest-dom": "^0.1.1",
111+
"vite-tsconfig-paths": "^4.3.1"
110112
},
111113
"scripts": {
112114
"dev": "vite ./dev --port 3000",

pnpm-lock.yaml

+1,059-470
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/features/__tests__/__fixtures__/schemas.ts

+62
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,15 @@ export const testSchema2 = {
9494
foo: { type: "string" },
9595
bar: { type: "number" },
9696
},
97+
additionalProperties: false,
9798
},
9899
fancyObject2: {
99100
type: "object",
100101
properties: {
101102
apple: { type: "string" },
102103
banana: { type: "number" },
103104
},
105+
additionalProperties: false,
104106
},
105107
},
106108
} as JSONSchema7;
@@ -183,3 +185,63 @@ export const testSchemaConditionalProperties = {
183185
},
184186
],
185187
} as JSONSchema7;
188+
189+
export const testSchemaConditionalPropertiesOnSameObject = {
190+
type: "object",
191+
properties: {
192+
type: {
193+
type: "string",
194+
enum: ["type1", "type2"],
195+
},
196+
},
197+
allOf: [
198+
{
199+
if: {
200+
properties: {
201+
type: { const: "type1" },
202+
},
203+
},
204+
then: {
205+
properties: {
206+
type1Prop: { type: "string" },
207+
commonEnum: {
208+
enum: ["common1", "common2"],
209+
},
210+
commonEnumWithDifferentValues: {
211+
enum: ["type1Specific", "common"],
212+
},
213+
},
214+
required: ["type1Prop", "commonEnum", "commonEnumWithDifferentValues"],
215+
},
216+
},
217+
{
218+
if: {
219+
properties: {
220+
type: { const: "type2" },
221+
},
222+
},
223+
then: {
224+
properties: {
225+
type2Prop: { type: "string" },
226+
commonEnum: {
227+
enum: ["common1", "common2"],
228+
},
229+
commonEnumWithDifferentValues: {
230+
enum: ["type2Specific", "common"],
231+
},
232+
},
233+
required: ["type2Prop", "commonEnum", "commonEnumWithDifferentValues"],
234+
},
235+
},
236+
],
237+
unevaluatedProperties: false,
238+
required: ["type"],
239+
} as JSONSchema7;
240+
241+
export const wrappedTestSchemaConditionalPropertiesOnSameObject = {
242+
type: "object",
243+
properties: {
244+
original: testSchemaConditionalPropertiesOnSameObject,
245+
},
246+
required: ["original"],
247+
} as JSONSchema7;

src/features/__tests__/json-completion.spec.ts

+182
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
testSchema3,
77
testSchema4,
88
testSchemaConditionalProperties,
9+
testSchemaConditionalPropertiesOnSameObject,
10+
wrappedTestSchemaConditionalPropertiesOnSameObject,
911
} from "./__fixtures__/schemas";
1012

1113
describe.each([
@@ -806,3 +808,183 @@ describe.each([
806808
await expectCompletion(doc, expectedResults, { mode, schema });
807809
});
808810
});
811+
812+
describe.each([
813+
{
814+
name: "newPartialProp for specific type",
815+
mode: MODES.JSON5,
816+
docs: ["{ type: 'type1', t| }"],
817+
expectedResults: [
818+
{
819+
type: "property",
820+
detail: "string",
821+
info: "",
822+
label: "type1Prop",
823+
template: "type1Prop: '#{}'",
824+
},
825+
],
826+
schema: testSchemaConditionalPropertiesOnSameObject,
827+
},
828+
{
829+
name: "newEmptyPropInQuotes",
830+
mode: MODES.JSON5,
831+
docs: [`{ type: 'type1', "|" }`],
832+
expectedResults: [
833+
{
834+
type: "property",
835+
detail: "string",
836+
info: "",
837+
label: "type1Prop",
838+
template: `"type1Prop": '#{}'`,
839+
},
840+
{
841+
type: "property",
842+
detail: "",
843+
info: "",
844+
label: "commonEnum",
845+
template: `"commonEnum": #{}`,
846+
},
847+
{
848+
type: "property",
849+
detail: "",
850+
info: "",
851+
label: "commonEnumWithDifferentValues",
852+
template: `"commonEnumWithDifferentValues": #{}`,
853+
},
854+
],
855+
schema: testSchemaConditionalPropertiesOnSameObject,
856+
},
857+
{
858+
name: "type-specific enum values",
859+
mode: MODES.JSON5,
860+
docs: [`{ type: 'type1', "commonEnumWithDifferentValues": "|" }`],
861+
expectedResults: [
862+
{
863+
label: "type1Specific",
864+
apply: `'type1Specific'`,
865+
// info: "",
866+
},
867+
{
868+
label: "common",
869+
apply: `'common'`,
870+
// info: "",
871+
},
872+
],
873+
schema: testSchemaConditionalPropertiesOnSameObject,
874+
},
875+
{
876+
name: "type-specific enum values - type2",
877+
mode: MODES.JSON5,
878+
docs: [`{ type: 'type2', "commonEnumWithDifferentValues": "|" }`],
879+
expectedResults: [
880+
{
881+
label: "type2Specific",
882+
apply: `'type2Specific'`,
883+
// info: "",
884+
},
885+
{
886+
label: "common",
887+
apply: `'common'`,
888+
// info: "",
889+
},
890+
],
891+
schema: testSchemaConditionalPropertiesOnSameObject,
892+
},
893+
{
894+
name: "allow changing type afterwards to anything",
895+
mode: MODES.JSON5,
896+
docs: ["{ type: '|', type1Prop: 'bla' }"],
897+
expectedResults: [
898+
{
899+
label: "type1",
900+
apply: "'type1'",
901+
type: "string",
902+
},
903+
{
904+
label: "type2",
905+
apply: "'type2'",
906+
type: "string",
907+
},
908+
],
909+
schema: testSchemaConditionalPropertiesOnSameObject,
910+
},
911+
{
912+
name: "suggests all possible properties if discriminator is not specified yet",
913+
mode: MODES.JSON5,
914+
docs: [`{ "|" }`],
915+
expectedResults: [
916+
{
917+
type: "property",
918+
detail: "string",
919+
info: "",
920+
label: "type",
921+
template: `"type": #{}`,
922+
},
923+
{
924+
type: "property",
925+
detail: "string",
926+
info: "",
927+
label: "type1Prop",
928+
template: `"type1Prop": '#{}'`,
929+
},
930+
{
931+
type: "property",
932+
detail: "",
933+
info: "",
934+
label: "commonEnum",
935+
template: `"commonEnum": #{}`,
936+
},
937+
{
938+
type: "property",
939+
detail: "",
940+
info: "",
941+
label: "commonEnumWithDifferentValues",
942+
template: `"commonEnumWithDifferentValues": #{}`,
943+
},
944+
{
945+
type: "property",
946+
detail: "string",
947+
info: "",
948+
label: "type2Prop",
949+
template: `"type2Prop": '#{}'`,
950+
},
951+
],
952+
schema: testSchemaConditionalPropertiesOnSameObject,
953+
},
954+
])(
955+
"jsonCompletionFor-testSchemaConditionalPropertiesOnSameObject",
956+
({ name, docs, mode, expectedResults, schema }) => {
957+
it.each(docs)(`${name} (mode: ${mode})`, async (doc) => {
958+
// if (name === 'autocomplete for array of objects with items (array of objects)') {
959+
await expectCompletion(doc, expectedResults, { mode, schema });
960+
// }
961+
});
962+
},
963+
);
964+
965+
describe.each([
966+
{
967+
name: "newProp",
968+
mode: MODES.JSON5,
969+
docs: ["{ original: { type: 'type1', t| }, }"],
970+
expectedResults: [
971+
{
972+
type: "property",
973+
detail: "string",
974+
info: "",
975+
label: "type1Prop",
976+
template: "type1Prop: '#{}'",
977+
},
978+
],
979+
schema: wrappedTestSchemaConditionalPropertiesOnSameObject,
980+
},
981+
])(
982+
"jsonCompletionFor-wrappedTestSchemaConditionalPropertiesOnSameObject",
983+
({ name, docs, mode, expectedResults, schema }) => {
984+
it.each(docs)(`${name} (mode: ${mode})`, async (doc) => {
985+
// if (name === 'autocomplete for array of objects with filter') {
986+
await expectCompletion(doc, expectedResults, { mode, schema });
987+
// }
988+
});
989+
},
990+
);

0 commit comments

Comments
 (0)