Complete rewrite of the Hash class.

This commit is contained in:
Tobie Langel 2007-10-13 10:55:52 +00:00
parent 4607e29fa2
commit d25b863c9a
10 changed files with 235 additions and 169 deletions

View File

@ -1,5 +1,29 @@
*SVN*
* Complete rewrite of the Hash class.
!! BACKWARDS COMPATIBILITY CHANGE !! This new version of Hash is NOT backwards compatible with the former Hash class.
Properties are now hidden away in an private store to prevent the risk of collision with Hash's instance and mixed-in methods.
This implies that properties of the hash can no longer be set, accessed or deleted directly: use the new Hash#get(key), Hash#set(key, value) and Hash#unset(key) instance methods instead.
- Make $H(object) equivalent to new Hash(object). Both now return a new (cloned) instance of Hash in all circumstances.
- Make Hash#merge non-destructive.
- Add Hash#update (a destructive version of Hash#merge).
- Add Hash#clone (returns a new, cloned instance of Hash).
- Add Hash#toObject (returns a clone of the contained object).
- Add Hash#get(key) (returns the value of the specified property).
- Add Hash#set(key, value) (sets the value of the given property. returns the value).
- Add Hash#unset(key) (deletes the specified property and returns its value).
- Add Hash.from as a alias to $H for consistency with Array.from.
- Add Object.toQueryString.
- Deprecate Hash.toQueryString (use Object.toQueryString or the instance method Hash#toQueryString instead).
- Remove Hash#remove (use Hash#unset instead).
- Remove Hash.toJSON (use Object.toJSON or the instance method Hash#toJSON instead). [sam, Tobie Langel]
* Element#wrap now returns the wrapper instead of the element being wrapped. [sam]
* Namespace all custom event names to avoid conflicts with native DOM events. [sam]

View File

@ -85,7 +85,7 @@ Ajax.Request = Class.create(Ajax.Base, {
this.parameters = params;
if (params = Hash.toQueryString(params)) {
if (params = Object.toQueryString(params)) {
// when GET, append parameters to URL
if (this.method == 'get')
this.url += (this.url.include('?') ? '&' : '?') + params;

View File

@ -97,6 +97,10 @@ Object.extend(Object, {
return '{' + results.join(', ') + '}';
},
toQueryString: function(object) {
return $H(object).toQueryString();
},
toHTML: function(object) {
return object && object.toHTML ? object.toHTML() : String.interpret(object);
},

View File

@ -1,5 +1,7 @@
/*------------------------------- DEPRECATED -------------------------------*/
Hash.toQueryString = Object.toQueryString;
var Toggle = { display: Element.toggle };
Element.Methods.childOf = Element.Methods.descendantOf;

View File

@ -25,7 +25,7 @@ var Form = {
return result;
});
return options.hash ? data : Hash.toQueryString(data);
return options.hash ? data : Object.toQueryString(data);
}
};
@ -132,7 +132,7 @@ Form.Element.Methods = {
if (value != undefined) {
var pair = { };
pair[element.name] = value;
return Hash.toQueryString(pair);
return Object.toQueryString(pair);
}
}
return '';

View File

@ -1,132 +1,119 @@
var Hash = function(object) {
if (object instanceof Hash) this.merge(object);
else Object.extend(this, object || { });
};
Object.extend(Hash, {
toQueryString: function(obj) {
var parts = [];
parts.add = arguments.callee.addPair;
this.prototype._each.call(obj, function(pair) {
if (!pair.key) return;
var value = pair.value;
if (value && typeof value == 'object') {
if (Object.isArray(value)) value.each(function(value) {
parts.add(pair.key, value);
});
return;
}
parts.add(pair.key, value);
});
return parts.join('&');
},
toJSON: function(object) {
var results = [];
this.prototype._each.call(object, function(pair) {
var value = Object.toJSON(pair.value);
if (value !== undefined) results.push(pair.key.toJSON() + ': ' + value);
});
return '{' + results.join(', ') + '}';
}
});
Hash.toQueryString.addPair = function(key, value, prefix) {
key = encodeURIComponent(key);
if (value === undefined) this.push(key);
else this.push(key + '=' + (value == null ? '' : encodeURIComponent(value)));
};
Object.extend(Hash.prototype, Enumerable);
Object.extend(Hash.prototype, {
_each: function(iterator) {
for (var key in this) {
var value = this[key];
if (value && value == Hash.prototype[key]) continue;
var pair = [key, value];
pair.key = key;
pair.value = value;
iterator(pair);
}
},
keys: function() {
return this.pluck('key');
},
values: function() {
return this.pluck('value');
},
index: function(value) {
var match = this.detect(function(pair) {
return pair.value === value;
});
return match && match.key;
},
merge: function(hash) {
return $H(hash).inject(this, function(mergedHash, pair) {
mergedHash[pair.key] = pair.value;
return mergedHash;
});
},
remove: function() {
var result;
for(var i = 0, length = arguments.length; i < length; i++) {
var value = this[arguments[i]];
if (value !== undefined){
if (result === undefined) result = value;
else {
if (!Object.isArray(result)) result = [result];
result.push(value);
}
}
delete this[arguments[i]];
}
return result;
},
toQueryString: function() {
return Hash.toQueryString(this);
},
inspect: function() {
return '#<Hash:{' + this.map(function(pair) {
return pair.map(Object.inspect).join(': ');
}).join(', ') + '}>';
},
toJSON: function() {
return Hash.toJSON(this);
}
});
function $H(object) {
if (object instanceof Hash) return object;
return new Hash(object);
};
// Safari iterates over shadowed properties
if (function() {
var i = 0, Test = function(value) { this.key = value };
Test.prototype.key = 'foo';
for (var property in new Test('bar')) i++;
return i > 1;
}()) Hash.prototype._each = function(iterator) {
var cache = [];
for (var key in this) {
var value = this[key];
if ((value && value == Hash.prototype[key]) || cache.include(key)) continue;
cache.push(key);
var pair = [key, value];
pair.key = key;
pair.value = value;
iterator(pair);
var Hash = Class.create(Enumerable, (function() {
if (function() {
var i = 0, Test = function(value) { this.key = value };
Test.prototype.key = 'foo';
for (var property in new Test('bar')) i++;
return i > 1;
}()) {
function each(iterator) {
var cache = [];
for (var key in this._object) {
var value = this._object[key];
if (cache.include(key)) continue;
cache.push(key);
var pair = [key, value];
pair.key = key;
pair.value = value;
iterator(pair);
}
}
} else {
function each(iterator) {
for (var key in this._object) {
var value = this._object[key], pair = [key, value];
pair.key = key;
pair.value = value;
iterator(pair);
}
}
}
};
function toQueryPair(key, value) {
if (Object.isUndefined(value)) return key;
return key + '=' + encodeURIComponent(String.interpret(value));
}
return {
initialize: function(object) {
this._object = object instanceof Hash ? object.toObject() : Object.clone(object);
},
_each: each,
set: function(key, value) {
return this._object[key] = value;
},
get: function(key) {
return this._object[key];
},
unset: function(key) {
var value = this._object[key];
delete this._object[key];
return value;
},
toObject: function() {
return Object.clone(this._object);
},
keys: function() {
return this.pluck('key');
},
values: function() {
return this.pluck('value');
},
index: function(value) {
var match = this.detect(function(pair) {
return pair.value === value;
});
return match && match.key;
},
merge: function(object) {
return this.clone().update(object);
},
update: function(object) {
return new Hash(object).inject(this, function(result, pair) {
result.set(pair.key, pair.value);
return result;
});
},
toQueryString: function() {
return this.map(function(pair) {
var key = encodeURIComponent(pair.key), values = pair.value;
if (values && typeof values == 'object') {
if (Object.isArray(values))
return values.map(toQueryPair.curry(key)).join('&');
}
return toQueryPair(key, values);
}).join('&');
},
inspect: function() {
return '#<Hash:{' + this.map(function(pair) {
return pair.map(Object.inspect).join(': ');
}).join(', ') + '}>';
},
toJSON: function() {
return Object.toJSON(this.toObject());
},
clone: function() {
return new Hash(this);
}
}
})());
Hash.from = $H;

View File

@ -230,7 +230,11 @@
assertHashEqual({foo: 'foo', bar: [1, 2, 3], bla: null},
Object.extend(object, {bla: null}));
}},
testObjectToQueryString: function() { with(this) {
assertEqual('a=A&b=B&c=C&d=D%23', Object.toQueryString({a: 'A', b: 'B', c: 'C', d: 'D#'}));
}},
testObjectClone: function() { with(this) {
var object = {foo: 'foo', bar: [1, 2, 3]};
assertNotIdentical(object, Object.clone(object));

View File

@ -368,7 +368,7 @@
// return params
assertHashEqual(expected, Form.serialize('various', true));
// return string
assertEnumEqual(Hash.toQueryString(expected).split('&').sort(),
assertEnumEqual(Object.toQueryString(expected).split('&').sort(),
Form.serialize('various').split('&').sort());
assertEqual('string', typeof $('form').serialize({ hash:false }));

View File

@ -50,28 +50,61 @@
value_undefined: { a:"b", c:undefined },
value_null: { a:"b", c:null },
value_zero: { a:"b", c:0 },
dangerous: {
_each: 'E',
map: 'M',
keys: 'K',
values: 'V',
collect: 'C',
inject: 'I'
}
value_zero: { a:"b", c:0 }
};
new Test.Unit.Runner({
testSet: function(){ with(this) {
var h = $H({a: 'A'})
assertEqual('B', h.set('b', 'B'));
assertHashEqual({a: 'A', b: 'B'}, h);
assertUndefined(h.set('c'));
assertHashEqual({a: 'A', b: 'B', c: undefined}, h);
}},
testGet: function(){ with(this) {
assertEqual('A', $H({a: 'A'}).get('a'));
assertUndefined($H({}).get('a'));
}},
testUnset: function(){ with(this) {
var hash = $H(Fixtures.many);
assertEqual('B', hash.unset('b'));
assertHashEqual({a:'A', c: 'C', d:'D#'}, hash);
assertUndefined(hash.unset('z'));
assertHashEqual({a:'A', c: 'C', d:'D#'}, hash);
}},
testToObject: function(){ with(this) {
var hash = $H(Fixtures.many), object = hash.toObject();
assertInstanceOf(Object, object);
assertHashEqual(Fixtures.many, object);
assertNotIdentical(Fixtures.many, object);
hash.set('foo', 'bar');
assertHashNotEqual(object, hash.toObject());
}},
testConstruct: function(){ with(this) {
var h = $H(Fixtures.one);
assertNotIdentical(Fixtures.one, h);
assertIdentical(h, $H(h));
var h2 = new Hash(h);
assertNotIdentical(h, h2);
assertHashEqual(h, h2);
var object = Object.clone(Fixtures.one);
var h = new Hash(object), h2 = $H(object);
assertInstanceOf(Hash, h);
assertInstanceOf(Hash, h2);
assertHashEqual({}, new Hash());
assertHashEqual(object, h);
assertHashEqual(object, h2);
h.set('foo', 'bar');
assertHashNotEqual(object, h);
var clone = $H(h);
assertInstanceOf(Hash, clone);
assertHashEqual(h, clone);
h.set('foo', 'foo');
assertHashNotEqual(h, clone);
assertIdentical($H, Hash.from);
}},
testKeys: function(){ with(this) {
@ -87,8 +120,8 @@
assertEnumEqual($w('A B C D#'), $H(Fixtures.many).values().sort());
assertEnumEqual($w('function function'),
$H(Fixtures.functions).values().map(function(i){ return typeof i }));
assertEqual(9, $H(Fixtures.functions).quad(3));
assertEqual(6, $H(Fixtures.functions).plus(3));
assertEqual(9, $H(Fixtures.functions).get('quad')(3));
assertEqual(6, $H(Fixtures.functions).get('plus')(3));
}},
testIndex: function(){ with(this) {
@ -104,22 +137,32 @@
}},
testMerge: function(){ with(this) {
assertHashEqual(Fixtures.many, $H(Fixtures.many).merge());
assertHashEqual(Fixtures.many, $H(Fixtures.many).merge({}));
assertHashEqual(Fixtures.many, $H(Fixtures.many).merge($H()));
assertHashEqual({a:'A', b:'B', c:'C', d:'D#', aaa:'AAA' }, $H(Fixtures.many).merge({aaa: 'AAA'}));
assertHashEqual({a:'A#', b:'B', c:'C', d:'D#' }, $H(Fixtures.many).merge(Fixtures.one));
var h = $H(Fixtures.many);
assertNotIdentical(h, h.merge());
assertNotIdentical(h, h.merge({}));
assertInstanceOf(Hash, h.merge());
assertInstanceOf(Hash, h.merge({}));
assertHashEqual(h, h.merge());
assertHashEqual(h, h.merge({}));
assertHashEqual(h, h.merge($H()));
assertHashEqual({a:'A', b:'B', c:'C', d:'D#', aaa:'AAA' }, h.merge({aaa: 'AAA'}));
assertHashEqual({a:'A#', b:'B', c:'C', d:'D#' }, h.merge(Fixtures.one));
}},
testRemove: function(){ with(this) {
var hash = $H(Fixtures.many);
var values = hash.remove('b', 'c');
assertHashEqual({a:'A', d:'D#'}, hash);
assertEnumEqual($w('B C'), values);
testUpdate: function(){ with(this) {
var h = $H(Fixtures.many);
assertIdentical(h, h.update());
assertIdentical(h, h.update({}));
assertHashEqual(h, h.update());
assertHashEqual(h, h.update({}));
assertHashEqual(h, h.update($H()));
assertHashEqual({a:'A', b:'B', c:'C', d:'D#', aaa:'AAA' }, h.update({aaa: 'AAA'}));
assertHashEqual({a:'A#', b:'B', c:'C', d:'D#', aaa:'AAA' }, h.update(Fixtures.one));
}},
testToQueryString: function(){ with(this) {
assertEqual('', $H({}).toQueryString());
assertEqual('a%23=A', $H({'a#': 'A'}).toQueryString());
assertEqual('a=A%23', $H(Fixtures.one).toQueryString());
assertEqual('a=A&b=B&c=C&d=D%23', $H(Fixtures.many).toQueryString());
assertEqual("a=b&c", $H(Fixtures.value_undefined).toQueryString());
@ -132,10 +175,7 @@
assertEqual("", $H(Fixtures.multiple_empty).toQueryString());
assertEqual("stuff%5B%5D=%24&stuff%5B%5D=a&stuff%5B%5D=%3B", $H(Fixtures.multiple_special).toQueryString());
assertHashEqual(Fixtures.multiple_special, $H(Fixtures.multiple_special).toQueryString().toQueryParams());
var danger = $w("_each=E collect=C inject=I keys=K map=M values=V");
assertEnumEqual(danger, Hash.toQueryString(Fixtures.dangerous).split('&').sort());
assertEnumEqual(danger, $H(Fixtures.dangerous).toQueryString().split('&').sort());
assertIdentical(Object.toQueryString, Hash.toQueryString);
}},
testInspect: function(){ with(this) {
@ -143,12 +183,17 @@
assertEqual("#<Hash:{'a': 'A#'}>", $H(Fixtures.one).inspect());
assertEqual("#<Hash:{'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D#'}>", $H(Fixtures.many).inspect());
}},
testClone: function(){ with(this) {
var h = $H(Fixtures.many);
assertHashEqual(h, h.clone());
assertInstanceOf(Hash, h.clone());
assertNotIdentical(h, h.clone());
}},
testToJSON: function(){ with(this) {
assertEqual('{\"b\": [false, true], \"c\": {\"a\": \"hello!\"}}',
$H({'b': [undefined, false, true, undefined], c: {a: 'hello!'}}).toJSON());
assertEqual('E', eval('(' + $H(Fixtures.dangerous).toJSON() + ')')._each);
}}
}, 'testlog');
// ]]>

View File

@ -58,7 +58,7 @@
testIsRunningFromRake: function() { with(this) {
if (window.location.toString().startsWith('http')) {
assert(isRunningFromRake);
info('These tests are runingn from rake.')
info('These tests are running from rake.')
} else {
assert(!isRunningFromRake);
info('These tests are *not* running from rake.')