Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(345)

Side by Side Diff: app/models/charm.js

Issue 6733067: change charm store data structures
Patch Set: change charm store data structures Created 12 years, 6 months ago
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « app/app.js ('k') | app/modules.js » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 'use strict'; 1 'use strict';
2 2
3 YUI.add('juju-charm-models', function(Y) { 3 YUI.add('juju-charm-models', function(Y) {
4 4
5
5 var models = Y.namespace('juju.models'); 6 var models = Y.namespace('juju.models');
6 7
7 // This is how the charm_id_re regex works for various inputs. The first 8 // Charms, once instantiated and loaded with data from their respective
8 // element is always the initial string, which we have elided in the 9 // sources, are immutable and read-only. This reflects the reality of how we
9 // examples. 10 // interact with them.
10 // 'cs:~marcoceppi/precise/word-press-17' ->
11 // [..."cs", "marcoceppi", "precise", "word-press", "17"]
12 // 'cs:~marcoceppi/precise/word-press' ->
13 // [..."cs", "marcoceppi", "precise", "word-press", undefined]
14 // 'cs:precise/word-press' ->
15 // [..."cs", undefined, "precise", "word-press", undefined]
16 // 'cs:precise/word-press-17'
17 // [..."cs", undefined, "precise", "word-press", "17"]
18 var charm_id_re = /^(?:(\w+):)?(?:~(\S+)\/)?(\w+)\/(\S+?)(?:-(\d+))?$/,
19 parse_charm_id = function(id) {
20 var parts = charm_id_re.exec(id),
21 result = {};
22 if (parts) {
23 parts.shift();
24 Y.each(
25 Y.Array.zip(
26 ['scheme', 'owner', 'series', 'package_name', 'revision'],
27 parts),
28 function(pair) { result[pair[0]] = pair[1]; });
29 if (!Y.Lang.isValue(result.scheme)) {
30 result.scheme = 'cs'; // This is the default.
31 }
32 return result;
33 }
34 // return undefined;
35 },
36 _calculate_full_charm_name = function(elements) {
37 var tmp = [elements.series, elements.package_name];
38 if (elements.owner) {
39 tmp.unshift('~' + elements.owner);
40 }
41 return tmp.join('/');
42 },
43 _calculate_charm_store_path = function(elements) {
44 return [(elements.owner ? '~' + elements.owner : 'charms'),
45 elements.series, elements.package_name, 'json'].join('/');
46 },
47 _calculate_base_charm_id = function(elements) {
48 return elements.scheme + ':' + _calculate_full_charm_name(elements);
49 },
50 _reconsititute_charm_id = function(elements) {
51 return _calculate_base_charm_id(elements) + '-' + elements.revision;
52 },
53 _clean_charm_data = function(data) {
54 data.is_subordinate = data.subordinate;
55 Y.each(['subordinate', 'name', 'revision', 'store_revision'],
56 function(nm) { delete data[nm]; });
57 return data;
58 };
59 // This is exposed for testing purposes.
60 models.parse_charm_id = parse_charm_id;
61
62 // For simplicity and uniformity, there is a single Charm class and a
63 // single CharmList class. Charms, once instantiated and loaded with data
64 // from their respective sources, are immutable and read-only. This reflects
65 // the reality of how we interact with them.
66 11
67 // Charm instances can represent both environment charms and charm store 12 // Charm instances can represent both environment charms and charm store
68 // charms. A charm id is reliably and uniquely associated with a given 13 // charms. A charm id is reliably and uniquely associated with a given
69 // charm only within a given context--the environment or the charm store. 14 // charm only within a given context--the environment or the charm store.
70 15
71 // Therefore, the database keeps these charms separate in two different 16 // Therefore, the database keeps these charms separate in two different
72 // CharmList instances. One is db.charms, representing the environment 17 // CharmList instances. One is db.charms, representing the environment
73 // charms. The other is maintained by and within the persistent charm panel 18 // charms. The other, from the charm store, is maintained by and within the
74 // instance. As you'd expect, environment charms are what to use when 19 // persistent charm panel instance. As you'd expect, environment charms are
75 // viewing or manipulating the environment. Charm store charms are what we 20 // what to use when viewing or manipulating the environment. Charm store
76 // can browse to select and deploy new charms to the environment. 21 // charms are what we can browse to select and deploy new charms to the
22 // environment.
77 23
78 // Environment charms begin their lives with full charm ids, as provided by 24 // Charms begin their lives with full charm ids, as provided by
79 // services in the environment: 25 // services in the environment and the charm store:
80 26
81 // [SCHEME]:(~[OWNER]/)?[SERIES]/[PACKAGE NAME]-[REVISION]. 27 // [SCHEME]:(~[OWNER]/)?[SERIES]/[PACKAGE NAME]-[REVISION].
82 28
83 // With an id, we can instantiate a charm: typically we use 29 // With an id, we can instantiate a charm: typically we use
84 // "db.charms.add({id: [ID]})". Finally, we load the charm's data from the 30 // "db.charms.add({id: [ID]})". Finally, we load the charm's data over the
85 // environment using the standard YUI Model method "load," providing an 31 // network using the standard YUI Model method "load," providing an object
86 // object with a get_charm callable, and an optional callback (see YUI 32 // with a get_charm callable, and an optional callback (see YUI docs). Both
87 // docs). The env has a get_charm method, so, by design, it works nicely: 33 // the env and the charm store have a get_charm method, so, by design, it
88 // "charm.load(env, optionalCallback)". The get_charm method is expected to 34 // works easily: "charm.load(env, optionalCallback)" or
89 // return what the env version does: either an object with a "result" object 35 // "charm.load(charm_store, optionalCallback)". The get_charm method must
90 // containing the charm data, or an object with an "err" attribute. 36 // either callback using the default YUI approach for this code, a boolean
91 37 // indicating failure, and a result; or it must return what the env version
92 // The charms in the charm store have a significant difference, beyond the 38 // does: an object with a "result" object containing the charm data, or an
93 // source of their data: they are addressed in the charm store by a path 39 // object with an "err" attribute.
94 // that does not include the revision number, and charm store searches do
95 // not include revision numbers in the results. Therefore, we cannot
96 // immediately instantiate a charm, because it requires a full id in order
97 // to maintain the idea of an immutable charm associated with a unique charm
98 // id. However, the charm information that returns does have a revision
99 // number (the most recent); moreover, over time the charm may be updated,
100 // leading to a new charm revision. We model this by creating a new charm.
101
102 // Since we cannot create or search for charms without a revision number
103 // using the normal methods, the charm list has a couple of helpers for this
104 // story. The workhorse is "loadOneByBaseId". A "base id" is an id without
105 // a revision.
106
107 // The arguments to "loadOneById" are a base id and a hash of other options.
108 // The hash must have a "charm_store" attribute, that itself loadByPath
109 // method, like the one in app/store/charm.js. It may have zero or more of
110 // the following: a success callback, accepting the fully loaded charm with
111 // the newest revision for the given base id; a failure callback, accepting
112 // the Y.io response object after a failure; and a "force" attribute that,
113 // if it is a Javascript boolean truth-y value, forces a load even if a
114 // charm with the given id already is in the charm list.
115
116 // "getOneByBaseId" simply returns the charm with the highest revision and
117 // "the given base id from the charm list, without trying to load
118 // "information.
119 40
120 // In both cases, environment charms and charm store charms, a charm's 41 // In both cases, environment charms and charm store charms, a charm's
121 // "loaded" attribute is set to true once it has all the data from its 42 // "loaded" attribute is set to true once it has all the data from its
122 // environment. 43 // environment.
123 44
124 var Charm = Y.Base.create('charm', Y.Model, [], { 45 var charmIdRe = /^(?:(\w+):)?(?:~(\S+)\/)?(\w+)\/(\S+?)-(\d+)$/,
125 initializer: function() { 46 idElements = ['scheme', 'owner', 'series', 'package_name', 'revision'],
126 this.loaded = false; 47 Charm = Y.Base.create('charm', Y.Model, [], {
127 this.on('load', function() { this.loaded = true; }); 48 initializer: function() {
128 }, 49 var id = this.get('id'),
129 sync: function(action, options, callback) { 50 parts = id && charmIdRe.exec(id),
130 if (action !== 'read') { 51 self = this;
131 throw ( 52 if (!Y.Lang.isValue(id) || !parts) {
132 'Only use the "read" action; "' + action + '" not supported.'); 53 throw 'Developers must initialize charms with a well-formed id.';
133 } 54 }
134 if (!Y.Lang.isValue(options.get_charm)) { 55 this.loaded = false;
135 throw 'You must supply a get_charm function.'; 56 this.on('load', function() { this.loaded = true; });
136 } 57 parts.shift();
137 options.get_charm( 58 Y.each(
138 this.get('id'), 59 Y.Array.zip(idElements, parts),
139 // This is the success callback, or the catch-all callback for 60 function(pair) { self.set(pair[0], pair[1]); });
140 // get_charm. 61 // full_name
141 function(response) { 62 var tmp = [this.get('series'), this.get('package_name')],
142 // Handle the env.get_charm response specially, for ease of use. If 63 owner = this.get('owner');
143 // it doesn't match that pattern, pass it through. 64 if (owner) {
144 if (response.err) { 65 tmp.unshift('~' + owner);
145 callback(true, response); 66 }
146 } else if (response.result) { 67 this.set('full_name', tmp.join('/'));
147 callback(false, response.result); 68 // charm_store_path
148 } else { // This would typically be a string. 69 this.set(
149 callback(false, response); 70 'charm_store_path',
71 [(owner ? '~' + owner : 'charms'),
72 this.get('series'),
73 (this.get('package_name') + '-' + this.get('revision')),
74 'json'
75 ].join('/'));
76 },
77 sync: function(action, options, callback) {
78 if (action !== 'read') {
79 throw (
80 'Only use the "read" action; "' + action + '" not supported.');
81 }
82 if (Y.Lang.isValue(options.get_charm)) {
83 // This is an env.
84 options.get_charm(
85 this.get('id'),
86 function(response) {
87 if (response.err) {
88 callback(true, response);
89 } else if (response.result) {
90 callback(false, response.result);
91 } else {
92 // What's going on? This does not look like either of our
93 // expected signatures. Declare a loading error.
94 callback(true, response);
95 }
96 }
97 );
98 } else if (Y.Lang.isValue(options.loadByPath)) {
99 // This is a charm store.
100 options.loadByPath(
101 this.get('charm_store_path'),
102 { success: function(response) {
103 callback(false, response);
104 },
105 failure: function(response) {
106 callback(true, response);
107 }
108 });
109 } else {
110 throw 'You must supply a get_charm or loadByPath function.';
111 }
112 },
113 parse: function() {
114 var data = Charm.superclass.parse.apply(this, arguments),
115 self = this;
116 data.is_subordinate = data.subordinate;
117 Y.each(data, function(value, key) {
118 if (!value ||
119 !self.attrAdded(key) ||
120 Y.Lang.isValue(self.get(key))) {
121 delete data[key];
122 }
123 });
124 if (data.owner === 'charmers') {
125 delete data.owner;
126 }
127 return data;
128 },
129 compare: function(other, relevance, otherRelevance) {
130 // Official charms sort before owned charms.
131 // If !X.owner, that means it is owned by charmers.
132 var owner = this.get('owner'),
133 otherOwner = other.get('owner');
134 if (!owner && otherOwner) {
135 return -1;
136 } else if (owner && !otherOwner) {
137 return 1;
138 // Relevance is next most important.
139 } else if (relevance && (relevance !== otherRelevance)) {
140 // Higher relevance comes first.
141 return otherRelevance - relevance;
142 // Otherwise sort by package name, then by owner, then by revision.
143 } else {
144 return (
145 (this.get('package_name').localeCompare(
146 other.get('package_name'))) ||
147 (owner ? owner.localeCompare(otherOwner) : 0) ||
148 (this.get('revision') - other.get('revision')));
149 }
150 }
151 }, {
152 ATTRS: {
153 id: {
154 writeOnce: true,
155 validator: function(val) {
156 return Y.Lang.isString(val) && !!charmIdRe.exec(val);
150 } 157 }
151 }, 158 },
152 // This is the optional error callback. 159 bzr_branch: {writeOnce: true},
153 function(response) { 160 charm_store_path: {writeOnce: true},
154 callback(true, response); 161 config: {writeOnce: true},
155 } 162 description: {writeOnce: true},
156 ); 163 full_name: {writeOnce: true},
157 }, 164 is_subordinate: {writeOnce: true},
158 parse: function() { 165 last_change: {
159 return _clean_charm_data(Charm.superclass.parse.apply(this, arguments)); 166 writeOnce: true,
160 }
161 }, {
162 ATTRS: {
163 id: {
164 lazyAdd: false,
165 setter: function(val) {
166 if (!val) {
167 return val;
168 }
169 var parts = parse_charm_id(val),
170 self = this;
171 parts.revision = parseInt(parts.revision, 10);
172 Y.each(parts, function(value, key) {
173 self._set(key, value);
174 });
175 this._set(
176 'charm_store_path', _calculate_charm_store_path(parts));
177 this._set('full_name', _calculate_full_charm_name(parts));
178 return _reconsititute_charm_id(parts);
179 },
180 validator: function(val) {
181 var parts = parse_charm_id(val);
182 return (parts && Y.Lang.isValue(parts.revision));
183 }
184 },
185 // All of the below are loaded except as noted.
186 bzr_branch: {writeOnce: true},
187 charm_store_path: {readOnly: true}, // calculated
188 config: {writeOnce: true},
189 description: {writeOnce: true},
190 full_name: {readOnly: true}, // calculated
191 is_subordinate: {writeOnce: true},
192 last_change:
193 { writeOnce: true,
194 setter: function(val) { 167 setter: function(val) {
195 // Normalize created value from float to date object. 168 // Normalize created value from float to date object.
196 if (val && val.created) { 169 if (val && val.created) {
197 // Mutating in place should be fine since this should only 170 // Mutating in place should be fine since this should only
198 // come from loading over the wire. 171 // come from loading over the wire.
199 val.created = new Date(val.created * 1000); 172 val.created = new Date(val.created * 1000);
200 } 173 }
201 return val; 174 return val;
202 } 175 }
203 }, 176 },
204 maintainer: {writeOnce: true}, 177 maintainer: {writeOnce: true},
205 metadata: {writeOnce: true}, 178 metadata: {writeOnce: true},
206 package_name: {readOnly: true}, // calculated 179 package_name: {writeOnce: true},
207 owner: {readOnly: true}, // calculated 180 owner: {writeOnce: true},
208 peers: {writeOnce: true}, 181 peers: {writeOnce: true},
209 proof: {writeOnce: true}, 182 proof: {writeOnce: true},
210 provides: {writeOnce: true}, 183 provides: {writeOnce: true},
211 requires: {writeOnce: true}, 184 requires: {writeOnce: true},
212 revision: {readOnly: true}, // calculated 185 revision: {
213 scheme: {readOnly: true}, // calculated 186 writeOnce: true,
214 series: {readOnly: true}, // calculated 187 setter: function(val) {
215 summary: {writeOnce: true}, 188 if (Y.Lang.isValue(val)) {
216 url: {writeOnce: true} 189 val = parseInt(val, 10);
217 } 190 }
218 }); 191 return val;
192 }
193 },
194 scheme: {
195 value: 'cs',
196 writeOnce: true,
197 setter: function(val) {
198 if (!Y.Lang.isValue(val)) {
199 val = 'cs';
200 }
201 return val;
202 }
203 },
204 series: {writeOnce: true},
205 summary: {writeOnce: true},
206 url: {writeOnce: true}
207 }
208 });
219 models.Charm = Charm; 209 models.Charm = Charm;
220 210
221 var CharmList = Y.Base.create('charmList', Y.ModelList, [], { 211 var CharmList = Y.Base.create('charmList', Y.ModelList, [], {
222 model: Charm, 212 model: Charm
223
224 initializer: function() {
225 this._baseIdHash = {}; // base id (without revision) to array of charms.
226 },
227
228 _addToBaseIdHash: function(charm) {
229 var baseId = charm.get('scheme') + ':' + charm.get('full_name'),
230 matches = this._baseIdHash[baseId];
231 if (!matches) {
232 matches = this._baseIdHash[baseId] = [];
233 }
234 matches.push(charm);
235 // Note that we don't handle changing baseIds or removed charms because
236 // that should not happen.
237 // Sort on newest charms first.
238 matches.sort(function(a, b) {
239 var revA = parseInt(a.get('revision'), 10),
240 revB = parseInt(b.get('revision'), 10);
241 return revB - revA;
242 });
243 },
244
245 add: function() {
246 var result = CharmList.superclass.add.apply(this, arguments);
247 if (Y.Lang.isArray(result)) {
248 Y.each(result, this._addToBaseIdHash, this);
249 } else {
250 this._addToBaseIdHash(result);
251 }
252 return result;
253 },
254
255 getOneByBaseId: function(id) {
256 var match = parse_charm_id(id),
257 baseId = match && _calculate_base_charm_id(match),
258 charms = baseId && this._baseIdHash[baseId];
259 return charms && charms[0];
260 },
261
262 loadOneByBaseId: function(id, options) {
263 var match = parse_charm_id(id);
264 if (match) {
265 if (!options.force) {
266 var charm = this.getOneByBaseId(_calculate_base_charm_id(match));
267 if (charm) {
268 if (options.success) {
269 options.success(charm);
270 }
271 return;
272 }
273 }
274 var path = _calculate_charm_store_path(match),
275 self = this;
276 options.charm_store.loadByPath(
277 path,
278 { success: function(data) {
279 // We fall back to 0 for revision. Some records do not have one
280 // still in the charm store, such as
281 // http://jujucharms.com/charms/precise/appflower/json (as of this
282 // writing).
283 match.revision = data.store_revision || 0;
284 id = _reconsititute_charm_id(match);
285 charm = self.getById(id);
286 if (!charm) {
287 charm = self.add({id: id});
288 charm.setAttrs(_clean_charm_data(data));
289 charm.loaded = true;
290 }
291 if (options.success) {
292 options.success(charm);
293 }
294 },
295 failure: options.failure });
296 } else {
297 throw id + ' is not a valid base charm id';
298 }
299 }
300 }, { 213 }, {
301 ATTRS: {} 214 ATTRS: {}
302 }); 215 });
303 models.CharmList = CharmList; 216 models.CharmList = CharmList;
304 217
305 }, '0.1.0', { 218 }, '0.1.0', {
306 requires: [ 219 requires: [
307 'model', 220 'model',
308 'model-list' 221 'model-list'
309 ] 222 ]
310 }); 223 });
OLDNEW
« no previous file with comments | « app/app.js ('k') | app/modules.js » ('j') | no next file with comments »

Powered by Google App Engine
RSS Feeds Recent Issues | This issue
This is Rietveld f62528b