UNPKG

armpit

Version:

Another resource manager programming interface toolkit.

460 lines 16.3 kB
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