Fix formatting

This commit is contained in:
Ola Hungerford
2025-02-27 07:21:04 -07:00
parent 426fb87640
commit 238c22830b
8 changed files with 283 additions and 246 deletions

View File

@@ -1,32 +1,37 @@
module.exports = { module.exports = {
preset: 'ts-jest', preset: "ts-jest",
testEnvironment: 'jsdom', testEnvironment: "jsdom",
moduleNameMapper: { moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1', "^@/(.*)$": "<rootDir>/src/$1",
'^../components/DynamicJsonForm$': '<rootDir>/src/utils/__mocks__/DynamicJsonForm.ts', "^../components/DynamicJsonForm$":
'^../../components/DynamicJsonForm$': '<rootDir>/src/utils/__mocks__/DynamicJsonForm.ts' "<rootDir>/src/utils/__mocks__/DynamicJsonForm.ts",
"^../../components/DynamicJsonForm$":
"<rootDir>/src/utils/__mocks__/DynamicJsonForm.ts",
}, },
transform: { transform: {
'^.+\\.tsx?$': ['ts-jest', { "^.+\\.tsx?$": [
useESM: true, "ts-jest",
jsx: 'react-jsx', {
tsconfig: 'tsconfig.jest.json' useESM: true,
}] jsx: "react-jsx",
tsconfig: "tsconfig.jest.json",
},
],
}, },
extensionsToTreatAsEsm: ['.ts', '.tsx'], extensionsToTreatAsEsm: [".ts", ".tsx"],
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
// Exclude directories and files that don't need to be tested // Exclude directories and files that don't need to be tested
testPathIgnorePatterns: [ testPathIgnorePatterns: [
'/node_modules/', "/node_modules/",
'/dist/', "/dist/",
'/bin/', "/bin/",
'\\.config\\.(js|ts|cjs|mjs)$' "\\.config\\.(js|ts|cjs|mjs)$",
], ],
// Exclude the same patterns from coverage reports // Exclude the same patterns from coverage reports
coveragePathIgnorePatterns: [ coveragePathIgnorePatterns: [
'/node_modules/', "/node_modules/",
'/dist/', "/dist/",
'/bin/', "/bin/",
'\\.config\\.(js|ts|cjs|mjs)$' "\\.config\\.(js|ts|cjs|mjs)$",
] ],
}; };

View File

