From 06cc3e4346228ee3e48047bb3fb447ed0eed011b Mon Sep 17 00:00:00 2001 From: Joe Bottigliero <694253+jbottigliero@users.noreply.github.com> Date: Wed, 9 Oct 2024 19:14:16 -0500 Subject: [PATCH] feat: adds "Input Schema" validation --- public/schemas/input_schema_schema.json | 246 ++++++++++++++++++++++++ src/components/Editor.tsx | 30 ++- src/pages/index.tsx | 54 +++++- src/stores/editor.ts | 9 + 4 files changed, 325 insertions(+), 14 deletions(-) create mode 100644 public/schemas/input_schema_schema.json diff --git a/public/schemas/input_schema_schema.json b/public/schemas/input_schema_schema.json new file mode 100644 index 0000000..4e2623a --- /dev/null +++ b/public/schemas/input_schema_schema.json @@ -0,0 +1,246 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#" + } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { + "$ref": "#/definitions/nonNegativeInteger" + }, + { + "default": 0 + } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$comment": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { + "$ref": "#/definitions/nonNegativeInteger" + }, + "minLength": { + "$ref": "#/definitions/nonNegativeIntegerDefault0" + }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { + "$ref": "#" + }, + "items": { + "anyOf": [ + { + "$ref": "#" + }, + { + "$ref": "#/definitions/schemaArray" + } + ], + "default": true + }, + "maxItems": { + "$ref": "#/definitions/nonNegativeInteger" + }, + "minItems": { + "$ref": "#/definitions/nonNegativeIntegerDefault0" + }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { + "$ref": "#" + }, + "maxProperties": { + "$ref": "#/definitions/nonNegativeInteger" + }, + "minProperties": { + "$ref": "#/definitions/nonNegativeIntegerDefault0" + }, + "required": { + "$ref": "#/definitions/stringArray" + }, + "additionalProperties": { + "$ref": "#" + }, + "definitions": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "propertyNames": { + "format": "regex" + }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#" + }, + { + "$ref": "#/definitions/stringArray" + } + ] + } + }, + "propertyNames": { + "$ref": "#" + }, + "const": true, + "enum": { + "type": "array", + "items": true, + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { + "$ref": "#/definitions/simpleTypes" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/simpleTypes" + }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { + "type": "string" + }, + "contentMediaType": { + "type": "string" + }, + "contentEncoding": { + "type": "string" + }, + "if": { + "$ref": "#" + }, + "then": { + "$ref": "#" + }, + "else": { + "$ref": "#" + }, + "allOf": { + "$ref": "#/definitions/schemaArray" + }, + "anyOf": { + "$ref": "#/definitions/schemaArray" + }, + "oneOf": { + "$ref": "#/definitions/schemaArray" + }, + "not": { + "$ref": "#" + }, + "propertyOrder": { + "$ref": "#/definitions/stringArray" + } + }, + "default": true, + "additionalProperties": false +} diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index 7bc06bf..76bca29 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -7,16 +7,30 @@ import MonacoEditor, { import { ValidateButton } from "./Validate"; import { Box } from "@chakra-ui/react"; +export const MODES = { + DEFINITION: "DEFINITION", + INPUT_SCHEMA: "INPUT_SCHEMA", +} as const; + +export type SupportedModes = keyof typeof MODES; + +function isDefinitionMode(settings?: InteralSettings) { + return settings?.mode === MODES.DEFINITION || !settings?.mode; +} + type InteralSettings = { - enableExperimentalValidation: boolean; + mode?: SupportedModes; + enableExperimentalValidation?: boolean; }; function configureEditor( monaco: Monaco, settings: InteralSettings = { + mode: MODES.DEFINITION, enableExperimentalValidation: true, }, ) { + console.log("Configuring editor with settings", settings); monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ enableSchemaRequest: true, validate: true, @@ -26,7 +40,11 @@ function configureEditor( schemas: [ { uri: `${window.location.origin}${process.env.NEXT_PUBLIC_BASE_PATH}/schemas/flow_definition_schema.json`, - fileMatch: ["*"], + fileMatch: ["definition.json"], + }, + { + uri: `${window.location.origin}${process.env.NEXT_PUBLIC_BASE_PATH}/schemas/input_schema_schema.json`, + fileMatch: ["input-schema.json"], }, ], }); @@ -46,9 +64,11 @@ export default function Editor( }} {...props} /> - - - + {isDefinitionMode(props.settings) && ( + + + + )} ); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index add8603..ff72a68 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -24,7 +24,7 @@ import { IconButton, } from "@chakra-ui/react"; import { CloseIcon, HamburgerIcon } from "@chakra-ui/icons"; -import Editor from "../components/Editor"; +import Editor, { MODES, SupportedModes } from "../components/Editor"; import Diagram from "../components/Diagram/Diagram"; import { compressToEncodedURIComponent, @@ -93,6 +93,14 @@ export default function Home() { const [invalidMarkers, setValidity] = useState([]); const [showPanel, setShowPanel] = useState(false); + const [mode, setMode] = useState(MODES.DEFINITION); + + const isDefinitionMode = mode === MODES.DEFINITION; + + const editorContents = isDefinitionMode + ? editorStore.definition + : editorStore.schmea; + useEffect(() => { if (bootstrapped.current) return; bootstrapped.current = true; @@ -115,11 +123,11 @@ export default function Home() { }, [editorStore]); useEffect(() => { - if (definition) { + if (isDefinitionMode && definition) { const v = compressToEncodedURIComponent(JSON.stringify(definition)); router.push(`/?d=${v}`); } - }, [definition, router]); + }, [isDefinitionMode, definition, router]); function handleEditorValidate(markers: any[]) { const errors = markers.filter( @@ -180,18 +188,44 @@ export default function Home() { - + {showPanel && } + { + setMode(index === 0 ? MODES.DEFINITION : MODES.INPUT_SCHEMA); + }} + > + + Definiton + Input Schema + + { + if (isDefinitionMode) { + editorStore.replaceDefinitionFromString(value); + } else { + editorStore.replaceSchemaFromString(value); + } + }} onValidate={handleEditorValidate} theme="vs-dark" - settings={{ enableExperimentalValidation: true }} + settings={{ enableExperimentalValidation: true, mode }} + path={ + mode === MODES.DEFINITION + ? "definition.json" + : "input-schema.json" + } /> - Diagram + Definiton Diagram + {/* Input Schema UI */} Documentation @@ -238,6 +273,7 @@ export default function Home() { )} + {/* */} | undefined; definition: FlowDefinition | undefined; replace: (def?: FlowDefinition) => void; + replaceSchemaFromString: (s?: string) => void; replaceDefinitionFromString: (s?: string) => void; /** * Attempt to restore the definition from local storage. @@ -26,7 +28,14 @@ type EditorState = { const DEFINITION_LOCAL_STORAGE_KEY = "definition"; export const useEditorStore = create((set, get) => ({ + schmea: undefined, definition: undefined, + replaceSchemaFromString(s?: string) { + try { + const v = s ? JSON.parse(s) : undefined; + set({ schmea: v }); + } catch {} + }, replaceDefinitionFromString(s?: string) { try { const v = s ? JSON.parse(s) : undefined;