'use strict';
/**
* Provide the Charm and CharmList classes.
*
* @module models
* @submodule models.charm
*/
YUI.add('juju-charm-models', function(Y) {
var RECENT_DAYS = 30;
var models = Y.namespace('juju.models');
var charmIdRe = /^(?:(\w+):)?(?:~(\S+)\/)?(\w+)\/(\S+?)-(\d+)$/;
var idElements = ['scheme', 'owner', 'series', 'package_name', 'revision'];
var simpleCharmIdRe = /^(?:(\w+):)?(?!:~)(\w+)$/;
var simpleIdElements = ['scheme', 'package_name'];
var parseCharmId = models.parseCharmId = function(charmId, defaultSeries) {
if (typeof charmId === 'string') {
var parts = charmIdRe.exec(charmId);
var pairs;
if (parts) {
parts.shift(); // Get rid of the first, full string.
pairs = Y.Array.zip(idElements, parts);
} else if (defaultSeries) {
parts = simpleCharmIdRe.exec(charmId);
if (parts) {
parts.shift(); // Get rid of the first, full string.
pairs = Y.Array.zip(simpleIdElements, parts);
pairs.push(['series', defaultSeries]);
}
}
if (parts) {
var result = {};
Y.Array.map(pairs, function(pair) { result[pair[0]] = pair[1]; });
result.charm_store_path = [
(result.owner ? '~' + result.owner : 'charms'),
result.series,
result.package_name + (
result.revision ? '-' + result.revision : ''),
'json'
].join('/');
return result;
}
}
};
/**
* Helper to use a setter so that we can set null when the api returns an
* empty object.
*
* @method unsetIfNoValue
* @param {Object} val the Object to check if it's empty.
*
*/
var unsetIfNoValue = function(val) {
if (Y.Object.keys(val).length === 0) {
return null;
} else {
return val;
}
};
/**
* Charms, once instantiated and loaded with data from their respective
* sources, are immutable and read-only. This reflects the reality of how
* we interact with them.
*
* Charm instances can represent both environment charms and charm store
* charms. A charm id is reliably and uniquely associated with a given
* charm only within a given context: the environment or the charm store.
*
* Charms begin their lives with full charm ids, as provided by
* services in the environment and the charm store:
*
* `[SCHEME]:(~[OWNER]/)?[SERIES]/[PACKAGE NAME]-[REVISION]`
*
* With an id, we can instantiate a charm: typically we use
* `db.charms.add({id: [ID]})`. Finally, we load the charm's data over the
* network using the standard YUI Model method `load`, providing an object
* with a get_charm callable, and an optional callback (see YUI docs). Both
* the env and the charm store have a `get_charm` method, so, by design, it
* works easily: `charm.load(env, optionalCallback)` or
* `charm.load(charm_store, optionalCallback)`. The `get_charm` method must
* either callback using the default YUI approach for this code, a boolean
* indicating failure, and a result; or it must return what the env version
* does: an object with a `result` object containing the charm data, or an
* object with an `err` attribute.
*
* In both cases, environment charms and charm store charms, a charm's
* `loaded` attribute is set to true once it has all the data from its
* environment.
*
* @class Charm
*/
var Charm = Y.Base.create('charm', Y.Model, [], {
initializer: function() {
var id = this.get('id'),
parts = parseCharmId(id),
self = this;
if (!parts) {
throw 'Developers must initialize charms with a well-formed id.';
}
this.loaded = false;
this.on('load', function() { this.loaded = true; });
Y.Object.each(
parts,
function(value, key) { self.set(key, value); });
},
sync: function(action, options, callback) {
if (action !== 'read') {
throw (
'Only use the "read" action; "' + action + '" not supported.');
}
if (Y.Lang.isValue(options.get_charm)) {
// This is an env.
options.get_charm(
this.get('id'),
function(response) {
if (response.err) {
callback(true, response);
} else if (response.result) {
callback(false, response.result);
} else {
// What's going on? This does not look like either of our
// expected signatures. Declare a loading error.
callback(true, response);
}
}
);
} else if (Y.Lang.isValue(options.loadByPath)) {
// This is a charm store.
options.loadByPath(
this.get('charm_store_path'),
{ success: function(response) {
callback(false, response);
},
failure: function(response) {
callback(true, response);
}
});
} else {
throw 'You must supply a get_charm or loadByPath function.';
}
},
parse: function() {
var data = Charm.superclass.parse.apply(this, arguments),
self = this;
data.is_subordinate = data.subordinate;
Y.each(data, function(value, key) {
if (!value ||
!self.attrAdded(key) ||
Y.Lang.isValue(self.get(key))) {
delete data[key];
}
});
if (data.owner === 'charmers') {
delete data.owner;
}
return data;
},
compare: function(other, relevance, otherRelevance) {
// Official charms sort before owned charms.
// If !X.owner, that means it is owned by charmers.
var owner = this.get('owner'),
otherOwner = other.get('owner');
if (!owner && otherOwner) {
return -1;
} else if (owner && !otherOwner) {
return 1;
// Relevance is next most important.
} else if (relevance && (relevance !== otherRelevance)) {
// Higher relevance comes first.
return otherRelevance - relevance;
// Otherwise sort by package name, then by owner, then by revision.
} else {
return (
(this.get('package_name').localeCompare(
other.get('package_name'))) ||
(owner ? owner.localeCompare(otherOwner) : 0) ||
(this.get('revision') - other.get('revision')));
}
}
}, {
ATTRS: {
id: {
validator: function(val) {
return Y.Lang.isString(val) && !!charmIdRe.exec(val);
}
},
bzr_branch: {},
charm_store_path: {
/**
* Generate the charm store path from the attributes of the charm.
*
* @method getter
*
*/
getter: function() {
// charm_store_path
var owner = this.get('owner');
return [
(owner ? '~' + owner : 'charms'),
this.get('series'),
(this.get('package_name') + '-' + this.get('revision')),
'json'
].join('/');
}
},
config: {},
description: {},
full_name: {
/**
* Generate the full name of the charm from its attributes.
*
* @method geetter
*
*/
getter: function() {
// full_name
var tmp = [this.get('series'), this.get('package_name')],
owner = this.get('owner');
if (owner) {
tmp.unshift('~' + owner);
}
return tmp.join('/');
}
},
is_subordinate: {},
last_change: {
/**
* Normalize created value from float to date object.
*
* @method last_change.writeOnce.setter
*/
setter: function(val) {
if (val && val.created) {
// Mutating in place should be fine since this should only
// come from loading over the wire.
val.created = new Date(val.created * 1000);
}
return val;
}
},
maintainer: {},
metadata: {},
owner: {},
package_name: {},
peers: {},
proof: {},
provides: {},
requires: {},
revision: {
/**
* Parse the revision number out of a string.
*
* @method revision.setter
*/
setter: function(val) {
return parseInt(val, 10);
}
},
scheme: {
value: 'cs',
/**
* If no value is given, "cs" is used as the default.
*
* @method scheme.setter
*/
setter: function(val) {
if (!Y.Lang.isValue(val)) {
val = 'cs';
}
return val;
}
},
series: {},
summary: {},
url: {}
}
});
models.Charm = Charm;
models.charmIdRe = charmIdRe;
/**
* The database keeps the charms separate in two different CharmList
* instances. One is `db.charms`, representing the environment charms.
* The other, from the charm store, is maintained by and within the
* persistent charm panel instance. As you would expect, environment
* charms are what to use when viewing or manipulating the environment.
* Charm store charms are what we can browse to select and deploy new
* charms to the environment.
*
* @class CharmList
*/
var CharmList = Y.Base.create('charmList', Y.ModelList, [], {
model: Charm
}, {
ATTRS: {}
});
models.CharmList = CharmList;
/**
* Model to represent the Charms from the Charmworld0 Api.
*
* @class BrowserCharm
* @extends {Charm}
*
*/
models.BrowserCharm = Y.Base.create('browser-charm', Charm, [], {
/**
* Load the recent commits into a format we can use nicely.
*
* @method _loadRecentCommits
*
*/
_loadRecentCommits: function() {
var source = this.get('code_source'),
commits = [];
if (source && source.revisions) {
Y.Array.each(source.revisions, function(commit) {
commits.push({
author: {
name: commit.authors[0].name,
email: commit.authors[0].email
},
date: new Date(commit.date),
message: commit.message,
revno: commit.revno
});
});
}
return commits;
},
/**
* Parse the relations ATTR from the api into specific provides/requires
* information.
*
* @method _parseRelations
* @param {String} attr the attribute to load from the relations object.
*
*/
_parseRelations: function(attr) {
var relations = this.get('relations');
if (relations && relations[attr]) {
return relations[attr];
} else {
return null;
}
},
/**
* Initializer
*
* @method initializer
* @param {Object} cfg The configuration object.
*/
initializer: function(cfg) {
if (cfg && cfg.downloads_in_past_30_days) {
this.set('recent_download_count', cfg.downloads_in_past_30_days);
}
}
}, {
ATTRS: {
id: {
validator: function(val) {
return Y.Lang.isString(val) && !!charmIdRe.exec(val);
}
},
bzr_branch: {},
categories: {
value: []
},
changelog: {
value: {}
},
charm_store_path: {},
/**
* Object of data about the source for this charm including bugs link,
* log, revisions, etc.
*
* @attribute code_source
* @default undefined
* @type {Object}
*
*/
code_source: {},
date_created: {},
description: {},
files: {
value: {}
},
full_name: {
/**
* Generate the full name of the charm from its attributes.
*
* @method full_name.getter
*
*/
getter: function() {
// full_name
var tmp = [this.get('series'), this.get('package_name')],
owner = this.get('owner');
if (owner) {
tmp.unshift('~' + owner);
}
return tmp.join('/');
}
},
is_approved: {},
is_new: {},
is_popular: {},
is_subordinate: {},
last_change: {
/**
* Normalize created value from float to date object.
*
* @method last_change.setter
*/
setter: function(val) {
if (val && val.created) {
// Mutating in place should be fine since this should only
// come from loading over the wire.
val.created = new Date(val.created * 1000);
}
return val;
}
},
maintainer: {},
metadata: {},
name: {},
icon: {},
/**
* options is the parsed YAML object from config.yaml in a charm. Do not
* set a value if there are no options to be had.
*
* @attribute options
* @default undefined
* @type {Object}
*
*/
options: {
setter: 'unsetIfNoValue'
},
owner: {},
peers: {},
proof: {},
/**
* This attr is a mapper to the relations ATTR in the new API. It's
* provided for backwards compatibility with the original Charm model.
* This can be removed when Charmworld0 is the one true model used in
* all Juju Gui code.
*
* @attribute provides
* @default undefined
* @type {Object}
*
*/
provides: {
/**
* provides is a subcomponent of relations in the new api.
*
* @method provides.getter
*
*/
getter: function(value, key) {
return this._parseRelations(key);
}
},
rating_numerator: {},
rating_denominator: {},
/**
* @attribute recent_commit_count
* @default 0
* @type {Int}
*
*/
'recent_commit_count': {
/**
* @method recent_commit_count.getter
* @return {Int} count of the commits in 'recent' time.
*
*/
getter: function() {
var count = 0,
commits = this.get('recent_commits'),
today = new Date(),
recentAgo = new Date();
recentAgo.setDate(today.getDate() - RECENT_DAYS);
Y.Array.each(commits, function(commit) {
if (commit.date > recentAgo) {
count += 1;
}
});
return count;
}
},
/**
* @attribute recent_commits
* @default undefined
* @type {Array} list of objects for each commit.
*
*/
'recent_commits': {
/**
* Return the commits of the charm in a format we can live with from
* the source code data provided by the api.
*
* @method recent_commits.valueFn
*
*/
valueFn: function() {
return this._loadRecentCommits();
}
},
/**
* Mapped from the downloads_in_past_30_days in the API.
*
* @attribute recent_download_count
* @default undefined
* @type {Int} number of downloads in 'recent' time.
*
*/
recent_download_count: {
/**
* @method recent_download_count.valueFn
* @return {Int} the number of downloads in the 'recent' time frame.
*
*/
valueFn: function() {
return 0;
}
},
relations: {},
/**
* This attr is a mapper to the relations ATTR in the new API. It's
* provided for backwards compatibility with the original Charm model.
*
* This can be removed when Charmworld0 is the one true model used in
* all Juju Gui code.
*
* @attribute requires
* @default undefined
* @type {Object}
*
*/
requires: {
/**
* requires is a subcomponent of relations in the new api.
*
* @method requires.getter
*
*/
getter: function(value, key) {
return this._parseRelations(key);
}
},
revision: {
/**
* Parse the revision number out of a string.
*
* @method revision.setter
*/
setter: function(val) {
return parseInt(val, 10);
}
},
scheme: {
value: 'cs',
/**
* If no value is given, "cs" is used as the default.
*
* @method scheme.setter
*/
setter: function(val) {
if (!Y.Lang.isValue(val)) {
val = 'cs';
}
return val;
}
},
series: {},
summary: {},
tested_providers: {},
url: {}
}
});
/**
* BrowserCharmList is set of BrowserCharms.
*
* @class BrowserCharmList
*/
models.BrowserCharmList = Y.Base.create('browserCharmList', Y.ModelList, [], {
model: models.BrowserCharm
}, {
ATTRS: {}
});
}, '0.1.0', {
requires: [
'model',
'model-list',
'juju-charm-id'
]
});