UNPKG

lively.lang

Version:

JavaScript utils providing useful abstractions for working with collections, functions, objects.

291 lines (263 loc) 9.93 kB
// -=-=-=-=-=-=-=-=-=-=-=-=-=- // js object path accessor // -=-=-=-=-=-=-=-=-=-=-=-=-=- // show-in-doc // A `Path` is an objectified chain of property names (kind of a "complex" // getter and setter). Path objects can make access and writes into deeply nested // structures more convenient. `Path` provide "safe" get and set operations and // can be used for debugging by providing a hook that allows users to find out // when get/set operations happen. import { inspect } from "./object.js"; export default function Path(p, splitter) { if (p instanceof Path) return p; if (!(this instanceof Path)) return new Path(p, splitter); this.setSplitter(splitter || '.'); this.fromPath(p); } Object.assign(Path.prototype, { get isPathAccessor() { return true }, fromPath(path) { // ignore-in-doc if (typeof path === "string" && path !== '' && path !== this.splitter) { this._parts = path.split(this.splitter); this._path = path; } else if (Array.isArray(path)) { this._parts = [].concat(path); this._path = path.join(this.splitter); } else { this._parts = []; this._path = ''; } return this; }, setSplitter(splitter) { // ignore-in-doc if (splitter) this.splitter = splitter; return this; }, parts() { /*key names as array*/ return this._parts; }, size() { /*show-in-doc*/ return this._parts.length; }, slice(n, m) { /*show-in-doc*/ return Path(this.parts().slice(n, m)); }, normalizePath() { // ignore-in-doc // FIXME: define normalization return this._path; }, isRoot(obj) { return this._parts.length === 0; }, isIn(obj) { // Does the Path resolve to a value when applied to `obj`? if (this.isRoot()) return true; var parent = this.get(obj, -1); return parent && parent.hasOwnProperty(this._parts[this._parts.length-1]); }, equals(obj) { // Example: // var p1 = Path("foo.1.bar.baz"), p2 = Path(["foo", 1, "bar", "baz"]); // // Path's can be both created via strings or pre-parsed with keys in a list. // p1.equals(p2) // => true return obj && obj.isPathAccessor && this.parts().equals(obj.parts()); }, isParentPathOf(otherPath) { // Example: // var p1 = Path("foo.1.bar.baz"), p2 = Path("foo.1.bar"); // p2.isParentPathOf(p1) // => true // p1.isParentPathOf(p2) // => false otherPath = otherPath && otherPath.isPathAccessor ? otherPath : Path(otherPath); var parts = this.parts(), otherParts = otherPath.parts(); for(var i = 0; i < parts.length; i ++) { if (parts[i] != otherParts[i]) return false } return true }, relativePathTo(otherPath) { // Example: // var p1 = Path("foo.1.bar.baz"), p2 = Path("foo.1"); // p2.relativePathTo(p1) // => Path(["bar","baz"]) // p1.relativePathTo(p2) // => undefined otherPath = Path(otherPath); return this.isParentPathOf(otherPath) ? otherPath.slice(this.size(), otherPath.size()) : undefined; }, del(obj) { if (this.isRoot()) return false; var parent = obj for (var i = 0; i < this._parts.length-1; i++) { var part = this._parts[i]; if (parent.hasOwnProperty(part)) { parent = parent[part]; } else return false; } return delete parent[this._parts[this._parts.length-1]]; }, withParentAndKeyDo(obj, ensure, doFunc) { // Deeply resolve path in `obj`, not fully, however, only to the parent // element of the last part of path. Take the parent, the key (the last // part of path) and pass it to `doFunc`. When `ensure` is true, create // objects along path it path does not resolve if (this.isRoot()) return doFunc(null, null); var parent = obj; for (var i = 0; i < this._parts.length-1; i++) { var part = this._parts[i]; if (parent.hasOwnProperty(part) && (typeof parent[part] === "object" || typeof parent[part] === "function")) { parent = parent[part]; } else if (ensure) { parent = parent[part] = {}; } else { return doFunc(null, part); } } return doFunc(parent, this._parts[this._parts.length-1]); }, set(obj, val, ensure) { // Deeply resolve path in `obj` and set the resulting property to `val`. If // `ensure` is true, create nested structure in between as necessary. // Example: // var o1 = {foo: {bar: {baz: 42}}}; // var path = Path("foo.bar.baz"); // path.set(o1, 43) // o1 // => {foo: {bar: {baz: 43}}} // var o2 = {foo: {}}; // path.set(o2, 43, true) // o2 // => {foo: {bar: {baz: 43}}} return this.withParentAndKeyDo(obj, ensure, function(parent, key) { return parent ? parent[key] = val : undefined; }); }, defineProperty(obj, propertySpec, ensure) { // like `Path>>set`, however uses Objeect.defineProperty return this.withParentAndKeyDo(obj, ensure, function(parent, key) { return parent ? Object.defineProperty(parent, key, propertySpec) : undefined; }); }, get(obj, n) { // show-in-doc var parts = n ? this._parts.slice(0, n) : this._parts; return parts.reduce(function(current, pathPart) { return current ? current[pathPart] : current; }, obj); }, concat(p, splitter) { // show-in-doc return Path(this.parts().concat(Path(p, splitter).parts())); }, toString() { return this.normalizePath(); }, serializeExpr() { // ignore-in-doc return 'lively.lang.Path(' + inspect(this.parts()) + ')'; }, watch(options) { // React or be notified on reads or writes to a path in a `target`. Options: // ```js // { // target: OBJECT, // uninstall: BOOLEAN, // onGet: FUNCTION, // onSet: FUNCTION, // haltWhenChanged: BOOLEAN, // verbose: BOOLEAN // } // ``` // Example: // // Quite useful for debugging to find out what call-sites change an object. // var o = {foo: {bar: 23}}; // Path("foo.bar").watch({target: o, verbose: true}); // o.foo.bar = 24; // => You should see: "[object Object].bar changed: 23 -> 24" if (!options || this.isRoot()) return; var target = options.target, parent = this.get(target, -1), propName = this.parts().slice(-1)[0], newPropName = 'propertyWatcher$' + propName, watcherIsInstalled = parent && parent.hasOwnProperty(newPropName), uninstall = options.uninstall, haltWhenChanged = options.haltWhenChanged, showStack = options.showStack, getter = parent.__lookupGetter__(propName), setter = parent.__lookupSetter__(propName); if (!target || !propName || !parent) return; if (uninstall) { if (!watcherIsInstalled) return; delete parent[propName]; parent[propName] = parent[newPropName]; delete parent[newPropName]; var msg = 'Watcher for ' + parent + '.' + propName + ' uninstalled'; show(msg); return; } if (watcherIsInstalled) { var msg = 'Watcher for ' + parent + '.' + propName + ' already installed'; show(msg); return; } if (getter || setter) { var msg = parent + '["' + propName + '"] is a getter/setter, watching not support'; console.log(msg); if (typeof show === "undefined") show(msg); return; } // observe slots, for debugging parent[newPropName] = parent[propName]; parent.__defineSetter__(propName, function(v) { var oldValue = parent[newPropName]; if (options.onSet) options.onSet(v, oldValue); var msg = parent + "." + propName + " changed: " + oldValue + " -> " + v; if (showStack) msg += '\n' + (typeof lively !== "undefined" ? lively.printStack() : console.trace()); if (options.verbose) { console.log(msg); if (typeof show !== 'undefined') show(msg); } if (haltWhenChanged) debugger; return parent[newPropName] = v; }); parent.__defineGetter__(propName, function() { if (options.onGet) options.onGet(parent[newPropName]); return parent[newPropName]; }); var msg = 'Watcher for ' + parent + '.' + propName + ' installed'; console.log(msg); if (typeof show !== 'undefined') show(msg); }, debugFunctionWrapper(options) { // ignore-in-doc // options = {target, [haltWhenChanged, showStack, verbose, uninstall]} var target = options.target, parent = this.get(target, -1), funcName = this.parts().slice(-1)[0], uninstall = options.uninstall, haltWhenChanged = options.haltWhenChanged === undefined ? true : options.haltWhenChanged, showStack = options.showStack, func = parent && funcName && parent[funcName], debuggerInstalled = func && func.isDebugFunctionWrapper; if (!target || !funcName || !func || !parent) return; if (uninstall) { if (!debuggerInstalled) return; parent[funcName] = parent[funcName].debugTargetFunction; var msg = 'Uninstalled debugFunctionWrapper for ' + parent + '.' + funcName; console.log(msg); if (typeof show !== 'undefined') show(msg); show(msg); return; } if (debuggerInstalled) { var msg = 'debugFunctionWrapper for ' + parent + '.' + funcName + ' already installed'; console.log(msg); if (typeof show !== 'undefined') show(msg); return; } var debugFunc = parent[funcName] = func.wrap(function(proceed) { var args = Array.from(arguments); if (haltWhenChanged) debugger; if (showStack) show(lively.printStack()); if (options.verbose) show(funcName + ' called'); return args.shift().apply(parent, args); }); debugFunc.isDebugFunctionWrapper = true; debugFunc.debugTargetFunction = func; var msg = 'debugFunctionWrapper for ' + parent + '.' + funcName + ' installed'; console.log(msg); if (typeof show !== 'undefined') show(msg); } });