@@ -28,25 +28,25 @@ interface DynamicJsonFormProps {
maxDepth?: number; maxDepth?: number;
} }
const DynamicJsonForm = ({ const DynamicJsonForm = ({
schema, schema,
value, value,
onChange, onChange,
maxDepth = 3, maxDepth = 3,
}: DynamicJsonFormProps) => { }: DynamicJsonFormProps) => {
const [isJsonMode, setIsJsonMode] = useState(false); const [isJsonMode, setIsJsonMode] = useState(false);
const [jsonError, setJsonError] = useState<string>(); const [jsonError, setJsonError] = useState<string>();
// Add state for storing raw JSON value // Add state for storing raw JSON value
const [rawJsonValue, setRawJsonValue] = useState<string>( const [rawJsonValue, setRawJsonValue] = useState<string>(
JSON.stringify(value ?? generateDefaultValue(schema), null, 2) JSON.stringify(value ?? generateDefaultValue(schema), null, 2),
); );
// Update rawJsonValue when value prop changes // Update rawJsonValue when value prop changes
useEffect(() => { useEffect(() => {
if (!isJsonMode) { if (!isJsonMode) {
setRawJsonValue(JSON.stringify(value ?? generateDefaultValue(schema), null, 2)); setRawJsonValue(
JSON.stringify(value ?? generateDefaultValue(schema), null, 2),
);
} }
}, [value, schema, isJsonMode]); }, [value, schema, isJsonMode]);
@@ -64,7 +64,9 @@ const DynamicJsonForm = ({
} }
} else { } else {
// Update raw JSON value when switching to JSON mode // Update raw JSON value when switching to JSON mode
setRawJsonValue(JSON.stringify(value ?? generateDefaultValue(schema), null, 2)); setRawJsonValue(
JSON.stringify(value ?? generateDefaultValue(schema), null, 2),
);
setIsJsonMode(true); setIsJsonMode(true);
} }
}; };
@@ -131,7 +133,7 @@ const DynamicJsonForm = ({
); );
case "object": { case "object": {
// Handle case where we have a value but no schema properties // Handle case where we have a value but no schema properties
const objectValue = currentValue as JsonObject || {}; const objectValue = (currentValue as JsonObject) || {};
// If we have schema properties, use them to render fields // If we have schema properties, use them to render fields
if (propSchema.properties) { if (propSchema.properties) {
@@ -254,11 +256,7 @@ const DynamicJsonForm = ({
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-end"> <div className="flex justify-end">
<Button <Button variant="outline" size="sm" onClick={handleSwitchToFormMode}>
variant="outline"
size="sm"
onClick={handleSwitchToFormMode}
>
{isJsonMode ? "Switch to Form" : "Switch to JSON"} {isJsonMode ? "Switch to Form" : "Switch to JSON"}
</Button> </Button>
</div> </div>
@@ -281,23 +279,28 @@ const DynamicJsonForm = ({
}} }}
error={jsonError} error={jsonError}
/> />
) : ( ) : // If schema type is object but value is not an object or is empty, and we have actual JSON data,
// If schema type is object but value is not an object or is empty, and we have actual JSON data, // render a simple representation of the JSON data
// render a simple representation of the JSON data schema.type === "object" &&
schema.type === "object" && (typeof value !== "object" ||
(typeof value !== "object" || value === null || Object.keys(value).length === 0) && value === null ||
Object.keys(value).length === 0) &&
rawJsonValue && rawJsonValue &&
rawJsonValue !== "{}" ? ( rawJsonValue !== "{}" ? (
<div className="space-y-4 border rounded-md p-4"> <div className="space-y-4 border rounded-md p-4">
<p className="text-sm text-gray-500">Form view not available for this JSON structure. Using simplified view:</p> <p className="text-sm text-gray-500">
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto"> Form view not available for this JSON structure. Using simplified
{rawJsonValue} view:
</pre> </p>
<p className="text-sm text-gray-500">Use JSON mode for full editing capabilities.</p> <pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto">
</div> {rawJsonValue}
) : ( </pre>
renderFormFields(schema, value) <p className="text-sm text-gray-500">
) Use JSON mode for full editing capabilities.
</p>
</div>
) : (
renderFormFields(schema, value)
)} )}
</div> </div>
); );

View File

