From f787954cc4fbab4f573ad3a894f6c8cf86bf2d22 Mon Sep 17 00:00:00 2001 From: Oliver Buchtala Date: Thu, 4 Feb 2016 00:36:59 +0100 Subject: [PATCH] Replaced PathAdapter by streamlined implementation. In DocumentIndexes we use now TreeIndex instances. The adapter to objects providing a path based API is called DataObject now. Both are based on lodash get/set/setWith etc. --- doc/model/MemberIndex.js | 4 +- model/AnchorIndex.js | 19 +- model/AnnotationIndex.js | 22 +- model/DocumentChange.js | 6 +- model/PathEventProxy.js | 31 +-- model/data/Data.js | 6 +- model/data/DataObject.js | 99 ++++++++ model/data/NodeIndex.js | 29 +-- model/data/ObjectOperation.js | 6 +- test/unit/model/data/ObjectOperation.test.js | 16 +- ...Selection.test.js => DOMSelection.test.js} | 35 +-- test/unit/util/PathAdapter.test.js | 25 -- test/unit/util/TreeIndex.test.js | 61 +++++ util/PathAdapter.js | 223 ------------------ util/TreeIndex.js | 188 +++++++++++++++ 15 files changed, 414 insertions(+), 356 deletions(-) create mode 100644 model/data/DataObject.js rename test/unit/ui/{SurfaceSelection.test.js => DOMSelection.test.js} (92%) delete mode 100644 test/unit/util/PathAdapter.test.js create mode 100644 test/unit/util/TreeIndex.test.js delete mode 100644 util/PathAdapter.js create mode 100644 util/TreeIndex.js diff --git a/doc/model/MemberIndex.js b/doc/model/MemberIndex.js index d6027a3b9..dac55510e 100644 --- a/doc/model/MemberIndex.js +++ b/doc/model/MemberIndex.js @@ -1,7 +1,7 @@ 'use strict'; var NodeIndex = require('../../model/data/NodeIndex'); -var PathAdapter = require('../../util/PathAdapter'); +var TreeIndex = require('../../util/TreeIndex'); /** @class @@ -10,7 +10,7 @@ function MemberIndex(doc) { NodeIndex.apply(this, arguments); this.doc = doc; - this.index = new PathAdapter(); + this.index = new TreeIndex(); } MemberIndex.Prototype = function() { diff --git a/model/AnchorIndex.js b/model/AnchorIndex.js index 765376d27..520251f4d 100644 --- a/model/AnchorIndex.js +++ b/model/AnchorIndex.js @@ -1,17 +1,17 @@ 'use strict'; var filter = require('lodash/filter'); -var PathAdapter = require('../util/PathAdapter'); +var TreeIndex = require('../util/TreeIndex'); var ContainerAnnotation = require('./ContainerAnnotation'); var DocumentIndex = require('./DocumentIndex'); -var ContainerAnnotationAnchorIndex = function(doc) { +var AnchorIndex = function(doc) { this.doc = doc; - this.byPath = new PathAdapter.Arrays(); + this.byPath = new TreeIndex.Arrays(); this.byId = {}; }; -DocumentIndex.extend(ContainerAnnotationAnchorIndex, function() { +DocumentIndex.extend(AnchorIndex, function() { this.select = function(node) { return (node instanceof ContainerAnnotation); }; @@ -23,14 +23,7 @@ DocumentIndex.extend(ContainerAnnotationAnchorIndex, function() { }; this.get = function(path, containerName) { - var anchors = this.byPath.get(path) || []; - if (!isArray(anchors)) { - var _anchors = []; - this.byPath._traverse(anchors, [], function(path, anchors) { - _anchors = _anchors.concat(anchors); - }); - anchors = _anchors; - } + var anchors = this.byPath.getAll(path); if (containerName) { return filter(anchors, function(anchor) { return (anchor.container === containerName); @@ -75,4 +68,4 @@ DocumentIndex.extend(ContainerAnnotationAnchorIndex, function() { }); -module.exports = ContainerAnnotationAnchorIndex; +module.exports = AnchorIndex; diff --git a/model/AnnotationIndex.js b/model/AnnotationIndex.js index 0a85c44a1..2c0b48019 100644 --- a/model/AnnotationIndex.js +++ b/model/AnnotationIndex.js @@ -3,7 +3,7 @@ var isString = require('lodash/isString'); var map = require('lodash/map'); var filter = require('lodash/filter'); -var PathAdapter = require('../util/PathAdapter'); +var TreeIndex = require('../util/TreeIndex'); var PropertyAnnotation = require('./PropertyAnnotation'); var DocumentIndex = require('./DocumentIndex'); @@ -22,8 +22,8 @@ var DocumentIndex = require('./DocumentIndex'); // aIndex.get(["text_1", "content"], 23, 45); var AnnotationIndex = function() { - this.byPath = new PathAdapter(); - this.byType = new PathAdapter(); + this.byPath = new TreeIndex(); + this.byType = new TreeIndex(); }; AnnotationIndex.Prototype = function() { @@ -42,21 +42,13 @@ AnnotationIndex.Prototype = function() { // TODO: use object interface? so we can combine filters (path and type) this.get = function(path, start, end, type) { - var annotations = this.byPath.get(path) || {}; + var annotations; if (isString(path) || path.length === 1) { - // flatten annotations if this is called via node id - var _annos = annotations; - annotations = []; - each(_annos, function(level) { - annotations = annotations.concat(map(level, function(anno) { - return anno; - })); - }); + annotations = this.byPath.getAll(path) || {}; } else { - annotations = map(annotations, function(anno) { - return anno; - }); + annotations = this.byPath.get(path); } + annotations = map(annotations); /* jshint eqnull:true */ // null check for null or undefined if (start != null) { diff --git a/model/DocumentChange.js b/model/DocumentChange.js index 01fd9045b..ef433ae66 100644 --- a/model/DocumentChange.js +++ b/model/DocumentChange.js @@ -6,7 +6,7 @@ var isArray = require('lodash/isArray'); var map = require('lodash/map'); var oo = require('../util/oo'); var uuid = require('../util/uuid'); -var PathAdapter = require('../util/PathAdapter'); +var TreeIndex = require('../util/TreeIndex'); var OperationSerializer = require('./data/OperationSerializer'); var ObjectOperation = require('./data/ObjectOperation'); @@ -92,7 +92,7 @@ DocumentChange.Prototype = function() { var ops = this.ops; var created = {}; var deleted = {}; - var updated = new PathAdapter.Arrays(); + var updated = new TreeIndex(); var i; for (i = 0; i < ops.length; i++) { var op = ops[i]; @@ -107,7 +107,7 @@ DocumentChange.Prototype = function() { } if (op.type === "set" || op.type === "update") { // The old as well the new one is affected - updated.add(op.path, op); + updated.set(op.path, true); } } this.created = created; diff --git a/model/PathEventProxy.js b/model/PathEventProxy.js index e7e4a3a6c..fbbe71043 100644 --- a/model/PathEventProxy.js +++ b/model/PathEventProxy.js @@ -2,17 +2,15 @@ var oo = require('../util/oo'); var each = require('lodash/each'); -var PathAdapter = require('../util/PathAdapter'); -var deleteFromArray = require('../util/deleteFromArray'); +var TreeIndex = require('../util/TreeIndex'); - -var NotifyByPathProxy = function(doc) { - this.listeners = new PathAdapter(); +var PathEventProxy = function(doc) { + this.listeners = new TreeIndex.Arrays(); this._list = []; this.doc = doc; }; -NotifyByPathProxy.Prototype = function() { +PathEventProxy.Prototype = function() { this.onDocumentChanged = function(change, info, doc) { // stop if no listeners registered @@ -61,9 +59,8 @@ NotifyByPathProxy.Prototype = function() { } } }.bind(this)); - change.updated.traverse(function(path) { - var key = path.concat(['listeners']); - var scopedListeners = listeners.get(key); + change.updated.forEach(function(_, path) { + var scopedListeners = listeners.get(path); each(scopedListeners, function(entry) { entry.method.call(entry.listener, change, info, doc); }); @@ -71,17 +68,11 @@ NotifyByPathProxy.Prototype = function() { }; this._add = function(path, listener, method) { - var key = path.concat(['listeners']); - var listeners = this.listeners.get(key); - if (!listeners) { - listeners = []; - this.listeners.set(key, listeners); - } if (!method) { throw new Error('Invalid argument: expected function but got ' + method); } var entry = { path: path, method: method, listener: listener }; - listeners.push(entry); + this.listeners.add(path, entry); this._list.push(entry); }; @@ -94,9 +85,7 @@ NotifyByPathProxy.Prototype = function() { if (this._list[i].listener === listener) { var entry = this._list[i]; this._list.splice(i, 1); - var key = entry.path.concat(['listeners']); - var listeners = this.listeners.get(key); - deleteFromArray(listeners, entry); + this.listeners.remove(entry.path, entry); } } }; @@ -113,6 +102,6 @@ NotifyByPathProxy.Prototype = function() { }; -oo.initClass(NotifyByPathProxy); +oo.initClass(PathEventProxy); -module.exports = NotifyByPathProxy; +module.exports = PathEventProxy; diff --git a/model/data/Data.js b/model/data/Data.js index 3138fe2b2..7b8fedd50 100644 --- a/model/data/Data.js +++ b/model/data/Data.js @@ -4,7 +4,7 @@ var isArray = require('lodash/isArray'); var isString = require('lodash/isString'); var each = require('lodash/each'); var cloneDeep = require('lodash/cloneDeep'); -var PathAdapter = require('../../util/PathAdapter'); +var DataObject = require('./DataObject'); var EventEmitter = require('../../util/EventEmitter'); /** @@ -27,7 +27,7 @@ function Data(schema, options) { EventEmitter.call(this); this.schema = schema; - this.nodes = new PathAdapter(); + this.nodes = new DataObject(); this.indexes = {}; this.options = options || {}; this.nodeFactory = options.nodeFactory || schema.getNodeFactory(); @@ -251,7 +251,7 @@ Data.Prototype = function() { @private */ this.reset = function() { - this.nodes = new PathAdapter(); + this.nodes.clear(); }; /** diff --git a/model/data/DataObject.js b/model/data/DataObject.js new file mode 100644 index 000000000..1b33f29d0 --- /dev/null +++ b/model/data/DataObject.js @@ -0,0 +1,99 @@ +'use strict'; + +var isString = require('lodash/isString'); +var isArray = require('lodash/isArray'); +var get = require('lodash/get'); +var setWith = require('lodash/setWith'); +var unset = require('lodash/unset'); +var oo = require('../../util/oo'); + +/* + An object that can be access via path API. + + @class + @param {object} [obj] An object to operate on + @example + + var obj = new DataObject({a: "aVal", b: {b1: 'b1Val', b2: 'b2Val'}}); +*/ + +function DataObject(root) { + if (root) { + this.__root__ = root; + } +} + +DataObject.Prototype = function() { + + this.getRoot = function() { + if (this.__root__) { + return this.__root__; + } else { + return this; + } + }; + + /** + Get value at path + + @return {object} The value stored for a given path + + @example + + obj.get(['b', 'b1']); + => b1Val + */ + this.get = function(path) { + if (!path) { + return undefined; + } + if (isString(path)) { + return this.getRoot()[path]; + } + if (arguments.length > 1) { + path = Array.prototype.slice(arguments, 0); + } + if (!isArray(path)) { + throw new Error('Illegal argument for DataObject.get()'); + } + return get(this.getRoot(), path); + }; + + this.set = function(path, value) { + if (!path) { + throw new Error('Illegal argument: DataObject.set(>path<, value) - path is mandatory.'); + } + if (isString(path)) { + this.getRoot()[path] = value; + } else { + setWith(this.getRoot(), path, value); + } + }; + + this.delete = function(path) { + if (isString(path)) { + delete this.getRoot()[path]; + } else if (path.length === 1) { + delete this.getRoot()[path[0]]; + } else { + var success = unset(this.getRoot(), path); + if (!success) { + throw new Error('Could not delete property at path' + path); + } + } + }; + + this.clear = function() { + var root = this.getRoot(); + for (var key in root) { + if (root.hasOwnProperty(key)) { + delete root[key]; + } + } + }; + +}; + +oo.initClass(DataObject); + +module.exports = DataObject; diff --git a/model/data/NodeIndex.js b/model/data/NodeIndex.js index 5a859e792..e04fea718 100644 --- a/model/data/NodeIndex.js +++ b/model/data/NodeIndex.js @@ -4,7 +4,7 @@ var oo = require('../../util/oo'); var isArray = require('lodash/isArray'); var each = require('lodash/each'); var extend = require('lodash/extend'); -var PathAdapter = require('../../util/PathAdapter'); +var TreeIndex = require('../../util/TreeIndex'); /** Index for Nodes. @@ -19,10 +19,10 @@ var NodeIndex = function() { /** * Internal storage. * - * @property {PathAdapter} index + * @property {TreeIndex} index * @private */ - this.index = new PathAdapter(); + this.index = new TreeIndex(); }; NodeIndex.Prototype = function() { @@ -34,23 +34,16 @@ NodeIndex.Prototype = function() { * @returns A node or an object with ids and nodes as values. */ this.get = function(path) { - // TODO: what is the correct return value. We have arrays at some places. - // HACK: unwrap objects on the index when method is called without a path - if (!path) return this.getAll(); return this.index.get(path) || {}; }; /** - * Collects all indexed nodes. + * Collects nodes recursively. * * @returns An object with ids as keys and nodes as values. */ - this.getAll = function() { - var result = {}; - each(this.index, function(values) { - extend(result, values); - }); - return result; + this.getAll = function(path) { + return this.index.getAll(path); }; /** @@ -93,7 +86,7 @@ NodeIndex.Prototype = function() { } each(values, function(value) { this.index.set([value, node.id], node); - }, this); + }.bind(this)); }; /** @@ -111,7 +104,7 @@ NodeIndex.Prototype = function() { } each(values, function(value) { this.index.delete([value, node.id]); - }, this); + }.bind(this)); }; /** @@ -130,14 +123,14 @@ NodeIndex.Prototype = function() { } each(values, function(value) { this.index.delete([value, node.id]); - }, this); + }.bind(this)); values = newValue; if (!isArray(values)) { values = [values]; } each(values, function(value) { this.index.set([value, node.id], node); - }, this); + }.bind(this)); }; this.set = function(node, path, newValue, oldValue) { @@ -170,7 +163,7 @@ NodeIndex.Prototype = function() { if (this.select(node)) { this.create(node); } - }, this); + }.bind(this)); }; }; diff --git a/model/data/ObjectOperation.js b/model/data/ObjectOperation.js index bbce10d21..2cf507453 100644 --- a/model/data/ObjectOperation.js +++ b/model/data/ObjectOperation.js @@ -3,7 +3,7 @@ var isString = require('lodash/isString'); var isEqual = require('lodash/isEqual'); var cloneDeep = require('lodash/cloneDeep'); -var PathAdapter = require('../../util/PathAdapter'); +var DataObject = require('./DataObject'); var Operation = require('./Operation'); var TextOperation = require('./TextOperation'); var ArrayOperation = require('./ArrayOperation'); @@ -86,10 +86,10 @@ ObjectOperation.Prototype = function() { this.apply = function(obj) { if (this.type === NOP) return obj; var adapter; - if (obj instanceof PathAdapter) { + if (obj instanceof DataObject) { adapter = obj; } else { - adapter = new PathAdapter(obj); + adapter = new DataObject(obj); } if (this.type === CREATE) { adapter.set(this.path, cloneDeep(this.val)); diff --git a/test/unit/model/data/ObjectOperation.test.js b/test/unit/model/data/ObjectOperation.test.js index 02edce070..09616991b 100644 --- a/test/unit/model/data/ObjectOperation.test.js +++ b/test/unit/model/data/ObjectOperation.test.js @@ -4,7 +4,7 @@ require('../../qunit_extensions'); var isEqual = require('lodash/isEqual'); var cloneDeep = require('lodash/cloneDeep'); -var PathAdapter = require('../../../../util/PathAdapter'); +var DataObject = require('../../../../model/data/DataObject'); var ObjectOperation = require('../../../../model/data/ObjectOperation'); var ArrayOperation = require('../../../../model/data/ArrayOperation'); var TextOperation = require('../../../../model/data/TextOperation'); @@ -59,16 +59,6 @@ QUnit.test("Deleting nested values.", function(assert) { assert.deepEqual(obj, expected, 'Should delete nested value.'); }); -QUnit.test("Deleting unknown values.", function(assert) { - var path = ["a", "b"]; - var val = "bla"; - var op = ObjectOperation.Delete(path, val); - var obj = { a: { c: "bla"} }; - assert.throws(function() { - op.apply(obj); - }, 'Should throw if deleting an unknown value.'); -}); - QUnit.test("Updating a text property.", function(assert) { var obj = {a: "bla"}; var path = ["a"]; @@ -115,8 +105,8 @@ QUnit.test("Deleting a top-level property using id.", function(assert) { assert.deepEqual(obj, expected); }); -QUnit.test("Apply operation on PathAdapter.", function(assert) { - var myObj = new PathAdapter(); +QUnit.test("Apply operation on DataObject.", function(assert) { + var myObj = new DataObject(); var op = ObjectOperation.Set(['foo', 'bar'], null, 'bla'); op.apply(myObj); assert.equal(myObj.get(['foo', 'bar']), 'bla'); diff --git a/test/unit/ui/SurfaceSelection.test.js b/test/unit/ui/DOMSelection.test.js similarity index 92% rename from test/unit/ui/SurfaceSelection.test.js rename to test/unit/ui/DOMSelection.test.js index 2f7eb1d5b..145780093 100644 --- a/test/unit/ui/SurfaceSelection.test.js +++ b/test/unit/ui/DOMSelection.test.js @@ -2,14 +2,15 @@ require('../qunit_extensions'); -var isArray = require('lodash/lang/isArray'); -var SurfaceSelection = require('../../../ui/SurfaceSelection'); +var isArray = require('lodash/isArray'); +var DOMSelection = require('../../../ui/DOMSelection'); var Document = require('../../../model/Document'); var $ = require('../../../util/jquery'); -QUnit.uiModule('ui/SurfaceSelection'); +QUnit.uiModule('ui/DOMSelection'); + +function StubDoc() {} -var StubDoc = function() {} StubDoc.prototype.get = function(path) { var pathStr = path; if (isArray(path)) { @@ -94,7 +95,7 @@ var surfaceWithParagraphs = [ QUnit.uiTest("Get coordinate for collapsed selection", function(assert) { var el = $('#qunit-fixture').html(singlePropertyFixture)[0]; - var surfaceSelection = new SurfaceSelection(el); + var surfaceSelection = new DOMSelection(el); var node = el.querySelector('#test1').childNodes[0].childNodes[0]; var offset = 5; var coor = surfaceSelection.getModelCoordinate(node, offset, {}); @@ -105,7 +106,7 @@ QUnit.uiTest("Get coordinate for collapsed selection", function(assert) { QUnit.uiTest("Search coordinate (before)", function(assert) { var el = $('#qunit-fixture').html(mixedFixture)[0]; - var surfaceSelection = new SurfaceSelection(el); + var surfaceSelection = new DOMSelection(el); var node = el.querySelector('#before').childNodes[0]; var offset = 1; var coor = surfaceSelection.searchForCoordinate(node, offset, {}); @@ -116,7 +117,7 @@ QUnit.uiTest("Search coordinate (before)", function(assert) { QUnit.uiTest("Search coordinate (between)", function(assert) { var el = $('#qunit-fixture').html(mixedFixture)[0]; - var surfaceSelection = new SurfaceSelection(el); + var surfaceSelection = new DOMSelection(el); var node = el.querySelector('#between').childNodes[0]; var offset = 1; var coor = surfaceSelection.searchForCoordinate(node, offset, {}); @@ -127,7 +128,7 @@ QUnit.uiTest("Search coordinate (between)", function(assert) { QUnit.uiTest("Search coordinate (between, left)", function(assert) { var el = $('#qunit-fixture').html(mixedFixture)[0]; - var surfaceSelection = new SurfaceSelection(el); + var surfaceSelection = new DOMSelection(el); var node = el.querySelector('#between').childNodes[0]; var offset = 1; var coor = surfaceSelection.searchForCoordinate(node, offset, {direction: 'left'}); @@ -138,7 +139,7 @@ QUnit.uiTest("Search coordinate (between, left)", function(assert) { QUnit.uiTest("Search coordinate (after)", function(assert) { var el = $('#qunit-fixture').html(mixedFixture)[0]; - var surfaceSelection = new SurfaceSelection(el); + var surfaceSelection = new DOMSelection(el); var node = el.querySelector('#after').childNodes[0]; var offset = 1; var coor = surfaceSelection.searchForCoordinate(node, offset, {}); @@ -149,7 +150,7 @@ QUnit.uiTest("Search coordinate (after)", function(assert) { QUnit.uiTest("coordinate via search", function(assert) { var el = $('#qunit-fixture').html(mixedFixture)[0]; - var surfaceSelection = new SurfaceSelection(el); + var surfaceSelection = new DOMSelection(el); var node = el.querySelector('#between').childNodes[0]; var offset = 1; var coor = surfaceSelection.getModelCoordinate(node, offset, {}); @@ -160,7 +161,7 @@ QUnit.uiTest("coordinate via search", function(assert) { QUnit.uiTest("coordinate for empty paragraph", function(assert) { var el = $('#qunit-fixture').html(emptyParagraphFixture)[0]; - var surfaceSelection = new SurfaceSelection(el); + var surfaceSelection = new DOMSelection(el); var node = el.querySelector('#test1'); var offset = 0; var coor = surfaceSelection.getModelCoordinate(node, offset, {}); @@ -171,7 +172,7 @@ QUnit.uiTest("coordinate for empty paragraph", function(assert) { QUnit.uiTest("coordinate from wrapped text nodes", function(assert) { var el = $('#qunit-fixture').html(wrappedTextNodes)[0]; - var surfaceSelection = new SurfaceSelection(el); + var surfaceSelection = new DOMSelection(el); var node = el.querySelector('#test1_content'); var offset = 4; var coor = surfaceSelection.getModelCoordinate(node, offset, {}); @@ -182,7 +183,7 @@ QUnit.uiTest("coordinate from wrapped text nodes", function(assert) { QUnit.uiTest("coordinate from wrapped text nodes with externals", function(assert) { var el = $('#qunit-fixture').html(wrappedTextNodesWithExternals)[0]; - var surfaceSelection = new SurfaceSelection(el); + var surfaceSelection = new DOMSelection(el); var node = el.querySelector('#test1_content'); var offset = 6; var coor = surfaceSelection.getModelCoordinate(node, offset, {}); @@ -193,7 +194,7 @@ QUnit.uiTest("coordinate from wrapped text nodes with externals", function(asser QUnit.uiTest("a selection spanning over a external at the end of a property", function(assert) { var el = $('#qunit-fixture').html(wrappedTextNodesWithExternals)[0]; - var surfaceSelection = new SurfaceSelection(el); + var surfaceSelection = new DOMSelection(el); var anchorNode = el.querySelector('#before-last').childNodes[0]; var anchorOffset = 2; var focusNode = el.querySelector('#test1_content'); @@ -230,7 +231,7 @@ var textPropWithInlineElements = [ QUnit.uiTest("Issue #273: 'Could not find char position' when clicking right above an inline node", function(assert) { var el = $('#qunit-fixture').html(textPropWithInlineElements)[0]; - var surfaceSelection = new SurfaceSelection(el); + var surfaceSelection = new DOMSelection(el); var node = el.querySelector('#test').childNodes[0]; var offset = 2; var coor = surfaceSelection.getModelCoordinate(node, offset, {}); @@ -241,7 +242,7 @@ QUnit.uiTest("Issue #273: 'Could not find char position' when clicking right abo QUnit.firefoxTest("Issue #354: Wrong selection in FF when double clicking between lines", function(assert) { var el = $('#qunit-fixture').html(surfaceWithParagraphs)[0]; - var surfaceSelection = new SurfaceSelection(el, new StubDoc()); + var surfaceSelection = new DOMSelection(el, new StubDoc()); var surface = el.querySelector('#surface'); QUnit.setDOMSelection(surface, 0, surface, 1); var sel = surfaceSelection.getSelection(); @@ -252,7 +253,7 @@ QUnit.firefoxTest("Issue #354: Wrong selection in FF when double clicking betwee QUnit.uiTest("Issue #376: Wrong selection mapping at end of paragraph", function(assert) { var el = $('#qunit-fixture').html(surfaceWithParagraphs)[0]; - var surfaceSelection = new SurfaceSelection(el, new StubDoc()); + var surfaceSelection = new DOMSelection(el, new StubDoc()); var p1span = el.querySelector('#p1 span'); var p2 = el.querySelector('#p2'); var anchorNode = p1span; diff --git a/test/unit/util/PathAdapter.test.js b/test/unit/util/PathAdapter.test.js deleted file mode 100644 index ee9297965..000000000 --- a/test/unit/util/PathAdapter.test.js +++ /dev/null @@ -1,25 +0,0 @@ -"use strict"; - -require('../qunit_extensions'); -var PathAdapter = require('../../../util/PathAdapter'); - -QUnit.module('util/PathAdapter'); - -QUnit.test("Setting and getting values from a PathAdapter", function(assert) { - var obj = new PathAdapter(); - obj.set(['a'], 1); - obj.set(['c', 'b'], 2); - assert.equal(obj.get('a'), 1, 'Top level value should be retrieved correctly.'); - assert.equal(obj.get(['c', 'b']), 2, 'Second level value should be retrieved correctly.'); -}); - -QUnit.test("Getting with invalid arguments", function(assert) { - var obj = new PathAdapter(); - obj.set(['a'], 1); - obj.set(['c', 'b'], 2); - assert.isNullOrUndefined(obj.get('e')); - assert.isNullOrUndefined(obj.get(['c', 'd'])); - assert.isNullOrUndefined(obj.get({})); - assert.isNullOrUndefined(obj.get(null)); - assert.isNullOrUndefined(obj.get([])); -}); diff --git a/test/unit/util/TreeIndex.test.js b/test/unit/util/TreeIndex.test.js new file mode 100644 index 000000000..a0dee997a --- /dev/null +++ b/test/unit/util/TreeIndex.test.js @@ -0,0 +1,61 @@ +"use strict"; + +require('../qunit_extensions'); +var TreeIndex = require('../../../util/TreeIndex'); + +QUnit.module('util/TreeIndex'); + +QUnit.test("Setting and getting values from a TreeIndex", function(assert) { + var adapter = new TreeIndex(); + adapter.set(['a'], 1); + adapter.set(['c', 'b'], 2); + assert.equal(adapter.get('a'), 1, 'Top level value should be retrieved correctly.'); + assert.equal(adapter.get(['c', 'b']), 2, 'Second level value should be retrieved correctly.'); +}); + +QUnit.test("Getting with invalid arguments", function(assert) { + var adapter = new TreeIndex(); + adapter.set(['a'], 1); + adapter.set(['c', 'b'], 2); + assert.isNullOrUndefined(adapter.get('e'), 'Should return no value for unknown id'); + assert.isNullOrUndefined(adapter.get(['c', 'd']), 'Should return no value for unknown path'); + assert.isNullOrUndefined(adapter.get(null), 'Should return no value for null'); + assert.isNullOrUndefined(adapter.get(), 'Should return no value for no path'); + assert.isNullOrUndefined(adapter.get([]), 'Should return no value for empty path'); + assert.isNullOrUndefined(adapter.get({}), 'Should return no value for object'); + assert.isNullOrUndefined(adapter.get(1), 'Should return no value for a number'); +}); + +// QUnit.test("Wrapping an existing object", function(assert) { +// // Note: this way you can create an adapter for plain objects +// var mine = {}; +// var adapter = TreeIndex.wrap(mine); +// adapter.set(['a', 'b'], 2); +// assert.equal(adapter.get(['a', 'b']), 2, 'Value should be retrievable.'); +// assert.equal(mine.a.b, 2, 'Value should have been written inplace.'); +// assert.deepEqual(adapter.get('a'), mine.a, 'Get on first level should return plain content.'); +// }); + +QUnit.test("Getting values recursively", function(assert) { + var adapter = new TreeIndex(); + adapter.set(['a', 'b'], 'foo'); + adapter.set(['a', 'c', 'd'], 'bar'); + assert.deepEqual(adapter.getAll('a'), { b: 'foo', d: 'bar'}, 'Values should be collected together into an object'); +}); + +QUnit.test("Arrays: basic usage", function(assert) { + var adapter = new TreeIndex.Arrays(); + assert.throws(function() { + adapter.set('a', []); + }, 'TreeIndex.set is not allowed for array type'); + adapter.add('a', 1); + assert.deepEqual(adapter.get('a'), [1], 'Get should return an array.'); + adapter.add(['a', 'b'], 2); + assert.deepEqual(adapter.get('a'), [1], 'Content should still be the same after adding a nested value.'); + assert.deepEqual(adapter.getAll('a'), [1, 2], 'Collect all values into an array.'); + adapter.add(['a', 'b'], 3); + adapter.remove(['a', 'b'], 2); + assert.deepEqual(adapter.get(['a', 'b']), [3], 'Only one value should be left after removal.'); + adapter.delete(['a','b']); + assert.isNullOrUndefined(adapter.get(['a', 'b']), 'Value should now be deleted.'); +}); diff --git a/util/PathAdapter.js b/util/PathAdapter.js deleted file mode 100644 index 72ac336b5..000000000 --- a/util/PathAdapter.js +++ /dev/null @@ -1,223 +0,0 @@ -'use strict'; - -var isString = require('lodash/lang/isString'); -var isArray = require('lodash/lang/isArray'); -var oo = require('./oo'); -var deleteFromArray = require('./deleteFromArray'); - -/* - * An adapter to access an object via path. - * - * @class PathAdapter - * @param {object} [obj] An object to operate on - * @memberof module:Basics - * @example - * - * var pathAdapter = new PathAdapter({a: "aVal", b: {b1: 'b1Val', b2: 'b2Val'}}); - */ - -function PathAdapter(obj) { - if (obj) { - this.root = obj; - } -} - -PathAdapter.Prototype = function() { - - // use this to create extra scope for children ids - // Example: { - // "foo": { - // "bar": true - // "children": { - // "bla": { - // "blupp": true - // } - // } - // } - // } - this.childrenScope = false; - - /** - * Get root object of the path adapter - * - * @return {object} The root object - * @method getRoot - * @memberof module:Basics.PathAdapter.prototype - * @example - * - * pathAdapter.getRoot(); - */ - this.getRoot = function() { - return this.root || this; - }; - - this._resolve = function(path, create) { - var lastIdx = path.length-1; - var context = this.getRoot(); - for (var i = 0; i < lastIdx; i++) { - var key = path[i]; - if (context[key] === undefined) { - if (create) { - context[key] = {}; - if (this.childrenScope) { - context[key].children = {}; - } - } else { - return undefined; - } - } - context = context[key]; - if (this.childrenScope) { - context = context.children; - } - } - return context; - }; - - /** - * Get value at path - * - * @return {object} The value stored for a given path - * - * @example - * - * obj.get(['b', 'b1']); - * => b1Val - */ - this.get = function(path) { - if (isString(path)) { - return this[path]; - } else if (isArray(path)) { - var key = path[path.length-1]; - var context = this._resolve(path); - if (context) { - return context[key]; - } else { - return undefined; - } - } else { - return undefined; - } - }; - - this.set = function(path, value) { - if (isString(path)) { - this[path] = value; - } else { - var key = path[path.length-1]; - this._resolve(path, true)[key] = value; - } - }; - - this.delete = function(path, strict) { - if (isString(path)) { - delete this[path]; - } else { - var key = path[path.length-1]; - var obj = this._resolve(path); - if (strict && !obj || !obj[key]) { - throw new Error('Invalid path.'); - } - delete obj[key]; - } - }; - - this.clear = function() { - var root = this.getRoot(); - for (var key in root) { - if (root.hasOwnProperty(key)) { - delete root[key]; - } - } - }; - - this._traverse = function(root, path, fn, ctx) { - for (var id in root) { - if (!root.hasOwnProperty(id)) continue; - if (id !== '__values__') { - var childPath = path.concat([id]); - fn.call(ctx, childPath, root[id]); - this._traverse(root[id], childPath, fn, ctx); - } - } - }; - - this.traverse = function(fn, ctx) { - this._traverse(this.getRoot(), [], fn, ctx); - }; - -}; - -oo.initClass(PathAdapter); - -PathAdapter.Arrays = function() { - PathAdapter.apply(this, arguments); -}; - -PathAdapter.Arrays.Prototype = function() { - - this.get = function(path) { - if (isString(path)) { - return this[path]; - } else if (!path || path.length === 0) { - return this.getRoot(); - } else { - var key = path[path.length-1]; - var context = this._resolve(path); - if (context && context[key]) { - return context[key].__values__; - } else { - return undefined; - } - } - }; - - this.add = function(path, value) { - var key = path[path.length-1]; - var context = this._resolve(path, true); - if (!context[key]) { - context[key] = {__values__: []}; - } - var values = context[key].__values__; - values.push(value); - }; - - this.remove = function(path, value) { - var values = this.get(path); - if (values) { - deleteFromArray(values, value); - } else { - console.warn('Warning: trying to remove a value for an unknown path.', path, value); - } - }; - - this.removeAll = function(path) { - var values = this.get(path); - values.splice(0, values.length); - }; - - this.set = function() { - throw new Error('This method is not available for PathAdapter.Arrays'); - }; - - this._traverse = function(root, path, fn, ctx) { - for (var id in root) { - if (!root.hasOwnProperty(id)) continue; - if (id === '__values__') { - fn.call(ctx, path, root.__values__); - } else { - var childPath = path.concat([id]); - this._traverse(root[id], childPath, fn, ctx); - } - } - }; - - this.forEach = function(fn) { - return this._traverse(this.getRoot(), [], fn); - }; - -}; - -PathAdapter.extend(PathAdapter.Arrays); - -module.exports = PathAdapter; diff --git a/util/TreeIndex.js b/util/TreeIndex.js new file mode 100644 index 000000000..127f9249f --- /dev/null +++ b/util/TreeIndex.js @@ -0,0 +1,188 @@ +'use strict'; + +var isString = require('lodash/isString'); +var isArray = require('lodash/isArray'); +var get = require('lodash/get'); +var setWith = require('lodash/setWith'); +var oo = require('./oo'); +var deleteFromArray = require('./deleteFromArray'); + +function TreeNode() {} + +/* + * A tree-structure for indexes. + * + * @class TreeIndex + * @param {object} [obj] An object to operate on + * @memberof module:Basics + * @example + * + * var pathAdapter = new TreeIndex({a: "aVal", b: {b1: 'b1Val', b2: 'b2Val'}}); + */ + +function TreeIndex() {} + +TreeIndex.Prototype = function() { + + /** + * Get value at path. + * + * @return {object} The value stored for a given path + * + * @example + * + * obj.get(['b', 'b1']); + * => b1Val + */ + this.get = function(path) { + if (arguments.length > 1) { + path = Array.prototype.slice(arguments, 0); + } + if (isString(path)) { + path = [path]; + } + return get(this, path); + }; + + this.getAll = function(path) { + if (arguments.length > 1) { + path = Array.prototype.slice(arguments, 0); + } + if (isString(path)) { + path = [path]; + } + if (!isArray(path)) { + throw new Error('Illegal argument for TreeIndex.get()'); + } + var node = get(this, path); + return this._collectValues(node); + }; + + this.set = function(path, value) { + if (isString(path)) { + path = [path]; + } + setWith(this, path, value, function(val) { if (!val) return new TreeNode(); }); + }; + + this.delete = function(path) { + if (isString(path)) { + delete this[path]; + } else if(path.length === 1) { + delete this[path[0]]; + } else { + var key = path[path.length-1]; + path = path.slice(0, -1); + var parent = get(this, path); + if (parent) { + delete parent[key]; + } + } + }; + + this.clear = function() { + var root = this; + for (var key in root) { + if (root.hasOwnProperty(key)) { + delete root[key]; + } + } + }; + + this.traverse = function(fn) { + this._traverse(this, [], fn); + }; + + this.forEach = this.traverse; + + this._traverse = function(root, path, fn) { + var id; + for (id in root) { + if (!root.hasOwnProperty(id)) continue; + var child = root[id]; + var childPath = path.concat([id]); + if (child instanceof TreeNode) { + this._traverse(child, childPath, fn); + } else { + fn(child, childPath); + } + } + }; + + this._collectValues = function(root) { + // TODO: don't know if this is the best solution + // We use this only for indexes, e.g., to get all annotation on one node + var vals = {}; + this._traverse(root, [], function(val, path) { + var key = path[path.length-1]; + vals[key] = val; + }); + return vals; + }; +}; + +oo.initClass(TreeIndex); + +TreeIndex.Arrays = function() {}; + +TreeIndex.Arrays.Prototype = function() { + + var _super = Object.getPrototypeOf(this); + + this.get = function(path) { + var val = _super.get.call(this, path); + if (val instanceof TreeNode) { + val = val.__values__ || []; + } + return val; + }; + + this.set = function() { + throw new Error('TreeIndex.set() is not supported for array type.'); + }; + + this.add = function(path, value) { + if (isString(path)) { + path = [path]; + } + if (!isArray(path)) { + throw new Error('Illegal arguments.'); + } + var arr; + setWith(this, path.concat(['__values__','__dummy__']), undefined, function(val, key) { + if (key === '__values__') { + if (!val) { + arr = []; + return arr; + } else { + arr = val; + } + } else if (!val) { + return new TreeNode(); + } + }); + delete arr.__dummy__; + arr.push(value); + }; + + this.remove = function(path, value) { + var arr = get(this, path); + if (arr instanceof TreeNode) { + deleteFromArray(arr.__values__, value); + } + }; + + this._collectValues = function(root) { + var vals = []; + this._traverse(root, [], function(val) { + vals.push(val); + }); + vals = Array.prototype.concat.apply([], vals); + return vals; + }; + +}; + +TreeIndex.extend(TreeIndex.Arrays); + +module.exports = TreeIndex;