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,119 +1,126 @@
import { updateValueAtPath, getValueAtPath } from '../jsonPathUtils';
import { JsonValue } from '../../components/DynamicJsonForm';
import { updateValueAtPath, getValueAtPath } from "../jsonPathUtils";
import { JsonValue } from "../../components/DynamicJsonForm";
describe('updateValueAtPath', () => {
describe("updateValueAtPath", () => {
// Basic functionality tests
test('returns the new value when path is empty', () => {
expect(updateValueAtPath({ foo: 'bar' }, [], 'newValue')).toBe('newValue');
test("returns the new value when path is empty", () => {
expect(updateValueAtPath({ foo: "bar" }, [], "newValue")).toBe("newValue");
});
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(undefined as any, ['foo'], 'bar')).toEqual({ foo: 'bar' });
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(undefined as any, ["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']);
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"]);
});
// Object update tests
test('updates a simple object property', () => {
const obj = { name: 'John', age: 30 };
expect(updateValueAtPath(obj, ['age'], 31)).toEqual({ name: 'John', age: 31 });
test("updates a simple object property", () => {
const obj = { name: "John", age: 30 };
expect(updateValueAtPath(obj, ["age"], 31)).toEqual({
name: "John",
age: 31,
});
});
test('updates a nested object property', () => {
const obj = { user: { name: 'John', address: { city: 'New York' } } };
expect(updateValueAtPath(obj, ['user', 'address', 'city'], 'Boston')).toEqual(
{ user: { name: 'John', address: { city: 'Boston' } } }
);
test("updates a nested object property", () => {
const obj = { user: { name: "John", address: { city: "New York" } } };
expect(
updateValueAtPath(obj, ["user", "address", "city"], "Boston"),
).toEqual({ user: { name: "John", address: { city: "Boston" } } });
});
test('creates missing object properties', () => {
const obj = { user: { name: 'John' } };
expect(updateValueAtPath(obj, ['user', 'address', 'city'], 'Boston')).toEqual(
{ user: { name: 'John', address: { city: 'Boston' } } }
);
test("creates missing object properties", () => {
const obj = { user: { name: "John" } };
expect(
updateValueAtPath(obj, ["user", "address", "city"], "Boston"),
).toEqual({ user: { name: "John", address: { city: "Boston" } } });
});
// Array update tests
test('updates an array item', () => {
test("updates an array item", () => {
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 result = updateValueAtPath(arr, ['5'], 'new') as JsonValue[];
const result = updateValueAtPath(arr, ["5"], "new") as JsonValue[];
// 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
expect(result[3]).toBe(null);
expect(result[4]).toBe(null);
// Verify these aren't "holes" in the array (important distinction)
expect(3 in result).toBe(true);
expect(4 in result).toBe(true);
// Verify the array has the correct length
expect(result.length).toBe(6);
// Verify the array doesn't have holes by checking every index exists
expect(result.every((_, index: number) => index in result)).toBe(true);
});
test('updates a nested array item', () => {
const obj = { users: [{ name: 'John' }, { name: 'Jane' }] };
expect(updateValueAtPath(obj, ['users', '1', 'name'], 'Janet')).toEqual(
{ users: [{ name: 'John' }, { name: 'Janet' }] }
);
test("updates a nested array item", () => {
const obj = { users: [{ name: "John" }, { name: "Jane" }] };
expect(updateValueAtPath(obj, ["users", "1", "name"], "Janet")).toEqual({
users: [{ name: "John" }, { name: "Janet" }],
});
});
// Error handling tests
test('returns original value when trying to update a primitive with a path', () => {
const spy = jest.spyOn(console, 'error').mockImplementation();
const result = updateValueAtPath('string', ['foo'], 'bar');
expect(result).toBe('string');
test("returns original value when trying to update a primitive with a path", () => {
const spy = jest.spyOn(console, "error").mockImplementation();
const result = updateValueAtPath("string", ["foo"], "bar");
expect(result).toBe("string");
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
test('returns original array when index is invalid', () => {
const spy = jest.spyOn(console, 'error').mockImplementation();
test("returns original array when index is invalid", () => {
const spy = jest.spyOn(console, "error").mockImplementation();
const arr = [1, 2, 3];
expect(updateValueAtPath(arr, ['invalid'], 4)).toEqual(arr);
expect(updateValueAtPath(arr, ["invalid"], 4)).toEqual(arr);
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
test('returns original array when index is negative', () => {
const spy = jest.spyOn(console, 'error').mockImplementation();
test("returns original array when index is negative", () => {
const spy = jest.spyOn(console, "error").mockImplementation();
const arr = [1, 2, 3];
expect(updateValueAtPath(arr, ['-1'], 4)).toEqual(arr);
expect(updateValueAtPath(arr, ["-1"], 4)).toEqual(arr);
expect(spy).toHaveBeenCalled();
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
const sparseArr = [1, 2, 3];
delete sparseArr[1]; // Now sparseArr is [1, <1 empty item>, 3]
// 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
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
expect(result[1]).toBe(null);
// Verify this isn't a hole in the array
expect(1 in result).toBe(true);
// Verify all indices contain null (not undefined)
expect(result[1]).not.toBe(undefined);
expect(result[3]).toBe(null);
@@ -121,49 +128,53 @@ describe('updateValueAtPath', () => {
});
});
describe('getValueAtPath', () => {
test('returns the original value when path is empty', () => {
const obj = { foo: 'bar' };
describe("getValueAtPath", () => {
test("returns the original value when path is empty", () => {
const obj = { foo: "bar" };
expect(getValueAtPath(obj, [])).toBe(obj);
});
test('returns the value at a simple path', () => {
const obj = { name: 'John', age: 30 };
expect(getValueAtPath(obj, ['name'])).toBe('John');
test("returns the value at a simple path", () => {
const obj = { name: "John", age: 30 };
expect(getValueAtPath(obj, ["name"])).toBe("John");
});
test('returns the value at a nested path', () => {
const obj = { user: { name: 'John', address: { city: 'New York' } } };
expect(getValueAtPath(obj, ['user', 'address', 'city'])).toBe('New York');
test("returns the value at a nested path", () => {
const obj = { user: { name: "John", address: { city: "New York" } } };
expect(getValueAtPath(obj, ["user", "address", "city"])).toBe("New York");
});
test('returns default value when path does not exist', () => {
const obj = { user: { name: 'John' } };
expect(getValueAtPath(obj, ['user', 'address', 'city'], 'Unknown')).toBe('Unknown');
test("returns default value when path does not exist", () => {
const obj = { user: { name: "John" } };
expect(getValueAtPath(obj, ["user", "address", "city"], "Unknown")).toBe(
"Unknown",
);
});
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');
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",
);
});
test('handles array indices correctly', () => {
const arr = ['a', 'b', 'c'];
expect(getValueAtPath(arr, ['1'])).toBe('b');
test("handles array indices correctly", () => {
const arr = ["a", "b", "c"];
expect(getValueAtPath(arr, ["1"])).toBe("b");
});
test('returns default value for out of bounds array indices', () => {
const arr = ['a', 'b', 'c'];
expect(getValueAtPath(arr, ['5'], 'default')).toBe('default');
test("returns default value for out of bounds array indices", () => {
const arr = ["a", "b", "c"];
expect(getValueAtPath(arr, ["5"], "default")).toBe("default");
});
test('returns default value for invalid array indices', () => {
const arr = ['a', 'b', 'c'];
expect(getValueAtPath(arr, ['invalid'], 'default')).toBe('default');
test("returns default value for invalid array indices", () => {
const arr = ["a", "b", "c"];
expect(getValueAtPath(arr, ["invalid"], "default")).toBe("default");
});
test('navigates through mixed object and array paths', () => {
const obj = { users: [{ name: 'John' }, { name: 'Jane' }] };
expect(getValueAtPath(obj, ['users', '1', 'name'])).toBe('Jane');
test("navigates through mixed object and array paths", () => {
const obj = { users: [{ name: "John" }, { name: "Jane" }] };
expect(getValueAtPath(obj, ["users", "1", "name"])).toBe("Jane");
});
});

View File

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

View File

@@ -10,9 +10,9 @@ export type JsonObject = { [key: string]: JsonValue };
* @returns A new JSON value with the updated path
*/
export function updateValueAtPath(
obj: JsonValue,
path: string[],
value: JsonValue
obj: JsonValue,
path: string[],
value: JsonValue,
): JsonValue {
if (path.length === 0) return value;
@@ -24,16 +24,16 @@ export function updateValueAtPath(
// Handle arrays
if (Array.isArray(obj)) {
return updateArray(obj, path, value);
}
}
// Handle objects
else if (typeof obj === "object" && obj !== null) {
return updateObject(obj as JsonObject, path, value);
}
}
// Cannot update primitives
else {
console.error(
`Cannot update path ${path.join(".")} in non-object/array value:`,
obj
obj,
);
return obj;
}
@@ -43,9 +43,9 @@ export function updateValueAtPath(
* Updates an array at a specific path
*/
function updateArray(
array: JsonValue[],
path: string[],
value: JsonValue
array: JsonValue[],
path: string[],
value: JsonValue,
): JsonValue[] {
const [index, ...restPath] = path;
const arrayIndex = Number(index);
@@ -82,7 +82,11 @@ function updateArray(
if (restPath.length === 0) {
newArray[arrayIndex] = value;
} else {
newArray[arrayIndex] = updateValueAtPath(newArray[arrayIndex], restPath, value);
newArray[arrayIndex] = updateValueAtPath(
newArray[arrayIndex],
restPath,
value,
);
}
return newArray;
}
@@ -91,9 +95,9 @@ function updateArray(
* Updates an object at a specific path
*/
function updateObject(
obj: JsonObject,
path: string[],
value: JsonValue
obj: JsonObject,
path: string[],
value: JsonValue,
): JsonObject {
const [key, ...restPath] = path;
@@ -125,18 +129,18 @@ function updateObject(
* @returns The value at the path, or defaultValue if not found
*/
export function getValueAtPath(
obj: JsonValue,
path: string[],
defaultValue: JsonValue = null
obj: JsonValue,
path: string[],
defaultValue: JsonValue = null,
): JsonValue {
if (path.length === 0) return obj;
const [first, ...rest] = path;
if (obj === null || obj === undefined) {
return defaultValue;
}
if (Array.isArray(obj)) {
const index = Number(first);
if (isNaN(index) || index < 0 || index >= obj.length) {
@@ -144,13 +148,13 @@ export function getValueAtPath(
}
return getValueAtPath(obj[index], rest, defaultValue);
}
if (typeof obj === "object" && obj !== null) {
if (!(first in obj)) {
return defaultValue;
}
return getValueAtPath((obj as JsonObject)[first], rest, defaultValue);
}
return defaultValue;
}

View File

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