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.
|
- `DateType`: date in the model, ISO formatted date in the serialized object.
|
||||||
- `ArrayType`: array in the model, array in the serialized object.
|
- `ArrayType`: array in the model, array in the serialized object.
|
||||||
- `ObjectType`: object in the model, object 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.
|
- `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.
|
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`
|
- `DateType` => `s.property.date`
|
||||||
- `ArrayType` => `s.property.array`
|
- `ArrayType` => `s.property.array`
|
||||||
- `ObjectType` => `s.property.object`
|
- `ObjectType` => `s.property.object`
|
||||||
|
- `MapType` => `s.property.map` or `s.property.stringMap`
|
||||||
- `ModelType` => `s.property.model`
|
- `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())`.
|
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 {numeric} from "./types/numeric";
|
||||||
export {object} from "./types/object";
|
export {object} from "./types/object";
|
||||||
export {string} from "./types/string";
|
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