first commit, implements most of the reading API

This commit is contained in:
Sami Samhuri 2010-11-07 15:58:39 -08:00
commit 3363be22e8
4 changed files with 433 additions and 0 deletions

18
LICENSE Normal file
View file

@ -0,0 +1,18 @@
Copyright 2010 Sami Samhuri. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.

52
Readme.md Normal file
View file

@ -0,0 +1,52 @@
gitter
======
A GitHub client inspired by pengwynn/octopussy.
v2 API
Installation
============
npm install gitter
Usage
=====
var gh = require('gitter')
gh.user('samsonjs', function(err, user) {
if (err) throw err
console.log('---- user: samsonjs ----')
console.dir(user)
})
gh.repo('samsonjs/gitter', function(err, repo) {
if (err) throw err
console.log('---- repo: ' + repo.owner + '/' + repo.name + ' ----')
console.dir(repo)
}).getWatchers(function(err, repos) {
if (err) throw err
console.log('---- watchers ----')
console.dir(repos)
}).getBranches(function(err, branches) {
if (err) throw err
console.log('---- branches: samsonjs/gitter ----')
console.dir(branches)
gh.commit(this.repo, branches['master'], function(err, commit) {
if (err) throw err
console.log('---- samsonjs/gitter/master commit: ' + commit.id + ' ----')
console.dir(commit.data())
})
})
For the full API have a look at the top of [lib/index.js](/samsonjs/gitter/blob/master/lib/index.js).
License
=======
Copyright 2010 Sami Samhuri sami.samhuri@gmail.com
MIT (see included [LICENSE](/samsonjs/gitter/blob/master/LICENSE))

338
lib/index.js Normal file
View file

@ -0,0 +1,338 @@
/// gitter
/// http://github.com/samsonjs/gitter
/// @_sjs
///
/// Copyright 2010 Sami Samhuri <sami.samhuri@gmail.com>
/// MIT License
// TODO:
// - authentication and write APIs
// - run in browsers (dojo?)
var request = require('request')
, util = require('util')
, Blob, Branch, Commit, Raw, Repo, Tree, User
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)
},
list: 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/:sha/:path', {
has: [ ['commits', 'commits/list/:repo/:sha/: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']
]
})
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)
resource.prototype[fn] = function(cb, force) {
return this._fetch({ prop: dataProp
, route: route || this._route + '/' + prop
, processData: function(d) {
getter(this, dataProp, function() { return camelize(unpack(d)) })
}.bind(this)
, result: function(resource) { return resource[dataProp] }
}, 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
}
// 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]
// assign params from args
this._params.forEach(function(param, i) {
this[param] = args[i]
}.bind(this))
// set the resource path
this.path = this.resolve(this._route)
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') return this._data && this._data[data]
getter(this, '_data', function() { return data }, {configurable: true})
if (!this._propsDefined) {
getter(this, '_propsDefined', function() { return true })
dataProps(Object.getPrototypeOf(this), Object.keys(this._data))
}
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.path
, 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))
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) 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]]
}
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) })
}
// define getters for a list of data properties
function dataProps(obj, keys) {
keys.forEach(function(key) {
if (obj.hasOwnProperty(key))
console.warn('property "' + key + '" already exists, skipping')
else
getter(obj, key, function() { return this.data(key) }, {enumerable: true})
})
}
function slice(x) { return [].slice.call(x) }
function titleCaseFirst(s) { return s.charAt(0).toUpperCase() + s.slice(1) }

25
package.json Normal file
View file

@ -0,0 +1,25 @@
{ "name" : "gitter"
, "description" : "GitHub client (API v2), inspired by pengwynn/octopussy"
, "version" : "0.0.1"
, "homepage" : "http://samhuri.net/node/gitter"
, "author" : "Sami Samhuri <sami.samhuri@gmail.com>"
, "repository" :
{ "type" : "git"
, "url" : "http://github.com/samsonjs/gitter.git"
}
, "bugs" :
{ "mail" : "sami.samhuri+gitter@gmail.com"
, "web" : "http://github.com/samsonjs/gitter/issues"
}
, "directories" : { "lib" : "./lib" }
, "main" : "./lib/index"
, "engines" : { "node" : ">=0.3.0" }
, "licenses" :
[ { "type" : "MIT"
, "url" : "http://github.com/samsonjs/gitter/raw/master/LICENSE"
}
]
, "dependencies" : { "request" : "0.10.0 - 0.10.999"
, "vows" : "0.5.0 - 0.5.999"
}
}