Default to nulls and update tests
This commit is contained in:
@@ -24,8 +24,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.4.1",
|
"@modelcontextprotocol/sdk": "^1.4.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.3",
|
|
||||||
"@radix-ui/react-checkbox": "^1.1.4",
|
"@radix-ui/react-checkbox": "^1.1.4",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.3",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-popover": "^1.1.3",
|
"@radix-ui/react-popover": "^1.1.3",
|
||||||
@@ -37,8 +37,8 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.4",
|
"cmdk": "^1.0.4",
|
||||||
"lucide-react": "^0.447.0",
|
"lucide-react": "^0.447.0",
|
||||||
"prismjs": "^1.29.0",
|
|
||||||
"pkce-challenge": "^4.1.0",
|
"pkce-challenge": "^4.1.0",
|
||||||
|
"prismjs": "^1.29.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-simple-code-editor": "^0.14.1",
|
"react-simple-code-editor": "^0.14.1",
|
||||||
@@ -57,6 +57,7 @@
|
|||||||
"@types/serve-handler": "^6.1.4",
|
"@types/serve-handler": "^6.1.4",
|
||||||
"@vitejs/plugin-react": "^4.3.2",
|
"@vitejs/plugin-react": "^4.3.2",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"co": "^4.6.0",
|
||||||
"eslint": "^9.11.1",
|
"eslint": "^9.11.1",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.12",
|
"eslint-plugin-react-refresh": "^0.4.12",
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ export type JsonValue =
|
|||||||
| number
|
| number
|
||||||
| boolean
|
| boolean
|
||||||
| null
|
| null
|
||||||
|
| undefined
|
||||||
| JsonValue[]
|
| JsonValue[]
|
||||||
| { [key: string]: JsonValue };
|
| { [key: string]: JsonValue };
|
||||||
|
|
||||||
export type JsonSchemaType = {
|
export type JsonSchemaType = {
|
||||||
type: "string" | "number" | "integer" | "boolean" | "array" | "object";
|
type: "string" | "number" | "integer" | "boolean" | "array" | "object" | "null";
|
||||||
description?: string;
|
description?: string;
|
||||||
|
required?: boolean;
|
||||||
|
default?: JsonValue;
|
||||||
properties?: Record<string, JsonSchemaType>;
|
properties?: Record<string, JsonSchemaType>;
|
||||||
items?: JsonSchemaType;
|
items?: JsonSchemaType;
|
||||||
};
|
};
|
||||||
@@ -105,21 +108,61 @@ const DynamicJsonForm = ({
|
|||||||
|
|
||||||
switch (propSchema.type) {
|
switch (propSchema.type) {
|
||||||
case "string":
|
case "string":
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={(currentValue as string) ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (!val && !propSchema.required) {
|
||||||
|
handleFieldChange(path, undefined);
|
||||||
|
} else {
|
||||||
|
handleFieldChange(path, val);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={propSchema.description}
|
||||||
|
required={propSchema.required}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case "number":
|
case "number":
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={(currentValue as number)?.toString() ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (!val && !propSchema.required) {
|
||||||
|
handleFieldChange(path, undefined);
|
||||||
|
} else {
|
||||||
|
const num = Number(val);
|
||||||
|
if (!isNaN(num)) {
|
||||||
|
handleFieldChange(path, num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={propSchema.description}
|
||||||
|
required={propSchema.required}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case "integer":
|
case "integer":
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
type={propSchema.type === "string" ? "text" : "number"}
|
type="number"
|
||||||
value={(currentValue as string | number) ?? ""}
|
step="1"
|
||||||
onChange={(e) =>
|
value={(currentValue as number)?.toString() ?? ""}
|
||||||
handleFieldChange(
|
onChange={(e) => {
|
||||||
path,
|
const val = e.target.value;
|
||||||
propSchema.type === "string"
|
if (!val && !propSchema.required) {
|
||||||
? e.target.value
|
handleFieldChange(path, undefined);
|
||||||
: Number(e.target.value),
|
} else {
|
||||||
)
|
const num = Number(val);
|
||||||
}
|
if (!isNaN(num) && Number.isInteger(num)) {
|
||||||
|
handleFieldChange(path, num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
placeholder={propSchema.description}
|
placeholder={propSchema.description}
|
||||||
|
required={propSchema.required}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "boolean":
|
case "boolean":
|
||||||
@@ -129,6 +172,7 @@ const DynamicJsonForm = ({
|
|||||||
checked={(currentValue as boolean) ?? false}
|
checked={(currentValue as boolean) ?? false}
|
||||||
onChange={(e) => handleFieldChange(path, e.target.checked)}
|
onChange={(e) => handleFieldChange(path, e.target.checked)}
|
||||||
className="w-4 h-4"
|
className="w-4 h-4"
|
||||||
|
required={propSchema.required}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "object": {
|
case "object": {
|
||||||
@@ -216,9 +260,12 @@ const DynamicJsonForm = ({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
const defaultValue = generateDefaultValue(
|
||||||
|
propSchema.items as JsonSchemaType
|
||||||
|
);
|
||||||
handleFieldChange(path, [
|
handleFieldChange(path, [
|
||||||
...arrayValue,
|
...arrayValue,
|
||||||
generateDefaultValue(propSchema.items as JsonSchemaType),
|
defaultValue ?? null
|
||||||
]);
|
]);
|
||||||
}}
|
}}
|
||||||
title={
|
title={
|
||||||
|
|||||||
@@ -7,40 +7,42 @@ import { JsonSchemaType } from "../../components/DynamicJsonForm";
|
|||||||
|
|
||||||
describe("generateDefaultValue", () => {
|
describe("generateDefaultValue", () => {
|
||||||
test("generates default string", () => {
|
test("generates default string", () => {
|
||||||
expect(generateDefaultValue({ type: "string" })).toBe("");
|
expect(generateDefaultValue({ type: "string", required: true })).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("generates default number", () => {
|
test("generates default number", () => {
|
||||||
expect(generateDefaultValue({ type: "number" })).toBe(0);
|
expect(generateDefaultValue({ type: "number", required: true })).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("generates default integer", () => {
|
test("generates default integer", () => {
|
||||||
expect(generateDefaultValue({ type: "integer" })).toBe(0);
|
expect(generateDefaultValue({ type: "integer", required: true })).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("generates default boolean", () => {
|
test("generates default boolean", () => {
|
||||||
expect(generateDefaultValue({ type: "boolean" })).toBe(false);
|
expect(generateDefaultValue({ type: "boolean", required: true })).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("generates default array", () => {
|
test("generates default array", () => {
|
||||||
expect(generateDefaultValue({ type: "array" })).toEqual([]);
|
expect(generateDefaultValue({ type: "array", required: true })).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("generates default empty object", () => {
|
test("generates default empty object", () => {
|
||||||
expect(generateDefaultValue({ type: "object" })).toEqual({});
|
expect(generateDefaultValue({ type: "object", required: true })).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("generates default null for unknown types", () => {
|
test("generates default null for unknown types", () => {
|
||||||
expect(generateDefaultValue({ type: "unknown" as any })).toBe(null);
|
// @ts-expect-error Testing with invalid type
|
||||||
|
expect(generateDefaultValue({ type: "unknown", required: true })).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("generates object with properties", () => {
|
test("generates object with properties", () => {
|
||||||
const schema: JsonSchemaType = {
|
const schema: JsonSchemaType = {
|
||||||
type: "object",
|
type: "object",
|
||||||
|
required: true,
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string", required: true },
|
||||||
age: { type: "number" },
|
age: { type: "number", required: true },
|
||||||
isActive: { type: "boolean" },
|
isActive: { type: "boolean", required: true },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(generateDefaultValue(schema)).toEqual({
|
expect(generateDefaultValue(schema)).toEqual({
|
||||||
@@ -53,15 +55,18 @@ describe("generateDefaultValue", () => {
|
|||||||
test("handles nested objects", () => {
|
test("handles nested objects", () => {
|
||||||
const schema: JsonSchemaType = {
|
const schema: JsonSchemaType = {
|
||||||
type: "object",
|
type: "object",
|
||||||
|
required: true,
|
||||||
properties: {
|
properties: {
|
||||||
user: {
|
user: {
|
||||||
type: "object",
|
type: "object",
|
||||||
|
required: true,
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string", required: true },
|
||||||
address: {
|
address: {
|
||||||
type: "object",
|
type: "object",
|
||||||
|
required: true,
|
||||||
properties: {
|
properties: {
|
||||||
city: { type: "string" },
|
city: { type: "string", required: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -135,7 +140,8 @@ describe("validateValueAgainstSchema", () => {
|
|||||||
|
|
||||||
test("returns true for unknown types", () => {
|
test("returns true for unknown types", () => {
|
||||||
expect(
|
expect(
|
||||||
validateValueAgainstSchema("anything", { type: "unknown" as any }),
|
// @ts-expect-error Testing with invalid type
|
||||||
|
validateValueAgainstSchema("anything", { type: "unknown" }),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,9 +4,19 @@ import { JsonObject } from "./jsonPathUtils";
|
|||||||
/**
|
/**
|
||||||
* Generates a default value based on a JSON schema type
|
* Generates a default value based on a JSON schema type
|
||||||
* @param schema The JSON schema definition
|
* @param schema The JSON schema definition
|
||||||
* @returns A default value matching the schema type
|
* @returns A default value matching the schema type, or null for non-required fields
|
||||||
*/
|
*/
|
||||||
export function generateDefaultValue(schema: JsonSchemaType): JsonValue {
|
export function generateDefaultValue(schema: JsonSchemaType): JsonValue {
|
||||||
|
|
||||||
|
if ("default" in schema) {
|
||||||
|
// Ensure we don't return undefined even if schema.default is undefined
|
||||||
|
return schema.default === undefined ? null : schema.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!schema.required) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
switch (schema.type) {
|
switch (schema.type) {
|
||||||
case "string":
|
case "string":
|
||||||
return "";
|
return "";
|
||||||
@@ -18,12 +28,15 @@ export function generateDefaultValue(schema: JsonSchemaType): JsonValue {
|
|||||||
case "array":
|
case "array":
|
||||||
return [];
|
return [];
|
||||||
case "object": {
|
case "object": {
|
||||||
|
if (!schema.properties) return {};
|
||||||
|
|
||||||
const obj: JsonObject = {};
|
const obj: JsonObject = {};
|
||||||
if (schema.properties) {
|
Object.entries(schema.properties)
|
||||||
Object.entries(schema.properties).forEach(([key, prop]) => {
|
.filter(([, prop]) => prop.required)
|
||||||
obj[key] = generateDefaultValue(prop);
|
.forEach(([key, prop]) => {
|
||||||
|
const value = generateDefaultValue(prop);
|
||||||
|
obj[key] = value;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -53,13 +66,19 @@ export function validateValueAgainstSchema(
|
|||||||
value: JsonValue,
|
value: JsonValue,
|
||||||
schema: JsonSchemaType,
|
schema: JsonSchemaType,
|
||||||
): boolean {
|
): boolean {
|
||||||
|
// Handle undefined values for non-required fields
|
||||||
|
if (value === undefined && !schema.required) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Basic type validation
|
// Basic type validation
|
||||||
switch (schema.type) {
|
switch (schema.type) {
|
||||||
case "string":
|
case "string":
|
||||||
return typeof value === "string";
|
return typeof value === "string";
|
||||||
case "number":
|
case "number":
|
||||||
case "integer":
|
|
||||||
return typeof value === "number";
|
return typeof value === "number";
|
||||||
|
case "integer":
|
||||||
|
return typeof value === "number" && Number.isInteger(value);
|
||||||
case "boolean":
|
case "boolean":
|
||||||
return typeof value === "boolean";
|
return typeof value === "boolean";
|
||||||
case "array":
|
case "array":
|
||||||
@@ -68,6 +87,8 @@ export function validateValueAgainstSchema(
|
|||||||
return (
|
return (
|
||||||
typeof value === "object" && value !== null && !Array.isArray(value)
|
typeof value === "object" && value !== null && !Array.isArray(value)
|
||||||
);
|
);
|
||||||
|
case "null":
|
||||||
|
return value === null;
|
||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -71,6 +71,7 @@
|
|||||||
"@types/serve-handler": "^6.1.4",
|
"@types/serve-handler": "^6.1.4",
|
||||||
"@vitejs/plugin-react": "^4.3.2",
|
"@vitejs/plugin-react": "^4.3.2",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"co": "^4.6.0",
|
||||||
"eslint": "^9.11.1",
|
"eslint": "^9.11.1",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.12",
|
"eslint-plugin-react-refresh": "^0.4.12",
|
||||||
@@ -4800,6 +4801,17 @@
|
|||||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/co": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"iojs": ">= 1.0.0",
|
||||||
|
"node": ">= 0.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
|||||||
Reference in New Issue
Block a user