Skip to content

Commit

Permalink
Much more robust auto-completion suggestions for conditional types (e…
Browse files Browse the repository at this point in the history
…ven 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]>
  • Loading branch information
thomasjahoda and acao authored Jan 6, 2025
1 parent 07c8b3a commit aa27ad7
Show file tree
Hide file tree
Showing 11 changed files with 1,828 additions and 611 deletions.
5 changes: 5 additions & 0 deletions .changeset/cuddly-toes-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"codemirror-json-schema": minor
---

More robust conditional types support (thanks @thomasjahoda!)
4 changes: 2 additions & 2 deletions dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const persistEditorStateOnChange = (key: string) => {
if (v.docChanged) {
ls.setItem(key, v.state.doc.toString());
}
}, 300)
}, 300),
);
};

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

const setFileName = (value) => {
const setFileName = (value: any) => {
document.querySelectorAll("h2 code span").forEach((el) => {
el.textContent = value;
});
Expand Down
14 changes: 8 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,19 @@
"@types/json-schema": "^7.0.12",
"@types/markdown-it": "^13.0.7",
"@types/node": "^20.4.2",
"@vitest/coverage-v8": "^0.34.6",
"@vitest/coverage-v8": "^2.0.5",
"codemirror-json5": "^1.0.3",
"happy-dom": "^10.3.2",
"jsdom": "^24.1.1",
"json5": "^2.2.3",
"prettier": "^3.3.3",
"typedoc": "^0.24.8",
"typedoc-plugin-markdown": "^3.15.3",
"typescript": "^5.1.6",
"vite": "^5.2.12",
"vite-tsconfig-paths": "^4.3.1",
"vitest": "0.34.6",
"vitest-dom": "^0.1.0"
"typescript": "^5.5.2",
"vite": "^5.3.1",
"vitest": "^1.6.0",
"vitest-dom": "^0.1.1",
"vite-tsconfig-paths": "^4.3.1"
},
"scripts": {
"dev": "vite ./dev --port 3000",
Expand Down
1,529 changes: 1,059 additions & 470 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions src/features/__tests__/__fixtures__/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,15 @@ export const testSchema2 = {
foo: { type: "string" },
bar: { type: "number" },
},
additionalProperties: false,
},
fancyObject2: {
type: "object",
properties: {
apple: { type: "string" },
banana: { type: "number" },
},
additionalProperties: false,
},
},
} as JSONSchema7;
Expand Down Expand Up @@ -183,3 +185,63 @@ export const testSchemaConditionalProperties = {
},
],
} as JSONSchema7;

export const testSchemaConditionalPropertiesOnSameObject = {
type: "object",
properties: {
type: {
type: "string",
enum: ["type1", "type2"],
},
},
allOf: [
{
if: {
properties: {
type: { const: "type1" },
},
},
then: {
properties: {
type1Prop: { type: "string" },
commonEnum: {
enum: ["common1", "common2"],
},
commonEnumWithDifferentValues: {
enum: ["type1Specific", "common"],
},
},
required: ["type1Prop", "commonEnum", "commonEnumWithDifferentValues"],
},
},
{
if: {
properties: {
type: { const: "type2" },
},
},
then: {
properties: {
type2Prop: { type: "string" },
commonEnum: {
enum: ["common1", "common2"],
},
commonEnumWithDifferentValues: {
enum: ["type2Specific", "common"],
},
},
required: ["type2Prop", "commonEnum", "commonEnumWithDifferentValues"],
},
},
],
unevaluatedProperties: false,
required: ["type"],
} as JSONSchema7;

export const wrappedTestSchemaConditionalPropertiesOnSameObject = {
type: "object",
properties: {
original: testSchemaConditionalPropertiesOnSameObject,
},
required: ["original"],
} as JSONSchema7;
182 changes: 182 additions & 0 deletions src/features/__tests__/json-completion.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
testSchema3,
testSchema4,
testSchemaConditionalProperties,
testSchemaConditionalPropertiesOnSameObject,
wrappedTestSchemaConditionalPropertiesOnSameObject,
} from "./__fixtures__/schemas";

describe.each([
Expand Down Expand Up @@ -806,3 +808,183 @@ describe.each([
await expectCompletion(doc, expectedResults, { mode, schema });
});
});

