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-environment-jsdom": "^29.7.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"monaco-sql-languages": "^0.13.1",
|
||||
|
@ -101,6 +102,7 @@
|
|||
"@testing-library/react": "^16.2.0",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/node": "^20",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
|
@ -7329,6 +7331,13 @@
|
|||
"dev": true,
|
||||
"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": {
|
||||
"version": "20.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",
|
||||
|
@ -8375,7 +8384,6 @@
|
|||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/aria-hidden": {
|
||||
|
@ -14910,7 +14918,6 @@
|
|||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
|
|
|
@ -64,6 +64,7 @@
|
|||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"monaco-sql-languages": "^0.13.1",
|
||||
|
@ -109,6 +110,7 @@
|
|||
"@testing-library/react": "^16.2.0",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/node": "^20",
|
||||
"@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