From 6038bf698c522c1883a1113c834e53256b35584f Mon Sep 17 00:00:00 2001 From: Mikko Tiihonen Date: Sat, 23 Jul 2016 20:13:16 +0300 Subject: [PATCH] Allow configuring how the set of preferred encodings is sorted. Options: client: use client quality levels and client order for equal quality levels clientThenServer: use client quality levels and server order for equal quality levels server: use server order The 'client' preferred order is the default to match with previous implementation. Signed-off-by: Jonas Berlin --- HISTORY.md | 5 ++ README.md | 14 +++++- index.js | 8 ++-- lib/encoding.js | 39 ++++++++++++--- test/encoding.js | 121 ++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 173 insertions(+), 14 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 10b6917..04f3552 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,8 @@ +unreleased +========== + + * Add configurable preferred sorting order for `Accept-Encoding` + 0.6.1 / 2016-05-02 ================== diff --git a/README.md b/README.md index 04a67ff..e3d5cee 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ You can check a working example at `examples/encoding.js`. Returns the most preferred encoding from the client. -##### encoding(availableEncodings) +##### encoding(availableEncodings, options) Returns the most preferred encoding from a list of available encodings. @@ -176,11 +176,21 @@ Returns the most preferred encoding from a list of available encodings. Returns an array of preferred encodings ordered by the client preference. -##### encodings(availableEncodings) +##### encodings(availableEncodings, options) Returns an array of preferred encodings ordered by priority from a list of available encodings. +The options has one available option. The default value if not specified is +```js +{ sortPreference: 'client' } +``` + +#### Sort options +`client`: sorts first by client quality level and then by client given order +`clientThenServer`: sorts first by client quality level and then by server given order +`server`: sorts by server given order + ## See Also The [accepts](https://npmjs.org/package/accepts#readme) module builds on diff --git a/index.js b/index.js index 8d4f6a2..a2eb666 100644 --- a/index.js +++ b/index.js @@ -47,14 +47,14 @@ Negotiator.prototype.charsets = function charsets(available) { return preferredCharsets(this.request.headers['accept-charset'], available); }; -Negotiator.prototype.encoding = function encoding(available) { - var set = this.encodings(available); +Negotiator.prototype.encoding = function encoding(available, options) { + var set = this.encodings(available, options); return set && set[0]; }; -Negotiator.prototype.encodings = function encodings(available) { +Negotiator.prototype.encodings = function encodings(available, options) { var preferredEncodings = loadModule('encoding').preferredEncodings; - return preferredEncodings(this.request.headers['accept-encoding'], available); + return preferredEncodings(this.request.headers['accept-encoding'], available, options); }; Negotiator.prototype.language = function language(available) { diff --git a/lib/encoding.js b/lib/encoding.js index 70ac3de..1c060f2 100644 --- a/lib/encoding.js +++ b/lib/encoding.js @@ -22,6 +22,11 @@ module.exports.preferredEncodings = preferredEncodings; */ var simpleEncodingRegExp = /^\s*([^\s;]+)\s*(?:;(.*))?$/; +var comparators = { + client: compareSpecsPreferClient, + clientThenServer: compareSpecsPreferClientThenServer, + server: compareSpecsPreferServer +}; /** * Parse the Accept-Encoding header. @@ -50,7 +55,7 @@ function parseAcceptEncoding(accept) { */ accepts[j++] = { encoding: 'identity', - q: minQuality, + q: minQuality * 0.1, i: i }; } @@ -74,8 +79,8 @@ function parseEncoding(str, i) { var q = 1; if (match[2]) { var params = match[2].split(';'); - for (var i = 0; i < params.length; i ++) { - var p = params[i].trim().split('='); + for (var j = 0; j < params.length; j ++) { + var p = params[j].trim().split('='); if (p[0] === 'q') { q = parseFloat(p[1]); break; @@ -135,14 +140,14 @@ function specify(encoding, spec, index) { * @public */ -function preferredEncodings(accept, provided) { +function preferredEncodings(accept, provided, options) { var accepts = parseAcceptEncoding(accept || ''); if (!provided) { // sorted list of all encodings return accepts .filter(isQuality) - .sort(compareSpecs) + .sort(compareSpecsPreferClient) .map(getFullEncoding); } @@ -150,8 +155,10 @@ function preferredEncodings(accept, provided) { return getEncodingPriority(type, accepts, index); }); + var comparator = options && comparators[options.sortPreference] || compareSpecsPreferClient + // sorted list of accepted encodings - return priorities.filter(isQuality).sort(compareSpecs).map(function getEncoding(priority) { + return priorities.filter(isQuality).sort(comparator).map(function getEncoding(priority) { return provided[priorities.indexOf(priority)]; }); } @@ -161,10 +168,28 @@ function preferredEncodings(accept, provided) { * @private */ -function compareSpecs(a, b) { +function compareSpecsPreferClient(a, b) { return (b.q - a.q) || (b.s - a.s) || (a.o - b.o) || (a.i - b.i) || 0; } +/** + * Compare two specs. + * @private + */ + +function compareSpecsPreferClientThenServer(a, b) { + return (b.q - a.q) || (a.i - b.i) || 0; +} + +/** + * Compare two specs, ignoring client quality levels. + * @private + */ + +function compareSpecsPreferServer(a, b) { + return (a.i - b.i) || 0; +} + /** * Get full encoding string. * @private diff --git a/test/encoding.js b/test/encoding.js index d6cc2b5..b274122 100644 --- a/test/encoding.js +++ b/test/encoding.js @@ -2,6 +2,10 @@ var assert = require('assert') var Negotiator = require('..') +var clientPreference = { sortPreference: 'client'} +var serverPreference = { sortPreference: 'server'} +var clientThenServerPreference = { sortPreference: 'clientThenServer'} + describe('negotiator.encoding()', function () { whenAcceptEncoding(undefined, function () { it('should return identity', function () { @@ -112,6 +116,12 @@ describe('negotiator.encoding(array)', function () { assert.strictEqual(this.negotiator.encoding(['gzip']), 'gzip') assert.strictEqual(this.negotiator.encoding(['compress', 'gzip']), 'gzip') }) + + it('clientThenServerPreference: should return server-preferred encoding', function () { + assert.strictEqual(this.negotiator.encoding(['identity'], clientThenServerPreference), 'identity') + assert.strictEqual(this.negotiator.encoding(['gzip'], clientThenServerPreference), 'gzip') + assert.strictEqual(this.negotiator.encoding(['compress', 'gzip'], clientThenServerPreference), 'compress') + }) }) whenAcceptEncoding('*, gzip;q=0', function () { @@ -206,6 +216,23 @@ describe('negotiator.encoding(array)', function () { assert.strictEqual(this.negotiator.encoding(['compress', 'identity']), 'identity') }) }) + + whenAcceptEncoding('gzip;q=0.9, sdhc, br;q=0.9', function () { + it('should return best server-preferred encoding of equal client-preferred encodings', function () { + assert.strictEqual(this.negotiator.encoding(['compress', 'br', 'gzip']), 'gzip') + assert.strictEqual(this.negotiator.encoding(['compress', 'gzip', 'br']), 'gzip') + }) + + it('should return best server-preferred encoding of equal client-preferred encodings', function () { + assert.strictEqual(this.negotiator.encoding(['compress', 'br', 'gzip'], clientThenServerPreference), 'br') + assert.strictEqual(this.negotiator.encoding(['compress', 'gzip', 'br'], clientThenServerPreference), 'gzip') + }) + + it('should return best server-preferred encoding of equal client-preferred encodings', function () { + assert.strictEqual(this.negotiator.encoding(['compress', 'br', 'gzip'], serverPreference), 'br') + assert.strictEqual(this.negotiator.encoding(['compress', 'gzip', 'br'], serverPreference), 'gzip') + }) + }) }) describe('negotiator.encodings()', function () { @@ -316,7 +343,13 @@ describe('negotiator.encodings(array)', function () { it('should prefer gzip', function () { assert.deepEqual(this.negotiator.encodings(['identity']), ['identity']) assert.deepEqual(this.negotiator.encodings(['gzip']), ['gzip']) - assert.deepEqual(this.negotiator.encodings(['compress', 'gzip']), ['gzip', 'compress']) + assert.deepEqual(this.negotiator.encodings(['compress', 'deflate', 'gzip']), ['gzip', 'compress', 'deflate']) + }) + + it('clientThenServerPreference: should return server preferred encodings', function () { + assert.deepEqual(this.negotiator.encodings(['identity'], clientThenServerPreference), ['identity']) + assert.deepEqual(this.negotiator.encodings(['gzip'], clientThenServerPreference), ['gzip']) + assert.deepEqual(this.negotiator.encodings(['compress', 'deflate', 'gzip'], clientThenServerPreference), ['compress', 'deflate', 'gzip']) }) }) @@ -403,6 +436,11 @@ describe('negotiator.encodings(array)', function () { assert.deepEqual(this.negotiator.encodings(['deflate']), ['deflate']) assert.deepEqual(this.negotiator.encodings(['deflate', 'gzip']), ['deflate', 'gzip']) }) + + it('serverPreference: should ignore client quality levels', function () { + assert.deepEqual(this.negotiator.encodings(['deflate', 'gzip'], serverPreference), ['deflate', 'gzip']) + assert.deepEqual(this.negotiator.encodings(['gzip', 'deflate'], serverPreference), ['gzip', 'deflate']) + }) }) whenAcceptEncoding('gzip;q=0.8, identity;q=0.5, *;q=0.3', function () { @@ -411,6 +449,87 @@ describe('negotiator.encodings(array)', function () { assert.deepEqual(this.negotiator.encodings(['identity', 'gzip', 'compress']), ['gzip', 'identity', 'compress']) }) }) + + whenAcceptEncoding('not;q=0, med1;q=0.9, med2;q=0.9, high, *;q=0.8', function () { + it('clientPreference', function () { + assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med2', 'other', 'med1'], clientPreference), ['high', 'med1', 'med2', 'other']) + assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med1', 'other', 'med2'], clientPreference), ['high', 'med1', 'med2', 'other']) + }) + + it('clientThenServerPreference', function () { + assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med2', 'other', 'med1'], clientThenServerPreference), ['high', 'med2', 'med1', 'other']) + assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med1', 'other', 'med2'], clientThenServerPreference), ['high', 'med1', 'med2', 'other']) + }) + + it('serverPreference', function () { + assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med2', 'other', 'med1'], serverPreference), ['high', 'med2', 'other', 'med1']) + assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med1', 'other', 'med2'], serverPreference), ['high', 'med1', 'other', 'med2']) + }) + }) + + whenAcceptEncoding('not;q=0, med1;q=0.9, med2;q=0.9, high, identity;q=0.9', function () { + it('clientPreference', function () { + assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med2', 'other', 'med1', 'identity'], clientPreference), ['high', 'med1', 'med2', 'identity']) + assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med1', 'other', 'med2', 'identity'], clientPreference), ['high', 'med1', 'med2', 'identity']) + }) + + it('clientThenServerPreference', function () { + assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med2', 'other', 'med1', 'identity'], clientThenServerPreference), ['high', 'med2', 'med1', 'identity']) + assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med1', 'other', 'med2', 'identity'], clientThenServerPreference), ['high', 'med1', 'med2', 'identity']) + }) + + it('serverPreference', function () { + assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med2', 'other', 'med1', 'identity'], serverPreference), ['high', 'med2', 'med1', 'identity']) + assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med1', 'other', 'med2', 'identity'], serverPreference), ['high', 'med1', 'med2', 'identity']) + }) + }) + + whenAcceptEncoding('c;q=0.9, b;q=0.89, a;q=0.9, d;q=0.91, e;q=0.9', function () { + it('clientPreference', function () { + assert.deepEqual(this.negotiator.encodings(['b','c','d','e'], clientPreference), ['d', 'c', 'e', 'b']); + assert.deepEqual(this.negotiator.encodings(['c','d','e','b'], clientPreference), ['d', 'c', 'e', 'b']); + assert.deepEqual(this.negotiator.encodings(['d','e','b','c'], clientPreference), ['d', 'c', 'e', 'b']); + assert.deepEqual(this.negotiator.encodings(['e','b','c','d'], clientPreference), ['d', 'c', 'e', 'b']); + + assert.deepEqual(this.negotiator.encodings(['a','c','e'], clientPreference), ['c', 'a', 'e']); + assert.deepEqual(this.negotiator.encodings(['c','e','a'], clientPreference), ['c', 'a', 'e']); + assert.deepEqual(this.negotiator.encodings(['e','a','c'], clientPreference), ['c', 'a', 'e']); + + assert.deepEqual(this.negotiator.encodings(['identity','e','f'], clientPreference), ['e','identity']); + assert.deepEqual(this.negotiator.encodings(['f','identity','e'], clientPreference), ['e','identity']); + assert.deepEqual(this.negotiator.encodings(['e','f','identity'], clientPreference), ['e','identity']); + }) + + it('clientThenServerPreference', function () { + assert.deepEqual(this.negotiator.encodings(['b','c','d','e'], clientThenServerPreference), ['d', 'c', 'e', 'b']); + assert.deepEqual(this.negotiator.encodings(['c','d','e','b'], clientThenServerPreference), ['d', 'c', 'e', 'b']); + assert.deepEqual(this.negotiator.encodings(['d','e','b','c'], clientThenServerPreference), ['d', 'e', 'c', 'b']); + assert.deepEqual(this.negotiator.encodings(['e','b','c','d'], clientThenServerPreference), ['d', 'e', 'c', 'b']); + + assert.deepEqual(this.negotiator.encodings(['a','c','e'], clientThenServerPreference), ['a', 'c', 'e']); + assert.deepEqual(this.negotiator.encodings(['c','e','a'], clientThenServerPreference), ['c', 'e', 'a']); + assert.deepEqual(this.negotiator.encodings(['e','a','c'], clientThenServerPreference), ['e', 'a', 'c']); + + assert.deepEqual(this.negotiator.encodings(['identity','e','f'], clientThenServerPreference), ['e','identity']); + assert.deepEqual(this.negotiator.encodings(['f','identity','e'], clientThenServerPreference), ['e','identity']); + assert.deepEqual(this.negotiator.encodings(['e','f','identity'], clientThenServerPreference), ['e','identity']); + }) + + it('serverPreference', function () { + assert.deepEqual(this.negotiator.encodings(['b','c','d','e'], serverPreference), ['b','c','d','e']); + assert.deepEqual(this.negotiator.encodings(['c','d','e','b'], serverPreference), ['c','d','e','b']); + assert.deepEqual(this.negotiator.encodings(['d','e','b','c'], serverPreference), ['d','e','b','c']); + assert.deepEqual(this.negotiator.encodings(['e','b','c','d'], serverPreference), ['e','b','c','d']); + + assert.deepEqual(this.negotiator.encodings(['a','c','e'], serverPreference), ['a', 'c', 'e']); + assert.deepEqual(this.negotiator.encodings(['c','e','a'], serverPreference), ['c', 'e', 'a']); + assert.deepEqual(this.negotiator.encodings(['e','a','c'], serverPreference), ['e', 'a', 'c']); + + assert.deepEqual(this.negotiator.encodings(['identity','e','f'], serverPreference), ['identity', 'e']); + assert.deepEqual(this.negotiator.encodings(['f','identity','e'], serverPreference), ['identity', 'e']); + assert.deepEqual(this.negotiator.encodings(['e','f','identity'], serverPreference), ['e','identity']); + }) + }) }) function createRequest(headers) {