diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index d76b151..4f986a8 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -3,6 +3,8 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import JsonEditor from "./JsonEditor"; +import { updateValueAtPath, JsonObject } from "@/utils/jsonPathUtils"; +import { generateDefaultValue, formatFieldLabel } from "@/utils/schemaUtils"; export type JsonValue = | string @@ -19,8 +21,6 @@ export type JsonSchemaType = { items?: JsonSchemaType; }; -type JsonObject = { [key: string]: JsonValue }; - interface DynamicJsonFormProps { schema: JsonSchemaType; value: JsonValue; @@ -28,12 +28,6 @@ interface DynamicJsonFormProps { maxDepth?: number; } -const formatFieldLabel = (key: string): string => { - return key - .replace(/([A-Z])/g, " $1") // Insert space before capital letters - .replace(/_/g, " ") // Replace underscores with spaces - .replace(/^\w/, (c) => c.toUpperCase()); // Capitalize first letter -}; const DynamicJsonForm = ({ schema, @@ -41,30 +35,6 @@ const DynamicJsonForm = ({ onChange, maxDepth = 3, }: DynamicJsonFormProps) => { - const generateDefaultValue = (propSchema: JsonSchemaType): JsonValue => { - switch (propSchema.type) { - case "string": - return ""; - case "number": - case "integer": - return 0; - case "boolean": - return false; - case "array": - return []; - case "object": { - const obj: JsonObject = {}; - if (propSchema.properties) { - Object.entries(propSchema.properties).forEach(([key, prop]) => { - obj[key] = generateDefaultValue(prop); - }); - } - return obj; - } - default: - return null; - } - }; const [isJsonMode, setIsJsonMode] = useState(false); const [jsonError, setJsonError] = useState(); @@ -272,106 +242,8 @@ const DynamicJsonForm = ({ return; } - const updateArray = ( - array: JsonValue[], - path: string[], - value: JsonValue, - ): JsonValue[] => { - const [index, ...restPath] = path; - const arrayIndex = Number(index); - - // Validate array index - if (isNaN(arrayIndex)) { - console.error(`Invalid array index: ${index}`); - return array; - } - - // Check array bounds - if (arrayIndex < 0) { - console.error(`Array index out of bounds: ${arrayIndex} < 0`); - return array; - } - - const newArray = [...array]; - - if (restPath.length === 0) { - newArray[arrayIndex] = value; - } else { - // Ensure index position exists - if (arrayIndex >= array.length) { - console.warn(`Extending array to index ${arrayIndex}`); - newArray.length = arrayIndex + 1; - newArray.fill(null, array.length, arrayIndex); - } - newArray[arrayIndex] = updateValue( - newArray[arrayIndex], - restPath, - value, - ); - } - return newArray; - }; - - const updateObject = ( - obj: JsonObject, - path: string[], - value: JsonValue, - ): JsonObject => { - const [key, ...restPath] = path; - - // Validate object key - if (typeof key !== "string") { - console.error(`Invalid object key: ${key}`); - return obj; - } - - const newObj = { ...obj }; - - if (restPath.length === 0) { - newObj[key] = value; - } else { - // Ensure key exists - if (!(key in newObj)) { - console.warn(`Creating new key in object: ${key}`); - newObj[key] = {}; - } - newObj[key] = updateValue(newObj[key], restPath, value); - } - return newObj; - }; - - const updateValue = ( - current: JsonValue, - path: string[], - value: JsonValue, - ): JsonValue => { - if (path.length === 0) return value; - - try { - if (!current) { - current = !isNaN(Number(path[0])) ? [] : {}; - } - - // Type checking - if (Array.isArray(current)) { - return updateArray(current, path, value); - } else if (typeof current === "object" && current !== null) { - return updateObject(current, path, value); - } else { - console.error( - `Cannot update path ${path.join(".")} in non-object/array value:`, - current, - ); - return current; - } - } catch (error) { - console.error(`Error updating value at path ${path.join(".")}:`, error); - return current; - } - }; - try { - const newValue = updateValue(value, path, fieldValue); + const newValue = updateValueAtPath(value, path, fieldValue); onChange(newValue); } catch (error) { console.error("Failed to update form value:", error); diff --git a/client/src/utils/jsonPathUtils.ts b/client/src/utils/jsonPathUtils.ts new file mode 100644 index 0000000..bbfe5ef --- /dev/null +++ b/client/src/utils/jsonPathUtils.ts @@ -0,0 +1,152 @@ +import { JsonValue } from "../components/DynamicJsonForm"; + +export type JsonObject = { [key: string]: JsonValue }; + +/** + * Updates a value at a specific path in a nested JSON structure + * @param obj The original JSON value + * @param path Array of keys/indices representing the path to the value + * @param value The new value to set + * @returns A new JSON value with the updated path + */ +export function updateValueAtPath( + obj: JsonValue, + path: string[], + value: JsonValue +): JsonValue { + if (path.length === 0) return value; + + // Initialize if null/undefined + if (obj === null || obj === undefined) { + obj = !isNaN(Number(path[0])) ? [] : {}; + } + + // Handle arrays + if (Array.isArray(obj)) { + return updateArray(obj, path, value); + } + // Handle objects + else if (typeof obj === "object" && obj !== null) { + return updateObject(obj as JsonObject, path, value); + } + // Cannot update primitives + else { + console.error( + `Cannot update path ${path.join(".")} in non-object/array value:`, + obj + ); + return obj; + } +} + +/** + * Updates an array at a specific path + */ +function updateArray( + array: JsonValue[], + path: string[], + value: JsonValue +): JsonValue[] { + const [index, ...restPath] = path; + const arrayIndex = Number(index); + + // Validate array index + if (isNaN(arrayIndex)) { + console.error(`Invalid array index: ${index}`); + return array; + } + + // Check array bounds + if (arrayIndex < 0) { + console.error(`Array index out of bounds: ${arrayIndex} < 0`); + return array; + } + + const newArray = [...array]; + + if (restPath.length === 0) { + newArray[arrayIndex] = value; + } else { + // Ensure index position exists + if (arrayIndex >= array.length) { + console.warn(`Extending array to index ${arrayIndex}`); + newArray.length = arrayIndex + 1; + newArray.fill(null, array.length, arrayIndex); + } + newArray[arrayIndex] = updateValueAtPath( + newArray[arrayIndex], + restPath, + value + ); + } + return newArray; +} + +/** + * Updates an object at a specific path + */ +function updateObject( + obj: JsonObject, + path: string[], + value: JsonValue +): JsonObject { + const [key, ...restPath] = path; + + // Validate object key + if (typeof key !== "string") { + console.error(`Invalid object key: ${key}`); + return obj; + } + + const newObj = { ...obj }; + + if (restPath.length === 0) { + newObj[key] = value; + } else { + // Ensure key exists + if (!(key in newObj)) { + console.warn(`Creating new key in object: ${key}`); + newObj[key] = {}; + } + newObj[key] = updateValueAtPath(newObj[key], restPath, value); + } + return newObj; +} + +/** + * Gets a value at a specific path in a nested JSON structure + * @param obj The JSON value to traverse + * @param path Array of keys/indices representing the path to the value + * @param defaultValue Value to return if path doesn't exist + * @returns The value at the path, or defaultValue if not found + */ +export function getValueAtPath( + obj: JsonValue, + path: string[], + defaultValue: JsonValue = null +): JsonValue { + if (path.length === 0) return obj; + + const [first, ...rest] = path; + + if (obj === null || obj === undefined) { + return defaultValue; + } + + if (Array.isArray(obj)) { + const index = Number(first); + if (isNaN(index) || index < 0 || index >= obj.length) { + return defaultValue; + } + return getValueAtPath(obj[index], rest, defaultValue); + } + + if (typeof obj === "object" && obj !== null) { + if (!(first in obj)) { + return defaultValue; + } + return getValueAtPath((obj as JsonObject)[first], rest, defaultValue); + } + + return defaultValue; +} diff --git a/client/src/utils/schemaUtils.ts b/client/src/utils/schemaUtils.ts new file mode 100644 index 0000000..feee4de --- /dev/null +++ b/client/src/utils/schemaUtils.ts @@ -0,0 +1,72 @@ +import { JsonValue, JsonSchemaType } from "../components/DynamicJsonForm"; +import { JsonObject } from "./jsonPathUtils"; + +/** + * Generates a default value based on a JSON schema type + * @param schema The JSON schema definition + * @returns A default value matching the schema type + */ +export function generateDefaultValue(schema: JsonSchemaType): JsonValue { + switch (schema.type) { + case "string": + return ""; + case "number": + case "integer": + return 0; + case "boolean": + return false; + case "array": + return []; + case "object": { + const obj: JsonObject = {}; + if (schema.properties) { + Object.entries(schema.properties).forEach(([key, prop]) => { + obj[key] = generateDefaultValue(prop); + }); + } + return obj; + } + default: + return null; + } +} + +/** + * Formats a field key into a human-readable label + * @param key The field key to format + * @returns A formatted label string + */ +export function formatFieldLabel(key: string): string { + return key + .replace(/([A-Z])/g, " $1") // Insert space before capital letters + .replace(/_/g, " ") // Replace underscores with spaces + .replace(/^\w/, (c) => c.toUpperCase()); // Capitalize first letter +} + +/** + * Validates if a value conforms to a JSON schema + * @param value The value to validate + * @param schema The JSON schema to validate against + * @returns True if valid, false otherwise + */ +export function validateValueAgainstSchema( + value: JsonValue, + schema: JsonSchemaType +): boolean { + // Basic type validation + switch (schema.type) { + case "string": + return typeof value === "string"; + case "number": + case "integer": + return typeof value === "number"; + case "boolean": + return typeof value === "boolean"; + case "array": + return Array.isArray(value); + case "object": + return typeof value === "object" && value !== null && !Array.isArray(value); + default: + return true; + } +}