nope-js-browser
Version:
NoPE Runtime for the Browser. For nodejs please use nope-js-node
633 lines (632 loc) • 20.2 kB
JavaScript
/**
* @author Martin Karkowski
* @email m.karkowski@zema.de
* @desc [description]
*/
export const SPLITCHAR = "/";
import { getLeastCommonPathSegment } from "./path";
import { comparePatternAndPath, containsWildcards, MULTI_LEVEL_WILDCARD, } from "./pathMatchingMethods";
const _sentinel = new Object();
/**
* Function to recurvely get an Attribute of the Object.
*
* @export
* @example data = [{a:1},{a:2}]; rgetattr(data, "0/a") -> 0; rgetattr(data,"hallo", "default") -> "default"
* @param {*} _data Data, where the item should be received
* @param {string} _path The path to extract
* @param {*} [_default=_sentinel] Default Object, if nothing else is provided
* @returns {*} The extracted data.
*/
export function rgetattr(_data, _path, _default = _sentinel, _SPLITCHAR = SPLITCHAR) {
// Extract the Path
let _obj = _data;
if (_path.length > 0) {
/** Check if there is a Substring available perform the normal method */
if (_path.indexOf(_SPLITCHAR) !== -1) {
for (const attr of _path.split(_SPLITCHAR)) {
/** Access a Map */
if (_obj instanceof Map) {
_obj = _obj.get(attr);
}
else {
/** Array or default Object */
_obj = _obj[attr];
}
if ((_obj == null || _obj == undefined) && _default === _sentinel) {
return null;
}
else if (_obj == null || _obj == undefined) {
return _default;
}
}
}
else {
/** Otherwise just return the Element */
if (_obj[_path] == null || _obj[_path] == undefined) {
if (_default == _sentinel) {
return null;
}
return _default;
}
return _obj[_path];
}
}
return _obj;
}
/**
* Helper to query data from an object.
* @example data = [{a:1},{a:2}]; rqueryAttr(data, "+/a") -> [{path: "0/a", data: 0},{path: "1/a", data: 1}]
* @param data The data
* @param query The query to use.
* @returns Returns an array
*/
export function rqueryAttr(data, query) {
if (!containsWildcards(query)) {
const _sentinel = {
id: Date.now(),
};
const extractedData = rgetattr(data, query, _sentinel);
if (extractedData === _sentinel) {
return [];
}
return [{ path: query, data: extractedData }];
}
let ret = [];
const multiLevel = query.includes(MULTI_LEVEL_WILDCARD);
// Determine the max depth
const maxDepth = multiLevel ? Infinity : query.split(SPLITCHAR).length;
// get the flatten object
const map = flattenObject(data, {
maxDepth,
onlyPathToSimpleValue: false,
});
// Iterate over the items and use our
// path matcher to extract the matching items.
for (const [path, value] of map.entries()) {
const r = comparePatternAndPath(query, path);
if (r.affectedOnSameLevel || (multiLevel && r.affectedByChild)) {
ret.push({
path,
data: value,
});
}
}
return ret;
}
/**
* Helper to query data from an object.
*
* props is defined as followed:
* ```typescript
* props: {
* key: string;
* query: string;
* }[]
* ```
*
* @example Example 1:
*
* ```javascript
*
* const data = { "deep": { "nested": "test" } };
* const result = convert_data(data, [
* {
* "key": "result",
* "query": "deep/nested",
* },
* ]);
*
* // ==> result = [{"result": "test"}]
* ```
*
* @example Example 2:
*
* ```javascript
* data = {
* "array": [
* {
* "data1": 0,
* "data2": "a",
* },
* {
* "data1": 1,
* "data2": "a",
* },
* ],
* "not": { "nested": "hello" }
* }
*
* let result = convert_data(data, [
* {
* "key": "a",
* "query": "array/+/data1",
* },
* {
* "key": "b",
* "query": "array/+/data2",
* },
* ])
*
* // ==> result = [{"a": 0, "b": "a"}, {"a": 1, "b": "a"}]
* ```
*
* @param data The data
* @param query The query to use.
* @returns Returns an array
*/
export function convertData(data, props) {
const ret = {};
const commonPattern = getLeastCommonPathSegment(props.map((item) => {
return item.query;
}));
props.map((prop) => {
ret[prop.key] = rqueryAttr(data, prop.query);
});
const helper = {};
for (const prop of props) {
// get the item
const items = ret[prop.key];
for (const [idx, item] of items.entries()) {
if (commonPattern !== false) {
const result = comparePatternAndPath(commonPattern, item.path);
if (result.pathToExtractData) {
if (helper[result.pathToExtractData] === undefined) {
helper[result.pathToExtractData] = {};
}
helper[result.pathToExtractData][prop.key] = item.data;
}
}
else {
if (helper[idx] === undefined) {
helper[idx] = {};
}
helper[idx][prop.key] = item.data;
}
}
}
return Object.getOwnPropertyNames(helper).map((key) => {
return helper[key];
});
}
/**
* Function to Set recursely a Attribute of an Object
*
* @author M.Karkowski
* @export
* @param {*} _data The Object, where the data should be stored
* @param {string} _path The Path of the Attribute. All are seprated by a the splitchar. (Defaults to'.' => For Instance 'a/b/0/a/c')
* @param {*} _value The Value which should be Stored in the Attribute.
* @param {string} [_SPLITCHAR=SPLITCHAR] The Splitchar to use. Defaults to "/"
*/
export function rsetattr(_data, _path, _value, _SPLITCHAR = SPLITCHAR) {
let _obj = _data;
const _ptrs = _path.split(_SPLITCHAR);
_ptrs.slice(0, -1).forEach(function (attr, idx) {
// Adapt the Object by going through a loop
let _sub = _obj[attr];
if (_sub === undefined || _sub === null) {
// _obj is an Array and it doesnt contain the index
// Extract the Next Element:
const _next = _ptrs[idx + 1];
const _next_is_int = isInt(_next);
if (Array.isArray(_obj)) {
if (_next_is_int) {
_obj[attr] = new Array();
}
else {
_obj[attr] = {};
}
}
else {
if (_next_is_int) {
_obj[attr] = [];
}
else {
_obj[attr] = {};
}
}
_sub = _obj[attr];
}
_obj = _sub;
});
_obj[_ptrs[_ptrs.length - 1]] = _value;
}
/**
* Checks whether the Value is an Integer
*
* @export
* @param {*} value Value to be checked
* @returns {boolean} Result
*/
export function isInt(value) {
return parseInt(value) === value;
}
/**
* Checks whether the Value is a Float
*
* @export
* @param {*} value Value to be checked
* @returns {boolean} Result
*/
export function isFloat(value) {
return !isNaN(Number(value));
}
/**
* Copys the Object. Creates a Deep-Copy
* of the Function
*
* @export
* @param {*} value The value which should be copied
* @returns {*} A Copy of the Value
*/
export function copy(value) {
// TODO RING
// const _copy = {};
// /** Perform a Recursevly Foreach an Set an Attribute. */
// recursiveForEach(value, '', (path: string, _data: any) => {
// rsetattr(_copy, path, _data);
// });
// return _copy;
return JSON.parse(JSON.stringify(value));
}
/**
* Function Converts a Object to a Map.
*
* @export
* @param {*} _obj The Object which should be converted.
* @returns {Map<string,any>}
*/
export function objectToMap(_obj) {
/** Define the Returntype */
const _ret = new Map();
/** Iterate through all properties of the Object */
for (const _prop of Object.getOwnPropertyNames(_obj)) {
/** If isnt a function it could be added */
if (typeof _obj !== "function") {
_ret.set(_prop, _obj[_prop]);
}
}
/** Return the Result */
return _ret;
}
/**
* Checks whether the Value is an Object
*
* @export
* @param {*} value Data to Test
* @returns {boolean} Flag showing whether the Presented Data is an Object
*/
export function isObject(value) {
/** Verify whether the value contains some data. */
if (value) {
if (typeof value === "object" && !Array.isArray(value)) {
return Object.keys(value).length > 0;
}
}
return false;
}
/**
* Checks whether the Value is an Object
*
* @export
* @param {*} value Data to Test
* @returns {boolean} Flag showing whether the Presented Data is an Object
*/
export function isObjectOrArray(value) {
/** Verify whether the value contains some data. */
return isObject(value) || Array.isArray(value);
}
/**
* Flattens an Object to a Map.
*
* For Instance:
*
* data = {a : { b : { c : 1, d: "hallo"}}}
*
* // Normal Call
* res = flatteObject(data)
* => res = {"a.b.c":1,"a.b.d":"hallo"}
*
* // With a Selected prefix 'additional.name'
* res = flatteObject(data,{prefix:'additional.name'})
* => res = {"additional.name.a.b.c":1,"additional.name.a.b.d":"hallo"}
*
* @export
* @param {*} data The Data that should be converted
* @param {string} [prefix=''] An additional prefix.
* @returns {Map<string, any>} The flatten Object
*/
export function flattenObject(data, options = {}) {
const _options = Object.assign({
prefix: "",
splitchar: SPLITCHAR,
onlyPathToSimpleValue: false,
maxDepth: Infinity,
}, options);
const _ret = new Map();
if (isObject(data) || Array.isArray(data)) {
recursiveForEach(data, _options.prefix, (path, _data) => {
_ret.set(path, _data);
}, _options.splitchar, _options.onlyPathToSimpleValue, _options.maxDepth);
}
return _ret;
}
/**
* Function, that will iterate over an object.
* It will call the callback on every element.
*
*
* @author M.Karkowski
* @export
* @param {*} obj The Object to iterate
* @param {string} [prefix=""] A prefix for the Path.
* @param {(
* path: string,
* data: any,
* parent?: string,
* level?: number
* ) => void} dataCallback Callback, that will be called.
* @param {string} [_SPLITCHAR=SPLITCHAR] The Splitchar to use, to generate the path
* @param {boolean} [_callOnlyOnValues=true] A Flag, to start the
* @param {*} [_maxDepth=Infinity] Determine the max Depth, after which the Iteration will be stopped.
* @param {string} [_parent=""] For Recursive call only
* @param {number} [_level=0] For Recursive call only
* @return {*} {*}
*/
export function recursiveForEach(obj, prefix = "", dataCallback, _SPLITCHAR = SPLITCHAR, _callOnlyOnValues = true, _maxDepth = Infinity, _parent = "", _level = 0) {
if (_level > _maxDepth) {
return;
}
// Create an Array with the Keys.
let keys = Array();
// Extract the keys of an object, but only if it isnt a
// string or function.
if (typeof obj !== "string" && typeof obj !== "function") {
keys = Object.getOwnPropertyNames(obj);
if (Array.isArray(obj)) {
keys.splice(keys.indexOf("length"), 1);
}
}
let called = false;
if (!_callOnlyOnValues) {
// Store the Element !
dataCallback(prefix, obj, _parent, _level);
called = true;
}
// If there are Keys => It is a List or a Default Object
if (keys.length > 0) {
for (const _key of keys) {
// Define the variable, containing the path
const _str = prefix === "" ? _key : prefix + _SPLITCHAR + _key;
if (obj[_key] !== null && obj[_key] !== undefined) {
// Test if there exist a specific function, which will convert the
// Object to JSON => if so, we use that function, otherwise we will
// just proceed.
if (typeof obj[_key].toJSON === "function") {
const data = obj[_key].toJSON();
// Recursive call this function.
recursiveForEach(data, _str, dataCallback, _SPLITCHAR, _callOnlyOnValues, _maxDepth, prefix, _level + 1);
}
else {
// Recursive call this function.
recursiveForEach(obj[_key], _str, dataCallback, _SPLITCHAR, _callOnlyOnValues, _maxDepth, prefix, _level + 1);
}
}
}
}
else if (!called) {
// Store the Element !
dataCallback(prefix, obj, prefix, _level);
}
}
/**
* Exports the used Types of an Object. The result is the
* a Map, where the key represents the path and the value
* represents the type of the element (stored in the path)
*
* @author M.Karkowski
* @export
* @param {*} data The Data to check
* @param {{
* prefix?: string,
* splitchar?: string,
* maxDepth?: number,
* }} [options={}]
* @return {Map<string, string>} `key` = `path`; `value` = `type of element` as string;
*/
export function flattenObjectType(data, options = {}) {
// Options which will be used
const _options = Object.assign({
prefix: "",
onlyPathToSimpleValue: false,
splitchar: SPLITCHAR,
maxDepth: Infinity,
}, options);
const _ret = new Map();
if (isObject(data)) {
recursiveForEach(data, _options.prefix, (path, _data) => {
_ret.set(path, typeof _data);
}, _options.splitchar, _options.onlyPathToSimpleValue, _options.maxDepth);
}
return _ret;
}
/**
* Deflattens an Dict Based Object. The Object it self is represented
* as Map, whereas the Key represents the path.
*
*
* @author M.Karkowski
* @export
* @param {Map<string, any>} _flattenObject
* @return {*} {*}
*/
export function deflattenObject(_flattenObject, options) {
// Options which will be used
const _options = Object.assign({
prefix: "",
splitchar: SPLITCHAR,
}, options);
const _ret = {};
_flattenObject.forEach((_val, _key) => {
// if there is a prefix, remove it:
if (_options.prefix !== "") {
_key = _key.slice(_options.prefix.length);
}
rsetattr(_ret, _key, _val, _options.splitchar);
});
return _ret;
}
/**
* Function for deeply assigning
*
* @export
* @param {*} target
* @param {*} source
* @returns
*/
export function deepAssign(target, source) {
const flattend = flattenObject(source);
for (const [path, value] of flattend.entries()) {
rsetattr(target, path, value);
}
return target;
}
/**
* Function to deeply clone the given object.
*
* @author M.Karkowski
* @export
* @template T
* @param {T} obj
* @return {*} {T}
*/
export function deepClone(obj) {
switch (typeof obj) {
case "object": {
if (obj === null) {
return null;
}
const clone = Object.assign({}, obj);
Object.keys(clone).forEach((key) => {
clone[key] =
typeof obj[key] === "object" ? deepClone(obj[key]) : obj[key];
});
return (Array.isArray(obj) && obj.length
? (clone.length = obj.length) && Array.from(clone)
: Array.isArray(obj)
? Array.from(obj)
: clone);
}
default: {
return obj;
}
}
}
/**
* Helper to get the Type of an Object.
* @param obj The Object
* @returns
*/
export function getType(obj) {
return Object.prototype.toString.call(obj).slice(8, -1);
}
/**
* Compares deep a and b.
* @param source The source item
* @param target The target item
* @param {number} maxDepth Max Depth, after which the test is skipped and the `onMaxDepth` value is returned
* @param {boolean} [onMaxDepth=false] Value to return if the maxDepth is reached.
* @returns
*/
export function deepEqual(a, b) {
if (a === b)
return true;
if (a && b && typeof a == "object" && typeof b == "object") {
if (a.constructor !== b.constructor)
return false;
let length, i, keys;
if (Array.isArray(a)) {
length = a.length;
if (length != b.length)
return false;
for (i = length; i-- !== 0;)
if (!deepEqual(a[i], b[i]))
return false;
return true;
}
if (a instanceof Map && b instanceof Map) {
if (a.size !== b.size)
return false;
for (i of a.entries())
if (!b.has(i[0]))
return false;
for (i of a.entries())
if (!deepEqual(i[1], b.get(i[0])))
return false;
return true;
}
if (a instanceof Set && b instanceof Set) {
if (a.size !== b.size)
return false;
for (i of a.entries())
if (!b.has(i[0]))
return false;
return true;
}
if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
length = a.byteLength;
if (length != b.byteLength)
return false;
for (i = length; i-- !== 0;)
if (a[i] !== b[i])
return false;
return true;
}
if (a.constructor === RegExp)
return a.source === b.source && a.flags === b.flags;
if (a.valueOf !== Object.prototype.valueOf)
return a.valueOf() === b.valueOf();
if (a.toString !== Object.prototype.toString)
return a.toString() === b.toString();
keys = Object.keys(a);
length = keys.length;
if (length !== Object.keys(b).length)
return false;
for (i = length; i-- !== 0;)
if (!Object.prototype.hasOwnProperty.call(b, keys[i]))
return false;
for (i = length; i-- !== 0;) {
let key = keys[i];
if (!deepEqual(a[key], b[key]))
return false;
}
return true;
}
// true if both NaN, false otherwise
return a !== a && b !== b;
}
/**
* Function to adapt the Object and only return a specific amount of elements.
* @param obj The Object itself
* @param properties a list of properties/pathes to keep
*/
export function keepPropertiesOfObject(obj, properties) {
if (isObject(obj)) {
const ret = {};
const defaultObj = { error: true };
// Iterate over the Properties, get the content of the path, clone it an put it to the
// provided path
Object.getOwnPropertyNames(properties).map((path) => {
const value = rgetattr(obj, path, defaultObj);
rsetattr(ret, path, value !== defaultObj
? typeof value === "object"
? deepClone(value)
: value
: properties[path]());
});
// Return the Object
return ret;
}
// Wrong Datatype provided.
throw TypeError("Function can only create Objects");
}