describe.each([
{
name: "newPartialProp for specific type",
mode: MODES.JSON5,
docs: ["{ type: 'type1', t| }"],
expectedResults: [
{
type: "property",
detail: "string",
info: "",
label: "type1Prop",
template: "type1Prop: '#{}'",
},
],
schema: testSchemaConditionalPropertiesOnSameObject,
},
{
name: "newEmptyPropInQuotes",
mode: MODES.JSON5,
docs: [`{ type: 'type1', "|" }`],
expectedResults: [
{
type: "property",
detail: "string",
info: "",
label: "type1Prop",
template: `"type1Prop": '#{}'`,
},
{
type: "property",
detail: "",
info: "",
label: "commonEnum",
template: `"commonEnum": #{}`,
},
{
type: "property",
detail: "",
info: "",
label: "commonEnumWithDifferentValues",
template: `"commonEnumWithDifferentValues": #{}`,
},
],
schema: testSchemaConditionalPropertiesOnSameObject,
},
{
name: "type-specific enum values",
mode: MODES.JSON5,
docs: [`{ type: 'type1', "commonEnumWithDifferentValues": "|" }`],
expectedResults: [
{
label: "type1Specific",
apply: `'type1Specific'`,
// info: "",
},
{
label: "common",
apply: `'common'`,
// info: "",
},
],
schema: testSchemaConditionalPropertiesOnSameObject,
},
{
name: "type-specific enum values - type2",
mode: MODES.JSON5,
docs: [`{ type: 'type2', "commonEnumWithDifferentValues": "|" }`],
expectedResults: [
{
label: "type2Specific",
apply: `'type2Specific'`,
// info: "",
},
{
label: "common",
apply: `'common'`,
// info: "",
},
],
schema: testSchemaConditionalPropertiesOnSameObject,
},
{
name: "allow changing type afterwards to anything",
mode: MODES.JSON5,
docs: ["{ type: '|', type1Prop: 'bla' }"],
expectedResults: [
{
label: "type1",
apply: "'type1'",
type: "string",
},
{
label: "type2",
apply: "'type2'",
type: "string",
},
],
schema: testSchemaConditionalPropertiesOnSameObject,
},
{
name: "suggests all possible properties if discriminator is not specified yet",
mode: MODES.JSON5,
docs: [`{ "|" }`],
expectedResults: [
{
type: "property",
detail: "string",
info: "",
label: "type",
template: `"type": #{}`,
},
{
type: "property",
detail: "string",
info: "",
label: "type1Prop",
template: `"type1Prop": '#{}'`,
},
{
type: "property",
detail: "",
info: "",
label: "commonEnum",
template: `"commonEnum": #{}`,
},
{
type: "property",
detail: "",
info: "",
label: "commonEnumWithDifferentValues",
template: `"commonEnumWithDifferentValues": #{}`,
},
{
type: "property",
detail: "string",
info: "",
label: "type2Prop",
template: `"type2Prop": '#{}'`,
},
],
schema: testSchemaConditionalPropertiesOnSameObject,
},
])(
"jsonCompletionFor-testSchemaConditionalPropertiesOnSameObject",
({ name, docs, mode, expectedResults, schema }) => {
it.each(docs)(`${name} (mode: ${mode})`, async (doc) => {
// if (name === 'autocomplete for array of objects with items (array of objects)') {
await expectCompletion(doc, expectedResults, { mode, schema });
// }
});
},
);

describe.each([
{
name: "newProp",
mode: MODES.JSON5,
docs: ["{ original: { type: 'type1', t| }, }"],
expectedResults: [
{
type: "property",
detail: "string",
info: "",
label: "type1Prop",
template: "type1Prop: '#{}'",
},
],
schema: wrappedTestSchemaConditionalPropertiesOnSameObject,
},
])(
"jsonCompletionFor-wrappedTestSchemaConditionalPropertiesOnSameObject",
({ name, docs, mode, expectedResults, schema }) => {
it.each(docs)(`${name} (mode: ${mode})`, async (doc) => {
// if (name === 'autocomplete for array of objects with filter') {
await expectCompletion(doc, expectedResults, { mode, schema });
// }
});
},
);
Loading

0 comments on commit aa27ad7

Please sign in to comment.