armpit
Version:
Another resource manager programming interface toolkit.
460 lines • 16.3 kB
JavaScript
import { isPrimitiveValue } from "./tsUtils.js";
export function shallowCloneDefinedValues(obj) {
return Object.entries(obj).reduce((acc, [key, value]) => {
if (value !== undefined) {
acc[key] = value;
}
return acc;
}, {});
}
export function shallowMergeDefinedValues(prev, next) {
return Object.entries(next).reduce((acc, [key, value]) => {
if (value !== undefined) {
acc[key] = value;
}
return acc;
}, shallowCloneDefinedValues(prev));
}
export function applyOptionsDifferencesShallow(target, source) {
let changesApplied = false;
for (const key of Object.keys(source)) {
const value = source[key];
if (value !== undefined && value !== target[key]) {
target[key] = value;
changesApplied = true;
}
}
return changesApplied;
}
export function applyOptionsDifferencesDeep(target, source) {
let changesApplied = false;
for (const key of Object.keys(source)) {
const value = source[key];
if (value === undefined) {
continue;
}
if (Array.isArray(value)) {
changesApplied = true;
target[key] = value;
}
else if (value != null && typeof value === "object" && target[key] != null) {
if (applyOptionsDifferencesDeep(target[key], value)) {
changesApplied = true;
}
}
else if (value !== target[key]) {
changesApplied = true;
target[key] = value;
}
}
return changesApplied;
}
export function applyArrayKeyedDescriptor(targets, sources, match, apply, create, options) {
const unmatchedTargets = [...targets];
const unmatchedSources = [...sources];
const matchFn = typeof match === "string"
? (t, s) => t[match] == s[match]
: match;
let appliedChanges = false;
for (let sourceIndex = 0; sourceIndex < unmatchedSources.length;) {
const source = unmatchedSources[sourceIndex];
const targetIndex = unmatchedTargets.findIndex(t => matchFn(t, source));
if (targetIndex >= 0) {
const target = unmatchedTargets[targetIndex];
unmatchedSources.splice(sourceIndex, 1);
unmatchedTargets.splice(targetIndex, 1);
if (apply(target, source)) {
appliedChanges = true;
}
}
else {
sourceIndex++;
}
}
if (unmatchedTargets.length > 0 && options?.deleteUnmatchedTargets === true) {
for (const toDelete of unmatchedTargets) {
const index = targets.findIndex(t => t === toDelete);
if (index >= 0) {
targets.splice(index, 1);
appliedChanges = true;
}
}
}
if (unmatchedSources.length > 0) {
targets.push(...unmatchedSources.map(create));
appliedChanges = true;
}
return appliedChanges;
}
export function applyArrayIdDescriptors(targets, sources, options) {
return applyArrayKeyedDescriptor(targets, sources, "id", () => false, s => ({ id: s.id }), options);
}
export function applyObjectKeyProperties(target, source, onAdd, onRemove, onMatch) {
let updated = false;
const sourceKeys = Object.keys(source);
const targetKeys = Object.keys(target);
if (onRemove != null && onRemove !== false) {
const removeFn = onRemove === true
? k => {
delete target[k];
updated = true;
}
: k => {
if (onRemove(k, target) !== false) {
updated = true;
}
};
targetKeys.filter(k => !sourceKeys.includes(k)).forEach(removeFn);
}
for (const sourceKey of sourceKeys) {
if (targetKeys.includes(sourceKey)) {
if (onMatch && onMatch(sourceKey, target, source) !== false) {
updated = true;
}
}
else {
if (onAdd && onAdd(sourceKey, target, source) !== false) {
updated = true;
}
}
}
return updated;
}
export function wrapPropObjectApply(applyFn) {
return ((targetObj, sourceObj, propName, context) => {
let appliedChanges = false;
const sourceValue = sourceObj[propName];
if (sourceValue == null) {
if (sourceValue === null) {
throw new Error("Null source value is not supported");
}
else {
return appliedChanges;
}
}
let targetValue = targetObj[propName];
if (targetValue == null) {
targetValue = {};
targetObj[propName] = targetValue;
appliedChanges = true;
}
if (applyFn(targetValue, sourceValue, context)) {
appliedChanges = true;
}
return appliedChanges;
});
}
export function createKeyedArrayPropApplyFn(match, apply, create, remove) {
const matchFn = typeof match === "function"
? match
: (t, s) => {
const sourceValue = s[match];
return sourceValue != null && t[match] === sourceValue;
};
const createFn = typeof create === "function"
? create
: create == null || create === true
? (s, c) => {
const t = {};
apply(t, s, c);
return t;
}
: null;
const removeFn = typeof remove === "function"
? remove
: remove === true
? (t, d) => {
let removed = 0;
if (d != null && d.length > 0) {
for (const toDelete of d) {
const index = t.indexOf(toDelete);
if (index >= 0) {
t.splice(index, 1);
removed++;
}
}
}
return removed > 0;
}
: null;
return ((targetObj, sourceObj, prop, context) => {
let appliedChanges = false;
const sourceItems = sourceObj[prop];
if (sourceItems == null) {
if (sourceItems === null) {
throw new Error("Null source item array is not supported");
}
else {
return appliedChanges;
}
}
let targetItems = targetObj[prop];
if (targetItems == null) {
targetItems = [];
targetObj[prop] = targetItems;
}
const unmatchedTargets = [...targetItems];
const matchedSources = [];
sourceItems.forEach(sourceItem => {
const targetIndex = unmatchedTargets.findIndex(t => matchFn(t, sourceItem));
if (targetIndex >= 0) {
const targetItem = unmatchedTargets[targetIndex];
unmatchedTargets.splice(targetIndex, 1);
matchedSources.push(sourceItem);
if (apply(targetItem, sourceItem, context)) {
appliedChanges = true;
}
}
});
if (unmatchedTargets.length > 0 && removeFn != null) {
if (removeFn(targetItems, unmatchedTargets)) {
appliedChanges = true;
}
}
if (createFn != null) {
const unmatchedSources = sourceItems.filter(s => !matchedSources.includes(s));
if (unmatchedSources.length > 0) {
targetItems.push(...unmatchedSources.map(s => createFn(s, context)));
appliedChanges = true;
}
}
return appliedChanges;
});
}
export function applySourceToTargetObject(target, source, context) {
return applySourceToTargetObjectWithTemplate(target, source, undefined, context);
}
export function applySourceToTargetObjectWithTemplate(target, source, template, context) {
let hasBeenUpdated = false;
if (context == null) {
context = {};
}
if (context.visitedSourceObjects == null) {
context.visitedSourceObjects = [source];
}
else {
if (context.visitedSourceObjects.includes(source)) {
throw new Error("Source object contains cyclical references");
}
context.visitedSourceObjects.push(source);
}
for (const [sourcePropName, sourceValue] of Object.entries(source)) {
if (sourceValue == null && sourceValue !== null) {
continue; // skip undefined values
}
const templateValue = template?.[sourcePropName];
if (templateValue === null) {
throw new Error("Null template handler not implemented");
}
else if (templateValue == null) {
if (sourceValue === null || isPrimitiveValue(sourceValue)) {
// TODO: extract basic equality to its own reusable function that can be explicitly specified in a template
if (target[sourcePropName] !== sourceValue) {
target[sourcePropName] = sourceValue;
hasBeenUpdated = true;
}
}
else if (Array.isArray(sourceValue)) {
if (target[sourcePropName] == null) {
target[sourcePropName] = [];
}
if (applyOrderedArray(target[sourcePropName], sourceValue)) {
hasBeenUpdated = true;
}
}
else if (typeof sourceValue === "object") {
if (target[sourcePropName] == null) {
target[sourcePropName] = {};
}
if (applySourceToTargetObject(target[sourcePropName], sourceValue, context)) {
hasBeenUpdated = true;
}
}
else {
throw new Error("Source value not supported");
}
}
else if (templateValue === "ignore") {
// Do nothing
}
else if (typeof templateValue === "function") {
if (templateValue(target, source, sourcePropName)) {
hasBeenUpdated = true;
}
}
else if (Array.isArray(templateValue)) {
throw new Error("Template array item is not supported");
}
else if (typeof templateValue === "object") {
if (target[sourcePropName] == null) {
target[sourcePropName] = {};
}
if (applySourceToTargetObjectWithTemplate(target[sourcePropName], sourceValue, templateValue, context)) {
hasBeenUpdated = true;
}
}
else {
throw new Error("Template item is unexpected");
}
}
return hasBeenUpdated;
}
function defaultEqualsTest(a, b) {
if (a === b) {
return true;
}
if (a == null || b == null) {
return false;
}
if (typeof a === "object" && typeof b === "object") {
return JSON.stringify(a) === JSON.stringify(b);
}
return false;
}
export function applyUnorderedArray(targetArray, sourceArray, test) {
let appliedChanges = false;
test ??= defaultEqualsTest;
const unmatchedSourceItems = [...sourceArray];
for (let targetIndex = 0; targetIndex < targetArray.length;) {
const targetItem = targetArray[targetIndex];
const searchIndex = unmatchedSourceItems.findIndex(sourceItem => test(targetItem, sourceItem));
if (searchIndex >= 0) {
// TODO: handle matches and `appliedChanges = true;` if required
unmatchedSourceItems.splice(searchIndex, 1);
targetIndex++;
}
else {
targetArray.splice(targetIndex, 1);
appliedChanges = true;
}
}
if (unmatchedSourceItems.length > 0) {
targetArray.push(...unmatchedSourceItems);
appliedChanges = true;
}
return appliedChanges;
}
export function applyUnorderedValueArrayProp(target, source, propName) {
let appliedChanges = false;
const sourceValues = source[propName];
if (sourceValues == null) {
return appliedChanges;
}
let targetValues = target[propName];
if (targetValues == null) {
targetValues = [];
target[propName] = targetValues;
appliedChanges = true;
}
if (applyUnorderedArray(targetValues, sourceValues)) {
appliedChanges = true;
}
return appliedChanges;
}
export function applyOrderedArray(targetArray, sourceArray, test) {
let appliedChanges = false;
test ??= defaultEqualsTest;
for (let sourceIndex = 0; sourceIndex < sourceArray.length; sourceIndex++) {
const sourceItem = sourceArray[sourceIndex];
const searchIndex = targetArray.findIndex((targetItem, targetIndex) => targetIndex >= sourceIndex && test(targetItem, sourceItem));
if (searchIndex >= 0) {
const searchItem = targetArray[searchIndex];
if (searchIndex === sourceIndex) {
// TODO: handle matches and `appliedChanges = true;` if required
}
else {
// Swap items to preserve existing values or objects
targetArray[searchIndex] = targetArray[sourceIndex];
targetArray[sourceIndex] = searchItem;
appliedChanges = true;
}
}
else {
if (sourceIndex === targetArray.length) {
targetArray.push(sourceItem);
appliedChanges = true;
}
else if (sourceIndex > targetArray.length) {
throw new Error("Unexpected index");
}
else {
targetArray.splice(sourceIndex, 0, sourceItem);
}
}
}
if (targetArray.length > sourceArray.length) {
targetArray.splice(sourceArray.length, targetArray.length - sourceArray.length);
appliedChanges = true;
}
return appliedChanges;
}
export function applyOrderedValueArrayProp(target, source, propName) {
let appliedChanges = false;
const sourceValues = source[propName];
if (sourceValues == null) {
return appliedChanges;
}
let targetValues = target[propName];
if (targetValues == null) {
targetValues = [];
target[propName] = targetValues;
appliedChanges = true;
}
if (applyOrderedArray(targetValues, sourceValues)) {
appliedChanges = true;
}
return appliedChanges;
}
export function applyResourceRefProperty(target, source, propName) {
let updated = false;
const sourceProp = source[propName];
if (sourceProp == null) {
if (sourceProp === null) {
// TODO: should this set target[propName] to null or delete it?
throw new Error("Null SubResource assignment is not supported");
}
else {
// If the whole object is undefined, then skip
return updated;
}
}
const sourceId = typeof sourceProp === "string" ? sourceProp : sourceProp?.id;
if (sourceId == null) {
throw new Error("SubResource assignment with invalid ID is not supported");
}
if (target[propName]?.id !== sourceId) {
target[propName] = { id: sourceId };
updated = true;
}
return updated;
}
export function applyResourceRefListProperty(target, source, propName) {
let updated = false;
const sourceArray = source[propName];
if (sourceArray == null) {
return updated;
}
let targetArray = target[propName];
if (targetArray == null) {
targetArray = [];
target[propName] = targetArray;
}
const sourceIds = sourceArray.map(r => r?.id).filter(id => id);
for (let i = 0; i < targetArray.length;) {
const targetId = targetArray[i]?.id;
if (targetId != null && sourceIds.includes(targetId)) {
i++;
}
else {
targetArray.splice(i, 1);
updated = true;
}
}
const toAdd = sourceIds.filter(id => !targetArray.some(r => r?.id === id)).map(id => ({ id }));
if (toAdd.length > 0) {
updated = true;
targetArray.push(...toAdd);
}
return updated;
}
//# sourceMappingURL=optionsUtils.js.map