123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- import { Injectable } from '@angular/core';
- import { get, set, unset, cloneDeep, merge, every, findLast, sortBy } from 'lodash';
- @Injectable()
- export class ModelMapperService {
- mapping: StringMap<any>;
- errors: string[] = [];
- debug = false;
- private clog = (...args) => this.debug ? console.log(...args) : {};
- constructor() { }
- public setMapping(mapping: StringMap<any>) {
- this.mapping = mapping;
- }
- public forwardMap = (
- model: StringMap<any>,
- mapping: StringMap<any> = this.mapping,
- mapMissing = false,
- res = {},
- stack = []
- ): StringMap<any> => {
-
- Object.keys(model).forEach(key => {
- const absPath = [...stack, key].join('.');
- const _mapping = get(mapping, key, mapMissing ? key : false);
- if (_mapping) {
- const mappingType = this.resolveMappingType(_mapping);
- switch(mappingType) {
- case 'simple':
- this.deepSet(res, _mapping, model[key]);
- break;
- case 'nested':
-
- if (_mapping.__) {
-
-
- this.clog('-------------------------------------------');
- this.clog('CONTAINER WITH STORED FUNCTIONAL MAPPING', absPath);
- try {
- const storedContainerMapping = _mapping.__;
- let targetPath = typeof storedContainerMapping[0] === 'string' ? storedContainerMapping[0] : absPath;
- if (targetPath[0] === '/') {
- targetPath = targetPath.slice(1);
- }
- this.clog('Target Path', targetPath);
- const func = storedContainerMapping.find(m => typeof m === 'function');
- const funcRes = func(model[key]);
- const fullRes = targetPath ? set({}, targetPath, funcRes) : funcRes;
- this.forwardMap(fullRes, {}, true, res);
- } catch (e) {
- this.clog(e);
- this.errors.push('Error in *container* mapping function at', _mapping, ':::', e.message);
- }
- this.clog('-------------------------------------------');
- } else {
- this.forwardMap(model[key], _mapping, mapMissing, res, [...stack, key]);
- }
- break;
- case 'functional':
- try {
- const result = _mapping.find(m => typeof m === 'function')(model[key]);
- key = _mapping.length === 3 ? _mapping[0] : key;
- this.deepSet(res, key, result);
- } catch (e) {
- this.errors.push('Error in mapping function at', _mapping, ':::', e.message);
- }
- break;
- default:
- this.deepSet(res, key, `MAPPING ERROR: Unknown mapping type in mapping at ${key}`);
- }
- }
- });
- return res;
- }
- public lazyForwardMap = (
- model: StringMap<any>,
- mapping: StringMap<any> = this.mapping
- ): StringMap<any> => this.forwardMap(model, mapping, true)
- public reverseMap = (
- model: StringMap<any>,
- mapping: StringMap<any> = this.mapping,
- mapMissing = false,
- pathsToDelete = [],
- stack = []
- ): StringMap<any> => {
-
- if (!mapping) {
- throw new Error('Attempting to use Model Mapper without mapping');
- }
- const res = {};
- const modelClone = stack.length ? model : cloneDeep(model);
- Object.keys(mapping).filter(key => key !== '__').forEach(key => {
- const dataMapping = mapping[key];
- const mappingType = this.resolveMappingType(dataMapping);
- switch(mappingType) {
- case 'simple':
- {
-
- const value = get(modelClone, dataMapping);
- if (typeof value !== 'undefined') {
- this.deepSet(res, key, value);
- }
- }
- break;
- case 'nested':
- {
- let alternativeModel = null;
- if (dataMapping.__) {
-
- const absKey = [...stack, key].join('.');
- alternativeModel = this.evaluateReverseFunctionalMapping(model, absKey, dataMapping.__);
- this.clog('Setting alternative model for container', alternativeModel);
- }
- const value = this.reverseMap(alternativeModel || modelClone, dataMapping, mapMissing, pathsToDelete, [...stack, key]);
- if (Object.keys(value).length) {
- this.deepSet(res, key, value);
- }
- }
- break;
- case 'functional':
- {
-
- const absKey = [...stack, key].join('.');
- const value = this.evaluateReverseFunctionalMapping(model, absKey, dataMapping);
- this.deepSet(res, key, value);
- }
- break;
- default:
- this.errors.push(`MAPPING ERROR: Unknown mapping type in mapping at ${key}`);
- }
- if (mappingType && mappingType !== 'nested') {
- pathsToDelete.push(dataMapping);
- }
- });
- if (mapMissing && !stack.length) {
-
-
- const deepestPathsLast = this.sortByPathDepth(pathsToDelete);
- while(deepestPathsLast.length) {
- const path = deepestPathsLast.pop();
- const t = typeof get(modelClone, path);
- if (t === 'number' || t === 'string' || t === 'boolean') {
- unset(modelClone, path);
- }
- }
- const modelRemainder = this.deepCleanse(modelClone);
- Object.keys(modelRemainder).forEach(key => {
- this.deepSetIfEmpty(res, key, modelRemainder[key]);
- });
- }
- return res;
- }
- public lazyReverseMap = (
- model: StringMap<any>,
- mapping: StringMap<any> = this.mapping
- ): StringMap<any> => this.reverseMap(model, mapping, true)
- public getErrors() {
- return this.errors;
- }
-
- private resolveMappingType = mappingPath => {
- let mappingType;
- const t = typeof mappingPath;
- if (t === 'number' || t === 'string') {
- mappingType = 'simple';
- } else if (t === 'object') {
-
- if (
- Array.isArray(mappingPath)
- && mappingPath.length === 2
- && every(mappingPath, m => typeof m === 'function')
- ||
- Array.isArray(mappingPath)
- && mappingPath.length === 3
- && (typeof mappingPath[0] === 'number' || typeof mappingPath[0] === 'string')
- && every(mappingPath.slice(1), m => typeof m === 'function')
- ) {
- mappingType = 'functional';
- } else {
- mappingType = 'nested';
- }
- }
- return mappingType;
- }
- private evaluateReverseFunctionalMapping = (model, absKey, fnMapping) => {
- this.clog('evaluateReverseFunctionalMapping');
- this.clog(model, absKey, fnMapping);
- let arg;
- let result;
- const path = typeof fnMapping[0] === 'string' ? fnMapping[0] : absKey;
- if (path === '/') {
- arg = model;
- } else {
- arg = get(model, path.replace(/^\//, ''));
- }
- const func = findLast(fnMapping, m => typeof m === 'function');
- try {
- result = func(arg);
- } catch(e) {
- this.errors.push('Error in mapping function at', absKey, ':::', e.message);
- }
- return result;
- }
- private deepSet = (obj, mappingPath, valueToSet, overwrite = true) => {
-
-
-
- const currentVal = get(obj, mappingPath);
- const t = typeof currentVal;
- if (t === 'undefined') {
- set(obj, mappingPath, valueToSet);
- } else if (t === 'number' || t === 'string' || t === 'boolean') {
-
- if (overwrite) {
- this.errors.push('WARNING: Overwriting scalar value at', mappingPath);
- set(obj, mappingPath, valueToSet);
- } else {
- this.errors.push('WARNING: Discarding scalar value at', mappingPath, 'as exisiting non-scalar value would be overwritten');
- }
- } else if (t === 'object' && typeof valueToSet === 'object') {
-
- let merged = merge(currentVal, valueToSet);
- if (!overwrite) {
- merged = merge(merged, currentVal);
- }
- set(obj, mappingPath, merged);
- } else {
- this.errors.push('WARNING: Could not merge', typeof valueToSet, 'with object');
- }
- }
- private deepSetIfEmpty = (obj, mappingPath, valueToSet) => this.deepSet(obj, mappingPath, valueToSet, false);
- private deepCleanse = obj => {
- const cleansedObj = Object.keys(obj)
- .filter(k => obj[k] !== null && obj[k] !== undefined)
- .reduce((newObj, k) => {
- typeof obj[k] === 'object' ?
- Object.assign(newObj, { [k]: this.deepCleanse(obj[k]) }) :
- Object.assign(newObj, { [k]: obj[k] });
- return newObj;
- }, {});
- Object.keys(cleansedObj).forEach(k => {
- if (typeof cleansedObj[k] === 'object' && !Object.keys(cleansedObj[k]).length) {
- delete cleansedObj[k];
- }
- });
- return cleansedObj;
- }
- private sortByPathDepth = pathArr => sortBy(pathArr, p => p.split('.').length);
- private clearErrors() {
- this.errors = [];
- }
- }
|