@@ -11,9 +11,15 @@ interface JsonEditorProps {
error?: string; error?: string;
} }
const JsonEditor = ({ value, onChange, error: externalError }: JsonEditorProps) => { const JsonEditor = ({
value,
onChange,
error: externalError,
}: JsonEditorProps) => {
const [editorContent, setEditorContent] = useState(value); const [editorContent, setEditorContent] = useState(value);
const [internalError, setInternalError] = useState<string | undefined>(undefined); const [internalError, setInternalError] = useState<string | undefined>(
undefined,
);
useEffect(() => { useEffect(() => {
setEditorContent(value); setEditorContent(value);
@@ -22,8 +28,8 @@ const JsonEditor = ({ value, onChange, error: externalError }: JsonEditorProps)
const formatJson = (json: string): string => { const formatJson = (json: string): string => {
try { try {
// Handle empty arrays and objects specifically // Handle empty arrays and objects specifically
if (json.trim() === '[]') return '[]'; if (json.trim() === "[]") return "[]";
if (json.trim() === '{}') return '{}'; if (json.trim() === "{}") return "{}";
return JSON.stringify(JSON.parse(json), null, 2); return JSON.stringify(JSON.parse(json), null, 2);
} catch { } catch {
return json; return json;
@@ -52,17 +58,15 @@ const JsonEditor = ({ value, onChange, error: externalError }: JsonEditorProps)
return ( return (
<div className="relative space-y-2"> <div className="relative space-y-2">
<div className="flex justify-end"> <div className="flex justify-end">
<Button <Button variant="outline" size="sm" onClick={handleFormatJson}>
variant="outline"
size="sm"
onClick={handleFormatJson}
>
Format JSON Format JSON
</Button> </Button>
</div> </div>
<div <div
className={`border rounded-md ${ className={`border rounded-md ${
displayError ? "border-red-500" : "border-gray-200 dark:border-gray-800" displayError
? "border-red-500"
: "border-gray-200 dark:border-gray-800"
}`} }`}
> >
<Editor <Editor
@@ -81,7 +85,9 @@ const JsonEditor = ({ value, onChange, error: externalError }: JsonEditorProps)
className="w-full" className="w-full"
/> />
</div> </div>
{displayError && <p className="text-sm text-red-500 mt-1">{displayError}</p>} {displayError && (
<p className="text-sm text-red-500 mt-1">{displayError}</p>
)}
</div> </div>
); );
}; };

View File

@@ -1,54 +1,61 @@
import { updateValueAtPath, getValueAtPath } from '../jsonPathUtils'; import { updateValueAtPath, getValueAtPath } from "../jsonPathUtils";
import { JsonValue } from '../../components/DynamicJsonForm'; import { JsonValue } from "../../components/DynamicJsonForm";
describe('updateValueAtPath', () => { describe("updateValueAtPath", () => {
// Basic functionality tests // Basic functionality tests
test('returns the new value when path is empty', () => { test("returns the new value when path is empty", () => {
expect(updateValueAtPath({ foo: 'bar' }, [], 'newValue')).toBe('newValue'); expect(updateValueAtPath({ foo: "bar" }, [], "newValue")).toBe("newValue");
}); });
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({ foo: 'bar' }); expect(updateValueAtPath(null as any, ["foo"], "bar")).toEqual({
expect(updateValueAtPath(undefined as any, ['foo'], 'bar')).toEqual({ foo: 'bar' }); foo: "bar",
});
expect(updateValueAtPath(undefined as any, ["foo"], "bar")).toEqual({
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 as any, ["0"], "bar")).toEqual(["bar"]);
expect(updateValueAtPath(undefined as any, ['0'], 'bar')).toEqual(['bar']); expect(updateValueAtPath(undefined as any, ["0"], "bar")).toEqual(["bar"]);
}); });
// Object update tests // Object update tests
test('updates a simple object property', () => { test("updates a simple object property", () => {
const obj = { name: 'John', age: 30 }; const obj = { name: "John", age: 30 };
expect(updateValueAtPath(obj, ['age'], 31)).toEqual({ name: 'John', age: 31 }); expect(updateValueAtPath(obj, ["age"], 31)).toEqual({
name: "John",
age: 31,
});
}); });
test('updates a nested object property', () => { test("updates a nested object property", () => {
const obj = { user: { name: 'John', address: { city: 'New York' } } }; const obj = { user: { name: "John", address: { city: "New York" } } };
expect(updateValueAtPath(obj, ['user', 'address', 'city'], 'Boston')).toEqual( expect(
{ user: { name: 'John', address: { city: 'Boston' } } } updateValueAtPath(obj, ["user", "address", "city"], "Boston"),
); ).toEqual({ user: { name: "John", address: { city: "Boston" } } });
}); });
test('creates missing object properties', () => { test("creates missing object properties", () => {
const obj = { user: { name: 'John' } }; const obj = { user: { name: "John" } };
expect(updateValueAtPath(obj, ['user', 'address', 'city'], 'Boston')).toEqual( expect(
{ user: { name: 'John', address: { city: 'Boston' } } } updateValueAtPath(obj, ["user", "address", "city"], "Boston"),
); ).toEqual({ user: { name: "John", address: { city: "Boston" } } });
}); });
// Array update tests // Array update tests
test('updates an array item', () => { test("updates an array item", () => {
const arr = [1, 2, 3, 4]; const arr = [1, 2, 3, 4];
expect(updateValueAtPath(arr, ['2'], 5)).toEqual([1, 2, 5, 4]); expect(updateValueAtPath(arr, ["2"], 5)).toEqual([1, 2, 5, 4]);
}); });
test('extends an array when index is out of bounds', () => { test("extends an array when index is out of bounds", () => {
const arr = [1, 2, 3]; const arr = [1, 2, 3];
const result = updateValueAtPath(arr, ['5'], 'new') as JsonValue[]; const result = updateValueAtPath(arr, ["5"], "new") as JsonValue[];
// Check overall array structure // Check overall array structure
expect(result).toEqual([1, 2, 3, null, null, 'new']); expect(result).toEqual([1, 2, 3, null, null, "new"]);
// Explicitly verify that indices 3 and 4 contain null, not undefined // Explicitly verify that indices 3 and 4 contain null, not undefined
expect(result[3]).toBe(null); expect(result[3]).toBe(null);
@@ -65,48 +72,48 @@ describe('updateValueAtPath', () => {
expect(result.every((_, index: number) => index in result)).toBe(true); expect(result.every((_, index: number) => index in result)).toBe(true);
}); });
test('updates a nested array item', () => { test("updates a nested array item", () => {
const obj = { users: [{ name: 'John' }, { name: 'Jane' }] }; const obj = { users: [{ name: "John" }, { name: "Jane" }] };
expect(updateValueAtPath(obj, ['users', '1', 'name'], 'Janet')).toEqual( expect(updateValueAtPath(obj, ["users", "1", "name"], "Janet")).toEqual({
{ users: [{ name: 'John' }, { name: 'Janet' }] } users: [{ name: "John" }, { name: "Janet" }],
); });
}); });
// Error handling tests // Error handling tests
test('returns original value when trying to update a primitive with a path', () => { test("returns original value when trying to update a primitive with a path", () => {
const spy = jest.spyOn(console, 'error').mockImplementation(); const spy = jest.spyOn(console, "error").mockImplementation();
const result = updateValueAtPath('string', ['foo'], 'bar'); const result = updateValueAtPath("string", ["foo"], "bar");
expect(result).toBe('string'); expect(result).toBe("string");
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
spy.mockRestore(); spy.mockRestore();
}); });
test('returns original array when index is invalid', () => { test("returns original array when index is invalid", () => {
const spy = jest.spyOn(console, 'error').mockImplementation(); const spy = jest.spyOn(console, "error").mockImplementation();
const arr = [1, 2, 3]; const arr = [1, 2, 3];
expect(updateValueAtPath(arr, ['invalid'], 4)).toEqual(arr); expect(updateValueAtPath(arr, ["invalid"], 4)).toEqual(arr);
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
spy.mockRestore(); spy.mockRestore();
}); });
test('returns original array when index is negative', () => { test("returns original array when index is negative", () => {
const spy = jest.spyOn(console, 'error').mockImplementation(); const spy = jest.spyOn(console, "error").mockImplementation();
const arr = [1, 2, 3]; const arr = [1, 2, 3];
expect(updateValueAtPath(arr, ['-1'], 4)).toEqual(arr); expect(updateValueAtPath(arr, ["-1"], 4)).toEqual(arr);
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
spy.mockRestore(); spy.mockRestore();
}); });
test('handles sparse arrays correctly by filling holes with null', () => { test("handles sparse arrays correctly by filling holes with null", () => {
// Create a sparse array by deleting an element // Create a sparse array by deleting an element
const sparseArr = [1, 2, 3]; const sparseArr = [1, 2, 3];
delete sparseArr[1]; // Now sparseArr is [1, <1 empty item>, 3] delete sparseArr[1]; // Now sparseArr is [1, <1 empty item>, 3]
// Update a value beyond the array length // Update a value beyond the array length
const result = updateValueAtPath(sparseArr, ['5'], 'new') as JsonValue[]; const result = updateValueAtPath(sparseArr, ["5"], "new") as JsonValue[];
// Check overall array structure // Check overall array structure
expect(result).toEqual([1, null, 3, null, null, 'new']); expect(result).toEqual([1, null, 3, null, null, "new"]);
// Explicitly verify that index 1 (the hole) contains null, not undefined // Explicitly verify that index 1 (the hole) contains null, not undefined
expect(result[1]).toBe(null); expect(result[1]).toBe(null);
@@ -121,49 +128,53 @@ describe('updateValueAtPath', () => {
}); });
}); });
describe('getValueAtPath', () => { describe("getValueAtPath", () => {
test('returns the original value when path is empty', () => { test("returns the original value when path is empty", () => {
const obj = { foo: 'bar' }; const obj = { foo: "bar" };
expect(getValueAtPath(obj, [])).toBe(obj); expect(getValueAtPath(obj, [])).toBe(obj);
}); });
test('returns the value at a simple path', () => { test("returns the value at a simple path", () => {
const obj = { name: 'John', age: 30 }; const obj = { name: "John", age: 30 };
expect(getValueAtPath(obj, ['name'])).toBe('John'); expect(getValueAtPath(obj, ["name"])).toBe("John");
}); });
test('returns the value at a nested path', () => { test("returns the value at a nested path", () => {
const obj = { user: { name: 'John', address: { city: 'New York' } } }; const obj = { user: { name: "John", address: { city: "New York" } } };
expect(getValueAtPath(obj, ['user', 'address', 'city'])).toBe('New York'); expect(getValueAtPath(obj, ["user", "address", "city"])).toBe("New York");
}); });
test('returns default value when path does not exist', () => { test("returns default value when path does not exist", () => {
const obj = { user: { name: 'John' } }; const obj = { user: { name: "John" } };
expect(getValueAtPath(obj, ['user', 'address', 'city'], 'Unknown')).toBe('Unknown'); expect(getValueAtPath(obj, ["user", "address", "city"], "Unknown")).toBe(
"Unknown",
);
}); });
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 as any, ["foo"], "default")).toBe("default");
expect(getValueAtPath(undefined as any, ['foo'], 'default')).toBe('default'); expect(getValueAtPath(undefined as any, ["foo"], "default")).toBe(
"default",
);
}); });
test('handles array indices correctly', () => { test("handles array indices correctly", () => {
const arr = ['a', 'b', 'c']; const arr = ["a", "b", "c"];
expect(getValueAtPath(arr, ['1'])).toBe('b'); expect(getValueAtPath(arr, ["1"])).toBe("b");
}); });
test('returns default value for out of bounds array indices', () => { test("returns default value for out of bounds array indices", () => {
const arr = ['a', 'b', 'c']; const arr = ["a", "b", "c"];
expect(getValueAtPath(arr, ['5'], 'default')).toBe('default'); expect(getValueAtPath(arr, ["5"], "default")).toBe("default");
}); });
test('returns default value for invalid array indices', () => { test("returns default value for invalid array indices", () => {
const arr = ['a', 'b', 'c']; const arr = ["a", "b", "c"];
expect(getValueAtPath(arr, ['invalid'], 'default')).toBe('default'); expect(getValueAtPath(arr, ["invalid"], "default")).toBe("default");
}); });
test('navigates through mixed object and array paths', () => { test("navigates through mixed object and array paths", () => {
const obj = { users: [{ name: 'John' }, { name: 'Jane' }] }; const obj = { users: [{ name: "John" }, { name: "Jane" }] };
expect(getValueAtPath(obj, ['users', '1', 'name'])).toBe('Jane'); expect(getValueAtPath(obj, ["users", "1", "name"])).toBe("Jane");
}); });
}); });

View File

@@ -1,135 +1,141 @@
import { generateDefaultValue, formatFieldLabel, validateValueAgainstSchema } from '../schemaUtils'; import {
import { JsonSchemaType } from '../../components/DynamicJsonForm'; generateDefaultValue,
formatFieldLabel,
validateValueAgainstSchema,
} from "../schemaUtils";
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" })).toBe("");
}); });
test('generates default number', () => { test("generates default number", () => {
expect(generateDefaultValue({ type: 'number' })).toBe(0); expect(generateDefaultValue({ type: "number" })).toBe(0);
}); });
test('generates default integer', () => { test("generates default integer", () => {
expect(generateDefaultValue({ type: 'integer' })).toBe(0); expect(generateDefaultValue({ type: "integer" })).toBe(0);
}); });
test('generates default boolean', () => { test("generates default boolean", () => {
expect(generateDefaultValue({ type: 'boolean' })).toBe(false); expect(generateDefaultValue({ type: "boolean" })).toBe(false);
}); });
test('generates default array', () => { test("generates default array", () => {
expect(generateDefaultValue({ type: 'array' })).toEqual([]); expect(generateDefaultValue({ type: "array" })).toEqual([]);
}); });
test('generates default empty object', () => { test("generates default empty object", () => {
expect(generateDefaultValue({ type: 'object' })).toEqual({}); expect(generateDefaultValue({ type: "object" })).toEqual({});
}); });
test('generates default null for unknown types', () => { test("generates default null for unknown types", () => {
expect(generateDefaultValue({ type: 'unknown' as any })).toBe(null); expect(generateDefaultValue({ type: "unknown" as any })).toBe(null);
}); });
test('generates object with properties', () => { test("generates object with properties", () => {
const schema: JsonSchemaType = { const schema: JsonSchemaType = {
type: 'object', type: "object",
properties: { properties: {
name: { type: 'string' }, name: { type: "string" },
age: { type: 'number' }, age: { type: "number" },
isActive: { type: 'boolean' } isActive: { type: "boolean" },
} },
}; };
expect(generateDefaultValue(schema)).toEqual({ expect(generateDefaultValue(schema)).toEqual({
name: '', name: "",
age: 0, age: 0,
isActive: false isActive: false,
}); });
}); });
test('handles nested objects', () => { test("handles nested objects", () => {
const schema: JsonSchemaType = { const schema: JsonSchemaType = {
type: 'object', type: "object",
properties: { properties: {
user: { user: {
type: 'object', type: "object",
properties: { properties: {
name: { type: 'string' }, name: { type: "string" },
address: { address: {
type: 'object', type: "object",
properties: { properties: {
city: { type: 'string' } city: { type: "string" },
} },
} },
} },
} },
} },
}; };
expect(generateDefaultValue(schema)).toEqual({ expect(generateDefaultValue(schema)).toEqual({
user: { user: {
name: '', name: "",
address: { address: {
city: '' city: "",
} },
} },
}); });
}); });
}); });
describe('formatFieldLabel', () => { describe("formatFieldLabel", () => {
test('formats camelCase', () => { test("formats camelCase", () => {
expect(formatFieldLabel('firstName')).toBe('First Name'); expect(formatFieldLabel("firstName")).toBe("First Name");
}); });
test('formats snake_case', () => { test("formats snake_case", () => {
expect(formatFieldLabel('first_name')).toBe('First name'); expect(formatFieldLabel("first_name")).toBe("First name");
}); });
test('formats single word', () => { test("formats single word", () => {
expect(formatFieldLabel('name')).toBe('Name'); expect(formatFieldLabel("name")).toBe("Name");
}); });
test('formats mixed case with underscores', () => { test("formats mixed case with underscores", () => {
expect(formatFieldLabel('user_firstName')).toBe('User first Name'); expect(formatFieldLabel("user_firstName")).toBe("User first Name");
}); });
test('handles empty string', () => { test("handles empty string", () => {
expect(formatFieldLabel('')).toBe(''); expect(formatFieldLabel("")).toBe("");
}); });
}); });
describe('validateValueAgainstSchema', () => { describe("validateValueAgainstSchema", () => {
test('validates string type', () => { test("validates string type", () => {
expect(validateValueAgainstSchema('test', { type: 'string' })).toBe(true); expect(validateValueAgainstSchema("test", { type: "string" })).toBe(true);
expect(validateValueAgainstSchema(123, { type: 'string' })).toBe(false); expect(validateValueAgainstSchema(123, { type: "string" })).toBe(false);
}); });
test('validates number type', () => { test("validates number type", () => {
expect(validateValueAgainstSchema(123, { type: 'number' })).toBe(true); expect(validateValueAgainstSchema(123, { type: "number" })).toBe(true);
expect(validateValueAgainstSchema('test', { type: 'number' })).toBe(false); expect(validateValueAgainstSchema("test", { type: "number" })).toBe(false);
}); });
test('validates integer type', () => { test("validates integer type", () => {
expect(validateValueAgainstSchema(123, { type: 'integer' })).toBe(true); expect(validateValueAgainstSchema(123, { type: "integer" })).toBe(true);
expect(validateValueAgainstSchema('test', { type: 'integer' })).toBe(false); expect(validateValueAgainstSchema("test", { type: "integer" })).toBe(false);
}); });
test('validates boolean type', () => { test("validates boolean type", () => {
expect(validateValueAgainstSchema(true, { type: 'boolean' })).toBe(true); expect(validateValueAgainstSchema(true, { type: "boolean" })).toBe(true);
expect(validateValueAgainstSchema('test', { type: 'boolean' })).toBe(false); expect(validateValueAgainstSchema("test", { type: "boolean" })).toBe(false);
}); });
test('validates array type', () => { test("validates array type", () => {
expect(validateValueAgainstSchema([], { type: 'array' })).toBe(true); expect(validateValueAgainstSchema([], { type: "array" })).toBe(true);
expect(validateValueAgainstSchema({}, { type: 'array' })).toBe(false); expect(validateValueAgainstSchema({}, { type: "array" })).toBe(false);
}); });
test('validates object type', () => { test("validates object type", () => {
expect(validateValueAgainstSchema({}, { type: 'object' })).toBe(true); expect(validateValueAgainstSchema({}, { type: "object" })).toBe(true);
expect(validateValueAgainstSchema([], { type: 'object' })).toBe(false); expect(validateValueAgainstSchema([], { type: "object" })).toBe(false);
expect(validateValueAgainstSchema('test', { type: 'object' })).toBe(false); expect(validateValueAgainstSchema("test", { type: "object" })).toBe(false);
}); });
test('returns true for unknown types', () => { test("returns true for unknown types", () => {
expect(validateValueAgainstSchema('anything', { type: 'unknown' as any })).toBe(true); expect(
validateValueAgainstSchema("anything", { type: "unknown" as any }),
).toBe(true);
}); });
}); });

