Skip to content

Commit 98160e1

Browse files
authored
848 improve schema validation errors (#849)
* implement bulk validation error position algorithm * update settings schema AI options * ensure path filtering matches only full path segments * apply formatting changes --------- Co-authored-by: Logende <[email protected]>
1 parent 04fe110 commit 98160e1

8 files changed

Lines changed: 149 additions & 9 deletions

File tree

meta_configurator/src/components/panels/code-editor/setupAnnotations.ts

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {type Annotation, Editor} from 'brace';
22
import type {ErrorObject} from 'ajv';
3-
import {jsonPointerToPath} from '@/utility/pathUtils';
3+
import {jsonPointerToPath, pathToJsonPointer} from '@/utility/pathUtils';
44
import {determineCursorPosition} from '@/components/panels/code-editor/aceUtility';
55
import {computed} from 'vue';
6-
import {useDataConverter} from '@/dataformats/formatRegistry';
6+
import {useDataConverter, usePathIndexLink} from '@/dataformats/formatRegistry';
77
import {watchDebounced} from '@vueuse/core';
88
import type {SessionMode} from '@/store/sessionMode';
99
import {getValidationForMode} from '@/data/useDataLink';
@@ -12,6 +12,7 @@ import {useSettings} from '@/settings/useSettings';
1212
/**
1313
* Sets up the editor to show validation errors.
1414
* @param editor the ace editor
15+
* @param mode
1516
*/
1617
export function setupAnnotationsFromValidationErrors(editor: Editor, mode: SessionMode) {
1718
const validationAnnotations = computed(() => {
@@ -22,11 +23,21 @@ export function setupAnnotationsFromValidationErrors(editor: Editor, mode: Sessi
2223
}
2324

2425
let {errors} = getValidationForMode(mode).currentValidationResult.value;
25-
const maxErrorsToShow = useSettings().value.performance.maxErrorsToShow;
26+
27+
const supportsBulkPathDetermination = usePathIndexLink().determineIndexesOfPaths !== undefined;
28+
29+
const maxErrorsToShow = supportsBulkPathDetermination
30+
? useSettings().value.performance.maxErrorsToShowBulkValidation
31+
: useSettings().value.performance.maxErrorsToShow;
2632
if (errors.length > maxErrorsToShow) {
2733
errors = errors.slice(0, maxErrorsToShow);
2834
}
29-
return errors.map(error => validationErrorToAnnotation(editor, error));
35+
36+
if (supportsBulkPathDetermination) {
37+
return validationErrorsToAnnotations(editor, errors);
38+
} else {
39+
return errors.map(error => validationErrorToAnnotation(editor, error));
40+
}
3041
});
3142

3243
watchDebounced(
@@ -46,3 +57,42 @@ function validationErrorToAnnotation(editor: Editor, error: ErrorObject): Annota
4657
type: 'error',
4758
};
4859
}
60+
61+
// optimized version that uses bulk path index determination
62+
function validationErrorsToAnnotations(editor: Editor, errors: ErrorObject[]): Annotation[] {
63+
const result: Annotation[] = [];
64+
const positions = usePathIndexLink().determineIndexesOfPaths!(
65+
editor.getValue(),
66+
errors.map(error => jsonPointerToPath(error.instancePath))
67+
);
68+
69+
const cachedPositionsForIndices: {[index: number]: {row: number; column: number}} = {};
70+
71+
for (const error of errors) {
72+
const instancePathTranslated = jsonPointerToPath(error.instancePath);
73+
// note that we use our own pathToJsonPointer here, to ensure consistent serialization
74+
const instancePathKey = pathToJsonPointer(instancePathTranslated);
75+
if (!(instancePathKey in positions)) {
76+
continue;
77+
}
78+
79+
const index = positions[instancePathKey];
80+
let position;
81+
if (index in cachedPositionsForIndices) {
82+
position = cachedPositionsForIndices[index];
83+
} else {
84+
position = editor.session.doc.indexToPosition(index, 0);
85+
cachedPositionsForIndices[index] = position;
86+
}
87+
88+
const annotation: Annotation = {
89+
row: position.row,
90+
column: position.column,
91+
text: error.message ?? 'Validation error',
92+
type: 'error',
93+
};
94+
result.push(annotation);
95+
}
96+
97+
return result;
98+
}

meta_configurator/src/components/panels/gui-editor/PropertyMetadata.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {focus, makeEditableAndSelectContents} from '@/utility/focusUtils';
1414
import {useSettings} from '@/settings/useSettings';
1515
import type {SessionMode} from '@/store/sessionMode';
1616
import {getSessionForMode, getUserSelectionForMode} from '@/data/useDataLink';
17-
import type {ValidationResult} from '@/schema/validationService';
17+
import type {ValidationResult} from '@/schema/validationUtils';
1818
import {
1919
getDisplayNameOfNode,
2020
isDeprecated,

meta_configurator/src/dataformats/pathIndexLink.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ export interface PathIndexLink {
1414
*/
1515
determineIndexOfPath(dataString: string, path: Path): number;
1616

17+
/**
18+
* (Optional) Determines the indexes of the first character of the data at the given paths.
19+
* This method can be implemented for performance optimization to avoid multiple calls to
20+
* `determineIndexOfPath`.
21+
*
22+
* @param dataString the unparsed data
23+
* @param paths the paths to determine the cursor positions for
24+
* @return an object which represents the mappings from path to index
25+
*/
26+
determineIndexesOfPaths?(dataString: string, paths: Path[]): {[pathKey: string]: number};
27+
1728
/**
1829
* Determines the path for the data that is at the given index of the data string.
1930
*

meta_configurator/src/dataformats/pathIndexLinkJson.ts

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type {PathIndexLink} from '@/dataformats/pathIndexLink';
2-
import type {Path} from '@/utility/path';
2+
import {type Path} from '@/utility/path';
33
import type {
44
CstDocument,
55
CstNode,
@@ -10,6 +10,7 @@ import type {
1010
} from 'json-cst';
1111
import {parse} from 'json-cst';
1212
import {errorService} from '@/main';
13+
import {pathToJsonPointer, pathToString} from '@/utility/pathUtils';
1314

1415
/**
1516
* Implementation of PathIndexLink for JSON data.
@@ -27,7 +28,6 @@ export class PathIndexLinkJson implements PathIndexLink {
2728
const cst = this.getCst(editorContent);
2829
return this.determineIndexStep(cst.root, currentPath);
2930
} catch (e) {
30-
errorService.onError(e);
3131
errorService.onError(e);
3232
return 0;
3333
}
@@ -157,4 +157,70 @@ export class PathIndexLinkJson implements PathIndexLink {
157157
const childPath = this.determinePathStep(currentNode.valueNode, targetCharacter);
158158
return childPath ?? [];
159159
}
160+
161+
// performance optimization to avoid multiple calls to determineIndexOfPath
162+
determineIndexesOfPaths(editorContent: string, paths: Path[]): {[pathKey: string]: number} {
163+
if (editorContent.length === 0) {
164+
return {};
165+
}
166+
try {
167+
const cst = this.getCst(editorContent);
168+
const result = {};
169+
// transform paths into a set of path keys for faster lookup
170+
const pathSet = new Set<string>();
171+
for (const path of paths) {
172+
pathSet.add(pathToJsonPointer(path));
173+
}
174+
this.traverseCstForIndexesForPaths(cst.root, pathSet, [], result);
175+
return result;
176+
} catch (e) {
177+
errorService.onError(e);
178+
return {};
179+
}
180+
}
181+
182+
// traverses the complete cst, always keeping track of the current path. When having a match with one of the requested paths, the index is stored in the result object.
183+
// only ends when end of cst is reached or all paths have been found
184+
private traverseCstForIndexesForPaths(
185+
currentNode: CstNode,
186+
paths: Set<string>,
187+
currentPath: Path,
188+
result: {[pathKey: string]: number}
189+
) {
190+
const pathKey = pathToJsonPointer(currentPath);
191+
if (paths.has(pathKey) && !(pathKey in result)) {
192+
result[pathKey] = currentNode.range.start;
193+
if (Object.keys(result).length === paths.size) {
194+
return; // all paths found
195+
}
196+
}
197+
switch (currentNode.kind) {
198+
case 'object':
199+
for (const childNode of currentNode.children) {
200+
this.traverseCstForIndexesForPaths(
201+
childNode,
202+
paths,
203+
currentPath.concat([childNode.key]),
204+
result
205+
);
206+
}
207+
break;
208+
case 'object-property':
209+
this.traverseCstForIndexesForPaths(currentNode.valueNode, paths, currentPath, result);
210+
break;
211+
case 'array':
212+
let index = 0;
213+
for (const childNode of currentNode.children) {
214+
this.traverseCstForIndexesForPaths(childNode, paths, currentPath.concat([index]), result);
215+
index++;
216+
}
217+
break;
218+
case 'array-element':
219+
this.traverseCstForIndexesForPaths(currentNode.valueNode, paths, currentPath, result);
220+
break;
221+
default:
222+
// do nothing for primitive nodes
223+
break;
224+
}
225+
}
160226
}

meta_configurator/src/schema/validationUtils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ export class ValidationResult {
5050
*/
5151
public filterForPath(path: Path): ValidationResult {
5252
const jsonPointer = pathToJsonPointer(path);
53-
const filteredErrors = this.errors.filter(error => error.instancePath.startsWith(jsonPointer));
53+
const filteredErrors = this.errors.filter(error => {
54+
const instancePath = error.instancePath;
55+
return instancePath === jsonPointer || instancePath.startsWith(jsonPointer + '/');
56+
});
5457
return new ValidationResult(filteredErrors);
5558
}
5659

meta_configurator/src/settings/defaultSettingsData.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const SETTINGS_DATA_DEFAULT = {
1515
minObjectPropertyCountToPreserve: 16, // when large document is trimmed, this is minimum count of object properties to be preserved
1616
maxShownChildrenInGuiEditor: 50,
1717
maxErrorsToShow: 15,
18+
maxErrorsToShowBulkValidation: 200,
1819
},
1920
codeEditor: {
2021
fontSize: 14,

meta_configurator/src/settings/settingsSchema.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export const SETTINGS_SCHEMA: TopLevelSchema = {
5252
'minObjectPropertyCountToPreserve',
5353
'maxShownChildrenInGuiEditor',
5454
'maxErrorsToShow',
55+
'maxErrorsToShowBulkValidation',
5556
],
5657
additionalProperties: false,
5758
description: 'Performance related settings belong here.',
@@ -98,6 +99,13 @@ export const SETTINGS_SCHEMA: TopLevelSchema = {
9899
default: 15,
99100
minimum: 1,
100101
},
102+
maxErrorsToShowBulkValidation: {
103+
type: 'integer',
104+
description:
105+
'The maximum number of validation errors to show in case of internal use of bulk validation, which is supported only for some data formats.',
106+
default: 200,
107+
minimum: 1,
108+
},
101109
},
102110
},
103111
codeEditor: {
@@ -489,7 +497,7 @@ export const SETTINGS_SCHEMA: TopLevelSchema = {
489497
// Perplexity (OpenAI-compatible)
490498
'https://api.perplexity.ai/',
491499
// OpenRouter (aggregator, OpenAI-compatible)
492-
'https://api.openrouter.ai/v1/',
500+
// 'https://api.openrouter.ai/v1/', seems to not support this kind of authentication
493501
// Academic / institutional deployments (OpenAI-compatible)
494502
'https://chat-ai.academiccloud.de/v1/',
495503
'https://api.helmholtz-blablador.fz-juelich.de/v1/',

meta_configurator/src/settings/settingsTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface SettingsInterfacePerformance {
2727
minObjectPropertyCountToPreserve: number; // when large document is trimmed, this is minimum count of object properties to be preserved
2828
maxShownChildrenInGuiEditor: number;
2929
maxErrorsToShow: number;
30+
maxErrorsToShowBulkValidation: number;
3031
}
3132

3233
export interface SettingsInterfaceCodeEditor {

0 commit comments

Comments
 (0)