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)) {
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));
},

View file

@ -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 <tt>str</tt> 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);
};

View file

@ -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);
};