mirror of https://github.com/buster-so/buster.git
validation yaml
This commit is contained in:
parent
32d43db1ea
commit
675379c00b
|
@ -56,6 +56,7 @@
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"monaco-sql-languages": "^0.13.1",
|
"monaco-sql-languages": "^0.13.1",
|
||||||
|
@ -101,6 +102,7 @@
|
||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/papaparse": "^5.3.15",
|
"@types/papaparse": "^5.3.15",
|
||||||
|
@ -7329,6 +7331,13 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/js-yaml": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/jsdom": {
|
"node_modules/@types/jsdom": {
|
||||||
"version": "20.0.1",
|
"version": "20.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",
|
||||||
|
@ -8375,7 +8384,6 @@
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/aria-hidden": {
|
"node_modules/aria-hidden": {
|
||||||
|
@ -14910,7 +14918,6 @@
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
|
|
|
@ -64,6 +64,7 @@
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"monaco-sql-languages": "^0.13.1",
|
"monaco-sql-languages": "^0.13.1",
|
||||||
|
@ -109,6 +110,7 @@
|
||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/papaparse": "^5.3.15",
|
"@types/papaparse": "^5.3.15",
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { MyYamlEditor } from './validateMetricYaml';
|
||||||
|
|
||||||
|
const meta: Meta<typeof MyYamlEditor> = {
|
||||||
|
title: 'Metrics/Files/MyYamlEditor',
|
||||||
|
component: MyYamlEditor,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered'
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<div className="m-12 h-[600px] min-h-[600px] w-[800px] min-w-[800px] border">
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof MyYamlEditor>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {}
|
||||||
|
};
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { validateMetricYaml } from './validateMetricYaml';
|
||||||
|
import * as yaml from 'js-yaml';
|
||||||
|
|
||||||
|
// Create a minimal mock for monaco
|
||||||
|
const mockMonaco = {
|
||||||
|
MarkerSeverity: {
|
||||||
|
Error: 8, // These values match monaco-editor's real values
|
||||||
|
Warning: 4,
|
||||||
|
Info: 2,
|
||||||
|
Hint: 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('validateMetricYaml', () => {
|
||||||
|
it('should validate a correctly formatted YAML', () => {
|
||||||
|
// Arrange
|
||||||
|
const validYaml = `Person: "John Doe"
|
||||||
|
Place: "Wonderland"
|
||||||
|
Age: 30
|
||||||
|
Siblings:
|
||||||
|
Jane: 25
|
||||||
|
Jim: 28`;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = validateMetricYaml(validYaml, mockMonaco as any);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect missing required keys', () => {
|
||||||
|
// Arrange
|
||||||
|
const invalidYaml = `Person: "John Doe"
|
||||||
|
Place: "Wonderland"
|
||||||
|
Age: 30
|
||||||
|
`; // Missing Siblings
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = validateMetricYaml(invalidYaml, mockMonaco as any);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.length).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
result.some(
|
||||||
|
(marker) =>
|
||||||
|
marker.message.includes('Missing required key "Siblings"') &&
|
||||||
|
marker.severity === mockMonaco.MarkerSeverity.Error
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect invalid types for fields', () => {
|
||||||
|
// Arrange
|
||||||
|
const invalidYaml = `Person: "John Doe"
|
||||||
|
Place: "Wonderland"
|
||||||
|
Age: "thirty" # Should be a number
|
||||||
|
Siblings:
|
||||||
|
Jane: 25
|
||||||
|
Jim: 28`;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = validateMetricYaml(invalidYaml, mockMonaco as any);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.length).toBeGreaterThan(0);
|
||||||
|
const ageErrorMarker = result.find((marker) =>
|
||||||
|
marker.message.includes('The "Age" field must be a number')
|
||||||
|
);
|
||||||
|
expect(ageErrorMarker).toBeDefined();
|
||||||
|
expect(ageErrorMarker?.severity).toBe(mockMonaco.MarkerSeverity.Error);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,180 @@
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
import * as yaml from 'js-yaml';
|
||||||
|
import * as monaco from 'monaco-editor';
|
||||||
|
import { type editor } from 'monaco-editor/esm/vs/editor/editor.api';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import MonacoEditor, { OnMount } from '@monaco-editor/react';
|
||||||
|
|
||||||
|
type IMarkerData = editor.IMarkerData;
|
||||||
|
|
||||||
|
// Initial YAML content for testing
|
||||||
|
const initialValue = `Person: "John Doe"
|
||||||
|
Place: "Wonderland"
|
||||||
|
Age: 30
|
||||||
|
Siblings:
|
||||||
|
Jane: 25
|
||||||
|
Jim: 28`;
|
||||||
|
|
||||||
|
// Linting function that validates YAML content
|
||||||
|
export const validateMetricYaml = (content: string, monaco: any): IMarkerData[] => {
|
||||||
|
const markers: IMarkerData[] = [];
|
||||||
|
let parsed: any;
|
||||||
|
|
||||||
|
// Parse YAML content
|
||||||
|
try {
|
||||||
|
parsed = yaml.load(content);
|
||||||
|
} catch (error: any) {
|
||||||
|
markers.push({
|
||||||
|
severity: monaco.MarkerSeverity.Error,
|
||||||
|
message: 'Invalid YAML 🤣: ' + error.message,
|
||||||
|
startLineNumber: 1,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: 1,
|
||||||
|
endColumn: 1
|
||||||
|
});
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the parsed result is an object
|
||||||
|
if (typeof parsed !== 'object' || parsed === null) {
|
||||||
|
markers.push({
|
||||||
|
severity: monaco.MarkerSeverity.Error,
|
||||||
|
message: 'YAML content should be an object with properties.',
|
||||||
|
startLineNumber: 1,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: 1,
|
||||||
|
endColumn: 1
|
||||||
|
});
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allowed keys
|
||||||
|
const allowedKeys = ['Person', 'Place', 'Age', 'Siblings'];
|
||||||
|
const keys = Object.keys(parsed);
|
||||||
|
|
||||||
|
// Check for any unexpected keys
|
||||||
|
keys.forEach((key) => {
|
||||||
|
if (!allowedKeys.includes(key)) {
|
||||||
|
markers.push({
|
||||||
|
severity: monaco.MarkerSeverity.Error,
|
||||||
|
message: `Unexpected key "${key}". Only ${allowedKeys.join(', ')} are allowed.`,
|
||||||
|
startLineNumber: 1,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: 1,
|
||||||
|
endColumn: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for missing keys
|
||||||
|
allowedKeys.forEach((key) => {
|
||||||
|
if (!(key in parsed)) {
|
||||||
|
markers.push({
|
||||||
|
severity: monaco.MarkerSeverity.Error,
|
||||||
|
message: `Missing required key "${key}".`,
|
||||||
|
startLineNumber: 1,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: 1,
|
||||||
|
endColumn: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate types for each field
|
||||||
|
if ('Person' in parsed && typeof parsed.Person !== 'string') {
|
||||||
|
markers.push({
|
||||||
|
severity: monaco.MarkerSeverity.Error,
|
||||||
|
message: 'The "Person" field must be a string.',
|
||||||
|
startLineNumber: 1,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: 1,
|
||||||
|
endColumn: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('Place' in parsed && typeof parsed.Place !== 'string') {
|
||||||
|
markers.push({
|
||||||
|
severity: monaco.MarkerSeverity.Error,
|
||||||
|
message: 'The "Place" field must be a string.',
|
||||||
|
startLineNumber: 1,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: 1,
|
||||||
|
endColumn: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('Age' in parsed && typeof parsed.Age !== 'number') {
|
||||||
|
markers.push({
|
||||||
|
severity: monaco.MarkerSeverity.Error,
|
||||||
|
message: 'The "Age" field must be a number.',
|
||||||
|
startLineNumber: 1,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: 1,
|
||||||
|
endColumn: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('Siblings' in parsed) {
|
||||||
|
if (
|
||||||
|
typeof parsed.Siblings !== 'object' ||
|
||||||
|
Array.isArray(parsed.Siblings) ||
|
||||||
|
parsed.Siblings === null
|
||||||
|
) {
|
||||||
|
markers.push({
|
||||||
|
severity: monaco.MarkerSeverity.Error,
|
||||||
|
message: 'The "Siblings" field must be an object with sibling name-age pairs.',
|
||||||
|
startLineNumber: 1,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: 1,
|
||||||
|
endColumn: 1
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Validate that each sibling’s age is a number
|
||||||
|
Object.entries(parsed.Siblings).forEach(([siblingName, siblingAge]) => {
|
||||||
|
if (typeof siblingAge !== 'number') {
|
||||||
|
markers.push({
|
||||||
|
severity: monaco.MarkerSeverity.Error,
|
||||||
|
message: `The age for sibling "${siblingName}" must be a number.`,
|
||||||
|
startLineNumber: 1,
|
||||||
|
startColumn: 1,
|
||||||
|
endLineNumber: 1,
|
||||||
|
endColumn: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return markers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MyYamlEditor: React.FC = () => {
|
||||||
|
const editorRef = useRef<any>(null);
|
||||||
|
|
||||||
|
// Called once the Monaco editor is mounted
|
||||||
|
const editorDidMount = (editor: any, monacoInstance: typeof import('monaco-editor')) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
|
||||||
|
// Lint the document on every change
|
||||||
|
editor.onDidChangeModelContent(() => {
|
||||||
|
const value = editor.getValue();
|
||||||
|
const markers = validateMetricYaml(value, monacoInstance);
|
||||||
|
monacoInstance.editor.setModelMarkers(editor.getModel(), 'yaml', markers);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MonacoEditor
|
||||||
|
width="800"
|
||||||
|
height="600"
|
||||||
|
language="yaml"
|
||||||
|
className="h-full min-h-[600px] w-full min-w-[800px] border"
|
||||||
|
theme="vs-light"
|
||||||
|
value={initialValue}
|
||||||
|
onMount={editorDidMount}
|
||||||
|
options={{
|
||||||
|
automaticLayout: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue