├── .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 |
--------------------------------------------------------------------------------