jsonpath-mapper
Version:
A json to json transformation utility with a few nice features to use when translating for example API responses into a domain object for use in your domain-driven JavaScript applications. Can be used in React applications with the 'useMapper' hook.
200 lines (177 loc) • 5.55 kB
text/typescript
import {
fromEntries,
isArray,
isMapperFunctions,
isNullOrUndefined,
isNumber,
isObject,
isString,
isUndefined,
isWireFunction,
jpath,
} from './util.js';
import { MappingTemplate, MappingElement } from './models/Template.js';
import {
FormatResult,
HandleMapperFunctions,
MapArrayFunction,
MapElement,
MapJsonAsync,
MapObject,
} from './models/MapperFunctions.js';
const formatResult: FormatResult = (value, $formatting, $root) =>
isWireFunction($formatting)
? $formatting(value, $root)
: isObject($formatting)
? mapObject(value, $formatting, $root)
: value;
const handleMapperFunctions: HandleMapperFunctions = async (
[k, v],
json,
$root
) => {
// store results from various stages of the evaluations
// eventually reducing this to a finalVal
let val, formatted, finalVal: any;
// if (v) {
// evaluate a given string as a jsonpath expression
if (isString(v.$path)) {
val = jpath(v.$path, json);
}
if (isArray(v.$path)) {
val = await tryMultiple(json, v.$path, $root, findMultiple);
}
if (!isUndefined(v.$formatting) && !isNullOrUndefined(val)) {
if (isArray(val)) {
formatted = await Promise.all(
val.map(
async inner =>
!isUndefined(v.$formatting) &&
(await formatResult(inner, v.$formatting, $root))
)
);
} else {
formatted = await formatResult(val, v.$formatting, $root);
}
val = formatted;
}
if (v.$return) {
finalVal = await formatResult(val, v.$return, $root);
} else {
finalVal = val;
}
if (!isUndefined(v.$default) && isNullOrUndefined(finalVal)) {
if (isWireFunction(v.$default)) {
finalVal = await v.$default(json, $root);
} else {
finalVal = v.$default;
}
}
if (
v.$disable &&
isWireFunction(v.$disable) &&
(await v.$disable(finalVal, $root))
) {
return null;
}
return finalVal;
};
const mapElement: MapElement = async ([k, v], json, $root) => {
if (isNullOrUndefined(v) || v === '') return [k, undefined as any];
// evaluate a given string as a jsonpath expression
if (isString(v)) {
return [k, jpath(v, json)];
}
// execute a given function
if (isWireFunction(v)) {
return [k, await v(json, $root)];
}
// evaluate all the array strings as jsonpath
// and then return the first match
if (isArray(v)) {
return [k, await tryMultiple(json, v, $root, findMultiple)];
}
// if typeof Object, this could be a template object
// look for reserved key $path and it can be combined with
// any of $formatting, $return, $default, $disable
// if not we will treat as plain object and call self recursively
if (isMapperFunctions(v)) {
return [k, await handleMapperFunctions([k, v], json, $root)];
}
// Otherwise we have ourselves a nested object
if (typeof v === 'object') return [k, await mapObject(json, v, $root)];
return [k, v];
};
const mapObject: MapObject = async (json, obj, $root) => {
const objectAsEntries = (
await Promise.all(
// For each key value entry in the object, evaluate each entry
// and make the mapping from the provided json according to the
// supplied object template that follows a few simple principles
Object.entries(obj).map(
async ([k, v]) => await mapElement([k, v], json, $root)
)
)
).filter(([, v]) => v !== null);
return fromEntries(objectAsEntries);
};
const mapArray: MapArrayFunction = async <S, T>(
json: S,
arr: MappingTemplate<S>[],
$root: S
) => {
return ((await Promise.all(
arr.map(async v => await mapObject(json, v, $root))
)) as unknown) as T;
};
const findMultiple = <S>(
json: S,
arr: MappingElement<S>[],
$root: S
): any[] => {
const results = arr.map(async (inner, i) => {
// evaluate any value supplied as string
if (isString(inner)) return jpath(inner, json);
// if typeof func
if (isWireFunction(inner)) return await inner(json, $root);
// if typeof array
if (isArray(inner))
return await tryMultiple(json, inner, $root, findMultiple);
// if typeof mapper functions
if (isMapperFunctions(inner))
return await handleMapperFunctions([i.toString(), inner], json, $root);
// if typeof plain object
if (typeof inner === 'object')
return await mapObject(json, inner as MappingTemplate<S>, $root);
});
return results;
};
const tryMultiple = async <S>(
json: S,
arr: MappingElement<S>[],
$root: S,
findMultiple: (json: S, arr: MappingElement<S>[], $root: S) => any[]
) => {
const result = (await Promise.all(findMultiple(json, arr, $root)))
.filter(r => isNumber(r) || r);
if (arr.every((i) => typeof i === 'string'))
return result.length > 0 ? result[0] : null;
return result;
};
const mapJson: MapJsonAsync = async (json, template) => {
const $root = json;
// console.log(template);
if (typeof template === 'function') {
return await template(json);
}
if (isArray(template)) {
// console.log(mapArray(json, template));
return await mapArray(json, template, $root);
}
// console.log(mapObject(json, template));
return await mapObject(json, template, $root);
};
export const useMapper: MapJsonAsync = async (json, template) => {
return await mapJson(json, template);
};
export default mapJson;