View File

@@ -12,7 +12,7 @@ export type JsonObject = { [key: string]: JsonValue };
export function updateValueAtPath( export function updateValueAtPath(
obj: JsonValue, obj: JsonValue,
path: string[], path: string[],
value: JsonValue value: JsonValue,
): JsonValue { ): JsonValue {
if (path.length === 0) return value; if (path.length === 0) return value;
@@ -33,7 +33,7 @@ export function updateValueAtPath(
else { else {
console.error( console.error(
`Cannot update path ${path.join(".")} in non-object/array value:`, `Cannot update path ${path.join(".")} in non-object/array value:`,
obj obj,
); );
return obj; return obj;
} }
@@ -45,7 +45,7 @@ export function updateValueAtPath(
function updateArray( function updateArray(
array: JsonValue[], array: JsonValue[],
path: string[], path: string[],
value: JsonValue value: JsonValue,
): JsonValue[] { ): JsonValue[] {
const [index, ...restPath] = path; const [index, ...restPath] = path;
const arrayIndex = Number(index); const arrayIndex = Number(index);
@@ -82,7 +82,11 @@ function updateArray(
if (restPath.length === 0) { if (restPath.length === 0) {
newArray[arrayIndex] = value; newArray[arrayIndex] = value;
} else { } else {
newArray[arrayIndex] = updateValueAtPath(newArray[arrayIndex], restPath, value); newArray[arrayIndex] = updateValueAtPath(
newArray[arrayIndex],
restPath,
value,
);
} }
return newArray; return newArray;
} }
@@ -93,7 +97,7 @@ function updateArray(
function updateObject( function updateObject(
obj: JsonObject, obj: JsonObject,
path: string[], path: string[],
value: JsonValue value: JsonValue,
): JsonObject { ): JsonObject {
const [key, ...restPath] = path; const [key, ...restPath] = path;
@@ -127,7 +131,7 @@ function updateObject(
export function getValueAtPath( export function getValueAtPath(
obj: JsonValue, obj: JsonValue,
path: string[], path: string[],
defaultValue: JsonValue = null defaultValue: JsonValue = null,
): JsonValue { ): JsonValue {
if (path.length === 0) return obj; if (path.length === 0) return obj;

View File

@@ -51,7 +51,7 @@ export function formatFieldLabel(key: string): string {
*/ */
export function validateValueAgainstSchema( export function validateValueAgainstSchema(
value: JsonValue, value: JsonValue,
schema: JsonSchemaType schema: JsonSchemaType,
): boolean { ): boolean {
// Basic type validation // Basic type validation
switch (schema.type) { switch (schema.type) {
@@ -65,7 +65,9 @@ export function validateValueAgainstSchema(
case "array": case "array":
return Array.isArray(value); return Array.isArray(value);
case "object": case "object":
return typeof value === "object" && value !== null && !Array.isArray(value); return (
typeof value === "object" && value !== null && !Array.isArray(value)
);
default: default:
return true; return true;
} }