├── .flowconfig ├── .gitignore ├── CHANGELOG.md ├── OSSMETADATA ├── README.md ├── gulpfile.js ├── lib ├── collapse.js ├── errors │ └── index.js ├── escape.js ├── followReference.js ├── hasIntersection.js ├── index.js ├── integerKey.js ├── iterateKeySet.js ├── jsonKey.js ├── materialize.js ├── optimizePathSets.js ├── pathCount.js ├── pathsComplementFromLengthTree.js ├── pathsComplementFromTree.js ├── support │ ├── catAndSlice.js │ └── cloneArray.js ├── toPaths.js ├── toTree.js └── unescape.js ├── package-lock.json ├── package.json └── test ├── collapse.spec.js ├── escape.spec.js ├── index.js ├── integerKey.spec.js ├── jsonKey.spec.js ├── materialize.spec.js ├── optimizePathSets.spec.js ├── pathCount.spec.js ├── pathsComplementFromTree.spec.js ├── toPaths.spec.js ├── toTree.spec.js └── unescape.spec.js /.flowconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netflix/falcor-path-utils/fd720a42945b62491fb5acc1b431a516ef078d31/.flowconfig -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.7.5 2 | ## Changes 3 | - [Bump mixin-deep from 1.3.1 to 1.3.2](https://github.com/Netflix/falcor-path-utils/pull/22) 4 | - [Use NOINLINE to avoid inlining function definitions](https://github.com/Netflix/falcor-path-utils/pull/23) 5 | 6 | 7 | ### 0.7.4 (2019-09-02) 8 | 9 | 10 | #### Bug Fixes 11 | 12 | * **toTree:** handle empty path ([8aa75109](git+https://github.com/Netflix/falcor-path-utils.git/commit/8aa75109)) 13 | 14 | 15 | 16 | ### 0.7.3 (2019-08-30) 17 | 18 | 19 | #### Bug Fixes 20 | 21 | * **toTree:** support reserved keyword (#20) ([6ed1e78d](git+https://github.com/Netflix/falcor-path-utils.git/commit/6ed1e78d)) 22 | 23 | 24 | # 0.7.2 25 | ## Bugs 26 | 27 | - `iterateKeySet()` no longer attempts to treat `null` as a range object 28 | 29 | # 0.7.1 30 | ## Features 31 | 32 | - [Do not sort collapse map path items](https://github.com/Netflix/falcor-path-utils/pull/19) 33 | 34 | ## Breaking Changes 35 | 36 | - `toPaths()` collapse for `["a", ["c", "b"]]` remains unchanged instead of sorting keys to `["a", ["b", "c"]]` 37 | -------------------------------------------------------------------------------- /OSSMETADATA: -------------------------------------------------------------------------------- 1 | osslifecycle=active 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Falcor Path Utils 2 | 3 | This repository contains utilities for transforming and manipulating Falcor paths. 4 | 5 | ## Utility functions: 6 | 7 | * `collapse(paths)`
8 | Simplifies a set of paths. Example: 9 | 10 | ~~~js 11 | var util = require("falcor-path-utils"); 12 | var collapsedPaths = util.collapse([ 13 | ["genres", 0, "titles", 0, "name"], 14 | ["genres", 0, "titles", 0, "rating"], 15 | ["genres", 0, "titles", 1, "name"], 16 | ["genres", 0, "titles", 1, "rating"] 17 | ]); 18 | 19 | // collapsed paths is ["genres", 0, "titles", {from: 0, to: 1}, ["name", "rating"]] 20 | ~~~ 21 | 22 | * `iterateKeySet(keySet, note)`
23 | Takes in a `keySet` and a `note` and attempts to iterate over it. 24 | 25 | * `toTree(paths)`
26 | Converts `paths` to a tree with null leaves. ([see spec](./test/toTree.spec.js)) 27 | 28 | * `toPaths(lengths)`
29 | Converts a `lengthTree` to paths. ([see spec](./test/toPaths.spec.js)) 30 | 31 | * `pathsComplementFromTree(paths, tree)`
32 | Returns a list of these `paths` that are not in the `tree`. ([see spec](./test/pathsComplementFromTree.spec.js)) 33 | 34 | * `pathsComplementFromLengthTree(paths, lengthTree)`
35 | Like above, but for use with length tree. 36 | 37 | * `hasIntersection(tree, path, depth)`
38 | Tests to see if the intersection should be stripped from the total paths. 39 | 40 | * `optimizePathSets(cache, paths, maxRefFollow)`
41 | ([see spec](./test/optimizePathSets.spec.js)) 42 | 43 | * `pathCount(pathSet)`
44 | Returns the number of paths in a PathSet. 45 | 46 | ~~~js 47 | var util = require("falcor-path-utils"); 48 | console.log(util.pathCount(["titlesById", [512, 628], ["name","rating"]])) 49 | // prints 4, because ["titlesById", [512, 628], ["name","rating"]] contains... 50 | // ["titlesById", 512, "name"] 51 | // ["titlesById", 512, "rating"] 52 | // ["titlesById", 628, "name"] 53 | // ["titlesById", 628, "rating"] 54 | ~~~ 55 | 56 | * `escape(string)`
57 | Escapes untrusted input to make it safe to include in a path. 58 | 59 | * `unescape(string)`
60 | Unescapes a string encoded with escape. 61 | 62 | * `materialize(pathSet, value)`
63 | Construct a JsonGraph of value at pathSet paths. 64 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var mocha = require('gulp-mocha'); 3 | 4 | gulp.task('test', function () { 5 | return gulp.src(['./test/index.js']) 6 | .pipe(mocha()); 7 | }); 8 | -------------------------------------------------------------------------------- /lib/collapse.js: -------------------------------------------------------------------------------- 1 | var toPaths = require('./toPaths'); 2 | var toTree = require('./toTree'); 3 | 4 | module.exports = function collapse(paths) { 5 | var collapseMap = paths. 6 | reduce(function(acc, path) { 7 | var len = path.length; 8 | if (!acc[len]) { 9 | acc[len] = []; 10 | } 11 | acc[len].push(path); 12 | return acc; 13 | }, {}); 14 | 15 | Object. 16 | keys(collapseMap). 17 | forEach(function(collapseKey) { 18 | collapseMap[collapseKey] = toTree(collapseMap[collapseKey]); 19 | }); 20 | 21 | return toPaths(collapseMap); 22 | }; 23 | -------------------------------------------------------------------------------- /lib/errors/index.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | module.exports = { 3 | innerReferences: 'References with inner references are not allowed.', 4 | circularReference: 'There appears to be a circular reference, maximum reference following exceeded.' 5 | }; 6 | 7 | -------------------------------------------------------------------------------- /lib/escape.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Escapes a string by prefixing it with "_". This function should be used on 3 | * untrusted input before it is embedded into paths. The goal is to ensure that 4 | * no reserved words (ex. "$type") make their way into paths and consequently 5 | * JSON Graph objects. 6 | */ 7 | module.exports = function escape(str) { 8 | return "_" + str; 9 | }; 10 | -------------------------------------------------------------------------------- /lib/followReference.js: -------------------------------------------------------------------------------- 1 | var errors = require('./errors'); 2 | 3 | /** 4 | * performs the simplified cache reference follow. This 5 | * differs from get as there is just following and reporting, 6 | * not much else. 7 | * 8 | * @param {Object} cacheRoot 9 | * @param {Array} ref 10 | */ 11 | function followReference(cacheRoot, ref, maxRefFollow) { 12 | if (typeof maxRefFollow === "undefined") { 13 | maxRefFollow = 5; 14 | } 15 | var branch = cacheRoot; 16 | var node = branch; 17 | var refPath = ref; 18 | var depth = -1; 19 | var referenceCount = 0; 20 | 21 | while (++depth < refPath.length) { 22 | var key = refPath[depth]; 23 | node = branch[key]; 24 | 25 | if ( 26 | node === null || 27 | typeof node !== "object" || 28 | (node.$type && node.$type !== "ref") 29 | ) { 30 | break; 31 | } 32 | 33 | if (node.$type === "ref") { 34 | // Show stopper exception. This route is malformed. 35 | if (depth + 1 < refPath.length) { 36 | return { error: new Error(errors.innerReferences) }; 37 | } 38 | if (referenceCount >= maxRefFollow) { 39 | return { error: new Error(errors.circularReference) }; 40 | } 41 | 42 | refPath = node.value; 43 | depth = -1; 44 | branch = cacheRoot; 45 | referenceCount++; 46 | } else { 47 | branch = node; 48 | } 49 | } 50 | return { node: node, refPath: refPath }; 51 | } 52 | 53 | module.exports = followReference; 54 | -------------------------------------------------------------------------------- /lib/hasIntersection.js: -------------------------------------------------------------------------------- 1 | var iterateKeySet = require('./iterateKeySet'); 2 | 3 | /** 4 | * Tests to see if the intersection should be stripped from the 5 | * total paths. The only way this happens currently is if the entirety 6 | * of the path is contained in the tree. 7 | * @private 8 | */ 9 | module.exports = function hasIntersection(tree, path, depth) { 10 | var current = tree; 11 | var intersects = true; 12 | 13 | // Continue iteratively going down a path until a complex key is 14 | // encountered, then recurse. 15 | for (;intersects && depth < path.length; ++depth) { 16 | var key = path[depth]; 17 | var keyType = typeof key; 18 | 19 | // We have to iterate key set 20 | if (key && keyType === 'object') { 21 | var note = {}; 22 | var innerKey = iterateKeySet(key, note); 23 | var nextDepth = depth + 1; 24 | 25 | // Loop through the innerKeys setting the intersects flag 26 | // to each result. Break out on false. 27 | do { 28 | var next = current[innerKey]; 29 | intersects = next !== undefined; 30 | 31 | if (intersects) { 32 | intersects = hasIntersection(next, path, nextDepth); 33 | } 34 | innerKey = iterateKeySet(key, note); 35 | } while (intersects && !note.done); 36 | 37 | // Since we recursed, we shall not pass any further! 38 | break; 39 | } 40 | 41 | // Its a simple key, just move forward with the testing. 42 | current = current[key]; 43 | intersects = current !== undefined; 44 | } 45 | 46 | return intersects; 47 | }; 48 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /*:: 3 | import type { Key, KeySet, PathSet, Path, JsonGraph, JsonGraphNode, JsonMap } from "falcor-json-graph"; 4 | export type PathTree = { [key: string]: PathTree | null | void }; 5 | export type LengthTree = { [key: number]: PathTree | void }; 6 | export type IteratorNote = { done?: boolean }; 7 | type FalcorPathUtils = { 8 | iterateKeySet(keySet: KeySet, note: IteratorNote): Key; 9 | toTree(paths: PathSet[]): PathTree; 10 | pathsComplementFromTree(paths: PathSet[], tree: PathTree): PathSet[]; 11 | pathsComplementFromLengthTree(paths: PathSet[], tree: LengthTree): PathSet[]; 12 | toJsonKey(obj: JsonMap): string; 13 | isJsonKey(key: Key): boolean; 14 | maybeJsonKey(key: Key): JsonMap | void; 15 | hasIntersection(tree: PathTree, path: PathSet, depth: number): boolean; 16 | toPaths(lengths: LengthTree): PathSet[]; 17 | isIntegerKey(key: Key): boolean; 18 | maybeIntegerKey(key: Key): number | void; 19 | collapse(paths: PathSet[]): PathSet[]; 20 | followReference( 21 | cacheRoot: JsonGraph, 22 | ref: Path, 23 | maxRefFollow?: number 24 | ): { error: Error } | { error?: empty, node: ?JsonGraphNode, refPath: Path }; 25 | optimizePathSets( 26 | cache: JsonGraph, 27 | paths: PathSet[], 28 | maxRefFollow?: number 29 | ): { error: Error } | { error?: empty, paths: PathSet[] }; 30 | pathCount(path: PathSet): number; 31 | escape(key: string): string; 32 | unescape(key: string): string; 33 | materialize(pathSet: PathSet, value: JsonGraphNode): JsonGraphNode; 34 | }; 35 | */ 36 | module.exports = ({ 37 | iterateKeySet: require('./iterateKeySet'), 38 | toTree: require('./toTree'), 39 | pathsComplementFromTree: require('./pathsComplementFromTree'), 40 | pathsComplementFromLengthTree: require('./pathsComplementFromLengthTree'), 41 | toJsonKey: require('./jsonKey').toJsonKey, 42 | isJsonKey: require('./jsonKey').isJsonKey, 43 | maybeJsonKey: require('./jsonKey').maybeJsonKey, 44 | hasIntersection: require('./hasIntersection'), 45 | toPaths: require('./toPaths'), 46 | isIntegerKey: require('./integerKey').isIntegerKey, 47 | maybeIntegerKey: require('./integerKey').maybeIntegerKey, 48 | collapse: require('./collapse'), 49 | followReference: require('./followReference'), 50 | optimizePathSets: require('./optimizePathSets'), 51 | pathCount: require('./pathCount'), 52 | escape: require('./escape'), 53 | unescape: require('./unescape'), 54 | materialize: require('./materialize') 55 | }/*: FalcorPathUtils*/); 56 | -------------------------------------------------------------------------------- /lib/integerKey.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var MAX_SAFE_INTEGER = 9007199254740991; // Number.MAX_SAFE_INTEGER in es6 3 | var abs = Math.abs; 4 | var isSafeInteger = Number.isSafeInteger || function isSafeInteger(num) { 5 | return typeof num === "number" && num % 1 === 0 && abs(num) <= MAX_SAFE_INTEGER; 6 | } 7 | 8 | /** 9 | * Return number if argument is a number or can be cast to a number which 10 | * roundtrips to the same string, otherwise return undefined. 11 | */ 12 | function maybeIntegerKey(val) { 13 | if (typeof val === "string") { 14 | var num = Number(val); 15 | if(isSafeInteger(num) && String(num) === val) { 16 | return num; 17 | } 18 | } else if (isSafeInteger(val)) { 19 | return val; 20 | } 21 | } 22 | 23 | /** 24 | * Return true if argument is a number or can be cast to a number which 25 | * roundtrips to the same string. 26 | */ 27 | function isIntegerKey(val) { 28 | if (typeof val === "string") { 29 | var num = Number(val); 30 | return isSafeInteger(num) && String(num) === val; 31 | } 32 | return isSafeInteger(val); 33 | } 34 | 35 | module.exports.isIntegerKey = isIntegerKey; 36 | module.exports.maybeIntegerKey = maybeIntegerKey; 37 | -------------------------------------------------------------------------------- /lib/iterateKeySet.js: -------------------------------------------------------------------------------- 1 | var isArray = Array.isArray; 2 | 3 | /** 4 | * Takes in a keySet and a note attempts to iterate over it. 5 | * If the value is a primitive, the key will be returned and the note will 6 | * be marked done 7 | * If the value is an object, then each value of the range will be returned 8 | * and when finished the note will be marked done. 9 | * If the value is an array, each value will be iterated over, if any of the 10 | * inner values are ranges, those will be iterated over. When fully done, 11 | * the note will be marked done. 12 | * 13 | * @param {Object|Array|String|Number} keySet - 14 | * @param {Object} note - The non filled note 15 | * @returns {String|Number|undefined} - The current iteration value. 16 | * If undefined, then the keySet is empty 17 | * @public 18 | */ 19 | module.exports = function iterateKeySet(keySet, note) { 20 | if (note.isArray === undefined) { 21 | /*#__NOINLINE__*/ initializeNote(keySet, note); 22 | } 23 | 24 | // Array iteration 25 | if (note.isArray) { 26 | var nextValue; 27 | 28 | // Cycle through the array and pluck out the next value. 29 | do { 30 | if (note.loaded && note.rangeOffset > note.to) { 31 | ++note.arrayOffset; 32 | note.loaded = false; 33 | } 34 | 35 | var idx = note.arrayOffset, length = keySet.length; 36 | if (idx >= length) { 37 | note.done = true; 38 | break; 39 | } 40 | 41 | var el = keySet[note.arrayOffset]; 42 | 43 | // Inner range iteration. 44 | if (el !== null && typeof el === 'object') { 45 | if (!note.loaded) { 46 | initializeRange(el, note); 47 | } 48 | 49 | // Empty to/from 50 | if (note.empty) { 51 | continue; 52 | } 53 | 54 | nextValue = note.rangeOffset++; 55 | } 56 | 57 | // Primitive iteration in array. 58 | else { 59 | ++note.arrayOffset; 60 | nextValue = el; 61 | } 62 | } while (nextValue === undefined); 63 | 64 | return nextValue; 65 | } 66 | 67 | // Range iteration 68 | else if (note.isObject) { 69 | if (!note.loaded) { 70 | initializeRange(keySet, note); 71 | } 72 | if (note.rangeOffset > note.to) { 73 | note.done = true; 74 | return undefined; 75 | } 76 | 77 | return note.rangeOffset++; 78 | } 79 | 80 | // Primitive value 81 | else { 82 | if (!note.loaded) { 83 | note.loaded = true; 84 | return keySet; 85 | } 86 | note.done = true; 87 | return undefined; 88 | } 89 | }; 90 | 91 | function initializeRange(key, memo) { 92 | var from = memo.from = key.from || 0; 93 | var to = memo.to = key.to || 94 | (typeof key.length === 'number' && 95 | memo.from + key.length - 1 || 0); 96 | memo.rangeOffset = memo.from; 97 | memo.loaded = true; 98 | if (from > to) { 99 | memo.empty = true; 100 | } 101 | } 102 | 103 | function initializeNote(key, note) { 104 | note.done = false; 105 | var isObject = note.isObject = !!(key && typeof key === 'object'); 106 | note.isArray = isObject && isArray(key); 107 | note.arrayOffset = 0; 108 | } 109 | -------------------------------------------------------------------------------- /lib/jsonKey.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Helper for getting a reproducible, key-sorted string representation of object. 5 | * Used to interpret an object as a falcor key. 6 | * @function 7 | * @param {Object} obj 8 | * @return stringified object with sorted keys. 9 | */ 10 | function toJsonKey(obj) { 11 | if (Object.prototype.toString.call(obj) === "[object Object]") { 12 | var key = JSON.stringify(obj, replacer); 13 | if (key[0] === "{") { 14 | return key; 15 | } 16 | } 17 | throw new TypeError("Only plain objects can be converted to JSON keys") 18 | } 19 | 20 | function replacer(key, value) { 21 | if (typeof value !== "object" || value === null || Array.isArray(value)) { 22 | return value; 23 | } 24 | return Object.keys(value) 25 | .sort() 26 | .reduce(function (acc, k) { 27 | acc[k] = value[k]; 28 | return acc; 29 | }, {}); 30 | } 31 | 32 | function maybeJsonKey(key) { 33 | if (typeof key !== 'string' || key[0] !== '{') { 34 | return; 35 | } 36 | var parsed; 37 | try { 38 | parsed = JSON.parse(key); 39 | } catch (e) { 40 | return; 41 | } 42 | if (JSON.stringify(parsed, replacer) !== key) { 43 | return; 44 | } 45 | return parsed; 46 | } 47 | 48 | function isJsonKey(key) { 49 | return typeof maybeJsonKey(key) !== "undefined"; 50 | } 51 | 52 | module.exports.toJsonKey = toJsonKey; 53 | module.exports.isJsonKey = isJsonKey; 54 | module.exports.maybeJsonKey = maybeJsonKey; 55 | -------------------------------------------------------------------------------- /lib/materialize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var iterateKeySet = require('./iterateKeySet'); 3 | 4 | /** 5 | * Construct a jsonGraph from a pathSet and a value. 6 | * 7 | * @param {PathSet} pathSet - pathSet of paths at which to materialize value. 8 | * @param {JsonGraphNode} value - value to materialize at pathSet paths. 9 | * @returns {JsonGraphNode} - JsonGraph of value at pathSet paths. 10 | * @public 11 | */ 12 | 13 | module.exports = function materialize(pathSet, value) { 14 | return pathSet.reduceRight(function materializeInner(acc, keySet) { 15 | var branch = {}; 16 | if (typeof keySet !== 'object' || keySet === null) { 17 | branch[keySet] = acc; 18 | return branch; 19 | } 20 | var iteratorNote = {}; 21 | var key = iterateKeySet(keySet, iteratorNote); 22 | while (!iteratorNote.done) { 23 | branch[key] = acc; 24 | key = iterateKeySet(keySet, iteratorNote); 25 | } 26 | return branch; 27 | }, value); 28 | }; 29 | -------------------------------------------------------------------------------- /lib/optimizePathSets.js: -------------------------------------------------------------------------------- 1 | var iterateKeySet = require('./iterateKeySet'); 2 | var cloneArray = require('./support/cloneArray'); 3 | var catAndSlice = require('./support/catAndSlice'); 4 | var followReference = require('./followReference'); 5 | 6 | /** 7 | * The fastest possible optimize of paths. 8 | * 9 | * What it does: 10 | * - Any atom short-circuit / found value will be removed from the path. 11 | * - All paths will be exploded which means that collapse will need to be 12 | * ran afterwords. 13 | * - Any missing path will be optimized as much as possible. 14 | */ 15 | module.exports = function optimizePathSets(cache, paths, maxRefFollow) { 16 | if (typeof maxRefFollow === "undefined") { 17 | maxRefFollow = 5; 18 | } 19 | var optimized = []; 20 | for (var i = 0, len = paths.length; i < len; ++i) { 21 | var error = optimizePathSet(cache, cache, paths[i], 0, optimized, [], maxRefFollow); 22 | if (error) { 23 | return { error: error }; 24 | } 25 | } 26 | return { paths: optimized }; 27 | }; 28 | 29 | 30 | /** 31 | * optimizes one pathSet at a time. 32 | */ 33 | function optimizePathSet(cache, cacheRoot, pathSet, 34 | depth, out, optimizedPath, maxRefFollow) { 35 | 36 | // at missing, report optimized path. 37 | if (cache === undefined) { 38 | out[out.length] = catAndSlice(optimizedPath, pathSet, depth); 39 | return; 40 | } 41 | 42 | // all other sentinels are short circuited. 43 | // Or we found a primitive (which includes null) 44 | if (cache === null || (cache.$type && cache.$type !== "ref") || 45 | (typeof cache !== 'object')) { 46 | return; 47 | } 48 | 49 | // If the reference is the last item in the path then do not 50 | // continue to search it. 51 | if (cache.$type === "ref" && depth === pathSet.length) { 52 | return; 53 | } 54 | 55 | var keySet = pathSet[depth]; 56 | var isKeySet = typeof keySet === 'object' && keySet !== null; 57 | var nextDepth = depth + 1; 58 | var iteratorNote = false; 59 | var key = keySet; 60 | if (isKeySet) { 61 | iteratorNote = {}; 62 | key = iterateKeySet(keySet, iteratorNote); 63 | } 64 | var next, nextOptimized; 65 | do { 66 | next = cache[key]; 67 | var optimizedPathLength = optimizedPath.length; 68 | optimizedPath[optimizedPathLength] = key; 69 | 70 | if (next && next.$type === "ref" && nextDepth < pathSet.length) { 71 | var refResults = 72 | followReference(cacheRoot, next.value, maxRefFollow); 73 | if (refResults.error) { 74 | return refResults.error; 75 | } 76 | next = refResults.node; 77 | // must clone to avoid the mutation from above destroying the cache. 78 | nextOptimized = cloneArray(refResults.refPath); 79 | } else { 80 | nextOptimized = optimizedPath; 81 | } 82 | 83 | var error = optimizePathSet(next, cacheRoot, pathSet, nextDepth, 84 | out, nextOptimized, maxRefFollow); 85 | if (error) { 86 | return error; 87 | } 88 | optimizedPath.length = optimizedPathLength; 89 | 90 | if (iteratorNote && !iteratorNote.done) { 91 | key = iterateKeySet(keySet, iteratorNote); 92 | } 93 | } while (iteratorNote && !iteratorNote.done); 94 | } 95 | -------------------------------------------------------------------------------- /lib/pathCount.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Helper for getPathCount. Used to determine the size of a key or range. 5 | * @function 6 | * @param {Object} rangeOrKey 7 | * @return The size of the key or range passed in. 8 | */ 9 | function getRangeOrKeySize(rangeOrKey) { 10 | if (rangeOrKey == null) { 11 | return 1; 12 | } else if (Array.isArray(rangeOrKey)) { 13 | throw new Error("Unexpected Array found in keySet: " + JSON.stringify(rangeOrKey)); 14 | } else if (typeof rangeOrKey === "object") { 15 | return getRangeSize(rangeOrKey); 16 | } else { 17 | return 1; 18 | } 19 | } 20 | 21 | /** 22 | * Returns the size (number of items) in a Range, 23 | * @function 24 | * @param {Object} range The Range with both "from" and "to", or just "to" 25 | * @return The number of items in the range. 26 | */ 27 | function getRangeSize(range) { 28 | 29 | var to = range.to; 30 | var length = range.length; 31 | 32 | if (to != null) { 33 | if (isNaN(to) || parseInt(to, 10) !== to) { 34 | throw new Error("Invalid range, 'to' is not an integer: " + JSON.stringify(range)); 35 | } 36 | var from = range.from || 0; 37 | if (isNaN(from) || parseInt(from, 10) !== from) { 38 | throw new Error("Invalid range, 'from' is not an integer: " + JSON.stringify(range)); 39 | } 40 | if (from <= to) { 41 | return (to - from) + 1; 42 | } else { 43 | return 0; 44 | } 45 | } else if (length != null) { 46 | if (isNaN(length) || parseInt(length, 10) !== length) { 47 | throw new Error("Invalid range, 'length' is not an integer: " + JSON.stringify(range)); 48 | } else { 49 | return length; 50 | } 51 | } else { 52 | throw new Error("Invalid range, expected 'to' or 'length': " + JSON.stringify(range)); 53 | } 54 | } 55 | 56 | /** 57 | * Returns a count of the number of paths this pathset 58 | * represents. 59 | * 60 | * For example, ["foo", {"from":0, "to":10}, "bar"], 61 | * would represent 11 paths (0 to 10, inclusive), and 62 | * ["foo, ["baz", "boo"], "bar"] would represent 2 paths. 63 | * 64 | * @function 65 | * @param {Object[]} pathSet the path set. 66 | * 67 | * @return The number of paths this represents 68 | */ 69 | function getPathCount(pathSet) { 70 | if (pathSet.length === 0) { 71 | throw new Error("All paths must have length larger than zero."); 72 | } 73 | 74 | var numPaths = 1; 75 | 76 | for (var i = 0; i < pathSet.length; i++) { 77 | var segment = pathSet[i]; 78 | 79 | if (Array.isArray(segment)) { 80 | 81 | var numKeys = 0; 82 | 83 | for (var j = 0; j < segment.length; j++) { 84 | var keySet = segment[j]; 85 | 86 | numKeys += getRangeOrKeySize(keySet); 87 | } 88 | 89 | numPaths *= numKeys; 90 | 91 | } else { 92 | numPaths *= getRangeOrKeySize(segment); 93 | } 94 | } 95 | 96 | return numPaths; 97 | } 98 | 99 | 100 | module.exports = getPathCount; 101 | -------------------------------------------------------------------------------- /lib/pathsComplementFromLengthTree.js: -------------------------------------------------------------------------------- 1 | var hasIntersection = require('./hasIntersection'); 2 | 3 | /** 4 | * Compares the paths passed in with the tree. Any of the paths that are in 5 | * the tree will be stripped from the paths. 6 | * 7 | * **Does not mutate** the incoming paths object. 8 | * **Proper subset** only matching. 9 | * 10 | * @param {Array} paths - A list of paths (complex or simple) to strip the 11 | * intersection 12 | * @param {Object} tree - 13 | * @public 14 | */ 15 | module.exports = function pathsComplementFromLengthTree(paths, tree) { 16 | var out = []; 17 | var outLength = -1; 18 | 19 | for (var i = 0, len = paths.length; i < len; ++i) { 20 | // If this does not intersect then add it to the output. 21 | var path = paths[i]; 22 | if (!hasIntersection(tree[path.length], path, 0)) { 23 | out[++outLength] = path; 24 | } 25 | } 26 | return out; 27 | }; 28 | 29 | -------------------------------------------------------------------------------- /lib/pathsComplementFromTree.js: -------------------------------------------------------------------------------- 1 | var hasIntersection = require('./hasIntersection'); 2 | 3 | /** 4 | * Compares the paths passed in with the tree. Any of the paths that are in 5 | * the tree will be stripped from the paths. 6 | * 7 | * **Does not mutate** the incoming paths object. 8 | * **Proper subset** only matching. 9 | * 10 | * @param {Array} paths - A list of paths (complex or simple) to strip the 11 | * intersection 12 | * @param {Object} tree - 13 | * @public 14 | */ 15 | module.exports = function pathsComplementFromTree(paths, tree) { 16 | var out = []; 17 | var outLength = -1; 18 | 19 | for (var i = 0, len = paths.length; i < len; ++i) { 20 | // If this does not intersect then add it to the output. 21 | if (!hasIntersection(tree, paths[i], 0)) { 22 | out[++outLength] = paths[i]; 23 | } 24 | } 25 | return out; 26 | }; 27 | 28 | -------------------------------------------------------------------------------- /lib/support/catAndSlice.js: -------------------------------------------------------------------------------- 1 | module.exports = function catAndSlice(a, b, slice) { 2 | var next = [], i, j, len; 3 | for (i = 0, len = a.length; i < len; ++i) { 4 | next[i] = a[i]; 5 | } 6 | 7 | for (j = slice || 0, len = b.length; j < len; ++j, ++i) { 8 | next[i] = b[j]; 9 | } 10 | 11 | return next; 12 | }; 13 | 14 | -------------------------------------------------------------------------------- /lib/support/cloneArray.js: -------------------------------------------------------------------------------- 1 | function cloneArray(arr, index) { 2 | var a = []; 3 | var len = arr.length; 4 | for (var i = index || 0; i < len; i++) { 5 | a[i] = arr[i]; 6 | } 7 | return a; 8 | } 9 | 10 | module.exports = cloneArray; 11 | 12 | -------------------------------------------------------------------------------- /lib/toPaths.js: -------------------------------------------------------------------------------- 1 | var maybeIntegerKey = require("./integerKey").maybeIntegerKey; 2 | var isIntegerKey = require("./integerKey").isIntegerKey; 3 | var isArray = Array.isArray; 4 | var typeOfObject = "object"; 5 | var typeOfNumber = "number"; 6 | 7 | /* jshint forin: false */ 8 | module.exports = function toPaths(lengths) { 9 | var pathmap; 10 | var allPaths = []; 11 | for (var length in lengths) { 12 | var num = maybeIntegerKey(length); 13 | if (typeof num === typeOfNumber && isObject(pathmap = lengths[length])) { 14 | var paths = collapsePathMap(pathmap, 0, num).sets; 15 | var pathsIndex = -1; 16 | var pathsCount = paths.length; 17 | while (++pathsIndex < pathsCount) { 18 | allPaths.push(collapsePathSetIndexes(paths[pathsIndex])); 19 | } 20 | } 21 | } 22 | return allPaths; 23 | }; 24 | 25 | function isObject(value) { 26 | return value !== null && typeof value === typeOfObject; 27 | } 28 | 29 | function collapsePathMap(pathmap, depth, length) { 30 | 31 | var key; 32 | var code = getHashCode(String(depth)); 33 | var subs = Object.create(null); 34 | 35 | var codes = []; 36 | var codesIndex = -1; 37 | var codesCount = 0; 38 | 39 | var pathsets = []; 40 | var pathsetsCount = 0; 41 | 42 | var subPath, subCode, 43 | subKeys, subKeysIndex, subKeysCount, 44 | subSets, subSetsIndex, subSetsCount, 45 | pathset, pathsetIndex, pathsetCount, 46 | firstSubKey, pathsetClone; 47 | 48 | subKeys = []; 49 | subKeysIndex = -1; 50 | 51 | if (depth < length - 1) { 52 | 53 | subKeysCount = getKeys(pathmap, subKeys); 54 | 55 | while (++subKeysIndex < subKeysCount) { 56 | key = subKeys[subKeysIndex]; 57 | subPath = collapsePathMap(pathmap[key], depth + 1, length); 58 | subCode = subPath.code; 59 | if(subs[subCode]) { 60 | subPath = subs[subCode]; 61 | } else { 62 | codes[codesCount++] = subCode; 63 | subPath = subs[subCode] = { 64 | keys: [], 65 | sets: subPath.sets 66 | }; 67 | } 68 | code = getHashCode(code + key + subCode); 69 | var num = maybeIntegerKey(key); 70 | subPath.keys.push(typeof num === typeOfNumber ? num : key); 71 | } 72 | 73 | while(++codesIndex < codesCount) { 74 | 75 | key = codes[codesIndex]; 76 | subPath = subs[key]; 77 | subKeys = subPath.keys; 78 | subKeysCount = subKeys.length; 79 | 80 | if (subKeysCount > 0) { 81 | 82 | subSets = subPath.sets; 83 | subSetsIndex = -1; 84 | subSetsCount = subSets.length; 85 | firstSubKey = subKeys[0]; 86 | 87 | while (++subSetsIndex < subSetsCount) { 88 | 89 | pathset = subSets[subSetsIndex]; 90 | pathsetIndex = -1; 91 | pathsetCount = pathset.length; 92 | pathsetClone = new Array(pathsetCount + 1); 93 | pathsetClone[0] = subKeysCount > 1 && subKeys || firstSubKey; 94 | 95 | while (++pathsetIndex < pathsetCount) { 96 | pathsetClone[pathsetIndex + 1] = pathset[pathsetIndex]; 97 | } 98 | 99 | pathsets[pathsetsCount++] = pathsetClone; 100 | } 101 | } 102 | } 103 | } else { 104 | subKeysCount = getKeys(pathmap, subKeys); 105 | if (subKeysCount > 1) { 106 | pathsets[pathsetsCount++] = [subKeys]; 107 | } else { 108 | pathsets[pathsetsCount++] = subKeys; 109 | } 110 | while (++subKeysIndex < subKeysCount) { 111 | code = getHashCode(code + subKeys[subKeysIndex]); 112 | } 113 | } 114 | 115 | return { 116 | code: code, 117 | sets: pathsets 118 | }; 119 | } 120 | 121 | function collapsePathSetIndexes(pathset) { 122 | 123 | var keysetIndex = -1; 124 | var keysetCount = pathset.length; 125 | 126 | while (++keysetIndex < keysetCount) { 127 | var keyset = pathset[keysetIndex]; 128 | if (isArray(keyset)) { 129 | pathset[keysetIndex] = collapseIndex(keyset); 130 | } 131 | } 132 | 133 | return pathset; 134 | } 135 | 136 | /** 137 | * Collapse range indexers, e.g. when there is a continuous 138 | * range in an array, turn it into an object instead: 139 | * 140 | * [1,2,3,4,5,6] => {"from":1, "to":6} 141 | * 142 | * @private 143 | */ 144 | function collapseIndex(keyset) { 145 | 146 | // Do we need to dedupe an indexer keyset if they're duplicate consecutive integers? 147 | // var hash = {}; 148 | var keyIndex = -1; 149 | var keyCount = keyset.length - 1; 150 | var isSparseRange = keyCount > 0; 151 | 152 | while (++keyIndex <= keyCount) { 153 | 154 | var key = keyset[keyIndex]; 155 | 156 | if (!isIntegerKey(key) /* || hash[key] === true*/ ) { 157 | isSparseRange = false; 158 | break; 159 | } 160 | // hash[key] = true; 161 | // Cast number indexes to integers. 162 | keyset[keyIndex] = parseInt(key, 10); 163 | } 164 | 165 | if (isSparseRange === true) { 166 | 167 | keyset.sort(sortListAscending); 168 | 169 | var from = keyset[0]; 170 | var to = keyset[keyCount]; 171 | 172 | // If we re-introduce deduped integer indexers, change this comparson to "===". 173 | if (to - from <= keyCount) { 174 | return { 175 | from: from, 176 | to: to 177 | }; 178 | } 179 | } 180 | 181 | return keyset; 182 | } 183 | 184 | function sortListAscending(a, b) { 185 | return a - b; 186 | } 187 | 188 | /* jshint forin: false */ 189 | function getKeys(map, keys, sort) { 190 | var len = 0; 191 | 192 | for (var key in map) { 193 | keys[len++] = key; 194 | } 195 | return len; 196 | } 197 | 198 | function getHashCode(key) { 199 | var code = 5381; 200 | var index = -1; 201 | var count = key.length; 202 | while (++index < count) { 203 | code = (code << 5) + code + key.charCodeAt(index); 204 | } 205 | return String(code); 206 | } 207 | 208 | // backwards-compatibility (temporary) 209 | module.exports._isSafeNumber = isIntegerKey; 210 | -------------------------------------------------------------------------------- /lib/toTree.js: -------------------------------------------------------------------------------- 1 | var iterateKeySet = require('./../lib/iterateKeySet'); 2 | 3 | /** 4 | * @param {Array} paths - 5 | * @returns {Object} - 6 | */ 7 | module.exports = function toTree(paths) { 8 | return paths.reduce(__reducer, {}); 9 | }; 10 | 11 | function __reducer(acc, path) { 12 | /*#__NOINLINE__*/ innerToTree(acc, path, 0); 13 | return acc; 14 | } 15 | 16 | function innerToTree(seed, path, depth) { 17 | var keySet = path[depth]; 18 | var iteratorNote = {}; 19 | var key; 20 | var nextDepth = depth + 1; 21 | 22 | key = iterateKeySet(keySet, iteratorNote); 23 | 24 | while (!iteratorNote.done) { 25 | var next = Object.prototype.hasOwnProperty.call(seed, key) && seed[key]; 26 | if (!next) { 27 | if (nextDepth === path.length) { 28 | seed[key] = null; 29 | } else if (key !== undefined) { 30 | next = seed[key] = {}; 31 | } 32 | } 33 | 34 | if (nextDepth < path.length) { 35 | innerToTree(next, path, nextDepth); 36 | } 37 | 38 | key = iterateKeySet(keySet, iteratorNote); 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /lib/unescape.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unescapes a string by removing the leading "_". This function is the inverse 3 | * of escape, which is used to encode untrusted input to ensure it 4 | * does not contain reserved JSON Graph keywords (ex. "$type"). 5 | */ 6 | module.exports = function unescape(str) { 7 | if (str.slice(0, 1) === "_") { 8 | return str.slice(1); 9 | } else { 10 | throw SyntaxError("Expected \"_\"."); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "falcor-path-utils", 3 | "version": "0.7.5", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/gulp test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Netflix/falcor-path-utils.git" 12 | }, 13 | "author": "Netflix", 14 | "license": "Apache-2.0", 15 | "bugs": { 16 | "url": "https://github.com/Netflix/falcor-path-utils/issues" 17 | }, 18 | "homepage": "https://github.com/Netflix/falcor-path-utils#readme", 19 | "devDependencies": { 20 | "chai": "^4.2.0", 21 | "falcor-json-graph": "^3.2.1", 22 | "gulp": "^4.0.2", 23 | "gulp-mocha": "^7.0.2", 24 | "sinon": "^9.0.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/collapse.spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var pathUtils = require('..'); 3 | var collapse = pathUtils.collapse; 4 | 5 | describe('collapse', function() { 6 | it('should collapse paths', function() { 7 | var paths = [ 8 | ['videos', 0], 9 | ['videos', 1], 10 | ['videos', { from: 2, to: 1000 }], 11 | ['videosById', 1, 'title'], 12 | ['videosById', 1, 'summary'] 13 | ]; 14 | 15 | var result = collapse(paths); 16 | 17 | expect(result).to.eql([ 18 | ['videos', { from: 0, to: 1000 }], 19 | ['videosById', 1, ['title', 'summary']], 20 | ]); 21 | }); 22 | 23 | it('should collapse a path with reserved keyword.', function() { 24 | var paths = [['foo', 'hasOwnProperty'], ['foo', 'constructor']]; 25 | 26 | var result = collapse(paths); 27 | expect(result).to.eql([ 28 | ['foo', ['hasOwnProperty', 'constructor']] 29 | ]); 30 | }); 31 | 32 | it('should collapse paths with mixed adjacents', function() { 33 | var paths = [ 34 | ['videosById', 1, 'title'], 35 | ['videos', 0], 36 | ['videosById', 1, 'artwork'], 37 | ['videos', 1], 38 | ['videosById', 1, 'summary'] 39 | ]; 40 | 41 | var result = collapse(paths); 42 | 43 | expect(result).to.eql([ 44 | ['videos', { from: 0, to: 1 }], 45 | ['videosById', 1, ['title', 'artwork', 'summary']], 46 | ]); 47 | }); 48 | 49 | it('should collapse paths with mixed ranges', function() { 50 | var paths = [ 51 | ['videos', [5, 1, 3], 'summary'], 52 | ['videos', [2, 4, 6], 'summary'] 53 | ]; 54 | 55 | var result = collapse(paths); 56 | 57 | expect(result).to.eql([ 58 | ['videos', { from: 1, to: 6}, 'summary'] 59 | ]); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/escape.spec.js: -------------------------------------------------------------------------------- 1 | var escape = require('./../lib/escape'); 2 | var expect = require('chai').expect; 3 | 4 | describe('escape', function() { 5 | it('should add a leading underscore to strings.', function() { 6 | expect(escape("test")).to.equal("_test"); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | describe('falcor-path-utils', function () { 2 | require('./collapse.spec'); 3 | require('./toPaths.spec'); 4 | require('./toTree.spec'); 5 | require('./pathsComplementFromTree.spec'); 6 | require('./jsonKey.spec'); 7 | require('./integerKey.spec'); 8 | require('./optimizePathSets.spec'); 9 | require('./pathCount.spec'); 10 | require("./escape.spec"); 11 | require("./unescape.spec"); 12 | }); 13 | -------------------------------------------------------------------------------- /test/integerKey.spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var pathUtils = require('..'); 3 | var isIntegerKey = pathUtils.isIntegerKey; 4 | var maybeIntegerKey = pathUtils.maybeIntegerKey; 5 | 6 | var thingsThatShouldReturnTrue = [ 7 | 0, 8 | 1, 9 | -0, 10 | -1, 11 | 10, 12 | -10, 13 | 9007199254740991, // max safe int 14 | -9007199254740991, // min safe int 15 | '0', 16 | '1', 17 | '-1', 18 | '10', 19 | '-10', 20 | '9007199254740991', // max safe int 21 | '-9007199254740991', // min safe int 22 | ]; 23 | 24 | var thingsThatShouldReturnFalse = [ 25 | [], 26 | {}, 27 | null, 28 | true, 29 | false, 30 | undefined, 31 | NaN, 32 | Infinity, 33 | -Infinity, 34 | '9007199254740992', // max safe int + 1 35 | '-9007199254740992', // min safe int - 1 36 | 9007199254740992, // max safe int + 1 37 | -9007199254740992, // min safe int - 1 38 | '648365838265483646384563538', 39 | '-0', 40 | '', 41 | '01', 42 | '0d', 43 | '1d', 44 | '_', 45 | ' 1', 46 | '- 1', 47 | ' ', 48 | '0x123', 49 | '0b1101', 50 | 'deadbeef', 51 | 0.1, 52 | -0.1, 53 | '1.0', 54 | '-1.0', 55 | '0.1', 56 | '-0.1', 57 | ]; 58 | 59 | describe('integerKey', function () { 60 | describe('isIntegerKey', function() { 61 | thingsThatShouldReturnTrue.forEach(function(thing) { 62 | var should = 'should return true on ' + JSON.stringify(thing); 63 | it(should, function() { 64 | expect(isIntegerKey(thing)).to.equal(true); 65 | }); 66 | }); 67 | 68 | thingsThatShouldReturnFalse.forEach(function(thing) { 69 | var should = 'should return false on ' + JSON.stringify(thing); 70 | it(should, function() { 71 | expect(isIntegerKey(thing)).to.equal(false); 72 | }); 73 | }); 74 | }); 75 | 76 | describe('maybeIntegerKey', function() { 77 | thingsThatShouldReturnTrue.forEach(function(thing) { 78 | var should = 'should return true on ' + JSON.stringify(thing); 79 | it(should, function() { 80 | expect(maybeIntegerKey(thing)).to.be.a("number"); 81 | }); 82 | }); 83 | 84 | thingsThatShouldReturnFalse.forEach(function(thing) { 85 | var should = 'should return false on ' + JSON.stringify(thing); 86 | it(should, function() { 87 | expect(maybeIntegerKey(thing)).to.be.undefined; 88 | }); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/jsonKey.spec.js: -------------------------------------------------------------------------------- 1 | var pathUtils = require('..'); 2 | var toJsonKey = pathUtils.toJsonKey; 3 | var isJsonKey = pathUtils.isJsonKey; 4 | var maybeJsonKey = pathUtils.maybeJsonKey; 5 | var expect = require('chai').expect; 6 | 7 | describe('jsonKey', function () { 8 | describe('toJsonKey', function () { 9 | it('should return serialized JSON with sorted keys for objects', function () { 10 | expect(toJsonKey({ 11 | b: 2, 12 | a: 1, 13 | c: 3 14 | })).to.equal('{"a":1,"b":2,"c":3}'); 15 | expect(toJsonKey({ a: 1 })).to.equal('{"a":1}'); 16 | expect(toJsonKey({ a: 1, b: 2 })).to.equal('{"a":1,"b":2}'); 17 | }); 18 | 19 | it('should throw TypeError for non-objects', function () { 20 | expect(() => toJsonKey('abc')).to.throw(TypeError); 21 | expect(() => toJsonKey(undefined)).to.throw(TypeError); 22 | expect(() => toJsonKey(12)).to.throw(TypeError); 23 | expect(() => toJsonKey(Symbol('foo'))).to.throw(TypeError); 24 | expect(() => toJsonKey(null)).to.throw(TypeError); 25 | expect(() => toJsonKey(false)).to.throw(TypeError); 26 | }); 27 | 28 | it('should throw TypeError for non-literal objects', function() { 29 | expect(() => toJsonKey([1, 2, 3])).to.throw(TypeError); 30 | expect(() => toJsonKey(new Date())).to.throw(TypeError); 31 | expect(() => toJsonKey(new Map())).to.throw(TypeError); 32 | expect(() => toJsonKey(new Int8Array())).to.throw(TypeError); 33 | }); 34 | 35 | it('should throw TypeError when toJSON returns non-object', function() { 36 | var o = { 37 | toJSON: function toJSON() { 38 | return null; 39 | } 40 | } 41 | expect(() => toJsonKey(o)).to.throw(TypeError); 42 | }); 43 | 44 | it('should allow objects created by custom classes', function() { 45 | function Config() { 46 | this.key = "value"; 47 | this.isValid = true; 48 | } 49 | expect(toJsonKey(new Config())).to.equal('{"isValid":true,"key":"value"}'); 50 | }); 51 | 52 | it('should return "{}" for empty object', function () { 53 | expect(toJsonKey({})).to.equal('{}'); 54 | }); 55 | 56 | it('should work recursively', function () { 57 | expect(toJsonKey({ 58 | b: 2, 59 | a: { 60 | ab: 1.2, 61 | aa: 1.1 62 | } 63 | })).to.equal('{"a":{"aa":1.1,"ab":1.2},"b":2}'); 64 | }); 65 | 66 | it('should be same for objects with keys created in different order', function () { 67 | expect(toJsonKey({ 68 | b: 2, 69 | a: 1 70 | })).to.equal(toJsonKey({ 71 | a: 1, 72 | b: 2 73 | })); 74 | 75 | var obj = { b: 2 }; 76 | obj.a = 1; 77 | expect(toJsonKey(obj)).to.equal('{"a":1,"b":2}'); 78 | }); 79 | 80 | it('should escape strings in values appropriately', function () { 81 | expect(toJsonKey({ a: "abc\"def" })).to.equal('{"a":"abc\\"def"}'); 82 | }); 83 | 84 | it('should escape strings in keys appropriately', function () { 85 | expect(toJsonKey({ "abc\"def": 1 })).to.equal('{"abc\\"def":1}'); 86 | }); 87 | 88 | it('should convert non-string keys to strings', function () { 89 | expect(toJsonKey({ null: 1 })).to.equal('{"null":1}'); 90 | expect(toJsonKey({ undefined: 1 })).to.equal('{"undefined":1}'); 91 | expect(toJsonKey({ 123: 1 })).to.equal('{"123":1}'); 92 | }); 93 | }); 94 | 95 | var thingsThatShouldReturnTrue = [ 96 | '{"valid json":"with no whitespace"}', 97 | '{"valid":"json"}', 98 | '{"nested":{"json":true}}', 99 | '{"key":1}', 100 | '{"key":null}', 101 | '{"keys":0,"sorted":1}', 102 | '{"1":"key"}', 103 | '{"multiple":1,"types":true,"work":"too"}', 104 | ]; 105 | 106 | var thingsThatShouldReturnFalse = [ 107 | [], 108 | {}, 109 | null, 110 | true, 111 | false, 112 | undefined, 113 | NaN, 114 | Infinity, 115 | -Infinity, 116 | '123', 117 | '', 118 | 'abc', 119 | ' {}', 120 | '{ }', 121 | ' ', 122 | '{} ', 123 | '{invalid json}', 124 | '{"valid json": "with whitespace"}', 125 | '{"unsorted":0,"keys":1}', 126 | '{1:"key"}', 127 | '{"key":undefined}', 128 | ' {"valid_json":"with_whitespace_at_start"}', 129 | '{"valid_json":"with_whitespace_at_end"} ', 130 | 1, 131 | -0, 132 | -1, 133 | 0.1, 134 | -0.1, 135 | '0.1', 136 | '-0.1', 137 | ]; 138 | 139 | describe('isJsonKey', function () { 140 | thingsThatShouldReturnTrue.forEach(function (thing) { 141 | var should = 'should return true on ' + JSON.stringify(thing); 142 | it(should, function () { 143 | expect(isJsonKey(thing)).to.equal(true); 144 | }); 145 | }); 146 | 147 | thingsThatShouldReturnFalse.forEach(function (thing) { 148 | var should = 'should return false on ' + JSON.stringify(thing); 149 | it(should, function () { 150 | expect(isJsonKey(thing)).to.equal(false); 151 | }); 152 | }); 153 | }); 154 | 155 | describe('maybeJsonKey', function () { 156 | thingsThatShouldReturnTrue.forEach(function (thing) { 157 | var should = 'should return true on ' + JSON.stringify(thing); 158 | it(should, function () { 159 | expect(maybeJsonKey(thing)).to.be.an.instanceof(Object); 160 | }); 161 | }); 162 | 163 | thingsThatShouldReturnFalse.forEach(function (thing) { 164 | var should = 'should return false on ' + JSON.stringify(thing); 165 | it(should, function () { 166 | expect(maybeJsonKey(thing)).to.be.undefined; 167 | }); 168 | }); 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /test/materialize.spec.js: -------------------------------------------------------------------------------- 1 | var materialize = require('./materialize'); 2 | var expect = require('chai').expect; 3 | 4 | describe('materialize', function() { 5 | it('materializes a simple path', function() { 6 | var out = materialize(['a', 1, 'b'], null); 7 | expect(out).to.deep.equal({ a: { '1': { b: null } } }); 8 | }); 9 | it('materializes a pathSet', function() { 10 | var out = materialize([[{ from: 0, to: 1 }, 'a']], null); 11 | expect(out).to.deep.equal({ '0': null, '1': null, a: null }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/optimizePathSets.spec.js: -------------------------------------------------------------------------------- 1 | var optimizePathSets = require('./../lib/optimizePathSets'); 2 | var falcorJsonGraph = require('falcor-json-graph'); 3 | var $ref = falcorJsonGraph.ref; 4 | var $atom = falcorJsonGraph.atom; 5 | var expect = require('chai').expect; 6 | var errors = require('./../lib/errors'); 7 | 8 | /** 9 | * normally i don't test internals but i think the merges 10 | * warrent internal testing. The reason being is that the 11 | * merges are core to the product. If i don't, i will have to 12 | * figure out where bugs are without much clarity into where they 13 | * are. 14 | */ 15 | describe('optimizePathSets', function() { 16 | it('should optimize simple path.', function() { 17 | var cache = getCache(); 18 | var paths = [['videosList', 3, 'summary']]; 19 | 20 | var out = optimizePathSets(cache, paths); 21 | var expected = [['videos', 956, 'summary']]; 22 | expect(out.paths).to.deep.equal(expected); 23 | }); 24 | 25 | it('should optimize a complex path.', function() { 26 | var cache = getCache(); 27 | var paths = [['videosList', [0, 3], 'summary']]; 28 | 29 | var out = optimizePathSets(cache, paths); 30 | var expected = [ 31 | ['videosList', 0, 'summary'], 32 | ['videos', 956, 'summary'] 33 | ]; 34 | expect(out.paths).to.deep.equal(expected); 35 | }); 36 | 37 | it('should remove found paths', function() { 38 | var cache = getCache(); 39 | var paths = [['videosList', [0, 3, 5], 'summary']]; 40 | 41 | var out = optimizePathSets(cache, paths); 42 | var expected = [ 43 | ['videosList', 0, 'summary'], 44 | ['videos', 956, 'summary'] 45 | ]; 46 | expect(out.paths).to.deep.equal(expected); 47 | }); 48 | 49 | it('should follow double references.', function() { 50 | var cache = getCache(); 51 | var paths = [['videosList', 'double', 'summary']]; 52 | 53 | var out = optimizePathSets(cache, paths); 54 | var expected = [ 55 | ['videos', 956, 'summary'] 56 | ]; 57 | expect(out.paths).to.deep.equal(expected); 58 | }); 59 | 60 | it('should short circuit on ref.', function() { 61 | var cache = getCache(); 62 | var paths = [['videosList', 'short', 'summary']]; 63 | 64 | var out = optimizePathSets(cache, paths); 65 | var expected = []; 66 | expect(out.paths).to.deep.equal(expected); 67 | }); 68 | 69 | it('should short circuit on primitive string values', function() { 70 | var cache = getCache(); 71 | var paths = [['videos', '6', 'summary']]; 72 | 73 | var out = optimizePathSets(cache, paths); 74 | var expected = []; 75 | expect(out.paths).to.deep.equal(expected); 76 | }); 77 | 78 | it('should short circuit on primitive number values', function() { 79 | var cache = getCache(); 80 | var paths = [['videos', '7', 'summary']]; 81 | 82 | var out = optimizePathSets(cache, paths); 83 | var expected = []; 84 | expect(out.paths).to.deep.equal(expected); 85 | }); 86 | 87 | it('should short circuit on primitive boolean values', function() { 88 | var cache = getCache(); 89 | var paths = [['videos', '8', 'summary']]; 90 | 91 | var out = optimizePathSets(cache, paths); 92 | var expected = []; 93 | expect(out.paths).to.deep.equal(expected); 94 | }); 95 | 96 | it('should short circuit on primitive null value', function() { 97 | var cache = getCache(); 98 | var paths = [['videos', '9', 'summary']]; 99 | 100 | var out = optimizePathSets(cache, paths); 101 | var expected = []; 102 | expect(out.paths).to.deep.equal(expected); 103 | }); 104 | 105 | it('should not treat falsey string as missing', function() { 106 | var cache = getCache(); 107 | var paths = [['falsey', 'string']]; 108 | 109 | var out = optimizePathSets(cache, paths); 110 | var expected = []; 111 | expect(out.paths).to.deep.equal(expected); 112 | }); 113 | 114 | it('should not treat falsey number as missing', function() { 115 | var cache = getCache(); 116 | var paths = [['falsey', 'number']]; 117 | 118 | var out = optimizePathSets(cache, paths); 119 | var expected = []; 120 | expect(out.paths).to.deep.equal(expected); 121 | }); 122 | 123 | it('should not treat falsey boolean as missing', function() { 124 | var cache = getCache(); 125 | var paths = [['falsey', 'boolean']]; 126 | 127 | var out = optimizePathSets(cache, paths); 128 | var expected = []; 129 | expect(out.paths).to.deep.equal(expected); 130 | }); 131 | 132 | it('should not treat falsey null as missing', function() { 133 | var cache = getCache(); 134 | var paths = [['falsey', 'null']]; 135 | 136 | var out = optimizePathSets(cache, paths); 137 | var expected = []; 138 | expect(out.paths).to.deep.equal(expected); 139 | }); 140 | 141 | it('should preserve null in middle of path.', function() { 142 | var cache = getCache(); 143 | var paths = [['videos', null, 'b']]; 144 | 145 | var out = optimizePathSets(cache, paths); 146 | var expected = [['videos', null, 'b']]; 147 | expect(out.paths).to.deep.equal(expected); 148 | }); 149 | 150 | it('should return an error.', function() { 151 | var cache = getCache(); 152 | var paths = [['videosList', 'inner', 'summary']]; 153 | 154 | var out = optimizePathSets(cache, paths); 155 | expect(out.error.message).to.equals(errors.innerReferences); 156 | }); 157 | 158 | }); 159 | 160 | function getCache() { 161 | return { 162 | videosList: { 163 | 3: $ref(['videos', 956]), 164 | 5: $ref(['videos', 5]), 165 | double: $ref(['videosList', 3]), 166 | short: $ref(['videos', 5, 'moreKeys']), 167 | inner: $ref(['videosList', 3, 'inner']) 168 | }, 169 | videos: { 170 | 5: $atom('title'), 171 | 172 | // Short circuit on primitives 173 | 6: 'a', 174 | 7: 1, 175 | 8: true, 176 | 9: null, 177 | 'null': { 178 | x: 1 179 | } 180 | }, 181 | falsey: { 182 | string: '', 183 | number: 0, 184 | boolean: false, 185 | 'null': null 186 | } 187 | }; 188 | } 189 | 190 | -------------------------------------------------------------------------------- /test/pathCount.spec.js: -------------------------------------------------------------------------------- 1 | var getPathCount = require('../lib/pathCount'); 2 | var expect = require('chai').expect; 3 | 4 | describe('pathCount', function() { 5 | 6 | it('should return 1 for the correct count: ["lomo", 2, 6, "name"]', function() { 7 | var count = getPathCount(["lomo", 2, 6, "name"]); 8 | expect(count).to.equals(1); 9 | }); 10 | 11 | it('should thow if "to" is a string containing an integer.', function() { 12 | try { 13 | getPathCount(["lomo", {from: 0, to: "1"}, "name"]); 14 | expect(1).to.equals(2) 15 | } catch (e) { 16 | expect(e.message).to.equals("Invalid range, 'to' is not an integer: {\"from\":0,\"to\":\"1\"}"); 17 | } 18 | }); 19 | 20 | 21 | it('should thow if "to" is not an integer in a range.', function() { 22 | try { 23 | getPathCount(["lomo", {from: 0, to: "hello"}, "name"]); 24 | expect(1).to.equals(2) 25 | } catch (e) { 26 | expect(e.message).to.equals("Invalid range, 'to' is not an integer: {\"from\":0,\"to\":\"hello\"}"); 27 | } 28 | }); 29 | 30 | it('should throw if "from" is not an integer in range.', function() { 31 | try { 32 | getPathCount(["lomo", {from: [], to: 0}, "name"]); 33 | expect(1).to.equals(2) 34 | } catch (e) { 35 | expect(e.message).to.equals("Invalid range, 'from' is not an integer: {\"from\":[],\"to\":0}"); 36 | } 37 | }); 38 | 39 | it('should throw if "length" is not an integer in range.', function() { 40 | try { 41 | getPathCount(["lomo", {from: 0, length: {}}, "name"]); 42 | expect(1).to.equals(2) 43 | } catch (e) { 44 | expect(e.message).to.equals("Invalid range, 'length' is not an integer: {\"from\":0,\"length\":{}}"); 45 | } 46 | }); 47 | 48 | it('should throw if neither "to" nor "length" is found in range.', function() { 49 | try { 50 | getPathCount(["lomo", {}, "name"]); 51 | expect(1).to.equals(2) 52 | } catch (e) { 53 | expect(e.message).to.equals("Invalid range, expected 'to' or 'length': {}"); 54 | } 55 | }); 56 | 57 | it('should return 7 for ["lolomo", [{from:7, to: 9}, {from: 70, to: 72}, 10], "name"]', function() { 58 | var count = getPathCount(["lolomo", [{from:7, to: 9}, {from: 70, to: 72}, 10], "name"]); 59 | expect(count).to.equals(7) 60 | }); 61 | 62 | it('should return 50 for ["lomo", {from: 6, length: 10}, 6, ["hi", {length: 4}], "name"]', function() { 63 | var count = getPathCount(["lomo", {from: 6, length: 10}, 9, ["hi", {length: 4}], "name"]); 64 | expect(count).to.equals(50); 65 | }); 66 | 67 | it("should return 1 for ['key1']", function() { 68 | var count = getPathCount(["key"]); 69 | expect(count).to.equals(1); 70 | }); 71 | 72 | it("should return 2 for [['key1a','key1b']]", function() { 73 | var count = getPathCount([['key1a','key1b']]); 74 | expect(count).to.equals(2); 75 | }); 76 | 77 | it("should return 10001 for ['key1', {'from': 0, 'to': 10000}, 'key3']", function() { 78 | var count = getPathCount(['key1', {'from': 0, 'to': 10000}, 'key3']); 79 | expect(count).to.equals(10001); 80 | }); 81 | 82 | it("should return 1 for ['key1', 'key2', 'key3']", function() { 83 | var count = getPathCount(['key1', 'key2', 'key3']); 84 | expect(count).to.equals(1); 85 | }); 86 | 87 | it("should return 2 for ['key1', ['key2a', 'key2b'], 'key3']", function() { 88 | var count = getPathCount(['key1', ['key2a', 'key2b'], 'key3']); 89 | expect(count).to.equals(2); 90 | }); 91 | 92 | it("should return 6 for ['key1', {from:0, to:5}, 'key3']", function() { 93 | var count = getPathCount(['key1', {from:0, to:5}, 'key3']); 94 | expect(count).to.equals(6); 95 | }); 96 | 97 | it("should return 12 for ['key1', {from:0, to:5}, ['key3a', 'key3b']]", function() { 98 | var count = getPathCount(['key1', {from:0, to:5}, ['key3a', 'key3b']]); 99 | expect(count).to.equals(12); 100 | }); 101 | 102 | it("should return 24 for ['key1', {from:0, to:5}, [{from:0, to:2}, 'key3b']]", function() { 103 | var count = getPathCount(['key1', {from:0, to:5}, [{from:0, to:2}, 'key3b']]); 104 | expect(count).to.equals(24); 105 | }); 106 | 107 | it("should return 3 for [['key1a', 'key1b', 'key1c'], 'key3', 'key4']", function() { 108 | var count = getPathCount([['key1a', 'key1b', 'key1c'], 'key3', 'key4']); 109 | expect(count).to.equals(3); 110 | }); 111 | 112 | it("should return 24 for [['key1a', 'key1b'], ['key3a', 'key3b'], {'from': 0, 'to': 5}]", function() { 113 | var count = getPathCount([['key1a', 'key1b'], ['key3a', 'key3b'], {'from': 0, 'to': 5}]); 114 | expect(count).to.equals(24); 115 | }); 116 | 117 | it("should return 10 for ['key1', {'to': 9}]", function() { 118 | var count = getPathCount(['key1', {'to': 9}]); 119 | expect(count).to.equals(10); 120 | }); 121 | 122 | it("should return 1 for ['key1', {'from': 1, 'to': 1}]", function() { 123 | var count = getPathCount(['key1', {'from': 1, 'to': 1}]); 124 | expect(count).to.equals(1); 125 | }); 126 | 127 | it("should return 15 for ['key1', {'to': 2}, {'to': 4}]", function() { 128 | var count = getPathCount(['key1', {'to': 2}, {'to': 4}]); 129 | expect(count).to.equals(15); 130 | }); 131 | 132 | it("should throw for invalid path: ['key1', {from:0}, 'key3']", function() { 133 | try { 134 | count = getPathCount(['key1', {from:0}, 'key3']); 135 | expect(1).to.equals(2) 136 | } catch (e) { 137 | expect(e.message).to.equals("Invalid range, expected \'to\' or \'length\': {\"from\":0}"); 138 | } 139 | }); 140 | 141 | it("should throw for invalid path: ['key1', {}, 'key3']", function() { 142 | try { 143 | count = getPathCount(['key1', {}, 'key3']); 144 | expect(1).to.equals(2) 145 | } catch (e) { 146 | expect(e.message).to.equals("Invalid range, expected 'to' or 'length': {}"); 147 | } 148 | }); 149 | 150 | it("should throw for invalid path: ['key1', [{}, 'key3']]", function() { 151 | try { 152 | var count = getPathCount(['key1', [{}, 'key3']]); 153 | expect(1).to.equals(2); 154 | } catch (e) { 155 | expect(e.message).to.equals("Invalid range, expected 'to' or 'length': {}"); 156 | } 157 | }); 158 | 159 | it("getPathCount for invalid path: ['key1', [['key2a', 'key2b'], 'key2c'], 'key3']", function() { 160 | try { 161 | getPathCount(['key1', [['key2a', 'key2b'], 'key2c'], 'key3']); 162 | expect(1).to.equals(2); 163 | } catch (e) { 164 | expect(e.message).to.equals("Unexpected Array found in keySet: [\"key2a\",\"key2b\"]"); 165 | } 166 | }); 167 | 168 | it("should return 1 for {from:0, to:0}", function() { 169 | var count = getPathCount([{from:0, to:0}]); 170 | expect(count).to.equals(1); 171 | }); 172 | 173 | it("should return 1 for {from:7, to:7}", function() { 174 | var count = getPathCount([{from:7, to:7}]); 175 | expect(count).to.equals(1); 176 | }); 177 | 178 | it("should return 5 for {from:1, to:5}", function() { 179 | var count = getPathCount([{from:1, to:5}]); 180 | expect(count).to.equals(5); 181 | }); 182 | 183 | it("should return 0 for {from:1, to:0} is 0", function() { 184 | var count = getPathCount([{from:1, to:0}]); 185 | expect(count).to.equals(0); 186 | }); 187 | 188 | it("should throw for invalid range {from:2}", function() { 189 | try { 190 | getPathCount([{from:2}]); 191 | expect(1).to.equals(2) 192 | } catch (e) { 193 | expect(e.message).to.equals("Invalid range, expected 'to' or 'length': {\"from\":2}"); 194 | } 195 | }); 196 | 197 | it("should throw for invalid range {foo:1, bar:2}", function() { 198 | try { 199 | getPathCount([{foo:1, bar:2}]); 200 | expect(1).to.equals(2) 201 | } catch (e) { 202 | expect(e.message).to.equals("Invalid range, expected 'to' or 'length': {\"foo\":1,\"bar\":2}"); 203 | } 204 | }); 205 | 206 | it("should throw for invalid range {}", function() { 207 | try { 208 | getPathCount([{}]); 209 | expect(1).to.equals(2) 210 | } catch (e) { 211 | expect(e.message).to.equals("Invalid range, expected 'to' or 'length': {}"); 212 | } 213 | }); 214 | 215 | }); 216 | -------------------------------------------------------------------------------- /test/pathsComplementFromTree.spec.js: -------------------------------------------------------------------------------- 1 | var pathsComplementFromTree = require('./../lib').pathsComplementFromTree; 2 | var pathsComplementFromLengthTree = require('./../lib').pathsComplementFromLengthTree; 3 | var expect = require('chai').expect; 4 | 5 | describe('pathsComplementFromTree and LengthTree', function() { 6 | it('should strip the single path from tree.', function() { 7 | var paths = [['one', 'two']]; 8 | var tree = {one: {two: null}}; 9 | var out = pathsComplementFromTree(paths, tree); 10 | expect(out).to.deep.equals([]); 11 | }); 12 | 13 | it('should not strip the single path from tree.', function() { 14 | var paths = [['one', 'two']]; 15 | var tree = {one: {too: null}}; 16 | var out = pathsComplementFromTree(paths, tree); 17 | expect(out).to.deep.equals([['one', 'two']]); 18 | }); 19 | 20 | it('should strip out one of the two paths, has complex paths.', function() { 21 | var paths = [ 22 | ['one', {from: 0, to: 1}, 'two'], 23 | ['one', {from: 0, to: 2}, 'two'] 24 | ]; 25 | var tree = { 26 | one: { 27 | 0: { 28 | two: null 29 | }, 30 | 1: { 31 | two: null 32 | } 33 | } 34 | }; 35 | var out = pathsComplementFromTree(paths, tree); 36 | expect(out).to.deep.equals([['one', {from: 0, to: 2}, 'two']]); 37 | }); 38 | 39 | it('should strip the single path from length tree.', function() { 40 | var paths = [['one', 'two']]; 41 | var tree = {2: {one: {two: null}}}; 42 | var out = pathsComplementFromLengthTree(paths, tree); 43 | expect(out).to.deep.equals([]); 44 | }); 45 | 46 | it('should not strip the single path from length tree.', function() { 47 | var paths = [['one', 'two']]; 48 | var tree = {2: {one: {too: null}}}; 49 | var out = pathsComplementFromLengthTree(paths, tree); 50 | expect(out).to.deep.equals([['one', 'two']]); 51 | }); 52 | 53 | it('should strip out one of the two paths, has complex paths from length tree.', function() { 54 | var paths = [ 55 | ['one', {from: 0, to: 1}, 'two'], 56 | ['one', {from: 0, to: 2}, 'two'] 57 | ]; 58 | var tree = { 59 | 3: { 60 | one: { 61 | 0: { 62 | two: null 63 | }, 64 | 1: { 65 | two: null 66 | } 67 | } 68 | } 69 | }; 70 | var out = pathsComplementFromLengthTree(paths, tree); 71 | expect(out).to.deep.equals([['one', {from: 0, to: 2}, 'two']]); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/toPaths.spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var pathUtils = require('..'); 3 | var toPaths = pathUtils.toPaths; 4 | var toTree = pathUtils.toTree; 5 | var isIntegerKey = pathUtils.isIntegerKey; 6 | 7 | describe('toPaths', function() { 8 | it('toPaths a pathmap that has overlapping branch and leaf nodes', function() { 9 | 10 | var pathMaps = [null, { 11 | lolomo: 1 12 | }, { 13 | lolomo: { 14 | summary: 1, 15 | 13: 1, 16 | 14: 1 17 | } 18 | }, { 19 | lolomo: { 20 | 15: { 21 | rating: 1, 22 | summary: 1 23 | }, 24 | 13: { 25 | summary: 1 26 | }, 27 | 16: { 28 | rating: 1, 29 | summary: 1 30 | }, 31 | 14: { 32 | summary: 1 33 | }, 34 | 17: { 35 | rating: 1, 36 | summary: 1 37 | } 38 | } 39 | }]; 40 | 41 | var paths = toPaths(pathMaps).sort(function(a, b) { 42 | return a.length - b.length; 43 | }); 44 | 45 | var first = paths[0]; 46 | var second = paths[1]; 47 | var third = paths[2]; 48 | var fourth = paths[3]; 49 | 50 | expect(first[0] === 'lolomo').to.equal(true); 51 | 52 | expect(( 53 | second[0] === 'lolomo') && ( 54 | second[1][0] === 13) && ( 55 | second[1][1] === 14) && ( 56 | second[1][2] === 'summary') 57 | ).to.equal(true); 58 | 59 | expect((third[0] === 'lolomo') && ( 60 | third[1].from === 13) && ( 61 | third[1].to === 14) && ( 62 | third[2] === 'summary') 63 | ).to.equal(true); 64 | 65 | expect((fourth[0] === 'lolomo') && ( 66 | fourth[1].from === 15) && ( 67 | fourth[1].to === 17) && ( 68 | fourth[2][0] === 'rating') && ( 69 | fourth[2][1] === 'summary') 70 | ).to.equal(true); 71 | }); 72 | 73 | it('toPaths should not coerce numbers to strings outside the safe range', function() { 74 | /* 75 | * For reference: 76 | * https://github.com/Netflix/falcor-router/issues/176 77 | * https://github.com/Netflix/falcor-router/issues/68 78 | * https://github.com/Netflix/falcor-path-utils/pull/6 79 | */ 80 | 81 | var pathMaps = [null, { 82 | lolomo: 1 83 | }, { 84 | lolomo: { 85 | '0': 1, 86 | '1': 1, 87 | '234': 1, 88 | '345678': 1, 89 | '4253674286': 1, 90 | '9007199254740991': 1, 91 | '9007199254740992': 1, 92 | '918572487653498743278645': 1, 93 | } 94 | }]; 95 | 96 | var paths = toPaths(pathMaps).sort(function(a, b) { 97 | return a.length - b.length; 98 | }); 99 | 100 | // NOTE: 101 | // chai equal() is strict such that expect(4).to.equal('4') 102 | // will fail and vice versa. 103 | expect(paths[1][1][0]).to.equal(0); 104 | expect(paths[1][1][1]).to.equal(1); 105 | expect(paths[1][1][2]).to.equal(234); 106 | expect(paths[1][1][3]).to.equal(345678); 107 | expect(paths[1][1][4]).to.equal(4253674286); 108 | expect(paths[1][1][5]).to.equal(9007199254740991); // max safe int 109 | expect(paths[1][1][6]).to.equal('9007199254740992'); // max safe int + 1 110 | expect(paths[1][1][7]).to.equal('918572487653498743278645'); // absurdly large 111 | }); 112 | 113 | 114 | it('should explode a simplePath.', function() { 115 | var out = ['one', 'two']; 116 | var input = {2: {one: {two: null}}}; 117 | 118 | expect(toPaths(input)).to.deep.equals([out]); 119 | }); 120 | 121 | it('should explode a complex.', function() { 122 | var input = {2: {one: {two: null, three: null}}}; 123 | var out = ['one', ['three', 'two']]; 124 | var output = toPaths(input); 125 | output[0][1].sort(); 126 | 127 | expect(output).to.deep.equals([out]); 128 | }); 129 | 130 | it('should explode a set of complex and simple paths.', function() { 131 | var out = [ 132 | ['one', ['three', 'two']], 133 | ['one', {from: 0, to: 3}, 'summary'] 134 | ]; 135 | var input = { 136 | 2: { 137 | one: { 138 | three: null, 139 | two: null 140 | } 141 | }, 142 | 3: { 143 | one: { 144 | 0: { summary: null }, 145 | 1: { summary: null }, 146 | 2: { summary: null }, 147 | 3: { summary: null } 148 | } 149 | } 150 | }; 151 | 152 | var output = toPaths(input); 153 | if (!Array.isArray(output[0][1])) { 154 | var tmp = output[0]; 155 | output[0] = output[1]; 156 | output[1] = tmp; 157 | } 158 | 159 | output[0][1].sort(); 160 | 161 | expect(output).to.deep.equals(out); 162 | }); 163 | 164 | it('should translate between toPaths and toTrees', function() { 165 | var expectedTree = { 166 | one: { 167 | 0: { summary: null }, 168 | 1: { summary: null }, 169 | 2: { summary: null }, 170 | 3: { summary: null }, 171 | three: null, 172 | two: null 173 | } 174 | }; 175 | var treeMap = { 176 | 2: { 177 | one: { 178 | three: null, 179 | two: null 180 | } 181 | }, 182 | 3: { 183 | one: { 184 | 0: { summary: null }, 185 | 1: { summary: null }, 186 | 2: { summary: null }, 187 | 3: { summary: null } 188 | } 189 | } 190 | }; 191 | 192 | expect(toTree(toPaths(treeMap))).to.deep.equals(expectedTree); 193 | }); 194 | 195 | describe('isIntegerKey', function() { 196 | 197 | var thingsThatShouldReturnTrue = [ 198 | 0, 199 | 1, 200 | -0, 201 | -1, 202 | 10, 203 | -10, 204 | 9007199254740991, // max safe int 205 | -9007199254740991, // min safe int 206 | '0', 207 | '1', 208 | '-1', 209 | '10', 210 | '-10', 211 | '9007199254740991', // max safe int 212 | '-9007199254740991', // min safe int 213 | ]; 214 | thingsThatShouldReturnTrue.forEach(function(thing) { 215 | var should = 'should return true on ' + JSON.stringify(thing); 216 | it(should, function() { 217 | expect(isIntegerKey(thing)).to.equal(true); 218 | }); 219 | }); 220 | 221 | var thingsThatShouldReturnFalse = [ 222 | [], 223 | {}, 224 | null, 225 | true, 226 | false, 227 | undefined, 228 | NaN, 229 | Infinity, 230 | -Infinity, 231 | '9007199254740992', // max safe int + 1 232 | '-9007199254740992', // min safe int - 1 233 | 9007199254740992, // max safe int + 1 234 | -9007199254740992, // min safe int - 1 235 | '648365838265483646384563538', 236 | '-0', 237 | '', 238 | '01', 239 | '0d', 240 | '1d', 241 | '_', 242 | ' 1', 243 | '- 1', 244 | ' ', 245 | '0x123', 246 | '0b1101', 247 | 'deadbeef', 248 | 0.1, 249 | -0.1, 250 | '1.0', 251 | '-1.0', 252 | '0.1', 253 | '-0.1', 254 | ]; 255 | thingsThatShouldReturnFalse.forEach(function(thing) { 256 | var should = 'should return false on ' + JSON.stringify(thing); 257 | it(should, function() { 258 | expect(isIntegerKey(thing)).to.equal(false); 259 | }); 260 | }); 261 | }); 262 | }); 263 | -------------------------------------------------------------------------------- /test/toTree.spec.js: -------------------------------------------------------------------------------- 1 | var toTree = require('../lib/toTree'); 2 | var toPaths = require('../lib/toPaths'); 3 | var expect = require('chai').expect; 4 | 5 | describe('toTree', function() { 6 | it('should explode a simplePath.', function() { 7 | var input = ['one', 'two']; 8 | var out = {one: {two: null}}; 9 | 10 | expect(toTree([input])).to.deep.equals(out); 11 | }); 12 | 13 | it('should explode an empty path.', function() { 14 | var input = []; 15 | var out = {}; 16 | 17 | expect(toTree([input])).to.deep.equals(out); 18 | }); 19 | 20 | it('should explode a simplePath with reserved keyword.', function() { 21 | var input = ['one', 'hasOwnProperty']; 22 | var out = {one: {hasOwnProperty: null}}; 23 | 24 | expect(toTree([input])).to.deep.equals(out); 25 | }); 26 | 27 | it('should explode a complex.', function() { 28 | var input = ['one', ['two', 'three']]; 29 | var out = {one: {three: null, two: null}}; 30 | 31 | expect(toTree([input])).to.deep.equals(out); 32 | }); 33 | 34 | it('should explode an empty path array.', function() { 35 | var input = ['one', []]; 36 | var out = {one: {}}; 37 | 38 | expect(toTree([input])).to.deep.equals(out); 39 | }); 40 | 41 | it('should explode an empty path range.', function() { 42 | var input = ['one', { from: 0, to: -1 }]; 43 | var out = {one: {}}; 44 | 45 | expect(toTree([input])).to.deep.equals(out); 46 | }); 47 | 48 | it('should explode an empty path range in array.', function() { 49 | var input = ['one', [{ from: 0, to: -1 }]]; 50 | var out = {one: {}}; 51 | 52 | expect(toTree([input])).to.deep.equals(out); 53 | }); 54 | 55 | it('should explode a set of complex and simple paths.', function() { 56 | var input = [ 57 | ['one', ['two', 'three']], 58 | ['one', {from: 0, to: 3}, 'summary'] 59 | ]; 60 | var out = { 61 | one: { 62 | three: null, 63 | two: null, 64 | 0: { summary: null }, 65 | 1: { summary: null }, 66 | 2: { summary: null }, 67 | 3: { summary: null } 68 | } 69 | }; 70 | 71 | expect(toTree(input)).to.deep.equals(out); 72 | }); 73 | 74 | it('should translate between toPaths and toTrees', function() { 75 | var input = [ 76 | ['one', ['two', 'three']], 77 | ['one', {from: 0, to: 3}, 'summary'] 78 | ]; 79 | var treeMap = { 80 | 2: toTree([input[0]]), 81 | 3: toTree([input[1]]) 82 | }; 83 | var output = toPaths(treeMap); 84 | output[0][1] = output[0][1].sort().reverse(); 85 | 86 | expect(output).to.deep.equals(input); 87 | }); 88 | }); 89 | 90 | -------------------------------------------------------------------------------- /test/unescape.spec.js: -------------------------------------------------------------------------------- 1 | var unescape = require('./../lib/unescape'); 2 | var expect = require('chai').expect; 3 | 4 | describe('unescape', function() { 5 | it('should remove the leading underscore from strings.', function() { 6 | expect(unescape("_test")).to.equal("test"); 7 | }); 8 | it('should throw if input string does not have leading underscore.', function() { 9 | expect(function() { return unescape("test") }).to.throw(SyntaxError, "Expected \"_\"."); 10 | }); 11 | }); 12 | --------------------------------------------------------------------------------