Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(390)

Unified Diff: sdk/lib/observe/observe_path.dart

Issue 14732003: Implement Model-Driven-Views spec for Dart (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: trying upload again Created 7 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: sdk/lib/observe/observe_path.dart
diff --git a/sdk/lib/observe/observe_path.dart b/sdk/lib/observe/observe_path.dart
new file mode 100644
index 0000000000000000000000000000000000000000..b22f9040d6389f2f97a5be7ac11f92d9884d7c67
--- /dev/null
+++ b/sdk/lib/observe/observe_path.dart
@@ -0,0 +1,264 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of dart.observe;
+
+// This code is inspired by ChangeSummary:
+// https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js
+// ...which underlies MDV. Since we don't need the functionality of
+// ChangeSummary, we just implement [observePath] and the ability to get/set
+// a value on an observed path. This allows our implementation to be much
+// simpler.
+
+// TODO(jmesserly): should we make these types stronger, and require
+// Observable objects? Currently, it is fine to say something like:
+// var path = observePath(123, '');
+// print(path.value); // "123"
+//
+// Furthermore this degenerate case is allowed:
+// var path = observePath(123, 'foo.bar.baz.qux');
+// print(path.value); // "null"
+//
+// Here we see that any invalid (i.e. not Observable) value will break the
+// path chain without producing an error or exception.
+//
+// Now the real question: should we do this? For the former case, the behavior
+// is correct but we could chose to handle it in the dart:html bindings layer.
+// For the latter case, it might be better to throw an error so users can find
+// the problem.
+
+/**
+ * Observes [path] on [object] for changes. If parsing was successful, returns
+ * an object that can be used to get the changes and get/set the value at this
+ * path. See [PathObserver.values] and [PathObserver.value].
+ */
+ PathObserver observePath(object, String path) {
+ if (!_isPathValid(path)) return null;
+
+ var segments = [];
+ for (var segment in path.trim().split('.')) {
+ if (segment == '') continue;
+ var index = int.parse(segment, onError: (_) {});
+ segments.add(index != null ? index : segment);
+ }
+
+ // TODO(jmesserly): if the path is empty, or the object is! Observable, we
+ // can optimize the PathObserver to be more lightweight.
+
+ // TODO(jmesserly): freeze segments list.
+ return new PathObserver._(object, segments);
+}
+
+// TODO(jmesserly): the primary reason to have this object exposed is because
+// we have get/set for value. Ideally "observePath" could just return the
+// stream.
+/**
+ * A data path on an object, which can be observed. For example: `foo.bar.baz`.
+ * This object is returned by [observePath].
+ */
+class PathObserver {
+ /** The object being observed. */
+ final object;
+
+ /**
+ * The path segments. This will be a list of strings or integers.
+ */
+ final List path;
+
+ // TODO(jmesserly): same issue here as ObservableMixin: is there an easier
+ // way to get a broadcast stream?
+ StreamController _values;
+ Stream _valueStream;
+
+ _PropertyObserver _observer, _lastObserver;
+
+ Object _lastValue;
+ bool _scheduled = false;
+
+ PathObserver._(this.object, this.path) {
+ _values = new StreamController(onListen: _observe, onCancel: _unobserve);
+ _valueStream = _values.stream.asBroadcastStream();
+
+ // Create the property observer linked list.
+ if (path.length > 0) {
+ _observer = new _PropertyObserver(this, path[0]);
+ var observer = _observer;
+ for (int i = 1; i < path.length; i++) {
+ observer._next = new _PropertyObserver(this, path[i]);
+ observer = observer._next;
+ }
+ _lastObserver = observer;
+ }
+ }
+
+ // TODO(jmesserly): use broadcast stream here. Need to check how it handles
+ // onListen/onCancel.
+ // TODO(jmesserly): should this be a change record with the old value?
+ /**
+ * Gets the values that were observed at this path.
+ * These are delivered asynchronously during [deliverChangeRecords].
+ */
+ Stream get values => _valueStream;
+
+ /** Force synchronous delivery of [values]. */
+ void _deliverValues() {
+ _scheduled = false;
+
+ var newValue = value;
blois 2013/05/01 17:00:42 I'm curious how often this will filter changes. I'
Jennifer Messerly 2013/05/02 02:58:33 yeah, right now: 1. Each step along the path is c
+ if (!identical(_lastValue, newValue)) {
+ _values.add(newValue);
+ _lastValue = newValue;
+ }
+ }
+
+ void _observe() {
+ if (_observer != null) {
+ _lastValue = value;
+ _observer.observe();
+ }
+ }
+
+ void _unobserve() {
+ if (_observer != null) _observer.unobserve();
+ }
+
+ void _notifyChange() {
+ if (_scheduled) return;
+ _scheduled = true;
+
+ // TODO(jmesserly): should we have a guarenteed order with respect to other
+ // paths? If so, we could implement this fairly easily by sorting instances
+ // of this class by birth order before delivery.
+ queueChangeRecords(_deliverValues);
+ }
+
+ /** Gets the last reported value at this path. */
+ get value {
+ if (path.length == 0) return object;
+ _observer.ensureValue(object);
+ return _lastObserver.value;
+ }
+
+ /** Sets the value at this path. */
+ void set value(Object value) {
+ // TODO(jmesserly): throw if property cannot be set?
+ // MDV seems tolerant of these error.
+ if (path.length == 0) return;
+ _observer.ensureValue(object);
+ var last = _lastObserver;
+ if (_setObjectProperty(last._object, last._property, value)) {
+ // Technically, this would get updated asynchronously via a change record.
+ // However, it is nice if calling the getter will yield the same value
+ // that was just set. So we use this opportunity to update our cache.
+ last.value = value;
+ }
+ }
+}
+
+// TODO(jmesserly): these should go away in favor of mirrors!
+_getObjectProperty(object, property) {
+ if (object is List && property is int) {
+ if (property >= 0 && property < object.length) {
+ return object[property];
+ } else {
+ return null;
+ }
+ }
+
+ // TODO(jmesserly): what about length?
+ if (object is Map) return object[property];
+
+ if (object is Observable) return object.getValue(property);
+
+ return null;
+}
+
+bool _setObjectProperty(object, property, value) {
+ if (object is List && property is int) {
+ object[property] = value;
+ } else if (object is Map) {
+ object[property] = value;
+ } else if (object is Observable) {
+ (object as Observable).setValue(property, value);
+ } else {
+ return false;
+ }
+ return true;
+}
+
+
+class _PropertyObserver {
+ final _property;
+ PathObserver _path;
Siggi Cherem (dart-lang) 2013/05/01 18:57:56 final
Jennifer Messerly 2013/05/02 02:58:33 Done.
+ _PropertyObserver _next;
Siggi Cherem (dart-lang) 2013/05/01 18:57:56 consider making this final too? this would probabl
Jennifer Messerly 2013/05/02 02:58:33 Great suggestion, done! FWIW, originally the chai
+
+ Object _object;
+ Object _value;
+ StreamSubscription _sub;
+
+ _PropertyObserver(this._path, this._property);
+
+ get value => _value;
+
+ void set value(Object newValue) {
+ _value = newValue;
+ if (_next != null) {
+ if (_sub != null) _next.unobserve();
+ _next.ensureValue(_value);
+ if (_sub != null) _next.observe();
+ }
+ }
+
+ void ensureValue(object) {
+ // If we're observing, values should be up to date already.
+ if (_sub != null) return;
+
+ _object = object;
+ value = _getObjectProperty(object, _property);
+ }
+
+ void observe() {
+ if (_object is Observable) {
+ assert(_sub == null);
+ _sub = (_object as Observable).changes.listen(_onChange);
+ }
+ if (_next != null) _next.observe();
+ }
+
+ void unobserve() {
+ if (_sub == null) return;
+
+ _sub.cancel();
+ _sub = null;
+ if (_next != null) _next.unobserve();
+ }
+
+ void _onChange(List<ChangeRecord> changes) {
+ for (var change in changes.reversed) {
+ if (_property == change.key) {
+ value = change.newValue;
+ _path._notifyChange();
+ return;
+ }
+ }
+ }
+}
+
+// From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js
+
+const _pathIndentPart = r'[$a-z0-9_]+[$a-z0-9_\d]*';
+final _pathRegExp = new RegExp('^'
+ '(?:#?' + _pathIndentPart + ')?'
+ '(?:'
+ '(?:\\.' + _pathIndentPart + ')'
+ ')*'
+ r'$', caseSensitive: false);
+
+bool _isPathValid(String s) {
+ s = s.replaceAll(new RegExp('\\s'), '');
blois 2013/05/01 17:00:42 cache the regexp?
Jennifer Messerly 2013/05/02 02:58:33 Done.
+
+ if (s == '') return true;
+ if (s[0] == '.') return false;
+ return _pathRegExp.hasMatch(s);
+}

Powered by Google App Engine
This is Rietveld 408576698