diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index f5b0d63..bd77639 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -3,33 +3,9 @@ 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 { updateValueAtPath } from "@/utils/jsonUtils"; import { generateDefaultValue, formatFieldLabel } from "@/utils/schemaUtils"; - -export type JsonValue = - | string - | number - | boolean - | null - | undefined - | JsonValue[] - | { [key: string]: JsonValue }; - -export type JsonSchemaType = { - type: - | "string" - | "number" - | "integer" - | "boolean" - | "array" - | "object" - | "null"; - description?: string; - required?: boolean; - default?: JsonValue; - properties?: Record; - items?: JsonSchemaType; -}; +import type { JsonValue, JsonSchemaType, JsonObject } from "@/utils/jsonUtils"; interface DynamicJsonFormProps { schema: JsonSchemaType; diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx index b59f3cc..9787b76 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -1,9 +1,10 @@ import { useState, memo, useMemo, useCallback, useEffect } from "react"; -import { JsonValue } from "./DynamicJsonForm"; +import type { JsonValue } from "@/utils/jsonUtils"; import clsx from "clsx"; import { Copy, CheckCheck } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; +import { getDataType, tryParseJson } from "@/utils/jsonUtils"; interface JsonViewProps { data: unknown; @@ -13,21 +14,6 @@ interface JsonViewProps { withCopyButton?: boolean; } -function tryParseJson(str: string): { success: boolean; data: JsonValue } { - const trimmed = str.trim(); - if ( - !(trimmed.startsWith("{") && trimmed.endsWith("}")) && - !(trimmed.startsWith("[") && trimmed.endsWith("]")) - ) { - return { success: false, data: str }; - } - try { - return { success: true, data: JSON.parse(str) }; - } catch { - return { success: false, data: str }; - } -} - const JsonView = memo( ({ data, @@ -119,23 +105,15 @@ interface JsonNodeProps { const JsonNode = memo( ({ data, name, depth = 0, initialExpandDepth }: JsonNodeProps) => { const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth); - - const getDataType = (value: JsonValue): string => { - if (Array.isArray(value)) return "array"; - if (value === null) return "null"; - return typeof value; - }; - - const dataType = getDataType(data); - - const typeStyleMap: Record = { + const [typeStyleMap] = useState>({ number: "text-blue-600", boolean: "text-amber-600", null: "text-purple-600", undefined: "text-gray-600", string: "text-green-600 break-all whitespace-pre-wrap", default: "text-gray-700", - }; + }); + const dataType = getDataType(data); const renderCollapsible = (isArray: boolean) => { const items = isArray diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 2fbe7d5..ea123db 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -5,7 +5,8 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { TabsContent } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; -import DynamicJsonForm, { JsonSchemaType, JsonValue } from "./DynamicJsonForm"; +import DynamicJsonForm from "./DynamicJsonForm"; +import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils"; import { generateDefaultValue } from "@/utils/schemaUtils"; import { CallToolResultSchema, diff --git a/client/src/components/__tests__/DynamicJsonForm.test.tsx b/client/src/components/__tests__/DynamicJsonForm.test.tsx index fce6014..1ad4d04 100644 --- a/client/src/components/__tests__/DynamicJsonForm.test.tsx +++ b/client/src/components/__tests__/DynamicJsonForm.test.tsx @@ -1,7 +1,7 @@ import { render, screen, fireEvent } from "@testing-library/react"; import { describe, it, expect, jest } from "@jest/globals"; import DynamicJsonForm from "../DynamicJsonForm"; -import type { JsonSchemaType } from "../DynamicJsonForm"; +import type { JsonSchemaType } from "@/utils/jsonUtils"; describe("DynamicJsonForm String Fields", () => { const renderForm = (props = {}) => { diff --git a/client/src/utils/__tests__/jsonPathUtils.test.ts b/client/src/utils/__tests__/jsonUtils.test.ts similarity index 58% rename from client/src/utils/__tests__/jsonPathUtils.test.ts rename to client/src/utils/__tests__/jsonUtils.test.ts index b6ef7e9..2a580f4 100644 --- a/client/src/utils/__tests__/jsonPathUtils.test.ts +++ b/client/src/utils/__tests__/jsonUtils.test.ts @@ -1,5 +1,146 @@ -import { updateValueAtPath, getValueAtPath } from "../jsonPathUtils"; -import { JsonValue } from "../../components/DynamicJsonForm"; +import { + getDataType, + tryParseJson, + updateValueAtPath, + getValueAtPath, +} from "../jsonUtils"; +import type { JsonValue } from "../jsonUtils"; + +describe("getDataType", () => { + test("should return 'string' for string values", () => { + expect(getDataType("hello")).toBe("string"); + expect(getDataType("")).toBe("string"); + }); + + test("should return 'number' for number values", () => { + expect(getDataType(123)).toBe("number"); + expect(getDataType(0)).toBe("number"); + expect(getDataType(-10)).toBe("number"); + expect(getDataType(1.5)).toBe("number"); + expect(getDataType(NaN)).toBe("number"); + expect(getDataType(Infinity)).toBe("number"); + }); + + test("should return 'boolean' for boolean values", () => { + expect(getDataType(true)).toBe("boolean"); + expect(getDataType(false)).toBe("boolean"); + }); + + test("should return 'undefined' for undefined value", () => { + expect(getDataType(undefined)).toBe("undefined"); + }); + + test("should return 'object' for object values", () => { + expect(getDataType({})).toBe("object"); + expect(getDataType({ key: "value" })).toBe("object"); + }); + + test("should return 'array' for array values", () => { + expect(getDataType([])).toBe("array"); + expect(getDataType([1, 2, 3])).toBe("array"); + expect(getDataType(["a", "b", "c"])).toBe("array"); + expect(getDataType([{}, { nested: true }])).toBe("array"); + }); + + test("should return 'null' for null value", () => { + expect(getDataType(null)).toBe("null"); + }); +}); + +describe("tryParseJson", () => { + test("should correctly parse valid JSON object", () => { + const jsonString = '{"name":"test","value":123}'; + const result = tryParseJson(jsonString); + + expect(result.success).toBe(true); + expect(result.data).toEqual({ name: "test", value: 123 }); + }); + + test("should correctly parse valid JSON array", () => { + const jsonString = '[1,2,3,"test"]'; + const result = tryParseJson(jsonString); + + expect(result.success).toBe(true); + expect(result.data).toEqual([1, 2, 3, "test"]); + }); + + test("should correctly parse JSON with whitespace", () => { + const jsonString = ' { "name" : "test" } '; + const result = tryParseJson(jsonString); + + expect(result.success).toBe(true); + expect(result.data).toEqual({ name: "test" }); + }); + + test("should correctly parse nested JSON structures", () => { + const jsonString = + '{"user":{"name":"test","details":{"age":30}},"items":[1,2,3]}'; + const result = tryParseJson(jsonString); + + expect(result.success).toBe(true); + expect(result.data).toEqual({ + user: { + name: "test", + details: { + age: 30, + }, + }, + items: [1, 2, 3], + }); + }); + + test("should correctly parse empty objects and arrays", () => { + expect(tryParseJson("{}").success).toBe(true); + expect(tryParseJson("{}").data).toEqual({}); + + expect(tryParseJson("[]").success).toBe(true); + expect(tryParseJson("[]").data).toEqual([]); + }); + + test("should return failure for non-JSON strings", () => { + const nonJsonString = "this is not json"; + const result = tryParseJson(nonJsonString); + + expect(result.success).toBe(false); + expect(result.data).toBe(nonJsonString); + }); + + test("should return failure for malformed JSON", () => { + const malformedJson = '{"name":"test",}'; + const result = tryParseJson(malformedJson); + + expect(result.success).toBe(false); + expect(result.data).toBe(malformedJson); + }); + + test("should return failure for strings with correct delimiters but invalid JSON", () => { + const invalidJson = "{name:test}"; + const result = tryParseJson(invalidJson); + + expect(result.success).toBe(false); + expect(result.data).toBe(invalidJson); + }); + + test("should handle edge cases", () => { + expect(tryParseJson("").success).toBe(false); + expect(tryParseJson("").data).toBe(""); + + expect(tryParseJson(" ").success).toBe(false); + expect(tryParseJson(" ").data).toBe(" "); + + expect(tryParseJson("null").success).toBe(false); + expect(tryParseJson("null").data).toBe("null"); + + expect(tryParseJson('"string"').success).toBe(false); + expect(tryParseJson('"string"').data).toBe('"string"'); + + expect(tryParseJson("123").success).toBe(false); + expect(tryParseJson("123").data).toBe("123"); + + expect(tryParseJson("true").success).toBe(false); + expect(tryParseJson("true").data).toBe("true"); + }); +}); describe("updateValueAtPath", () => { // Basic functionality tests diff --git a/client/src/utils/__tests__/schemaUtils.test.ts b/client/src/utils/__tests__/schemaUtils.test.ts index 6462e12..4c834fe 100644 --- a/client/src/utils/__tests__/schemaUtils.test.ts +++ b/client/src/utils/__tests__/schemaUtils.test.ts @@ -1,5 +1,5 @@ import { generateDefaultValue, formatFieldLabel } from "../schemaUtils"; -import { JsonSchemaType } from "../../components/DynamicJsonForm"; +import type { JsonSchemaType } from "../jsonUtils"; describe("generateDefaultValue", () => { test("generates default string", () => { diff --git a/client/src/utils/jsonPathUtils.ts b/client/src/utils/jsonUtils.ts similarity index 76% rename from client/src/utils/jsonPathUtils.ts rename to client/src/utils/jsonUtils.ts index ac99940..0987859 100644 --- a/client/src/utils/jsonPathUtils.ts +++ b/client/src/utils/jsonUtils.ts @@ -1,7 +1,57 @@ -import { JsonValue } from "../components/DynamicJsonForm"; +export type JsonValue = + | string + | number + | boolean + | null + | undefined + | JsonValue[] + | { [key: string]: JsonValue }; + +export type JsonSchemaType = { + type: + | "string" + | "number" + | "integer" + | "boolean" + | "array" + | "object" + | "null"; + description?: string; + required?: boolean; + default?: JsonValue; + properties?: Record; + items?: JsonSchemaType; +}; export type JsonObject = { [key: string]: JsonValue }; +const typeofVariable = typeof "random variable"; +export function getDataType( + value: JsonValue, +): typeof typeofVariable | "array" | "null" { + if (Array.isArray(value)) return "array"; + if (value === null) return "null"; + return typeof value; +} + +export function tryParseJson(str: string): { + success: boolean; + data: JsonValue; +} { + const trimmed = str.trim(); + if ( + !(trimmed.startsWith("{") && trimmed.endsWith("}")) && + !(trimmed.startsWith("[") && trimmed.endsWith("]")) + ) { + return { success: false, data: str }; + } + try { + return { success: true, data: JSON.parse(str) }; + } catch { + return { success: false, data: str }; + } +} + /** * Updates a value at a specific path in a nested JSON structure * @param obj The original JSON value diff --git a/client/src/utils/schemaUtils.ts b/client/src/utils/schemaUtils.ts index 686030f..ea92065 100644 --- a/client/src/utils/schemaUtils.ts +++ b/client/src/utils/schemaUtils.ts @@ -1,5 +1,4 @@ -import { JsonValue, JsonSchemaType } from "../components/DynamicJsonForm"; -import { JsonObject } from "./jsonPathUtils"; +import type { JsonValue, JsonSchemaType, JsonObject } from "./jsonUtils"; /** * Generates a default value based on a JSON schema type