From 3a826bc20fe8fbed77059c8c0dbf38b5790bbc1c Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Thu, 17 Aug 2023 04:35:13 +0200 Subject: [PATCH] bug for no value context --- .gitignore | 1 + package.json | 2 +- pnpm-lock.yaml | 8 +- src/__tests__/json-validation.spec.ts | 22 +-- src/json-completion.ts | 17 +- src/json-hover.ts | 54 +++++-- src/json-validation.ts | 26 ++- src/utils/dom.ts | 6 +- src/utils/formatting.ts | 2 +- src/utils/schema-lib/README.md | 3 - src/utils/schema-lib/getSchema.ts | 58 ------- src/utils/schema-lib/step.ts | 218 -------------------------- 12 files changed, 94 insertions(+), 323 deletions(-) delete mode 100644 src/utils/schema-lib/README.md delete mode 100644 src/utils/schema-lib/getSchema.ts delete mode 100644 src/utils/schema-lib/step.ts diff --git a/.gitignore b/.gitignore index 07ab3ad..f72544f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules dist /public coverage +cjs diff --git a/package.json b/package.json index 094cc44..3563ac7 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@types/json-schema": "^7.0.12", "@types/node": "^20.4.2", "json-schema": "^0.2.3", - "json-schema-library": "^8.0.0" + "json-schema-library": "^9.0.0" }, "optionalDependencies": { "@codemirror/lang-json": "^6.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c1018d..d5e8568 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,8 +36,8 @@ dependencies: specifier: ^0.2.3 version: 0.2.3 json-schema-library: - specifier: ^8.0.0 - version: 8.0.0 + specifier: ^9.0.0 + version: 9.0.0 optionalDependencies: '@codemirror/lang-json': @@ -1938,8 +1938,8 @@ packages: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} dev: true - /json-schema-library@8.0.0: - resolution: {integrity: sha512-qqsEdyhuA68YHzuWNGrOk9ViknRKw1NfIbhT9wQ0z6l5cpfuYoqKRkbu8tgHAXjahmLEkpNdGwHq+gCgIrMYeA==} + /json-schema-library@9.0.0: + resolution: {integrity: sha512-X0eC3rcC1kAoR5YXG8h6X205PMjlBzmWZWpRCRI2taESZLYS2TglKiY8u/RuuMWWDaE1tgSkcpees5G2swOQug==} dependencies: '@sagold/json-pointer': 5.1.1 '@sagold/json-query': 6.1.1 diff --git a/src/__tests__/json-validation.spec.ts b/src/__tests__/json-validation.spec.ts index afcfa32..7997c0a 100644 --- a/src/__tests__/json-validation.spec.ts +++ b/src/__tests__/json-validation.spec.ts @@ -11,21 +11,25 @@ const getErrors = (jsonString: string, schema?: JSONSchema7) => { const view = new EditorView({ doc: jsonString, extensions: [json()] }); return new JSONValidation(schema || testSchema).doValidation(view); }; + +const common = { + severity: "error" as Diagnostic["severity"], + source: "json-schema", +}; + const expectErrors = ( jsonString: string, errors: [from: number, to: number, message: string][], schema?: JSONSchema7 ) => { - expect(getErrors(jsonString, schema)).toEqual( + const filteredErrors = getErrors(jsonString, schema).map( + ({ renderMessage, ...error }) => error + ); + expect(filteredErrors).toEqual( errors.map(([from, to, message]) => ({ ...common, from, to, message })) ); }; -const common = { - severity: "error" as Diagnostic["severity"], - source: "json-schema", -}; - describe("json-validation", () => { it("should provide range for a value error", () => { expectErrors('{"foo": 123}', [ @@ -34,7 +38,7 @@ describe("json-validation", () => { }); it("should provide range for an unknown key error", () => { expectErrors('{"foo": "example", "bar": 123}', [ - [19, 24, "Additional property `bar` in `#` is not allowed"], + [19, 24, "Additional property `bar` is not allowed"], ]); }); it("should not handle invalid json", () => { @@ -46,7 +50,7 @@ describe("json-validation", () => { "foo": "example", "bar": "something else" }`, - [[32, 37, "Additional property `bar` in `#` is not allowed"]] + [[32, 37, "Additional property `bar` is not allowed"]] ); }); it("should provide formatted error message for oneOf fields with more than 2 items", () => { @@ -65,7 +69,7 @@ describe("json-validation", () => { "foo": "example", "oneOfEg2": 123 }`, - [[44, 47, 'Expected one of `"string"` or `"array"`']], + [[44, 47, "Expected one of string or array"]], testSchema2 ); }); diff --git a/src/json-completion.ts b/src/json-completion.ts index fb56f3c..8cbfc8f 100644 --- a/src/json-completion.ts +++ b/src/json-completion.ts @@ -17,10 +17,9 @@ import { stripSurroundingQuotes, getNodeAtPosition, } from "./utils/node"; -import { Draft07, JsonError } from "json-schema-library"; +import { Draft07, JsonError, isJsonError } from "json-schema-library"; import { jsonPointerForPosition } from "./utils/jsonPointers"; import { TOKENS } from "./constants"; -import getSchema from "./utils/schema-lib/getSchema"; function json5PropertyInsertSnippet(rawWord: string, value: string) { if (rawWord.startsWith('"')) { @@ -683,7 +682,10 @@ export class JSONCompletion { ): JSONSchema7Definition[] { const draft = new Draft07(this.schema); let pointer = jsonPointerForPosition(ctx.state, ctx.pos); - let subSchema = getSchema(draft, pointer); + let subSchema = draft.getSchema({ pointer }); + if (isJsonError(subSchema)) { + subSchema = subSchema.data?.schema; + } // if we don't have a schema for the current pointer, try the parent pointer if ( !subSchema || @@ -692,7 +694,7 @@ export class JSONCompletion { subSchema.type === "undefined" ) { pointer = pointer.replace(/\/[^/]*$/, "/"); - subSchema = getSchema(draft, pointer); + subSchema = draft.getSchema({ pointer }); } debug.log("xxx", "pointer..", JSON.stringify(pointer)); @@ -703,8 +705,7 @@ export class JSONCompletion { } // const subSchema = new Draft07(this.schema).getSchema(pointer); debug.log("xxx", "subSchema..", subSchema); - - if (this.isJsonError(subSchema)) { + if (!subSchema) { return []; } @@ -730,10 +731,6 @@ export class JSONCompletion { return [subSchema as JSONSchema7]; } - isJsonError(d: JSONSchema7 | JsonError): d is JsonError { - return d.type === "error"; - } - private expandSchemaProperty( property: JSONSchema7Definition, schema: JSONSchema7 diff --git a/src/json-hover.ts b/src/json-hover.ts index df924ba..e52be49 100644 --- a/src/json-hover.ts +++ b/src/json-hover.ts @@ -1,10 +1,14 @@ import { type EditorView, Tooltip } from "@codemirror/view"; -import { type Draft, Draft04, JsonSchema } from "json-schema-library"; +import { + type Draft, + Draft04, + JsonSchema, + isJsonError, +} from "json-schema-library"; import type { JSONSchema7 } from "json-schema"; import { JSONMode, jsonPointerForPosition } from "./utils/jsonPointers"; import { joinWithOr } from "./utils/formatting"; -import getSchema from "./utils/schema-lib/getSchema"; import { debug } from "./utils/debug"; import { Side } from "./types"; import { el } from "./utils/dom"; @@ -56,8 +60,11 @@ function formatComplexType( export class JSONHover { private schema: Draft; - public constructor(schema: JSONSchema7, private opts?: HoverOptions) { - this.schema = new Draft04(schema); + public constructor( + private _schema: JSONSchema7, + private opts?: HoverOptions + ) { + this.schema = new Draft04(_schema); this.opts = { parser: JSON.parse, ...this.opts, @@ -80,12 +87,19 @@ export class JSONHover { return null; } // if the data is valid, we can infer a type for complex types - let subSchema = getSchema(this.schema, pointer, data); - if (subSchema.type === "error" && data !== undefined) { - // if the data is invalid, we won't get the type - try again without the data - subSchema = getSchema(this.schema, pointer, undefined); - if (subSchema.type === "error") { - return { pointer }; + let subSchema = this.schema.getSchema({ + pointer, + data, + schema: this._schema, + withSchemaWarning: true, + }); + if (isJsonError(subSchema)) { + console.log("subschema", subSchema.data); + + if (subSchema?.data.schema["$ref"]) { + subSchema = this.schema.resolveRef(subSchema); + } else { + subSchema = subSchema?.data.schema; } } @@ -101,7 +115,15 @@ export class JSONHover { text: message, }), el("div", { class: "cm6-json-schema-hover--code-wrapper" }, [ - el("code", { class: "cm6-json-schema-hover--code", text: typeInfo }), + typeInfo.includes("") + ? el("div", { + class: "cm6-json-schema-hover--code", + inner: typeInfo, + }) + : el("code", { + class: "cm6-json-schema-hover--code", + text: typeInfo, + }), ]), ]); } @@ -117,6 +139,7 @@ export class JSONHover { let message = null; const { schema } = data; + console.log(schema, data); if (schema.oneOf) { typeInfo = formatComplexType(schema, "oneOf", draft); } @@ -131,6 +154,15 @@ export class JSONHover { ? joinWithOr(schema.type) : schema.type; } + if (schema.enum) { + typeInfo = `enum: ${joinWithOr(schema.enum)}`; + } + if (schema.format) { + typeInfo += ` format: ${schema.format}`; + } + if (schema.pattern) { + typeInfo += ` pattern: ${schema.pattern}`; + } if (schema.description) { message = schema.description; } diff --git a/src/json-validation.ts b/src/json-validation.ts index bd84391..a2f7028 100644 --- a/src/json-validation.ts +++ b/src/json-validation.ts @@ -6,6 +6,7 @@ import { joinWithOr } from "./utils/formatting"; import { JSONPointerData } from "./types"; import { parseJSONDocumentState } from "./utils/parseJSONDocument"; import { RequiredPick } from "./types"; +import { el } from "./utils/dom"; // return an object path that matches with the json-source-map pointer const getErrorPath = (error: JsonError): string => { @@ -59,26 +60,33 @@ export class JSONValidation { this.schema = new Draft04(schema); } private get schemaTitle() { - return this.schema.getSchema().title ?? "json-schema"; + return this.schema.getSchema()?.title ?? "json-schema"; } // rewrite the error message to be more human readable private rewriteError = (error: JsonError): string => { if (error.code === "one-of-error") { + console.log("raw", error?.data?.received); return `Expected one of ${joinWithOr( error?.data?.errors, (data) => data.data.expected )}`; } if (error.code === "type-error") { - return `Expected \`${ + console.log("raw", error?.data?.received); + return `Expected ${ error?.data?.expected && Array.isArray(error?.data?.expected) ? joinWithOr(error?.data?.expected) : error?.data?.expected - }\` but received \`${error?.data?.received}\``; + } but received ${error?.data?.received}`; } - const message = error.message.replaceAll("#/", "").replaceAll("/", "."); - + const message = error.message + // don't mention root object + .replaceAll("in `#` ", "") + .replaceAll("/", ".") + .replaceAll("#.", "") + // replace backticks with tags + .replaceAll(/`([^`]*)`/gm, "$1"); return message; }; @@ -100,6 +108,7 @@ export class JSONValidation { if (!errors.length) return []; // reduce() because we want to filter out errors that don't have a pointer return errors.reduce((acc, error) => { + console.log(this.rewriteError(error)); const errorPath = getErrorPath(error); const pointer = json.pointers.get(errorPath) as JSONPointerData; if (pointer) { @@ -108,9 +117,12 @@ export class JSONValidation { acc.push({ from: isPropertyError ? pointer.keyFrom : pointer.valueFrom, to: isPropertyError ? pointer.keyTo : pointer.valueTo, - // TODO: create a domnode and replace `` with - // renderMessage: () => error.message, message: this.rewriteError(error), + renderMessage: () => { + const dom = el("div", {}); + dom.innerHTML = this.rewriteError(error); + return dom; + }, severity: "error", source: this.schemaTitle, }); diff --git a/src/utils/dom.ts b/src/utils/dom.ts index d9c2975..b7def98 100644 --- a/src/utils/dom.ts +++ b/src/utils/dom.ts @@ -6,7 +6,7 @@ // return e; // } -type Attributes = "class" | "text" | "id" | "role" | "aria-label"; +type Attributes = "class" | "text" | "id" | "role" | "aria-label" | "inner"; export function el( tagName: string, @@ -19,6 +19,10 @@ export function el( e.innerText = v; return; } + if (k === "inner") { + e.innerHTML = v; + return; + } e.setAttribute(k, v); }); children.forEach((c) => e.appendChild(c)); diff --git a/src/utils/formatting.ts b/src/utils/formatting.ts index c86ab07..c492c2f 100644 --- a/src/utils/formatting.ts +++ b/src/utils/formatting.ts @@ -3,7 +3,7 @@ export const joinWithOr = (arr: string[], getPath?: (err: any) => any) => { const needsComma = arr.length > 2; let data = arr.map((err: any, i: number) => { - const result = `\`` + (getPath ? JSON.stringify(getPath(err)) : err) + `\``; + const result = `` + (getPath ? getPath(err) : err) + ``; if (i === arr.length - 1) return "or " + result; return result; }); diff --git a/src/utils/schema-lib/README.md b/src/utils/schema-lib/README.md deleted file mode 100644 index f52ae25..0000000 --- a/src/utils/schema-lib/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## Temporary - -These methods are borrowed from `json-schema-library` until one of our proposals for `oneOf` is merged diff --git a/src/utils/schema-lib/getSchema.ts b/src/utils/schema-lib/getSchema.ts deleted file mode 100644 index 60e0c5f..0000000 --- a/src/utils/schema-lib/getSchema.ts +++ /dev/null @@ -1,58 +0,0 @@ -import gp from "@sagold/json-pointer"; - -import { - isJsonError, - type Draft, - type JsonSchema, - type JsonPointer, -} from "json-schema-library"; - -import step from "./step"; - -const emptyObject = {}; - -/** - * Returns the json-schema of a data-json-pointer. - * - * Notes - * - Uses draft.step to walk through data and schema - * - * @param draft - * @param pointer - json pointer in data to get the json schema for - * @param [data] - the data object, which includes the json pointers value. This is optional, as - * long as no oneOf, anyOf, etc statement is part of the pointers schema - * @param [schema] - the json schema to iterate. Defaults to draft.rootSchema - * @return json schema object of the json-pointer or an error - */ -export default function getSchema( - draft: Draft, - pointer: JsonPointer, - data?: unknown, - schema: JsonSchema = draft.rootSchema -): JsonSchema { - const frags = gp.split(pointer); - schema = draft.resolveRef(schema); - return _get(draft, schema, frags, pointer, data); -} - -function _get( - draft: Draft, - schema: JsonSchema, - frags: Array, - pointer: JsonPointer, - data: unknown = emptyObject -): JsonSchema { - if (frags.length === 0) { - return draft.resolveRef(schema); - } - - const key = frags.shift(); // step key - // @ts-expect-error - schema = step(draft, key, schema, data, pointer); // step schema - if (isJsonError(schema)) { - return schema; - } - // @ts-expect-error - data = data[key]; // step data - return _get(draft, schema, frags, `${pointer}/${key}`, data); -} diff --git a/src/utils/schema-lib/step.ts b/src/utils/schema-lib/step.ts deleted file mode 100644 index bbf5493..0000000 --- a/src/utils/schema-lib/step.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { - getTypeOf, - Draft, - JsonSchema, - JsonPointer, - JsonError, - isJsonError, - reduceSchema, -} from "json-schema-library"; - -// @ts-expect-error -import errors from "json-schema-library/dist/module/lib/validation/errors"; - -// @ts-expect-error -import createSchemaOf from "json-schema-library/dist/module/lib/createSchemaOf"; - -// import getTypeOf from "./getTypeOf"; -// import errors from "./validation/errors"; -// import { JsonSchema, JsonPointer, JsonError, isJsonError } from "./types"; -// import { Draft } from "./draft"; - -type StepFunction = ( - draft: Draft, - key: string, - schema: JsonSchema, - data: any, - pointer: JsonPointer -) => JsonSchema | JsonError; - -const stepType: Record = { - array: (draft, key, schema, data, pointer) => { - const itemValue = data?.[key]; - const itemsType = getTypeOf(schema.items); - - if (itemsType === "object") { - // @spec: ignore additionalItems, when items is schema-object - return ( - reduceSchema(draft, schema.items, itemValue) || - draft.resolveRef(schema.items) - ); - } - - if (itemsType === "array") { - // @draft >= 7 bool schema, items:[true, false] - if (schema.items[key] === true) { - return createSchemaOf(itemValue); - } - // @draft >= 7 bool schema, items:[true, false] - if (schema.items[key] === false) { - return errors.invalidDataError({ - key, - value: itemValue, - pointer, - }); - } - - if (schema.items[key]) { - return draft.resolveRef(schema.items[key]); - } - - if (schema.additionalItems === false) { - return errors.additionalItemsError({ - key, - value: itemValue, - pointer, - }); - } - - if ( - schema.additionalItems === true || - schema.additionalItems === undefined - ) { - return createSchemaOf(itemValue); - } - - if (getTypeOf(schema.additionalItems) === "object") { - return schema.additionalItems; - } - - throw new Error( - `Invalid schema ${JSON.stringify(schema, null, 4)} for ${JSON.stringify( - data, - null, - 4 - )}` - ); - } - - if (schema.additionalItems !== false && itemValue) { - // @todo reevaluate: incomplete schema is created here - // @todo support additionalItems: {schema} - return createSchemaOf(itemValue); - } - - return new Error( - `Invalid array schema for ${key} at ${pointer}` - ) as JsonError; - }, - - object: (draft, key, schema, data, pointer) => { - schema = reduceSchema(draft, schema, data); - - // @feature properties - const property = schema?.properties?.[key]; - if (property !== undefined) { - // @todo patternProperties also validate properties - - // @feature boolean schema - if (property === false) { - return errors.forbiddenPropertyError({ - property: key, - value: data, - pointer: `${pointer}`, - }); - } else if (property === true) { - return createSchemaOf(data?.[key]); - } - - const targetSchema = draft.resolveRef(property); - if (isJsonError(targetSchema)) { - return targetSchema; - } - - // check if there is a oneOf selection, which must be resolved - if (targetSchema && Array.isArray(targetSchema.oneOf)) { - if (data === undefined || data[key] === undefined) { - return targetSchema; - } - // @special case: this is a mix of a schema and optional definitions - // we resolve the schema here and add the original schema to `oneOfSchema` - return draft.resolveOneOf(data[key], targetSchema, `${pointer}/${key}`); - } - - // resolved schema or error - if (targetSchema) { - return targetSchema; - } - } - - // @feature patternProperties - const { patternProperties } = schema; - if (getTypeOf(patternProperties) === "object") { - // find matching property key - let regex; - const patterns = Object.keys(patternProperties); - for (let i = 0, l = patterns.length; i < l; i += 1) { - regex = new RegExp(patterns[i]); - if (regex.test(key)) { - return patternProperties[patterns[i]]; - } - } - } - - // @feature additionalProperties - const { additionalProperties } = schema; - if (getTypeOf(additionalProperties) === "object") { - return schema.additionalProperties; - } - if ( - data && - (additionalProperties === undefined || additionalProperties === true) - ) { - return createSchemaOf(data[key]); - } - - return errors.unknownPropertyError({ - property: key, - value: data, - pointer: `${pointer}`, - }); - }, -}; - -/** - * Returns the json-schema of the given object property or array item. - * e.g. it steps by one key into the data - * - * This helper determines the location of the property within the schema (additional properties, oneOf, ...) and - * returns the correct schema. - * - * @param draft - validator - * @param key - property-name or array-index - * @param schema - json schema of current data - * @param data - parent of key - * @param [pointer] - pointer to schema and data (parent of key) - * @return Schema or Error if failed resolving key - */ -export default function step( - draft: Draft, - key: string | number, - schema: JsonSchema, - data?: any, - pointer: JsonPointer = "#" -): JsonSchema | JsonError { - // @draft >= 4 ? - if (Array.isArray(schema.type)) { - const dataType = getTypeOf(data); - if (schema.type.includes(dataType)) { - return stepType[dataType](draft, `${key}`, schema, data, pointer); - } - return draft.errors.typeError({ - value: data, - pointer, - expected: schema.type, - received: dataType, - }); - } - - const expectedType = schema.type || getTypeOf(data); - const stepFunction = stepType[expectedType]; - if (stepFunction) { - return stepFunction(draft, `${key}`, schema, data, pointer); - } - - return new Error( - `Unsupported schema type ${schema.type} for key ${key}` - ) as JsonError; -}