This commit is contained in:
parent
2d86f0fa1a
commit
75b7b35dd6
4 changed files with 343 additions and 0 deletions
|
@ -219,6 +219,7 @@ Sharkitek defines some basic types by default, in these classes:
|
|||
- `DateType`: date in the model, ISO formatted date in the serialized object.
|
||||
- `ArrayType`: array in the model, array in the serialized object.
|
||||
- `ObjectType`: object in the model, object in the serialized object.
|
||||
- `MapType`: map in the model, record object in the serialized object.
|
||||
- `ModelType`: instance of a specific class in the model, object in the serialized object.
|
||||
|
||||
When you are defining a property of a Sharkitek model, you must provide its type by instantiating one of these classes.
|
||||
|
@ -246,6 +247,7 @@ To ease the use of these classes and reduce read complexity, properties of each
|
|||
- `DateType` => `s.property.date`
|
||||
- `ArrayType` => `s.property.array`
|
||||
- `ObjectType` => `s.property.object`
|
||||
- `MapType` => `s.property.map` or `s.property.stringMap`
|
||||
- `ModelType` => `s.property.model`
|
||||
|
||||
Type implementers should provide a corresponding function for each defined type. They can even provide multiple functions or constants with predefined parameters. For example, we could define `s.property.stringArray()` which would be similar to `s.property.array(s.property.string())`.
|
||||
|
|
|
@ -8,3 +8,5 @@ export {model} from "./types/model";
|
|||
export {numeric} from "./types/numeric";
|
||||
export {object} from "./types/object";
|
||||
export {string} from "./types/string";
|
||||
export {map} from "./types/map";
|
||||
export {stringMap} from "./types/map";
|
||||
|
|
177
src/model/types/map.ts
Normal file
177
src/model/types/map.ts
Normal file
|
@ -0,0 +1,177 @@
|
|||
import {Type} from "./type";
|
||||
import {define, Definition} from "../property-definition";
|
||||
import {InvalidTypeValueError} from "../../errors";
|
||||
import {type} from "node:os";
|
||||
import {string} from "./string";
|
||||
|
||||
/**
|
||||
* Type of a key-value map.
|
||||
*/
|
||||
export class MapType<KeyType, ValueType, SerializedValueType, SerializedMapType extends Record<string, SerializedValueType> = Record<string, SerializedValueType>> extends Type<SerializedMapType, Map<KeyType, ValueType>>
|
||||
{
|
||||
/**
|
||||
* Initialize a new map type of a Sharkitek model property.
|
||||
* @param keyDefinition Definition of the map keys.
|
||||
* @param valueDefinition Definition of the map values.
|
||||
*/
|
||||
constructor(
|
||||
protected keyDefinition: Definition<string, KeyType>,
|
||||
protected valueDefinition: Definition<SerializedValueType, ValueType>,
|
||||
)
|
||||
{
|
||||
super();
|
||||
}
|
||||
|
||||
serialize(value: Map<KeyType, ValueType>|null|undefined): SerializedMapType|null|undefined
|
||||
{
|
||||
if (value === undefined) return undefined;
|
||||
if (value === null) return null;
|
||||
|
||||
if (!(value instanceof Map)) throw new InvalidTypeValueError(this, value, "value must be an instance of map");
|
||||
|
||||
return Object.fromEntries(
|
||||
// Serializing each key-value pair of the map.
|
||||
value.entries().map(([key, value]) =>
|
||||
([this.keyDefinition.type.serialize(key), this.valueDefinition.type.serialize(value)]))
|
||||
) as SerializedMapType;
|
||||
}
|
||||
|
||||
deserialize(value: SerializedMapType|null|undefined): Map<KeyType, ValueType>|null|undefined
|
||||
{
|
||||
if (value === undefined) return undefined;
|
||||
if (value === null) return null;
|
||||
|
||||
if (typeof value !== "object" || Array.isArray(value)) throw new InvalidTypeValueError(this, value, "value must be an object");
|
||||
|
||||
const map = new Map<KeyType, ValueType>;
|
||||
for (const [serializedKey, serializedValue] of Object.entries(value))
|
||||
{ // Deserializing each key-value pair of the map.
|
||||
map.set(this.keyDefinition.type.deserialize(serializedKey), this.valueDefinition.type.deserialize(serializedValue));
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
serializeDiff(value: Map<KeyType, ValueType>|null|undefined): SerializedMapType|null|undefined
|
||||
{
|
||||
if (value === undefined) return undefined;
|
||||
if (value === null) return null;
|
||||
|
||||
if (!(value instanceof Map)) throw new InvalidTypeValueError(this, value, "value must be an instance of map");
|
||||
|
||||
return Object.fromEntries(
|
||||
// Serializing the diff of each key-value pair of the map.
|
||||
value.entries().map(([key, value]) =>
|
||||
([this.keyDefinition.type.serializeDiff(key), this.valueDefinition.type.serializeDiff(value)]))
|
||||
) as SerializedMapType;
|
||||
}
|
||||
|
||||
resetDiff(value: Map<KeyType, ValueType>|null|undefined): void
|
||||
{
|
||||
// Do nothing if it is not a map.
|
||||
if (!(value instanceof Map)) return;
|
||||
|
||||
// Reset diff of all key-value pairs.
|
||||
value.forEach((value, key) => {
|
||||
this.keyDefinition.type.resetDiff(key);
|
||||
this.valueDefinition.type.resetDiff(value);
|
||||
});
|
||||
}
|
||||
|
||||
hasChanged(originalValue: Map<KeyType, ValueType>|null|undefined, currentValue: Map<KeyType, ValueType>|null|undefined): boolean
|
||||
{
|
||||
// If any map size is different, maps are different.
|
||||
if (originalValue?.size != currentValue?.size) return true;
|
||||
// If size is undefined, values are probably not maps.
|
||||
if (originalValue?.size == undefined) return super.hasChanged(originalValue, currentValue);
|
||||
|
||||
for (const [key, value] of originalValue.entries())
|
||||
{ // Check for any change for each key-value in the map.
|
||||
if (this.valueDefinition.type.hasChanged(value, currentValue.get(key)))
|
||||
// The value has changed, the map is different.
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // No change detected.
|
||||
}
|
||||
|
||||
serializedHasChanged(originalValue: SerializedMapType | null | undefined, currentValue: SerializedMapType | null | undefined): boolean
|
||||
{
|
||||
// If any value is not a defined object, use the default comparison function.
|
||||
if (!originalValue || !currentValue || typeof originalValue !== "object" || typeof currentValue !== "object") return super.serializedHasChanged(originalValue, currentValue);
|
||||
|
||||
// If any object size is different, objects are different.
|
||||
if (Object.keys(originalValue)?.length != Object.keys(currentValue)?.length) return true;
|
||||
|
||||
for (const [key, value] of Object.entries(originalValue))
|
||||
{ // Check for any change for each key-value pair in the object.
|
||||
if (this.valueDefinition.type.serializedHasChanged(value, currentValue[key]))
|
||||
// The value has changed, the object is different.
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // No change detected.
|
||||
}
|
||||
|
||||
clone<T extends Map<KeyType, ValueType>>(map: T|null|undefined): T
|
||||
{
|
||||
// Handle NULL / undefined map.
|
||||
if (!map) return super.clone(map);
|
||||
|
||||
if (!(map instanceof Map)) throw new InvalidTypeValueError(this, map, "value must be an instance of map");
|
||||
|
||||
// Initialize an empty map.
|
||||
const cloned = new Map<KeyType, ValueType>() as T;
|
||||
|
||||
for (const [key, value] of map.entries())
|
||||
{ // Clone each value of the map.
|
||||
cloned.set(this.keyDefinition.type.clone(key), this.valueDefinition.type.clone(value));
|
||||
}
|
||||
|
||||
return cloned; // Returning cloned map.
|
||||
}
|
||||
|
||||
applyPatch<T extends Map<KeyType, ValueType>>(currentValue: T|null|undefined, patchValue: SerializedMapType|null|undefined, updateOriginals: boolean): T|null|undefined
|
||||
{
|
||||
if (patchValue === undefined) return undefined;
|
||||
if (patchValue === null) return null;
|
||||
|
||||
if (typeof patchValue !== "object")
|
||||
throw new InvalidTypeValueError(this, patchValue, "value must be an object");
|
||||
|
||||
currentValue = currentValue instanceof Map ? currentValue : new Map<KeyType, ValueType>() as T;
|
||||
|
||||
for (const [key, value] of Object.entries(patchValue))
|
||||
{ // Apply the patch to all values of the map.
|
||||
const patchedKey = this.keyDefinition.type.deserialize(key);
|
||||
const patchedElement = this.valueDefinition.type.applyPatch(currentValue.get(patchedKey), value, updateOriginals);
|
||||
currentValue.set(patchedKey, patchedElement);
|
||||
}
|
||||
|
||||
return currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* New map property definition.
|
||||
* @param keyDefinition Definition of the map keys.
|
||||
* @param valueDefinition Definition of the map values.
|
||||
*/
|
||||
export function map<KeyType, ValueType, SerializedValueType, SerializedMapType extends Record<string, SerializedValueType> = Record<string, SerializedValueType>>(
|
||||
keyDefinition: Definition<string, KeyType>,
|
||||
valueDefinition: Definition<SerializedValueType, ValueType>,
|
||||
): Definition<SerializedMapType, Map<KeyType, ValueType>>
|
||||
{
|
||||
return define(new MapType<KeyType, ValueType, SerializedValueType, SerializedMapType>(keyDefinition, valueDefinition));
|
||||
}
|
||||
|
||||
/**
|
||||
* New map property definition, with string as index.
|
||||
* @param valueDefinition Definition of the map values.
|
||||
*/
|
||||
export function stringMap<ValueType, SerializedValueType, SerializedMapType extends Record<string, SerializedValueType> = Record<string, SerializedValueType>>(
|
||||
valueDefinition: Definition<SerializedValueType, ValueType>,
|
||||
): Definition<SerializedMapType, Map<string, ValueType>>
|
||||
{
|
||||
return define(new MapType<string, ValueType, SerializedValueType, SerializedMapType>(string(), valueDefinition));
|
||||
}
|
162
tests/model/types/map.test.ts
Normal file
162
tests/model/types/map.test.ts
Normal file
|
@ -0,0 +1,162 @@
|
|||
import {describe, expect, test} from "vitest";
|
||||
import {InvalidTypeValueError, NumericType, s, StringType} from "../../../src/library";
|
||||
import {MapType} from "../../../src/model/types/map";
|
||||
|
||||
describe("map type", () => {
|
||||
test("map type definition", () => {
|
||||
const mapType = s.property.map(s.property.string(), s.property.numeric());
|
||||
expect(mapType.type).toBeInstanceOf(MapType);
|
||||
});
|
||||
|
||||
const testProperty = s.property.map(s.property.string(), s.property.decimal());
|
||||
const testMapValue = new Map<string, number>();
|
||||
testMapValue.set("test", 1.52);
|
||||
testMapValue.set("another", 55);
|
||||
|
||||
test("object type functions", () => {
|
||||
expect(testProperty.type.serialize(testMapValue)).toEqual({
|
||||
test: "1.52",
|
||||
another: "55",
|
||||
});
|
||||
expect(testProperty.type.deserialize({
|
||||
test: "1.52",
|
||||
another: "55",
|
||||
})).toEqual(testMapValue);
|
||||
expect(testProperty.type.serializeDiff(testMapValue)).toEqual({
|
||||
test: "1.52",
|
||||
another: "55",
|
||||
});
|
||||
|
||||
expect(testProperty.type.serialize(null)).toEqual(null);
|
||||
expect(testProperty.type.deserialize(null)).toEqual(null);
|
||||
expect(testProperty.type.serializeDiff(null)).toEqual(null);
|
||||
|
||||
expect(testProperty.type.serialize(undefined)).toEqual(undefined);
|
||||
expect(testProperty.type.deserialize(undefined)).toEqual(undefined);
|
||||
expect(testProperty.type.serializeDiff(undefined)).toEqual(undefined);
|
||||
|
||||
const anotherTestMapValue = new Map<string, number>();
|
||||
anotherTestMapValue.set("test", 1.52);
|
||||
anotherTestMapValue.set("another", 55);
|
||||
expect(testProperty.type.hasChanged(testMapValue, anotherTestMapValue)).toBeFalsy();
|
||||
anotherTestMapValue.set("test", 1.521);
|
||||
expect(testProperty.type.hasChanged(testMapValue, anotherTestMapValue)).toBeTruthy();
|
||||
anotherTestMapValue.delete("test");
|
||||
expect(testProperty.type.hasChanged(testMapValue, anotherTestMapValue)).toBeTruthy();
|
||||
expect(testProperty.type.hasChanged(null, null)).toBeFalsy();
|
||||
expect(testProperty.type.hasChanged(undefined, undefined)).toBeFalsy();
|
||||
expect(testProperty.type.hasChanged(null, undefined)).toBeTruthy();
|
||||
expect(testProperty.type.hasChanged(undefined, null)).toBeTruthy();
|
||||
expect(testProperty.type.hasChanged(null, testMapValue)).toBeTruthy();
|
||||
expect(testProperty.type.hasChanged(undefined, testMapValue)).toBeTruthy();
|
||||
expect(testProperty.type.hasChanged(testMapValue, null)).toBeTruthy();
|
||||
expect(testProperty.type.hasChanged(testMapValue, undefined)).toBeTruthy();
|
||||
|
||||
expect(testProperty.type.serializedHasChanged({ test: "1.52", another: "55" }, { test: "1.52", another: "55" })).toBeFalsy();
|
||||
expect(testProperty.type.serializedHasChanged({ test: "1.52", another: "55" }, { test: "1.521", another: "55" })).toBeTruthy();
|
||||
expect(testProperty.type.serializedHasChanged({ test: "1.52", another: "55" }, { another: "55" })).toBeTruthy();
|
||||
expect(testProperty.type.serializedHasChanged(null, null)).toBeFalsy();
|
||||
expect(testProperty.type.serializedHasChanged(undefined, undefined)).toBeFalsy();
|
||||
expect(testProperty.type.serializedHasChanged(null, undefined)).toBeTruthy();
|
||||
expect(testProperty.type.serializedHasChanged(undefined, null)).toBeTruthy();
|
||||
expect(testProperty.type.serializedHasChanged(null, { test: "1.52", another: "55" })).toBeTruthy();
|
||||
expect(testProperty.type.serializedHasChanged(undefined, { test: "1.52", another: "55" })).toBeTruthy();
|
||||
expect(testProperty.type.serializedHasChanged({ test: "1.52", another: "55" }, null)).toBeTruthy();
|
||||
expect(testProperty.type.serializedHasChanged({ test: "1.52", another: "55" }, undefined)).toBeTruthy();
|
||||
|
||||
testProperty.type.resetDiff(testMapValue);
|
||||
testProperty.type.resetDiff(undefined);
|
||||
testProperty.type.resetDiff(null);
|
||||
|
||||
{ // Test that keys and values are cloned in a different map.
|
||||
const clonedTestMapValue = testProperty.type.clone(testMapValue);
|
||||
expect(clonedTestMapValue).not.toBe(testMapValue);
|
||||
expect(clonedTestMapValue).toEqual(testMapValue);
|
||||
}
|
||||
{ // Test that values are cloned in a different object.
|
||||
const propertyValue = new Map();
|
||||
propertyValue.set("test", [12, 11]);
|
||||
const clonedPropertyValue = s.property.stringMap(s.property.array(s.property.numeric())).type.clone(propertyValue);
|
||||
expect(clonedPropertyValue).not.toBe(propertyValue);
|
||||
expect(clonedPropertyValue).toEqual(propertyValue);
|
||||
expect(clonedPropertyValue.get("test")).not.toBe(propertyValue.get("test"));
|
||||
expect(clonedPropertyValue.get("test")).toEqual(propertyValue.get("test"));
|
||||
}
|
||||
expect(testProperty.type.clone(undefined)).toBe(undefined);
|
||||
expect(testProperty.type.clone(null)).toBe(null);
|
||||
|
||||
{ // Apply a patch with undefined / NULL values.
|
||||
expect(testProperty.type.applyPatch(
|
||||
testMapValue,
|
||||
undefined,
|
||||
false
|
||||
)).toBeUndefined();
|
||||
expect(testProperty.type.applyPatch(
|
||||
testMapValue,
|
||||
null,
|
||||
true
|
||||
)).toBeNull();
|
||||
}
|
||||
|
||||
{ // Invalid patch.
|
||||
expect(
|
||||
() => testProperty.type.applyPatch(testMapValue, 5416 as any, false)
|
||||
).toThrow(InvalidTypeValueError);
|
||||
}
|
||||
|
||||
{ // Apply a patch.
|
||||
{
|
||||
const objectInstance = testProperty.type.applyPatch(
|
||||
testMapValue,
|
||||
{ test: "1.521" },
|
||||
true,
|
||||
);
|
||||
|
||||
const expectedMapValue = new Map<string, number>();
|
||||
expectedMapValue.set("test", 1.521);
|
||||
expectedMapValue.set("another", 55);
|
||||
expect(objectInstance).toStrictEqual(expectedMapValue);
|
||||
}
|
||||
|
||||
{
|
||||
const objectInstance = testProperty.type.applyPatch(
|
||||
undefined,
|
||||
{ test: "1.52" },
|
||||
false
|
||||
);
|
||||
|
||||
const expectedMapValue = new Map<string, number>();
|
||||
expectedMapValue.set("test", 1.52);
|
||||
expect(objectInstance).toStrictEqual(expectedMapValue);
|
||||
}
|
||||
|
||||
{
|
||||
const objectInstance = testProperty.type.applyPatch(
|
||||
null,
|
||||
{ test: "1.52" },
|
||||
false
|
||||
);
|
||||
|
||||
const expectedMapValue = new Map<string, number>();
|
||||
expectedMapValue.set("test", 1.52);
|
||||
expect(objectInstance).toStrictEqual(expectedMapValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("invalid parameters types", () => {
|
||||
expect(() => testProperty.type.serialize(5 as any)).toThrowError(InvalidTypeValueError);
|
||||
expect(() => testProperty.type.deserialize(5 as any)).toThrowError(InvalidTypeValueError);
|
||||
expect(() => testProperty.type.serializeDiff(5 as any)).toThrowError(InvalidTypeValueError);
|
||||
expect(() => testProperty.type.clone(5 as any)).toThrowError(InvalidTypeValueError);
|
||||
|
||||
expect(() => testProperty.type.serialize([] as any)).toThrowError(InvalidTypeValueError);
|
||||
expect(() => testProperty.type.deserialize([] as any)).toThrowError(InvalidTypeValueError);
|
||||
expect(() => testProperty.type.serializeDiff([] as any)).toThrowError(InvalidTypeValueError);
|
||||
expect(() => testProperty.type.clone([] as any)).toThrowError(InvalidTypeValueError);
|
||||
|
||||
expect(() => testProperty.type.serialize({} as any)).toThrowError(InvalidTypeValueError);
|
||||
expect(() => testProperty.type.serializeDiff({} as any)).toThrowError(InvalidTypeValueError);
|
||||
expect(() => testProperty.type.clone({} as any)).toThrowError(InvalidTypeValueError);
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue