mirror of
https://github.com/samsonjs/gitter.git
synced 2026-04-27 15:07:41 +00:00
first commit, implements most of the reading API
This commit is contained in:
commit
3363be22e8
4 changed files with 433 additions and 0 deletions
18
LICENSE
Normal file
18
LICENSE
Normal 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
52
Readme.md
Normal 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
338
lib/index.js
Normal 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
25
package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue