Skip to content

Instantly share code, notes, and snippets.

@hongymagic
Created July 5, 2012 02:54
Show Gist options
  • Save hongymagic/3050812 to your computer and use it in GitHub Desktop.
Save hongymagic/3050812 to your computer and use it in GitHub Desktop.
Backbone PagedCollection – HTTP Link aware Backbone Collection

Backbone.PagedCollection

This is an experiment to create a Backbone collection that is aware of HTTP Response header Link field (RFC5988). Using this technique, we can navigate between different parts of the collection resource without any programmatic interference.

Yes, this does mean that your HTTP API should make use of RFC5988. Checkout Github API for examples on Link response field.

API

var gists = new Backbone.PagedCollection({
	urlRoot: 'https://api.github.com/gists/public',
	mode: Gist
});

gists.fetch();

// After the initial fetch
gists.first(options);
gists.prev(options);
gists.next(options);
gists.last(options);

How it works

TODO: Go through the following deps and code

  • PEG.js generated RFC5988 parser (is huge)
  • Backbone.PagedCollection.js

Resources

  1. RFC5988 – Web Linking
//
// Backbone.PagedCollection
//
// A smarter way to consume list resources over HTTP. Call it Hypermedia, REST,
// whatever. There are other problems that we may wish to solve with the
// current implementation of PagedCollection:
//
// 1. Jumping between specific pages;
// 2. Web Linking works, however `per_page` paramter must be implemented;
// 3. Call to `fetch` is required to retreive Link field, which means bootstrapping is a problem; and
// 4. `parser.js` which is used to parse Link field is too big. May have to move off PEG.js version.
//
// @depends on parser.js which was generated via PEG.js to parse Link-field in the HTTP response header.
//
Backbone.PagedCollection = (function (parser) {
var find = function (link, type) {
return _.find(link || [], function (value) {
return value.rel == type;
});
};
var PagedCollection = Backbone.Collection.extend({
parse: function (response, xhr) {
var link = xhr.getResponseHeader('Link');
if (link) {
this.link = parser.parse(link);
}
return Backbone.Collection.prototype.parse.apply(this, arguments);
},
// Jump to a given related link as specified by the `rel` attribute.
jump: function (type, options) {
var link = find(this.link, type);
if (link && link.href) {
this.url = link.href;
return this.fetch(options);
}
}
});
// As a collection, the only type of related links that we care about are the
// the following 4 methods. You can use `jump` directly, however types such as
// `document`, `external` won't make sense in this context. We can probably
// make `Backbone.Model` smarter as well using similar Web Linking techniques
// however, that is not part of this exercise.
_.each(['first', 'prev', 'next', 'last'], function (method) {
PagedCollection.prototype[method] = function (options) {
return this.jump(method, options);
};
});
return PagedCollection;
}(parser));
/* Github Gist model */
var Gist = Backbone.Model.extend({
urlRoot: 'https://api.github.com/gists/'
});
/* Gist collection */
var Gists = Backbone.PagedCollection.extend({
url: 'https://api.github.com/gists/public',
model: Gist
});
var publicGists = new Gists();
var options = {
success: function (gists) {
console.log(gists.link);
gists.last(options);
}
};
publicGists.fetch(options);
var parser = (function(){
/*
* Generated by PEG.js 0.7.0.
*
* http://pegjs.majda.cz/
*/
function quote(s) {
/*
* ECMA-262, 5th ed., 7.8.4: All characters may appear literally in a
* string literal except for the closing quote character, backslash,
* carriage return, line separator, paragraph separator, and line feed.
* Any character may appear in the form of an escape sequence.
*
* For portability, we also escape escape all control and non-ASCII
* characters. Note that "\0" and "\v" escape sequences are not used
* because JSHint does not like the first and IE the second.
*/
return '"' + s
.replace(/\\/g, '\\\\') // backslash
.replace(/"/g, '\\"') // closing quote character
.replace(/\x08/g, '\\b') // backspace
.replace(/\t/g, '\\t') // horizontal tab
.replace(/\n/g, '\\n') // line feed
.replace(/\f/g, '\\f') // form feed
.replace(/\r/g, '\\r') // carriage return
.replace(/[\x00-\x07\x0B\x0E-\x1F\x80-\uFFFF]/g, escape)
+ '"';
}
var result = {
/*
* Parses the input with a generated parser. If the parsing is successfull,
* returns a value explicitly or implicitly specified by the grammar from
* which the parser was generated (see |PEG.buildParser|). If the parsing is
* unsuccessful, throws |PEG.parser.SyntaxError| describing the error.
*/
parse: function(input, startRule) {
var parseFunctions = {
"links": parse_links,
"link": parse_link,
"href": parse_href,
"attributes": parse_attributes,
"attribute": parse_attribute,
"name": parse_name,
"value": parse_value,
"url": parse_url,
"ws": parse_ws
};
if (startRule !== undefined) {
if (parseFunctions[startRule] === undefined) {
throw new Error("Invalid rule name: " + quote(startRule) + ".");
}
} else {
startRule = "links";
}
var pos = 0;
var reportFailures = 0;
var rightmostFailuresPos = 0;
var rightmostFailuresExpected = [];
function padLeft(input, padding, length) {
var result = input;
var padLength = length - input.length;
for (var i = 0; i < padLength; i++) {
result = padding + result;
}
return result;
}
function escape(ch) {
var charCode = ch.charCodeAt(0);
var escapeChar;
var length;
if (charCode <= 0xFF) {
escapeChar = 'x';
length = 2;
} else {
escapeChar = 'u';
length = 4;
}
return '\\' + escapeChar + padLeft(charCode.toString(16).toUpperCase(), '0', length);
}
function matchFailed(failure) {
if (pos < rightmostFailuresPos) {
return;
}
if (pos > rightmostFailuresPos) {
rightmostFailuresPos = pos;
rightmostFailuresExpected = [];
}
rightmostFailuresExpected.push(failure);
}
function parse_links() {
var result0, result1;
var pos0;
pos0 = pos;
result1 = parse_link();
if (result1 !== null) {
result0 = [];
while (result1 !== null) {
result0.push(result1);
result1 = parse_link();
}
} else {
result0 = null;
}
if (result0 !== null) {
result0 = (function(offset, links) {
var arr = [],
i;
for (i = 0; i < links.length; i++) {
arr.push(links[i]);
}
return arr;
})(pos0, result0);
}
if (result0 === null) {
pos = pos0;
}
return result0;
}
function parse_link() {
var result0, result1, result2, result3, result4;
var pos0, pos1;
pos0 = pos;
pos1 = pos;
result0 = parse_href();
if (result0 !== null) {
result1 = parse_ws();
if (result1 !== null) {
result2 = parse_attributes();
if (result2 !== null) {
if (input.charCodeAt(pos) === 44) {
result3 = ",";
pos++;
} else {
result3 = null;
if (reportFailures === 0) {
matchFailed("\",\"");
}
}
result3 = result3 !== null ? result3 : "";
if (result3 !== null) {
result4 = parse_ws();
if (result4 !== null) {
result0 = [result0, result1, result2, result3, result4];
} else {
result0 = null;
pos = pos1;
}
} else {
result0 = null;
pos = pos1;
}
} else {
result0 = null;
pos = pos1;
}
} else {
result0 = null;
pos = pos1;
}
} else {
result0 = null;
pos = pos1;
}
if (result0 !== null) {
result0 = (function(offset, href, attributes) {
var obj = attributes;
obj['href'] = href;
return obj;
})(pos0, result0[0], result0[2]);
}
if (result0 === null) {
pos = pos0;
}
return result0;
}
function parse_href() {
var result0, result1, result2, result3;
var pos0, pos1;
pos0 = pos;
pos1 = pos;
if (input.charCodeAt(pos) === 60) {
result0 = "<";
pos++;
} else {
result0 = null;
if (reportFailures === 0) {
matchFailed("\"<\"");
}
}
if (result0 !== null) {
result1 = parse_url();
if (result1 !== null) {
if (input.charCodeAt(pos) === 62) {
result2 = ">";
pos++;
} else {
result2 = null;
if (reportFailures === 0) {
matchFailed("\">\"");
}
}
if (result2 !== null) {
if (input.charCodeAt(pos) === 59) {
result3 = ";";
pos++;
} else {
result3 = null;
if (reportFailures === 0) {
matchFailed("\";\"");
}
}
if (result3 !== null) {
result0 = [result0, result1, result2, result3];
} else {
result0 = null;
pos = pos1;
}
} else {
result0 = null;
pos = pos1;
}
} else {
result0 = null;
pos = pos1;
}
} else {
result0 = null;
pos = pos1;
}
if (result0 !== null) {
result0 = (function(offset, href) {
return href;
})(pos0, result0[1]);
}
if (result0 === null) {
pos = pos0;
}
return result0;
}
function parse_attributes() {
var result0, result1;
var pos0;
pos0 = pos;
result1 = parse_attribute();
if (result1 !== null) {
result0 = [];
while (result1 !== null) {
result0.push(result1);
result1 = parse_attribute();
}
} else {
result0 = null;
}
if (result0 !== null) {
result0 = (function(offset, attrs) {
var obj = { },
i;
for (i = 0; i < attrs.length; i++) {
obj[attrs[i].name] = attrs[i].value;
}
return obj;
})(pos0, result0);
}
if (result0 === null) {
pos = pos0;
}
return result0;
}
function parse_attribute() {
var result0, result1, result2, result3, result4, result5, result6;
var pos0, pos1;
pos0 = pos;
pos1 = pos;
result0 = parse_name();
if (result0 !== null) {
result1 = parse_ws();
if (result1 !== null) {
if (input.charCodeAt(pos) === 61) {
result2 = "=";
pos++;
} else {
result2 = null;
if (reportFailures === 0) {
matchFailed("\"=\"");
}
}
if (result2 !== null) {
result3 = parse_ws();
if (result3 !== null) {
result4 = parse_value();
if (result4 !== null) {
if (input.charCodeAt(pos) === 59) {
result5 = ";";
pos++;
} else {
result5 = null;
if (reportFailures === 0) {
matchFailed("\";\"");
}
}
result5 = result5 !== null ? result5 : "";
if (result5 !== null) {
result6 = parse_ws();
if (result6 !== null) {
result0 = [result0, result1, result2, result3, result4, result5, result6];
} else {
result0 = null;
pos = pos1;
}
} else {
result0 = null;
pos = pos1;
}
} else {
result0 = null;
pos = pos1;
}
} else {
result0 = null;
pos = pos1;
}
} else {
result0 = null;
pos = pos1;
}
} else {
result0 = null;
pos = pos1;
}
} else {
result0 = null;
pos = pos1;
}
if (result0 !== null) {
result0 = (function(offset, name, v) { return { name: name, value: v }; })(pos0, result0[0], result0[4]);
}
if (result0 === null) {
pos = pos0;
}
return result0;
}
function parse_name() {
var result0, result1;
var pos0;
pos0 = pos;
if (/^[a-zA-Z]/.test(input.charAt(pos))) {
result1 = input.charAt(pos);
pos++;
} else {
result1 = null;
if (reportFailures === 0) {
matchFailed("[a-zA-Z]");
}
}
if (result1 !== null) {
result0 = [];
while (result1 !== null) {
result0.push(result1);
if (/^[a-zA-Z]/.test(input.charAt(pos))) {
result1 = input.charAt(pos);
pos++;
} else {
result1 = null;
if (reportFailures === 0) {
matchFailed("[a-zA-Z]");
}
}
}
} else {
result0 = null;
}
if (result0 !== null) {
result0 = (function(offset, name) { return name.join(''); })(pos0, result0);
}
if (result0 === null) {
pos = pos0;
}
return result0;
}
function parse_value() {
var result0, result1, result2;
var pos0, pos1;
pos0 = pos;
pos1 = pos;
if (/^["]/.test(input.charAt(pos))) {
result0 = input.charAt(pos);
pos++;
} else {
result0 = null;
if (reportFailures === 0) {
matchFailed("[\"]");
}
}
if (result0 !== null) {
if (/^[^"]/.test(input.charAt(pos))) {
result2 = input.charAt(pos);
pos++;
} else {
result2 = null;
if (reportFailures === 0) {
matchFailed("[^\"]");
}
}
if (result2 !== null) {
result1 = [];
while (result2 !== null) {
result1.push(result2);
if (/^[^"]/.test(input.charAt(pos))) {
result2 = input.charAt(pos);
pos++;
} else {
result2 = null;
if (reportFailures === 0) {
matchFailed("[^\"]");
}
}
}
} else {
result1 = null;
}
if (result1 !== null) {
if (/^["]/.test(input.charAt(pos))) {
result2 = input.charAt(pos);
pos++;
} else {
result2 = null;
if (reportFailures === 0) {
matchFailed("[\"]");
}
}
if (result2 !== null) {
result0 = [result0, result1, result2];
} else {
result0 = null;
pos = pos1;
}
} else {
result0 = null;
pos = pos1;
}
} else {
result0 = null;
pos = pos1;
}
if (result0 !== null) {
result0 = (function(offset, v) { return v.join(''); })(pos0, result0[1]);
}
if (result0 === null) {
pos = pos0;
}
if (result0 === null) {
pos0 = pos;
if (/^[^";,]/.test(input.charAt(pos))) {
result1 = input.charAt(pos);
pos++;
} else {
result1 = null;
if (reportFailures === 0) {
matchFailed("[^\";,]");
}
}
if (result1 !== null) {
result0 = [];
while (result1 !== null) {
result0.push(result1);
if (/^[^";,]/.test(input.charAt(pos))) {
result1 = input.charAt(pos);
pos++;
} else {
result1 = null;
if (reportFailures === 0) {
matchFailed("[^\";,]");
}
}
}
} else {
result0 = null;
}
if (result0 !== null) {
result0 = (function(offset, v) { return v.join(''); })(pos0, result0);
}
if (result0 === null) {
pos = pos0;
}
}
return result0;
}
function parse_url() {
var result0, result1;
var pos0;
pos0 = pos;
if (/^[^>]/.test(input.charAt(pos))) {
result1 = input.charAt(pos);
pos++;
} else {
result1 = null;
if (reportFailures === 0) {
matchFailed("[^>]");
}
}
if (result1 !== null) {
result0 = [];
while (result1 !== null) {
result0.push(result1);
if (/^[^>]/.test(input.charAt(pos))) {
result1 = input.charAt(pos);
pos++;
} else {
result1 = null;
if (reportFailures === 0) {
matchFailed("[^>]");
}
}
}
} else {
result0 = null;
}
if (result0 !== null) {
result0 = (function(offset, url) { return url.join(''); })(pos0, result0);
}
if (result0 === null) {
pos = pos0;
}
return result0;
}
function parse_ws() {
var result0, result1;
result0 = [];
if (/^[ ]/.test(input.charAt(pos))) {
result1 = input.charAt(pos);
pos++;
} else {
result1 = null;
if (reportFailures === 0) {
matchFailed("[ ]");
}
}
while (result1 !== null) {
result0.push(result1);
if (/^[ ]/.test(input.charAt(pos))) {
result1 = input.charAt(pos);
pos++;
} else {
result1 = null;
if (reportFailures === 0) {
matchFailed("[ ]");
}
}
}
return result0;
}
function cleanupExpected(expected) {
expected.sort();
var lastExpected = null;
var cleanExpected = [];
for (var i = 0; i < expected.length; i++) {
if (expected[i] !== lastExpected) {
cleanExpected.push(expected[i]);
lastExpected = expected[i];
}
}
return cleanExpected;
}
function computeErrorPosition() {
/*
* The first idea was to use |String.split| to break the input up to the
* error position along newlines and derive the line and column from
* there. However IE's |split| implementation is so broken that it was
* enough to prevent it.
*/
var line = 1;
var column = 1;
var seenCR = false;
for (var i = 0; i < Math.max(pos, rightmostFailuresPos); i++) {
var ch = input.charAt(i);
if (ch === "\n") {
if (!seenCR) { line++; }
column = 1;
seenCR = false;
} else if (ch === "\r" || ch === "\u2028" || ch === "\u2029") {
line++;
column = 1;
seenCR = true;
} else {
column++;
seenCR = false;
}
}
return { line: line, column: column };
}
var result = parseFunctions[startRule]();
/*
* The parser is now in one of the following three states:
*
* 1. The parser successfully parsed the whole input.
*
* - |result !== null|
* - |pos === input.length|
* - |rightmostFailuresExpected| may or may not contain something
*
* 2. The parser successfully parsed only a part of the input.
*
* - |result !== null|
* - |pos < input.length|
* - |rightmostFailuresExpected| may or may not contain something
*
* 3. The parser did not successfully parse any part of the input.
*
* - |result === null|
* - |pos === 0|
* - |rightmostFailuresExpected| contains at least one failure
*
* All code following this comment (including called functions) must
* handle these states.
*/
if (result === null || pos !== input.length) {
var offset = Math.max(pos, rightmostFailuresPos);
var found = offset < input.length ? input.charAt(offset) : null;
var errorPosition = computeErrorPosition();
throw new this.SyntaxError(
cleanupExpected(rightmostFailuresExpected),
found,
offset,
errorPosition.line,
errorPosition.column
);
}
return result;
},
/* Returns the parser source code. */
toSource: function() { return this._source; }
};
/* Thrown when a parser encounters a syntax error. */
result.SyntaxError = function(expected, found, offset, line, column) {
function buildMessage(expected, found) {
var expectedHumanized, foundHumanized;
switch (expected.length) {
case 0:
expectedHumanized = "end of input";
break;
case 1:
expectedHumanized = expected[0];
break;
default:
expectedHumanized = expected.slice(0, expected.length - 1).join(", ")
+ " or "
+ expected[expected.length - 1];
}
foundHumanized = found ? quote(found) : "end of input";
return "Expected " + expectedHumanized + " but " + foundHumanized + " found.";
}
this.name = "SyntaxError";
this.expected = expected;
this.found = found;
this.message = buildMessage(expected, found);
this.offset = offset;
this.line = line;
this.column = column;
};
result.SyntaxError.prototype = Error.prototype;
return result;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment