rename Type methods: s/toString/stringify and s/fromString/parse to avoid clash with built in meaning for toString. Also add lots of documentation, a new DeferredType, and lots more checks to how types are created

This commit is contained in:
Joe Walker 2010-12-02 10:59:20 +00:00
commit e544ad30fc
3 changed files with 137 additions and 54 deletions

View file

@ -240,7 +240,7 @@ Settings.prototype = {
if (data.hasOwnProperty(key)) { if (data.hasOwnProperty(key)) {
var setting = this._settings[key]; var setting = this._settings[key];
if (setting) { if (setting) {
var value = setting.type.fromString(data[key]); var value = setting.type.parse(data[key]);
this.set(key, value); this.set(key, value);
} else { } else {
this.set(key, data[key]); this.set(key, data[key]);
@ -254,7 +254,7 @@ Settings.prototype = {
*/ */
_saveToObject: function() { _saveToObject: function() {
return this.getSettingNames().map(function(key) { return this.getSettingNames().map(function(key) {
return this._settings[key].type.toString(this.get(key)); return this._settings[key].type.stringify(this.get(key));
}.bind(this)); }.bind(this));
}, },

View file

@ -49,28 +49,41 @@ var Status = {
*/ */
VALID: 1, VALID: 1,
/**
* The conversion process did not work like Status.INVALID, however it was
* noted that the string provided to 'parse()' could be VALID by the
* addition of more characters, so the typing may not be actually incorrect
* yet, just unfinished.
* @see Status.INVALID
*/
INCOMPLETE: 2,
/** /**
* The conversion process did not work, the value should be null and a * The conversion process did not work, the value should be null and a
* reason for failure should have been provided. In addition some completion * reason for failure should have been provided. In addition some completion
* values may be available. * values may be available.
* @see Status.INCOMPLETE * @see Status.INCOMPLETE
*/ */
INVALID: 2, INVALID: 3,
/** /**
* The conversion process did not work like Status.INVALID, however it was * A combined status is the worser of the provided statuses
* noted that the string provided to 'fromString()' could be VALID by
* the addition of more characters, so the typing may not be actually
* incorrect yet, just unfinished.
* @see Status.INVALID
*/ */
INCOMPLETE: 3 combine: function(statuses) {
var combined = Status.VALID;
for (var i = 0; i < arguments; i++) {
if (arguments[i] > combined) {
combined = arguments[i];
}
}
return combined;
}
}; };
exports.Status = Status; exports.Status = Status;
/** /**
* The type.fromString() method returns a Conversion to inform the user about * The type.parse() method returns a Conversion to inform the user about not
* not only the result of a Conversion but also about what went wrong. * only the result of a Conversion but also about what went wrong.
* We could use an exception, and throw if the conversion failed, but that * We could use an exception, and throw if the conversion failed, but that
* seems to violate the idea that exceptions should be exceptional. Typos are * seems to violate the idea that exceptions should be exceptional. Typos are
* not. Also in order to store both a status and a message we'd still need * not. Also in order to store both a status and a message we'd still need
@ -123,7 +136,7 @@ Type.prototype = {
* Where possible, there should be round-tripping between values and their * Where possible, there should be round-tripping between values and their
* string representations. * string representations.
*/ */
toString: function(value) { throw new Error("not implemented"); }, stringify: function(value) { throw new Error("not implemented"); },
/** /**
* Convert the given <tt>str</tt> to an instance of this type. * Convert the given <tt>str</tt> to an instance of this type.
@ -131,7 +144,7 @@ Type.prototype = {
* string representations. * string representations.
* @return Conversion * @return Conversion
*/ */
fromString: function(str) { throw new Error("not implemented"); }, parse: function(str) { throw new Error("not implemented"); },
/** /**
* The plug-in system, and other things need to know what this type is * The plug-in system, and other things need to know what this type is
@ -152,10 +165,35 @@ exports.Type = Type;
var types = {}; var types = {};
/** /**
* Add a new type to the list available to the system * Add a new type to the list available to the system.
* You can pass 2 things to this function - either an instance of Type, in
* which case we return this instance when #getType() is called with a 'name'
* that matches type.name.
* Also you can pass in a constructor (i.e. function) in which case when
* #getType() is called with a 'name' that matches Type.prototype.name we will
* pass the typeSpec into this constructor. See #reconstituteType().
*/ */
exports.registerType = function(type) { exports.registerType = function(type) {
types[type.name] = type; if (typeof type === 'object') {
if (type instanceof Type) {
if (!type.name) {
throw new Error('All registered types must have a name');
}
types[type.name] = type;
}
else {
throw new Error('Can\'t registerType using: ' + type);
}
}
else if (typeof type === 'function') {
if (!type.prototype.name) {
throw new Error('All registered types must have a name');
}
types[type.prototype.name] = type;
}
else {
throw new Error('Unknown type: ' + type);
}
}; };
/** /**
@ -165,30 +203,34 @@ exports.deregisterType = function(type) {
delete types[type.name]; delete types[type.name];
}; };
/**
* See description of #exports.registerType()
*/
function reconstituteType(name, typeSpec) {
var type = types[name];
if (typeof type === 'function') {
// TODO: should we complain if typeSpec is a string?
type = new type(typeSpec);
}
return type;
}
/** /**
* Find a type, previously registered using #registerType() * Find a type, previously registered using #registerType()
*/ */
exports.getType = function(typeSpec) { exports.getType = function(typeSpec) {
var type;
if (typeof typeSpec === 'string') { if (typeof typeSpec === 'string') {
type = types[typeSpec]; return reconstituteType(typeSpec, typeSpec);
} }
if (typeof typeSpec == 'object') { if (typeof typeSpec == 'object') {
if (!typeSpec.name) { if (!typeSpec.name) {
throw new Error('Missing \'name\' member to typeSpec'); throw new Error('Missing \'name\' member to typeSpec');
} }
return reconstituteType(typeSpec.name, typeSpec);
type = types[typeSpec.name];
} }
if (type instanceof Type) { throw new Error('Can\'t extract type from ' + typeSpec);
return type;
}
if (typeof type === 'function') {
return type(typeSpec);
}
}; };

View file

@ -41,6 +41,7 @@ define(function(require, exports, module) {
var types = require("pilot/types"); var types = require("pilot/types");
var Type = types.Type; var Type = types.Type;
var Conversion = types.Conversion; var Conversion = types.Conversion;
var Status = types.Status;
/** /**
* These are the basic types that we accept. They are vaguely based on the * These are the basic types that we accept. They are vaguely based on the
@ -59,13 +60,13 @@ var Conversion = types.Conversion;
*/ */
var text = new Type(); var text = new Type();
text.toString = function(value) { text.stringify = function(value) {
return value; return value;
}; };
text.fromString = function(value) { text.parse = function(value) {
if (typeof value != 'string') { if (typeof value != 'string') {
throw new Error('non-string passed to text.fromString()'); throw new Error('non-string passed to text.parse()');
} }
return new Conversion(value); return new Conversion(value);
}; };
@ -77,16 +78,16 @@ text.name = 'text';
*/ */
var number = new Type(); var number = new Type();
number.toString = function(value) { number.stringify = function(value) {
if (!value) { if (!value) {
return null; return null;
} }
return '' + value; return '' + value;
}; };
number.fromString = function(value) { number.parse = function(value) {
if (typeof value != 'string') { if (typeof value != 'string') {
throw new Error('non-string passed to number.fromString()'); throw new Error('non-string passed to number.parse()');
} }
var reply = new Conversion(parseInt(value, 10)); var reply = new Conversion(parseInt(value, 10));
@ -103,24 +104,29 @@ number.name = 'number';
/** /**
* One of a known set of options * One of a known set of options
*/ */
function SelectionType(data) { function SelectionType(typeSpec) {
this._data = data; if (!Array.isArray(typeSpec.data) && typeof typeSpec.data !== 'function') {
throw new Error('instances of SelectionType need typeSpec.data to be an array or function that returns an array:' + JSON.stringify(typeSpec));
}
Object.keys(typeSpec).forEach(function(key) {
this[key] = typeSpec[key];
}, this);
}; };
SelectionType.prototype = new Type(); SelectionType.prototype = new Type();
SelectionType.prototype.toString = function(value) { SelectionType.prototype.stringify = function(value) {
return value; return value;
}; };
SelectionType.prototype.fromString = function(value) { SelectionType.prototype.parse = function(value) {
if (typeof value != 'string') { if (typeof value != 'string') {
throw new Error('non-string passed to fromString()'); throw new Error('non-string passed to parse()');
} }
if (!this._data) { if (!this.data) {
throw new Error('Missing data on selection type extension.'); throw new Error('Missing data on selection type extension.');
} }
var data = (typeof(this._data) === "function") ? this._data() : this._data; var data = (typeof(this.data) === "function") ? this.data() : this.data;
var match = false; var match = false;
var completions = []; var completions = [];
@ -143,37 +149,70 @@ SelectionType.prototype.fromString = function(value) {
// TODO: better completions - we're just using the extensions // TODO: better completions - we're just using the extensions
return new Conversion(null, return new Conversion(null,
status, status,
'Can\'t convert "' + value + '" to a selection.', 'Can\'t convert \'' + value + '\' to a selection.',
completions); completions);
} }
}; };
SelectionType.prototype.name = 'selection'; SelectionType.prototype.name = 'selection';
/**
* SelectionType is a base class for other types
*/
exports.SelectionType = SelectionType;
/** /**
* true/false values * true/false values
*/ */
var bool = new SelectionType([ 'true', 'false' ]); var bool = new SelectionType({
name: 'bool',
data: [ 'true', 'false' ],
stringify: function(value) {
return '' + value;
},
parse: function(value) {
var conversion = SelectionType.prototype.parse(value);
bool.toString = function(value) { if (conversion.value === 'true') {
return '' + value; conversion.value = true;
}
if (conversion.value === 'false') {
conversion.value = false;
}
return conversion;
}
});
/**
* One of a known set of options
*/
function DeferredType(typeSpec) {
if (typeof typeSpec.defer !== 'function') {
throw new Error('Instances of DeferredType need typeSpec.defer to be a function that returns a type');
}
Object.keys(typeSpec).forEach(function(key) {
this[key] = typeSpec[key];
}, this);
}; };
bool.fromString = function(value) { DeferredType.prototype = new Type();
var conversion = SelectionType.prototype.fromString(value);
if (conversion.value === 'true') { DeferredType.prototype.stringify = function(value) {
conversion.value = true; return this.defer.stringify(value);
}
if (conversion.value === 'false') {
conversion.value = false;
}
return conversion;
}; };
bool.name = 'bool'; DeferredType.prototype.parse = function(value) {
return this.defer.parse(value);
};
DeferredType.prototype.name = 'deferred';
/**
* DeferredType is a base class for other types
*/
exports.DeferredType = DeferredType;
/** /**
* Registration and de-registration. * Registration and de-registration.
@ -183,6 +222,7 @@ exports.startup = function() {
types.registerType(number); types.registerType(number);
types.registerType(bool); types.registerType(bool);
types.registerType(SelectionType); types.registerType(SelectionType);
types.registerType(DeferredType);
}; };
exports.shutdown = function() { exports.shutdown = function() {
@ -190,6 +230,7 @@ exports.shutdown = function() {
types.unregisterType(number); types.unregisterType(number);
types.unregisterType(bool); types.unregisterType(bool);
types.unregisterType(SelectionType); types.unregisterType(SelectionType);
types.unregisterType(DeferredType);
}; };