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 { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import JsonEditor from "./JsonEditor";
|
import JsonEditor from "./JsonEditor";
|
||||||
import { updateValueAtPath, JsonObject } from "@/utils/jsonPathUtils";
|
import { updateValueAtPath } from "@/utils/jsonUtils";
|
||||||
import { generateDefaultValue, formatFieldLabel } from "@/utils/schemaUtils";
|
import { generateDefaultValue, formatFieldLabel } from "@/utils/schemaUtils";
|
||||||
|
import type { JsonValue, JsonSchemaType, JsonObject } from "@/utils/jsonUtils";
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface DynamicJsonFormProps {
|
interface DynamicJsonFormProps {
|
||||||
schema: JsonSchemaType;
|
schema: JsonSchemaType;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useState, memo, useMemo, useCallback, useEffect } from "react";
|
import { useState, memo, useMemo, useCallback, useEffect } from "react";
|
||||||
import { JsonValue } from "./DynamicJsonForm";
|
import type { JsonValue } from "@/utils/jsonUtils";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Copy, CheckCheck } from "lucide-react";
|
import { Copy, CheckCheck } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { getDataType, tryParseJson } from "@/utils/jsonUtils";
|
||||||
|
|
||||||
interface JsonViewProps {
|
interface JsonViewProps {
|
||||||
data: unknown;
|
data: unknown;
|
||||||
@@ -13,21 +14,6 @@ interface JsonViewProps {
|
|||||||
withCopyButton?: boolean;
|
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(
|
const JsonView = memo(
|
||||||
({
|
({
|
||||||
data,
|
data,
|
||||||
@@ -119,23 +105,15 @@ interface JsonNodeProps {
|
|||||||
const JsonNode = memo(
|
const JsonNode = memo(
|
||||||
({ data, name, depth = 0, initialExpandDepth }: JsonNodeProps) => {
|
({ data, name, depth = 0, initialExpandDepth }: JsonNodeProps) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth);
|
const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth);
|
||||||
|
const [typeStyleMap] = useState<Record<string, string>>({
|
||||||
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> = {
|
|
||||||
number: "text-blue-600",
|
number: "text-blue-600",
|
||||||
boolean: "text-amber-600",
|
boolean: "text-amber-600",
|
||||||
null: "text-purple-600",
|
null: "text-purple-600",
|
||||||
undefined: "text-gray-600",
|
undefined: "text-gray-600",
|
||||||
string: "text-green-600 break-all whitespace-pre-wrap",
|
string: "text-green-600 break-all whitespace-pre-wrap",
|
||||||
default: "text-gray-700",
|
default: "text-gray-700",
|
||||||
};
|
});
|
||||||
|
const dataType = getDataType(data);
|
||||||
|
|
||||||
const renderCollapsible = (isArray: boolean) => {
|
const renderCollapsible = (isArray: boolean) => {
|
||||||
const items = isArray
|
const items = isArray
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { TabsContent } from "@/components/ui/tabs";
|
import { TabsContent } from "@/components/ui/tabs";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
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 { generateDefaultValue } from "@/utils/schemaUtils";
|
||||||
import {
|
import {
|
||||||
CallToolResultSchema,
|
CallToolResultSchema,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
import { describe, it, expect, jest } from "@jest/globals";
|
import { describe, it, expect, jest } from "@jest/globals";
|
||||||
import DynamicJsonForm from "../DynamicJsonForm";
|
import DynamicJsonForm from "../DynamicJsonForm";
|
||||||
import type { JsonSchemaType } from "../DynamicJsonForm";
|
import type { JsonSchemaType } from "@/utils/jsonUtils";
|
||||||
|
|
||||||
describe("DynamicJsonForm String Fields", () => {
|
describe("DynamicJsonForm String Fields", () => {
|
||||||
const renderForm = (props = {}) => {
|
const renderForm = (props = {}) => {
|
||||||
|
|||||||
@@ -1,5 +1,146 @@
|
|||||||
import { updateValueAtPath, getValueAtPath } from "../jsonPathUtils";
|
import {
|
||||||
import { JsonValue } from "../../components/DynamicJsonForm";
|
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", () => {
|
describe("updateValueAtPath", () => {
|
||||||
// Basic functionality tests
|
// 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", () => {
|
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",
|
foo: "bar",
|
||||||
});
|
});
|
||||||
expect(updateValueAtPath(undefined as any, ["foo"], "bar")).toEqual({
|
expect(updateValueAtPath(undefined, ["foo"], "bar")).toEqual({
|
||||||
foo: "bar",
|
foo: "bar",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("initializes an empty array when input is null/undefined and path starts with a number", () => {
|
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(null, ["0"], "bar")).toEqual(["bar"]);
|
||||||
expect(updateValueAtPath(undefined as any, ["0"], "bar")).toEqual(["bar"]);
|
expect(updateValueAtPath(undefined, ["0"], "bar")).toEqual(["bar"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Object update tests
|
// Object update tests
|
||||||
@@ -152,10 +293,8 @@ describe("getValueAtPath", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("returns default value when input is null/undefined", () => {
|
test("returns default value when input is null/undefined", () => {
|
||||||
expect(getValueAtPath(null as any, ["foo"], "default")).toBe("default");
|
expect(getValueAtPath(null, ["foo"], "default")).toBe("default");
|
||||||
expect(getValueAtPath(undefined as any, ["foo"], "default")).toBe(
|
expect(getValueAtPath(undefined, ["foo"], "default")).toBe("default");
|
||||||
"default",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("handles array indices correctly", () => {
|
test("handles array indices correctly", () => {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { generateDefaultValue, formatFieldLabel } from "../schemaUtils";
|
import { generateDefaultValue, formatFieldLabel } from "../schemaUtils";
|
||||||
import { JsonSchemaType } from "../../components/DynamicJsonForm";
|
import type { JsonSchemaType } from "../jsonUtils";
|
||||||
|
|
||||||
describe("generateDefaultValue", () => {
|
describe("generateDefaultValue", () => {
|
||||||
test("generates default string", () => {
|
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 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
|
* Updates a value at a specific path in a nested JSON structure
|
||||||
* @param obj The original JSON value
|
* @param obj The original JSON value
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { JsonValue, JsonSchemaType } from "../components/DynamicJsonForm";
|
import type { JsonValue, JsonSchemaType, JsonObject } from "./jsonUtils";
|
||||||
import { JsonObject } from "./jsonPathUtils";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a default value based on a JSON schema type
|
* Generates a default value based on a JSON schema type
|
||||||
|
|||||||
Reference in New Issue
Block a user