From b0254932d076d2afd2d3d1534f93bd46f838c6b4 Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Mon, 8 Nov 2010 22:33:59 -0800 Subject: [PATCH] works in Chrome 7 and Safari 5 --- lib/index.js | 689 ++++++++++++++++++++++++++++----------------------- 1 file changed, 385 insertions(+), 304 deletions(-) diff --git a/lib/index.js b/lib/index.js index d28fa4a..53d3222 100644 --- a/lib/index.js +++ b/lib/index.js @@ -7,325 +7,406 @@ // TODO: // - authentication and write APIs -// - run in browsers (dojo?) -var request = require('request') - , util = require('util') - , Blob, Branch, Commit, Raw, Repo, Tree, User +(function() { + var global = this + , isBrowser = 'document' in global + , request = isBrowser ? xhr : require('request') + , Blob, Branch, Commit, Raw, Repo, Tree, User + , api -module.exports = { - blob: function(repo, sha, path, cb) { - return new Blob(repo, sha, path, cb) - }, - branch: function(repo, branch, cb) { - return new Branch(repo, branch, cb) - }, - commits: function(repo, branch, cb) { - return new Branch(repo, branch).getCommits(cb) - }, - commit: function(repo, sha, cb) { - return new Commit(repo, sha, cb) - }, - raw: function(repo, sha, cb) { - return new Raw(repo, sha, cb) - }, - repo: function(repo, cb) { - return new Repo(repo, cb) - }, - branches: function(repo, cb) { - return new Repo(repo).getBranches(cb) - }, - collaborators: function(repo, cb) { - return new Repo(repo).getCollaborators(cb) - }, - contributors: function(repo, cb) { - return new Repo(repo).getContributors(cb) - }, - languages: function(repo, cb) { - return new Repo(repo).getLanguages(cb) - }, - network: function(repo, cb) { - return new Repo(repo).getNetwork(cb) - }, - tags: function(repo, cb) { - return new Repo(repo).getTags(cb) - }, - watchers: function(repo, cb) { - return new Repo(repo).getWatchers(cb) - }, - tree: function(repo, sha, cb) { - return new Tree(repo, sha, cb) - }, - blobs: function(repo, sha, cb) { - return new Tree(repo, sha).getBlobs(cb) - }, - user: function(user, cb) { - return new User(user, cb) - }, - followers: function(user, cb) { - return new User(user).getFollowers(cb) - }, - following: function(user, cb) { - return new User(user).getFollowing(cb) - }, - repos: function(user, cb) { - return new User(user).getRepos(cb) - }, - watched: function(user, cb) { - return new User(user).getWatched(cb) - } -} - -// Define resources // - -Blob = createResource('blob/show/:repo/:tree/:path', { - has: [ ['commits', 'commits/list/:repo/:tree/:path'] ] -}) -Branch = createResource('commits/show/:repo/:branch', { - has: [ ['commits', 'commits/list/:repo/:branch'] ] -}) -Commit = createResource('commits/show/:repo/:sha') -Raw = createResource('blob/show/:repo/:sha') -Repo = createResource('repos/show/:repo', { - has: [ 'branches' - , 'collaborators' - , 'contributors' - , 'languages' - , 'network' - , 'tags' - , 'watchers' - ] -}) -Tree = createResource('tree/show/:repo/:sha', { - has: [ ['blobs', 'blob/all/:repo/:sha'] - , ['fullBlobs', 'blob/full/:repo/:sha'] - , ['fullTree', 'tree/full/:repo/:sha'] - ] -}) -Tree.prototype._processData = function(data) { - Resource.prototype._processData.call(this, data) - this.blobs = this.data() -} - -User = createResource('user/show/:user', { - has: [ 'followers' - , 'following' - , ['repos', 'repos/show/:user'] - , ['watched', 'repos/watched/:user'] - ] -}) - -// Construct a new github resource. -// -// options: -// - params: params for constructor (optional, inferred from route if missing) -// - has: list of related resources, accessors are created for each item -// -// The members of the `has` list are arrays of the form [name, route, unpack]. -// The first member, name, is used to create an accessor (e.g. getName), and -// is required. -// -// Route and unpack are optional. Route specifies the endpoint for this -// resource and defaults to the name appended to the main resource's endpoint. -// -// Unpack is a function that extracts the desired value from the object fetched -// for this resource. It defaults to a function that picks out the only property -// from an object, or returns the entire walue if not an object or it contains -// more than one property. -// -// When passing only the name you may pass it directly without wrapping it in -// an array. -function createResource(route, options) { - if (!route) throw new Error('route is required') - options = options || {} - - var resource = function() { Resource.apply(this, slice(arguments)) } - util.inherits(resource, Resource) - - resource.prototype._route = route - resource.prototype._params = options.params || paramsFromRoute(route) - - resource.has = function(prop, route, unpack) { - unpack = unpack || onlyProp - var dataProp = '_' + prop - , fn = 'get' + titleCaseFirst(prop) - , processData = function(d) { - getter(this, dataProp, function() { return camelize(unpack(d))}) - } - , result = function(resource) { return this[dataProp] } - resource.prototype[fn] = function(cb, force) { - return this._fetch({ prop: dataProp - , route: route || this._route + '/' + prop - , processData: processData.bind(this) - , result: result.bind(this) - }, cb.bind(this), force) + api = { + blob: function(repo, sha, path, cb) { + return new Blob(repo, sha, path, cb) + }, + branch: function(repo, branch, cb) { + return new Branch(repo, branch, cb) + }, + commits: function(repo, branch, cb) { + return new Branch(repo, branch).getCommits(cb) + }, + commit: function(repo, sha, cb) { + return new Commit(repo, sha, cb) + }, + raw: function(repo, sha, cb) { + return new Raw(repo, sha, cb) + }, + repo: function(repo, cb) { + return new Repo(repo, cb) + }, + branches: function(repo, cb) { + return new Repo(repo).getBranches(cb) + }, + collaborators: function(repo, cb) { + return new Repo(repo).getCollaborators(cb) + }, + contributors: function(repo, cb) { + return new Repo(repo).getContributors(cb) + }, + languages: function(repo, cb) { + return new Repo(repo).getLanguages(cb) + }, + network: function(repo, cb) { + return new Repo(repo).getNetwork(cb) + }, + tags: function(repo, cb) { + return new Repo(repo).getTags(cb) + }, + watchers: function(repo, cb) { + return new Repo(repo).getWatchers(cb) + }, + tree: function(repo, sha, cb) { + return new Tree(repo, sha, cb) + }, + blobs: function(repo, sha, cb) { + return new Tree(repo, sha).getBlobs(cb) + }, + user: function(user, cb) { + return new User(user, cb) + }, + followers: function(user, cb) { + return new User(user).getFollowers(cb) + }, + following: function(user, cb) { + return new User(user).getFollowing(cb) + }, + repos: function(user, cb) { + return new User(user).getRepos(cb) + }, + watched: function(user, cb) { + return new User(user).getWatched(cb) } + } + if (isBrowser) global.GITR = api + else module.exports = api + + + // Define resources // + + Blob = createResource('blob/show/:repo/:tree/:path', { + has: [ ['commits', 'commits/list/:repo/:tree/:path'] ] + }) + Branch = createResource('commits/show/:repo/:branch', { + has: [ ['commits', 'commits/list/:repo/:branch'] ] + }) + Commit = createResource('commits/show/:repo/:sha') + Raw = createResource('blob/show/:repo/:sha') + Repo = createResource('repos/show/:repo', { + has: [ 'branches' + , 'collaborators' + , 'contributors' + , 'languages' + , 'network' + , 'tags' + , 'watchers' + ] + }) + Tree = createResource('tree/show/:repo/:sha', { + has: [ ['blobs', 'blob/all/:repo/:sha'] + , ['fullBlobs', 'blob/full/:repo/:sha'] + , ['fullTree', 'tree/full/:repo/:sha'] + ] + }) + Tree.prototype._processData = function(data) { + Resource.prototype._processData.call(this, data) + this.blobs = this.data() + } + + User = createResource('user/show/:user', { + has: [ 'followers' + , 'following' + , ['repos', 'repos/show/:user'] + , ['watched', 'repos/watched/:user'] + ] + }) + + // Construct a new github resource. + // + // options: + // - params: params for constructor (optional, inferred from route if missing) + // - has: list of related resources, accessors are created for each item + // + // The members of the `has` list are arrays of the form [name, route, unpack]. + // The first member, name, is used to create an accessor (e.g. getName), and + // is required. + // + // Route and unpack are optional. Route specifies the endpoint for this + // resource and defaults to the name appended to the main resource's endpoint. + // + // Unpack is a function that extracts the desired value from the object fetched + // for this resource. It defaults to a function that picks out the only property + // from an object, or returns the entire walue if not an object or it contains + // more than one property. + // + // When passing only the name you may pass it directly without wrapping it in + // an array. + function createResource(route, options) { + if (!route) throw new Error('route is required') + options = options || {} + + var resource = function() { Resource.apply(this, [].slice.call(arguments)) } + inherits(resource, Resource) + + resource.prototype._route = route + resource.prototype._params = options.params || paramsFromRoute(route) + + resource.has = function(prop, route, unpack) { + unpack = unpack || onlyProp + var dataProp = '_' + prop + , fn = 'get' + titleCaseFirst(prop) + , processData = function(d) { + getter(this, dataProp, function() { return camelize(unpack(d))}) + } + , result = function(resource) { return this[dataProp] } + resource.prototype[fn] = function(cb, force) { + return this._fetch({ prop: dataProp + , route: route || this._route + '/' + prop + , processData: processData.bind(this) + , result: result.bind(this) + }, cb.bind(this), force) + } + return resource + } + if (options.has) options.has.forEach(function(args) { + resource.has.apply(resource, Array.isArray(args) ? args : [args]) + }) + return resource } - if (options.has) options.has.forEach(function(args) { - resource.has.apply(resource, Array.isArray(args) ? args : [args]) - }) - - return resource -} -// Assigns the given resource args to the new instance. Sets the path to the -// endpoint for main resource data. -// -// If the optional last arg is a function main data is fetched immediately, -// and that function is used as the callback. -// -// If the optional last arg is an object then it is set as the main resource -// data. -function Resource(/* ...args, opt: data or callback */) { - var args = slice(arguments) - , last = args[args.length - 1] + // Assigns the given resource args to the new instance. Sets the path to the + // endpoint for main resource data. + // + // If the optional last arg is a function main data is fetched immediately, + // and that function is used as the callback. + // + // If the optional last arg is an object then it is set as the main resource + // data. + function Resource(/* ...args, opt: data or callback */) { + var args = [].slice.call(arguments) + , last = args[args.length - 1] - // assign params from args - this._params.forEach(function(param, i) { - this[param] = args[i] - }.bind(this)) + // assign params from args + this._params.forEach(function(param, i) { + this[param] = args[i] + }.bind(this)) - // set the resource path - this.urlPath = this.resolve(this._route) + // set the resource path + this.urlPath = this.resolve(this._route) - if (typeof last === 'function') this.fetch(last) - else if (typeof last === 'object') this.data(last) -} + if (typeof last === 'function') this.fetch(last) + else if (typeof last === 'object') this.data(last) + } -// Set or get main data for this resource, or fetch -// a specific property from the data. -// -// When the data param is empty cached data is returned. -// -// When the data param is a string the property by that name -// is looked up in the cached data. -// -// Otherwise cached data is set to the data param. -Resource.prototype.data = function(data) { - if (!data) return this._data - if (typeof data === 'string' && typeof this._data === 'object') return this._data[data] + // Set or get main data for this resource, or fetch + // a specific property from the data. + // + // When the data param is empty cached data is returned. + // + // When the data param is a string the property by that name + // is looked up in the cached data. + // + // Otherwise cached data is set to the data param. + Resource.prototype.data = function(data) { + if (!data) return this._data + if (typeof data === 'string' && typeof this._data === 'object') return this._data[data] - getter(this, '_data', function() { return data }, {configurable: true}) - return this -} - -// Fetch the main data for this resource. -// -// cb: callback(err, data) -// force: if true load data from github, bypassing the local cache -Resource.prototype.fetch = function(cb, force) { - return this._fetch({ prop: '_data' - , route: this.urlPath - , processData: this._processData.bind(this) - , result: function(resource) { return resource } - }, cb.bind(this), force) -} - -// 'repos/show/:user/:repo/branches' -> 'repos/show/samsonjs/gitter -Resource.prototype.resolve = function(route) { // based on crock's supplant - if (route.indexOf(':') < 0) return route - return route.replace(/:(\w+)\b/g, function (s, prop) { - var val = this[prop] - if (typeof val !== 'string' && typeof val !== 'number') - throw new Error('no suitable property named "' + prop + '" (found ' + val + ')') - return val - }.bind(this)) -} - -// Fetch arbitrary data from github. -// -// options: -// - prop: name of data cache property -// - route: route to github endpoint (can contain resource params) -// - processData: function that processes fetched data -// - result: function to obtain the result passed to the callback -// cb: callback(err, data) -// force: if true load data from github, bypassing the local cache -Resource.prototype._fetch = function(options, cb, force) { - if (!force && this[options.prop]) { - cb(null, options.result(this)) + getter(this, '_data', function() { return data }, {configurable: true}) return this } - - // Interpolate resource params - var path = this.resolve(options.route) - - // Make the request - return this._get(path, function(err, data) { - if (err) { - cb(err) - return - } - options.processData(data) - cb(null, options.result(this)) - }.bind(this)) -} -// Fetch data from github. JSON responses are parsed. -// -// path: github endpoint -// cb: callback(err, data) -Resource.prototype._get = function(path, cb) { - request({uri: 'http://github.com/api/v2/json/' + path}, function(err, response, body) { - if (err) - cb(err) - else if (response.statusCode !== 200) - cb(new Error('failed to fetch ' + path + ': ' + response.statusCode)) - else if (response.headers['content-type'].match(/json/)) - cb(null, JSON.parse(body)) - else - cb(null, body) - }) - return this -} - -// Descendents of Resource can overwrite _processData and _unpack to process -// the main resource data differently. - -Resource.prototype._processData = function(data) { - return this.data(camelize(this._unpack(data))) -} -Resource.prototype._unpack = onlyProp - - -// Utilities // - -function camel(s) { // created_at => createdAt - return s.replace(/_(.)/g, function(_, l) { return l.toUpperCase() }) -} -function camelize(obj) { // camelize all keys of an object, or all objects in an array - if (!obj || typeof obj === 'string') return obj - if (Array.isArray(obj)) return obj.map(camelize) - return Object.keys(obj).reduce(function(newObj, k) { - newObj[camel(k)] = obj[k] - return newObj - }, {}) -} - -function getter(obj, prop, fn, opts) { // minor convenience - opts = opts || {} - opts.get = fn - Object.defineProperty(obj, prop, opts) -} - -// get an only property, if any -function onlyProp(obj) { - if (obj && typeof obj === 'object') { - var keys = Object.keys(obj) - if (keys.length === 1) return obj[keys[0]] + // Fetch the main data for this resource. + // + // cb: callback(err, data) + // force: if true load data from github, bypassing the local cache + Resource.prototype.fetch = function(cb, force) { + return this._fetch({ prop: '_data' + , route: this.urlPath + , processData: this._processData.bind(this) + , result: function(resource) { return resource } + }, cb.bind(this), force) } - return obj -} -// 'repos/show/:user/:repo/branches' -> ['user', 'repo'] -function paramsFromRoute(route) { - if (route.indexOf(':') === -1) return [] - return route.split('/') - .filter(function(s) { return s.charAt(0) === ':' }) - .map(function(s) { return s.slice(1) }) -} + // 'repos/show/:user/:repo/branches' -> 'repos/show/samsonjs/gitter + Resource.prototype.resolve = function(route) { // based on crock's supplant + if (route.indexOf(':') < 0) return route + return route.replace(/:(\w+)\b/g, function (s, prop) { + var val = this[prop] + if (typeof val !== 'string' && typeof val !== 'number') + throw new Error('no suitable property named "' + prop + '" (found ' + val + ')') + return val + }.bind(this)) + } -function slice(x) { return [].slice.call(x) } + // Fetch arbitrary data from github. + // + // options: + // - prop: name of data cache property + // - route: route to github endpoint (can contain resource params) + // - processData: function that processes fetched data + // - result: function to obtain the result passed to the callback + // cb: callback(err, data) + // force: if true load data from github, bypassing the local cache + Resource.prototype._fetch = function(options, cb, force) { + if (!force && this[options.prop]) { + cb(null, options.result(this)) + return this + } + + // Interpolate resource params + var path = this.resolve(options.route) + + // Make the request + return this._get(path, function(err, data) { + if (err) { + cb(err) + return + } + options.processData(data) + cb(null, options.result(this)) + }.bind(this)) + } -function titleCaseFirst(s) { return s.charAt(0).toUpperCase() + s.slice(1) } + // Fetch data from github. JSON responses are parsed. + // + // path: github endpoint + // cb: callback(err, data) + Resource.prototype._get = function(path, cb) { + request({uri: 'http://github.com/api/v2/json/' + path}, function(err, response, body) { + if (err) + cb(err) + else if (isBrowser) + cb(null, body) // body is an object + else if (response.statusCode !== 200) + cb(new Error('failed to fetch ' + path + ': ' + response.statusCode)) + else if (response.headers['content-type'].match(/json/)) + cb(null, JSON.parse(body)) + else + cb(null, body) + }) + return this + } + + // Descendents of Resource can overwrite _processData and _unpack to process + // the main resource data differently. + + Resource.prototype._processData = function(data) { + return this.data(camelize(this._unpack(data))) + } + Resource.prototype._unpack = onlyProp + + + // Utilities // + + function camel(s) { // created_at => createdAt + return s.replace(/_(.)/g, function(_, l) { return l.toUpperCase() }) + } + function camelize(obj) { // camelize all keys of an object, or all objects in an array + if (!obj || typeof obj === 'string') return obj + if (Array.isArray(obj)) return obj.map(camelize) + return Object.keys(obj).reduce(function(newObj, k) { + newObj[camel(k)] = obj[k] + return newObj + }, {}) + } + + function getter(obj, prop, fn, opts) { // minor convenience + opts = opts || {} + opts.get = fn + Object.defineProperty(obj, prop, opts) + } + + function inherits(ctor, superCtor) { + ctor.super_ = superCtor + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false + } + }) + } + + function xhr(options, cb) { + GITR._jsonpCallback = function(obj) { + cb(null, null, obj) + } + ;(function (global, oDOC, handler) { + options.uri += '?callback=GITR._jsonpCallback' + var head = oDOC.head || oDOC.getElementsByTagName("head") + + // loading code borrowed directly from LABjs itself + setTimeout(function () { + if ("item" in head) { // check if ref is still a live node list + if (!head[0]) { // append_to node not yet ready + setTimeout(arguments.callee, 25) + return + } + head = head[0]; // reassign from live node list ref to pure node ref -- avoids nasty IE bug where changes to DOM invalidate live node lists + } + var scriptElem = oDOC.createElement("script"), + scriptdone = false + scriptElem.onload = scriptElem.onreadystatechange = function () { + if ((scriptElem.readyState && scriptElem.readyState !== "complete" && scriptElem.readyState !== "loaded") || scriptdone) { + return false + } + scriptElem.onload = scriptElem.onreadystatechange = null + scriptdone = true + }; + scriptElem.src = options.uri + head.insertBefore(scriptElem, head.firstChild) + }, 0) + + // required: shim for FF <= 3.5 not having document.readyState + if (oDOC.readyState == null && oDOC.addEventListener) { + oDOC.readyState = "loading" + oDOC.addEventListener("DOMContentLoaded", handler = function () { + oDOC.removeEventListener("DOMContentLoaded", handler, false) + oDOC.readyState = "complete" + }, false) + } + })(window, document) + } + + // get an only property, if any + function onlyProp(obj) { + if (obj && typeof obj === 'object') { + var keys = Object.keys(obj) + if (keys.length === 1) return obj[keys[0]] + } + return obj + } + + // 'repos/show/:user/:repo/branches' -> ['user', 'repo'] + function paramsFromRoute(route) { + if (route.indexOf(':') === -1) return [] + return route.split('/') + .filter(function(s) { return s.charAt(0) === ':' }) + .map(function(s) { return s.slice(1) }) + } + + function titleCaseFirst(s) { return s.charAt(0).toUpperCase() + s.slice(1) } + + if (isBrowser) { + function update(array, args) { + var arrayLength = array.length, length = args.length; + while (length--) array[arrayLength + length] = args[length]; + return array; + } + + function merge(array, args) { + array = [].slice.call(array, 0); + return update(array, args); + } + + if (!Function.prototype.bind) { + Function.prototype.bind = function(context) { + if (arguments.length < 2 && typeof arguments[0] === 'undefined') return this + var __method = this, args = [].slice.call(arguments, 1) + return function() { + var a = merge(args, arguments) + return __method.apply(context, a) + } + } + } + } +}()) \ No newline at end of file