From e544ad30fc83d6e5172d9cb838330339f90492e1 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 2 Dec 2010 10:59:20 +0000 Subject: [PATCH] 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 --- plugins/pilot/lib/settings.js | 4 +- plugins/pilot/lib/types.js | 90 +++++++++++++++++++++-------- plugins/pilot/lib/types/basic.js | 97 +++++++++++++++++++++++--------- 3 files changed, 137 insertions(+), 54 deletions(-) diff --git a/plugins/pilot/lib/settings.js b/plugins/pilot/lib/settings.js index 1f7ed2ea..b427b2af 100644 --- a/plugins/pilot/lib/settings.js +++ b/plugins/pilot/lib/settings.js @@ -240,7 +240,7 @@ Settings.prototype = { if (data.hasOwnProperty(key)) { var setting = this._settings[key]; if (setting) { - var value = setting.type.fromString(data[key]); + var value = setting.type.parse(data[key]); this.set(key, value); } else { this.set(key, data[key]); @@ -254,7 +254,7 @@ Settings.prototype = { */ _saveToObject: function() { 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)); }, diff --git a/plugins/pilot/lib/types.js b/plugins/pilot/lib/types.js index d98fcaed..a0e76cd2 100644 --- a/plugins/pilot/lib/types.js +++ b/plugins/pilot/lib/types.js @@ -49,28 +49,41 @@ var Status = { */ 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 * reason for failure should have been provided. In addition some completion * values may be available. * @see Status.INCOMPLETE */ - INVALID: 2, + INVALID: 3, /** - * The conversion process did not work like Status.INVALID, however it was - * 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 + * A combined status is the worser of the provided statuses */ - 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; /** - * The type.fromString() method returns a Conversion to inform the user about - * not only the result of a Conversion but also about what went wrong. + * The type.parse() method returns a Conversion to inform the user about not + * 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 * 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 @@ -123,7 +136,7 @@ Type.prototype = { * Where possible, there should be round-tripping between values and their * string representations. */ - toString: function(value) { throw new Error("not implemented"); }, + stringify: function(value) { throw new Error("not implemented"); }, /** * Convert the given str to an instance of this type. @@ -131,7 +144,7 @@ Type.prototype = { * string representations. * @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 @@ -152,10 +165,35 @@ exports.Type = Type; 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) { - 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]; }; +/** + * 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() */ exports.getType = function(typeSpec) { - var type; if (typeof typeSpec === 'string') { - type = types[typeSpec]; + return reconstituteType(typeSpec, typeSpec); } if (typeof typeSpec == 'object') { if (!typeSpec.name) { throw new Error('Missing \'name\' member to typeSpec'); } - - type = types[typeSpec.name]; + return reconstituteType(typeSpec.name, typeSpec); } - if (type instanceof Type) { - return type; - } - - if (typeof type === 'function') { - return type(typeSpec); - } + throw new Error('Can\'t extract type from ' + typeSpec); }; diff --git a/plugins/pilot/lib/types/basic.js b/plugins/pilot/lib/types/basic.js index f7ad2859..52b4c215 100644 --- a/plugins/pilot/lib/types/basic.js +++ b/plugins/pilot/lib/types/basic.js @@ -41,6 +41,7 @@ define(function(require, exports, module) { var types = require("pilot/types"); var Type = types.Type; var Conversion = types.Conversion; +var Status = types.Status; /** * 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(); -text.toString = function(value) { +text.stringify = function(value) { return value; }; -text.fromString = function(value) { +text.parse = function(value) { 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); }; @@ -77,16 +78,16 @@ text.name = 'text'; */ var number = new Type(); -number.toString = function(value) { +number.stringify = function(value) { if (!value) { return null; } return '' + value; }; -number.fromString = function(value) { +number.parse = function(value) { 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)); @@ -103,24 +104,29 @@ number.name = 'number'; /** * One of a known set of options */ -function SelectionType(data) { - this._data = data; +function SelectionType(typeSpec) { + 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.toString = function(value) { +SelectionType.prototype.stringify = function(value) { return value; }; -SelectionType.prototype.fromString = function(value) { +SelectionType.prototype.parse = function(value) { 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.'); } - var data = (typeof(this._data) === "function") ? this._data() : this._data; + var data = (typeof(this.data) === "function") ? this.data() : this.data; var match = false; var completions = []; @@ -143,37 +149,70 @@ SelectionType.prototype.fromString = function(value) { // TODO: better completions - we're just using the extensions return new Conversion(null, status, - 'Can\'t convert "' + value + '" to a selection.', + 'Can\'t convert \'' + value + '\' to a selection.', completions); } }; SelectionType.prototype.name = 'selection'; +/** + * SelectionType is a base class for other types + */ +exports.SelectionType = SelectionType; /** * 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) { - return '' + value; + if (conversion.value === 'true') { + 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) { - var conversion = SelectionType.prototype.fromString(value); +DeferredType.prototype = new Type(); - if (conversion.value === 'true') { - conversion.value = true; - } - if (conversion.value === 'false') { - conversion.value = false; - } - - return conversion; +DeferredType.prototype.stringify = function(value) { + return this.defer.stringify(value); }; -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. @@ -183,6 +222,7 @@ exports.startup = function() { types.registerType(number); types.registerType(bool); types.registerType(SelectionType); + types.registerType(DeferredType); }; exports.shutdown = function() { @@ -190,6 +230,7 @@ exports.shutdown = function() { types.unregisterType(number); types.unregisterType(bool); types.unregisterType(SelectionType); + types.unregisterType(DeferredType); };