Permalink
Browse files

First WIP commit for node-gcph-client

  • Loading branch information...
1 parent 4cba6ce commit 0ccc2b82ba68ff490b152cbf875bd766abf79610 @JamesMGreene committed Nov 17, 2012
Showing with 638 additions and 3 deletions.
  1. +3 −0 .gitignore
  2. +15 −0 .jshintrc
  3. +1 −0 .npmignore
  4. +36 −0 Gruntfile.js
  5. +22 −0 LICENSE-MIT
  6. +26 −3 README.md
  7. +318 −0 lib/gcph-client.js
  8. +49 −0 lib/gcph-comment.js
  9. +49 −0 lib/gcph-issue.js
  10. +53 −0 lib/gcph-query.js
  11. +24 −0 lib/gcph.js
  12. +42 −0 package.json
View
3 .gitignore
@@ -12,3 +12,6 @@ logs
results
npm-debug.log
+
+# Added for this project
+node_modules
View
15 .jshintrc
@@ -0,0 +1,15 @@
+{
+ "curly": true,
+ "eqeqeq": true,
+ "immed": true,
+ "latedef": true,
+ "newcap": true,
+ "noarg": true,
+ "sub": true,
+ "undef": true,
+ "unused": true,
+ "boss": true,
+ "eqnull": true,
+ "node": true,
+ "es5": true
+}
View
1 .npmignore
@@ -0,0 +1 @@
+/node_modules/
View
36 Gruntfile.js
@@ -0,0 +1,36 @@
+'use strict';
+
+module.exports = function(grunt) {
+
+ // Project configuration.
+ grunt.initConfig({
+ jshint: {
+ options: {
+ jshintrc: '.jshintrc'
+ },
+ gruntfile: {
+ src: 'Gruntfile.js'
+ },
+ lib: {
+ src: ['lib/**/*.js']
+ }
+ },
+ watch: {
+ gruntfile: {
+ files: '<%= jshint.gruntfile.src %>',
+ tasks: ['jshint:gruntfile']
+ },
+ lib: {
+ files: '<%= jshint.lib.src %>',
+ tasks: ['jshint:lib', 'nodeunit']
+ }
+ }
+ });
+
+ // Load tasks from Node modules.
+ grunt.loadNpmTask('grunt-contrib-jshint');
+
+ // Default task.
+ grunt.registerTask('default', ['jshint']);
+
+};
View
22 LICENSE-MIT
@@ -0,0 +1,22 @@
+Copyright (c) 2012 James M. Greene
+
+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.
View
29 README.md
@@ -1,4 +1,27 @@
-node-gcph-client
-================
+# gcph-client
-A Node.js client for the Google Code Project Hosting Issue Tracker API.
+A Node.js client for the Google Code Project Hosting Issue Tracker API.
+
+## Getting Started
+Install the module with: `npm install gcph-client`
+
+```js
+var gcph = require('gcph-client');
+var client = new gcph.Client();
+```
+
+## Documentation
+_(Coming soon)_
+
+## Examples
+_(Coming soon)_
+
+## Contributing
+In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using [grunt](http://gruntjs.com/).
+
+## Release History
+_(Nothing yet)_
+
+## License
+Copyright (c) 2012 James M. Greene
+Licensed under the MIT license.
View
318 lib/gcph-client.js
@@ -0,0 +1,318 @@
+/*
+ * gcph-client
+ * https://github.com/JamesMGreene/node-gcph-client
+ *
+ * Copyright (c) 2012 James M. Greene
+ * Licensed under the MIT license.
+ */
+
+'use strict';
+
+exports = Client;
+
+var appMeta = require('../package.json');
+var querystring = require('querystring')
+var XmlParser = require('xml2js').Parser;
+var Query = require('./gcph-query');
+
+// Convert dashed name to Pascal-cased name, e.g. 'gcph-client' => 'GcphClient'
+var appName = appMeta.name.split('-').map(function(e) { return e ? e.slice(0, 1).toUpperCase() + e.slice(1).toLowerCase() : e; }).join('');
+var sourceId = ['NodeJS', appName, appMeta.version].join('-'); // e.g. 'NodeJS-GcphClient-0.1.0'
+var authTokenPropName = '_authToken';
+
+var getAuthHeaders = function(client) {
+ return {
+ 'Authorization': 'GoogleLogin auth=' + client[authTokenPropName]
+ };
+};
+
+var isAuthenticated = function(client) {
+ return Object.prototype.hasOwnProperty.call(client, authTokenPropName) && !!client[authTokenPropName];
+};
+
+var Client = function() {
+ // Allow `Client()` to work the same as `new Client()`
+ if (!(this instanceof Client)) {
+ return new Client();
+ }
+};
+Client.prototype = Object.create(null);
+Client.constructor = Client;
+
+/**
+ *
+ * Target URL: https://www.google.com/accounts/ClientLogin?alt=json
+ */
+Client.prototype.login = function(username, password, done) {
+ // Input validation
+ if (typeof done !== 'function') {
+ throw new TypeError('`done` was not a function');
+ }
+
+ var me = this;
+ if (isAuthenticated(me)) {
+ console.warn('WARNING: Someone is already logged in! Create a new Client instance.');
+ done(me[authTokenPropName]);
+ }
+ else {
+ // Input validation continued
+ if (!username) {
+ done(null, new TypeError('`username` was empty');
+ }
+ else if (!password) {
+ done(null, new TypeError('`password` was empty');
+ }
+ else {
+ // Do the real work
+ var postData = querystring.stringify({
+ accountType: 'HOSTED_OR_GOOGLE',
+ Email: username,
+ Passwd: password,
+ source: sourceId,
+ service: 'code'
+ });
+ var requestMeta = {
+ method: 'POST',
+ host: 'www.google.com',
+ path: '/accounts/ClientLogin?alt=json',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'Content-Length': postData.length
+ }
+ };
+
+ var req = https.request(requestMeta, function(res) {
+ var responseData = '';
+ if (res.statusCode === 200) {
+ res.setEncoding('utf8');
+ res.on('data', function (chunk) {
+ responseData += chunk;
+ });
+ res.on('end', function() {
+ var parser = new XmlParser();
+ parser.parseString(responseData, function (err, result) {
+ if (!err) {
+ var authToken = result.Auth;
+ // Store the authorization token item as a read-only property
+ Object.defineProperty(me, authTokenPropName, { value: authToken });
+ done(authToken);
+ }
+ else {
+ done(null, err);
+ }
+ });
+ });
+ }
+ else {
+ // For Google's subset of status codes, see:
+ // https://developers.google.com/accounts/docs/AuthForInstalledApps#Errors
+ done(null, new Error('HTTP ' + res.statusCode + ': Failed to login!'));
+ }
+ });
+ req.on('error', function(err) {
+ done(null, err);
+ });
+ req.write(postData);
+ req.end();
+ }
+ }
+};
+
+/**
+ * Get all the issues, or all issues matching a query.
+ * Target URL: https://code.google.com/feeds/issues/p/{@project}/issues/full?alt=json
+ */
+Client.prototype.getIssues = function(project, query, done) {
+ // Shift the arguments if no `query` was provided
+ if (typeof query === 'function' && done == null) {
+ done = query;
+ query = null;
+ }
+
+ // Input validation
+ if (typeof done !== 'function') {
+ throw new TypeError('`done` was not a function');
+ }
+
+ if (!isAuthenticated(this)) {
+ console.error('ERROR: Nobody is logged in!');
+ done(null, new Error('This request was unauthorized. Please login first!'));
+ }
+ else {
+ // Input validation continued
+ if (!project) {
+ done(null, new TypeError('`project` was empty');
+ }
+ else if (query && !(query instanceof Query)) {
+ done(null, new TypeError('`query` was provided but was not an instance of Query');
+ }
+ else {
+ // Do the real work
+ query = query || new Query();
+ var queryParamsString = (function() {
+ var qs = querystring.stringify(query);
+ return qs ? '&' + qs : '';
+ })();
+ var requestMeta = {
+ method: 'GET',
+ host: 'code.google.com',
+ path: '/feeds/issues/p/' + project + '/issues/full?alt=json' + queryParamsString,
+ headers: getAuthHeaders(this)
+ };
+
+ var req = https.request(requestMeta, function(res) {
+
+ });
+ req.on('error', function(err) {
+ done(null, err);
+ });
+ req.end();
+ }
+ }
+};
+
+/**
+ *
+ * Target URL: https://code.google.com/feeds/issues/p/{@project}/issues/{@issueId}/comments/full?alt=json
+ */
+Client.prototype.getComments = function(project, issueId, done) {
+ // Input validation
+ if (typeof done !== 'function') {
+ throw new TypeError('`done` was not a function');
+ }
+
+ if (!isAuthenticated(this)) {
+ console.error('ERROR: Nobody is logged in!');
+ done(null, new Error('This request was unauthorized. Please login first!'));
+ }
+ else {
+ // Input validation continued
+ if (!project) {
+ done(null, new TypeError('`project` was empty');
+ }
+ else if (!issueId) {
+ done(null, new TypeError('`issueId` was empty');
+ }
+ else {
+ // Do the real work
+ var requestMeta = {
+ method: 'GET',
+ host: 'code.google.com',
+ path: '/feeds/issues/p/' + project + '/issues/' + issueId + '/comments/full?alt=json',
+ headers: getAuthHeaders(this)
+ };
+
+ var req = https.request(requestMeta, function(res) {
+
+ });
+ req.on('error', function(err) {
+ done(null, err);
+ });
+ req.end();
+ }
+ }
+};
+
+/**
+ * Add a new issue.
+ * Target URL:
+ */
+Client.prototype.addIssue = function(project, issue, done) {
+ // Input validation
+ if (typeof done !== 'function') {
+ throw new TypeError('`done` was not a function');
+ }
+
+ if (!isAuthenticated(this)) {
+ console.error('ERROR: Nobody is logged in!');
+ done(null, new Error('This request was unauthorized. Please login first!'));
+ }
+ else {
+ // Input validation continued
+ if (!project) {
+ done(null, new TypeError('`project` was empty');
+ }
+ else if (!issue) {
+ done(null, new TypeError('`issue` was empty');
+ }
+ else if (issue && !(issue instanceof Issue)) {
+ done(null, new TypeError('`issue` was provided but was not an instance of Issue');
+ }
+ else {
+ // Do the real work
+ var requestMeta = {
+ method: 'POST',
+ host: 'code.google.com',
+ path: '/feeds/issues/p/' + project + '/issues/full?alt=json',
+ headers: getAuthHeaders(this)
+ };
+
+ var req = https.request(requestMeta, function(res) {
+
+ });
+ req.on('error', function(err) {
+ done(null, err);
+ });
+ //req.write();
+ req.end();
+ }
+ }
+};
+
+/**
+ * Can do any and/or all of the following:
+ * - Update an existing issue's data
+ * - Close an existing issue
+ * - Add a new comment to an existing issue
+ *
+ * Target URL: https://code.google.com/feeds/issues/p/{@project}/issues/{@issueId}/comments/full?alt=json
+ */
+Client.prototype.updateIssue = function(project, issue, comment, done) {
+ // Input validation
+ if (typeof done !== 'function') {
+ throw new TypeError('`done` was not a function');
+ }
+
+ if (!isAuthenticated(this)) {
+ console.error('ERROR: Nobody is logged in!');
+ done(null, new Error('This request was unauthorized. Please login first!'));
+ }
+ else {
+ // Input validation continued
+ if (!project) {
+ done(null, new TypeError('`project` was empty');
+ }
+ else if (!issue) {
+ done(null, new TypeError('`issue` was empty');
+ }
+ else if (issue && !(issue instanceof Issue)) {
+ done(null, new TypeError('`issue` was provided but was not an instance of Issue');
+ }
+ else if (!issue.id) {
+ done(null, new TypeError('`issue` was provided but it did not have a valid `id` property');
+ }
+ else if (!comment) {
+ done(null, new TypeError('`comment` was empty');
+ }
+ else if (comment && !(comment instanceof Comment)) {
+ done(null, new TypeError('`comment` was provided but was not an instance of Comment');
+ }
+ else {
+ // Do the real work
+ var requestMeta = {
+ method: 'POST',
+ host: 'code.google.com',
+ path: '/feeds/issues/p/' + project + '/issues/' + issue.id + '/comments/full?alt=json',
+ headers: getAuthHeaders(this)
+ };
+
+ var req = https.get(requestMeta, function(res) {
+
+ });
+ req.on('error', function(err) {
+ done(null, err);
+ });
+ }
+ }
+};
+
View
49 lib/gcph-comment.js
@@ -0,0 +1,49 @@
+/*
+ * gcph-client
+ * https://github.com/JamesMGreene/node-gcph-client
+ *
+ * Copyright (c) 2012 James M. Greene
+ * Licensed under the MIT license.
+ */
+
+'use strict';
+
+exports = Comment;
+
+var Comment = function(values) {
+ var me = this;
+
+ // Allow `Comment()` to work the same as `new Comment()`
+ if (!(me instanceof Comment)) {
+ return new Comment(values);
+ }
+
+ var commentFields = [
+ 'alt',
+ 'author',
+ 'can',
+ 'id',
+ 'label',
+ 'max-results',
+ 'owner',
+ 'published-min',
+ 'published-max',
+ 'q',
+ 'status',
+ 'start-index',
+ 'status',
+ 'updated-min',
+ 'updated-max'
+ ];
+ commentFields.map(function(e) {
+ Object.defineProperty(me, e, {
+ value: values[e] || null,
+ writable: true
+ });
+ });
+
+ // Seal it off!
+ Object.seal(me);
+};
+Comment.prototype = Object.create(null);
+Comment.constructor = Comment;
View
49 lib/gcph-issue.js
@@ -0,0 +1,49 @@
+/*
+ * gcph-client
+ * https://github.com/JamesMGreene/node-gcph-client
+ *
+ * Copyright (c) 2012 James M. Greene
+ * Licensed under the MIT license.
+ */
+
+'use strict';
+
+exports = Issue;
+
+var Issue = function(values) {
+ var me = this;
+
+ // Allow `Issue()` to work the same as `new Issue()`
+ if (!(me instanceof Issue)) {
+ return new Issue(values);
+ }
+
+ var commentFields = [
+ 'alt',
+ 'author',
+ 'can',
+ 'id',
+ 'label',
+ 'max-results',
+ 'owner',
+ 'published-min',
+ 'published-max',
+ 'q',
+ 'status',
+ 'start-index',
+ 'status',
+ 'updated-min',
+ 'updated-max'
+ ];
+ commentFields.map(function(e) {
+ Object.defineProperty(me, e, {
+ value: values[e] || null,
+ writable: true
+ });
+ });
+
+ // Seal it off!
+ Object.seal(me);
+};
+Issue.prototype = Object.create(null);
+Issue.constructor = Issue;
View
53 lib/gcph-query.js
@@ -0,0 +1,53 @@
+/*
+ * gcph-client
+ * https://github.com/JamesMGreene/node-gcph-client
+ *
+ * Copyright (c) 2012 James M. Greene
+ * Licensed under the MIT license.
+ */
+
+'use strict';
+
+exports = Query;
+
+/**
+ *
+ * Definition of properties: http://code.google.com/p/support/wiki/IssueTrackerAPIPython#Retrieving_issues_using_query_parameters
+ */
+var Query = function(values) {
+ var me = this;
+
+ // Allow `Query()` to work the same as `new Query()`
+ if (!(me instanceof Query)) {
+ return new Query(values);
+ }
+
+ var queryFields = [
+ 'alt',
+ 'author',
+ 'can',
+ 'id',
+ 'label',
+ 'max-results',
+ 'owner',
+ 'published-min',
+ 'published-max',
+ 'q',
+ 'status',
+ 'start-index',
+ 'status',
+ 'updated-min',
+ 'updated-max'
+ ];
+ queryFields.map(function(e) {
+ Object.defineProperty(me, e, {
+ value: values[e] || null,
+ writable: true
+ });
+ });
+
+ // Seal it off!
+ Object.seal(me);
+};
+Query.prototype = Object.create(null);
+Query.constructor = Query;
View
24 lib/gcph.js
@@ -0,0 +1,24 @@
+/*
+ * gcph-client
+ * https://github.com/JamesMGreene/node-gcph-client
+ *
+ * Copyright (c) 2012 James M. Greene
+ * Licensed under the MIT license.
+ */
+
+'use strict';
+
+exports = Api;
+
+var Api = {
+ Client: require('./gcph-client'),
+ Query: require('./gcph-query'),
+ Issue: require('./gcph-issue'),
+ Comment: require('./gcph-comment')
+};
+Api.prototype = Object.create(null);
+
+// Define some read-only properties
+Object.defineProperty(Api, 'version', {
+ value: require('../package.json').version
+});
View
42 package.json
@@ -0,0 +1,42 @@
+{
+ "name": "gcph-client",
+ "description": "A Node.js client for the Google Code Project Hosting Issue Tracker API.",
+ "version": "0.1.0",
+ "homepage": "https://github.com/JamesMGreene/node-gcph-client",
+ "author": {
+ "name": "James M. Greene",
+ "email": "james.m.greene@gmail.com",
+ "url": "http://jamesgreene.net/"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/JamesMGreene/node-gcph-client"
+ },
+ "bugs": {
+ "url": "https://github.com/JamesMGreene/node-gcph-client/issues"
+ },
+ "licenses": [
+ {
+ "type": "MIT",
+ "url": "https://github.com/JamesMGreene/node-gcph-client/blob/master/LICENSE-MIT"
+ }
+ ],
+ "main": "lib/gcph",
+ "engines": {
+ "node": ">= 0.6.0"
+ },
+ "scripts": {},
+ "dependencies": {
+ "xml2js": "0.2.x"
+ },
+ "devDependencies": {
+ "grunt": "~0.4.0a",
+ "grunt-contrib-jshint": "0.1.x"
+ },
+ "keywords": [
+ "google code",
+ "project hosting",
+ "issue tracker",
+ "issues"
+ ]
+}

0 comments on commit 0ccc2b8

Please sign in to comment.