Merge pull request #264 from samuel871211/addUnitTest
refactor(json): Consolidate JSON utilities and type definitions
This commit is contained in:
@@ -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<string, JsonSchemaType>;
|
||||
items?: JsonSchemaType;
|
||||
};
|
||||
import type { JsonValue, JsonSchemaType, JsonObject } from "@/utils/jsonUtils";
|
||||
|
||||
interface DynamicJsonFormProps {
|
||||
schema: JsonSchemaType;
|
||||
|
||||
@@ -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<string, string> = {
|
||||
const [typeStyleMap] = useState<Record<string, string>>({
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {}) => {
|
||||
|
||||
@@ -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
|
||||
@@ -8,17 +149,17 @@ describe("updateValueAtPath", () => {
|
||||
});
|
||||
|
||||
test("initializes an empty object when input is null/undefined and path starts with a string", () => {
|
||||
expect(updateValueAtPath(null as any, ["foo"], "bar")).toEqual({
|
||||
expect(updateValueAtPath(null, ["foo"], "bar")).toEqual({
|
||||
foo: "bar",
|
||||
});
|
||||
expect(updateValueAtPath(undefined as any, ["foo"], "bar")).toEqual({
|
||||
expect(updateValueAtPath(undefined, ["foo"], "bar")).toEqual({
|
||||
foo: "bar",
|
||||
});
|
||||
});
|
||||
|
||||
test("initializes an empty array when input is null/undefined and path starts with a number", () => {
|
||||
expect(updateValueAtPath(null as any, ["0"], "bar")).toEqual(["bar"]);
|
||||
expect(updateValueAtPath(undefined as any, ["0"], "bar")).toEqual(["bar"]);
|
||||
expect(updateValueAtPath(null, ["0"], "bar")).toEqual(["bar"]);
|
||||
expect(updateValueAtPath(undefined, ["0"], "bar")).toEqual(["bar"]);
|
||||
});
|
||||
|
||||
// Object update tests
|
||||
@@ -152,10 +293,8 @@ describe("getValueAtPath", () => {
|
||||
});
|
||||
|
||||
test("returns default value when input is null/undefined", () => {
|
||||
expect(getValueAtPath(null as any, ["foo"], "default")).toBe("default");
|
||||
expect(getValueAtPath(undefined as any, ["foo"], "default")).toBe(
|
||||
"default",
|
||||
);
|
||||
expect(getValueAtPath(null, ["foo"], "default")).toBe("default");
|
||||
expect(getValueAtPath(undefined, ["foo"], "default")).toBe("default");
|
||||
});
|
||||
|
||||
test("handles array indices correctly", () => {
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -1,7 +1,66 @@
|
||||
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<string, JsonSchemaType>;
|
||||
items?: JsonSchemaType;
|
||||
};
|
||||
|
||||
export type JsonObject = { [key: string]: JsonValue };
|
||||
|
||||
export type DataType =
|
||||
| "string"
|
||||
| "number"
|
||||
| "bigint"
|
||||
| "boolean"
|
||||
| "symbol"
|
||||
| "undefined"
|
||||
| "object"
|
||||
| "function"
|
||||
| "array"
|
||||
| "null";
|
||||
|
||||
export function getDataType(value: JsonValue): DataType {
|
||||
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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user