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

Side by Side 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, 7 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 unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 part of dart.observe;
6
7 // This code is inspired by ChangeSummary:
8 // https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js
9 // ...which underlies MDV. Since we don't need the functionality of
10 // ChangeSummary, we just implement [observePath] and the ability to get/set
11 // a value on an observed path. This allows our implementation to be much
12 // simpler.
13
14 // TODO(jmesserly): should we make these types stronger, and require
15 // Observable objects? Currently, it is fine to say something like:
16 // var path = observePath(123, '');
17 // print(path.value); // "123"
18 //
19 // Furthermore this degenerate case is allowed:
20 // var path = observePath(123, 'foo.bar.baz.qux');
21 // print(path.value); // "null"
22 //
23 // Here we see that any invalid (i.e. not Observable) value will break the
24 // path chain without producing an error or exception.
25 //
26 // Now the real question: should we do this? For the former case, the behavior
27 // is correct but we could chose to handle it in the dart:html bindings layer.
28 // For the latter case, it might be better to throw an error so users can find
29 // the problem.
30
31 /**
32 * Observes [path] on [object] for changes. If parsing was successful, returns
33 * an object that can be used to get the changes and get/set the value at this
34 * path. See [PathObserver.values] and [PathObserver.value].
35 */
36 PathObserver observePath(object, String path) {
37 if (!_isPathValid(path)) return null;
38
39 var segments = [];
40 for (var segment in path.trim().split('.')) {
41 if (segment == '') continue;
42 var index = int.parse(segment, onError: (_) {});
43 segments.add(index != null ? index : segment);
44 }
45
46 // TODO(jmesserly): if the path is empty, or the object is! Observable, we
47 // can optimize the PathObserver to be more lightweight.
48
49 // TODO(jmesserly): freeze segments list.
50 return new PathObserver._(object, segments);
51 }
52
53 // TODO(jmesserly): the primary reason to have this object exposed is because
54 // we have get/set for value. Ideally "observePath" could just return the
55 // stream.
56 /**
57 * A data path on an object, which can be observed. For example: `foo.bar.baz`.
58 * This object is returned by [observePath].
59 */
60 class PathObserver {
61 /** The object being observed. */
62 final object;
63
64 /**
65 * The path segments. This will be a list of strings or integers.
66 */
67 final List path;
68
69 // TODO(jmesserly): same issue here as ObservableMixin: is there an easier
70 // way to get a broadcast stream?
71 StreamController _values;
72 Stream _valueStream;
73
74 _PropertyObserver _observer, _lastObserver;
75
76 Object _lastValue;
77 bool _scheduled = false;
78
79 PathObserver._(this.object, this.path) {
80 _values = new StreamController(onListen: _observe, onCancel: _unobserve);
81 _valueStream = _values.stream.asBroadcastStream();
82
83 // Create the property observer linked list.
84 if (path.length > 0) {
85 _observer = new _PropertyObserver(this, path[0]);
86 var observer = _observer;
87 for (int i = 1; i < path.length; i++) {
88 observer._next = new _PropertyObserver(this, path[i]);
89 observer = observer._next;
90 }
91 _lastObserver = observer;
92 }
93 }
94
95 // TODO(jmesserly): use broadcast stream here. Need to check how it handles
96 // onListen/onCancel.
97 // TODO(jmesserly): should this be a change record with the old value?
98 /**
99 * Gets the values that were observed at this path.
100 * These are delivered asynchronously during [deliverChangeRecords].
101 */
102 Stream get values => _valueStream;
103
104 /** Force synchronous delivery of [values]. */
105 void _deliverValues() {
106 _scheduled = false;
107
108 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
109 if (!identical(_lastValue, newValue)) {
110 _values.add(newValue);
111 _lastValue = newValue;
112 }
113 }
114
115 void _observe() {
116 if (_observer != null) {
117 _lastValue = value;
118 _observer.observe();
119 }
120 }
121
122 void _unobserve() {
123 if (_observer != null) _observer.unobserve();
124 }
125
126 void _notifyChange() {
127 if (_scheduled) return;
128 _scheduled = true;
129
130 // TODO(jmesserly): should we have a guarenteed order with respect to other
131 // paths? If so, we could implement this fairly easily by sorting instances
132 // of this class by birth order before delivery.
133 queueChangeRecords(_deliverValues);
134 }
135
136 /** Gets the last reported value at this path. */
137 get value {
138 if (path.length == 0) return object;
139 _observer.ensureValue(object);
140 return _lastObserver.value;
141 }
142
143 /** Sets the value at this path. */
144 void set value(Object value) {
145 // TODO(jmesserly): throw if property cannot be set?
146 // MDV seems tolerant of these error.
147 if (path.length == 0) return;
148 _observer.ensureValue(object);
149 var last = _lastObserver;
150 if (_setObjectProperty(last._object, last._property, value)) {
151 // Technically, this would get updated asynchronously via a change record.
152 // However, it is nice if calling the getter will yield the same value
153 // that was just set. So we use this opportunity to update our cache.
154 last.value = value;
155 }
156 }
157 }
158
159 // TODO(jmesserly): these should go away in favor of mirrors!
160 _getObjectProperty(object, property) {
161 if (object is List && property is int) {
162 if (property >= 0 && property < object.length) {
163 return object[property];
164 } else {
165 return null;
166 }
167 }
168
169 // TODO(jmesserly): what about length?
170 if (object is Map) return object[property];
171
172 if (object is Observable) return object.getValue(property);
173
174 return null;
175 }
176
177 bool _setObjectProperty(object, property, value) {
178 if (object is List && property is int) {
179 object[property] = value;
180 } else if (object is Map) {
181 object[property] = value;
182 } else if (object is Observable) {
183 (object as Observable).setValue(property, value);
184 } else {
185 return false;
186 }
187 return true;
188 }
189
190
191 class _PropertyObserver {
192 final _property;
193 PathObserver _path;
Siggi Cherem (dart-lang) 2013/05/01 18:57:56 final
Jennifer Messerly 2013/05/02 02:58:33 Done.
194 _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
195
196 Object _object;
197 Object _value;
198 StreamSubscription _sub;
199
200 _PropertyObserver(this._path, this._property);
201
202 get value => _value;
203
204 void set value(Object newValue) {
205 _value = newValue;
206 if (_next != null) {
207 if (_sub != null) _next.unobserve();
208 _next.ensureValue(_value);
209 if (_sub != null) _next.observe();
210 }
211 }
212
213 void ensureValue(object) {
214 // If we're observing, values should be up to date already.
215 if (_sub != null) return;
216
217 _object = object;
218 value = _getObjectProperty(object, _property);
219 }
220
221 void observe() {
222 if (_object is Observable) {
223 assert(_sub == null);
224 _sub = (_object as Observable).changes.listen(_onChange);
225 }
226 if (_next != null) _next.observe();
227 }
228
229 void unobserve() {
230 if (_sub == null) return;
231
232 _sub.cancel();
233 _sub = null;
234 if (_next != null) _next.unobserve();
235 }
236
237 void _onChange(List<ChangeRecord> changes) {
238 for (var change in changes.reversed) {
239 if (_property == change.key) {
240 value = change.newValue;
241 _path._notifyChange();
242 return;
243 }
244 }
245 }
246 }
247
248 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js
249
250 const _pathIndentPart = r'[$a-z0-9_]+[$a-z0-9_\d]*';
251 final _pathRegExp = new RegExp('^'
252 '(?:#?' + _pathIndentPart + ')?'
253 '(?:'
254 '(?:\\.' + _pathIndentPart + ')'
255 ')*'
256 r'$', caseSensitive: false);
257
258 bool _isPathValid(String s) {
259 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.
260
261 if (s == '') return true;
262 if (s[0] == '.') return false;
263 return _pathRegExp.hasMatch(s);
264 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698