Skip to content

Commit

Permalink
New API for registering services - v2
Browse files Browse the repository at this point in the history
  • Loading branch information
Daniel15 committed Dec 4, 2017
1 parent 8f7f16d commit 1593ea0
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 43 deletions.
8 changes: 7 additions & 1 deletion lib/load-logos.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@
const fs = require('fs');
const path = require('path');

let logos;
function loadLogos () {
if (logos) {
// Logos have already been loaded; just returned the cached version
return logos;
}

// Cache svg logos from disk in base64 string
const logos = {};
logos = {};
const logoDir = path.join(__dirname, '..', 'logo');
const logoFiles = fs.readdirSync(logoDir);
logoFiles.forEach(function(filename) {
Expand Down
12 changes: 12 additions & 0 deletions lib/request-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,18 @@ function handleRequest (handlerOptions) {
});
}

// Wrapper around `cachingRequest` that returns a promise rather than
// needing to pass a callback.
cachingRequest.asPromise = (uri, options) => new Promise((resolve, reject) => {
cachingRequest(uri, options, (err, res, buffer) => {
if (err) {
reject(err);
} else {
resolve({res, buffer});
}
});
});

vendorDomain.run(() => {
handlerOptions.handler(filteredQueryParams, match, function sendBadge(format, badgeData) {
if (serverUnresponsive) { return; }
Expand Down
103 changes: 61 additions & 42 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const countBy = require('lodash.countby');
const jp = require('jsonpath');
const path = require('path');
const prettyBytes = require('pretty-bytes');
const glob = require('glob');
const queryString = require('query-string');
const semver = require('semver');
const xml2js = require('xml2js');
Expand Down Expand Up @@ -172,6 +173,66 @@ camp.notfound(/.*/, function(query, match, end, request) {

// Vendors.

/**
* Registers a new service in the shields.io system. `serviceClass` must be a
* class that extends `BaseService`.
*/
function registerService(serviceClass) {
// Regular expressions treat "/" specially, so we need to escape them
const escapedPath = serviceClass.uri.format.replace(/\//g, '\\/');
const fullRegex = '^' + escapedPath + '.(svg|png|gif|jpg|json)$';

camp.route(new RegExp(fullRegex),
cache(async (data, match, sendBadge, request) => {
// Assumes the final capture group is the extension
const format = match.pop();
const badgeData = getBadgeData(
serviceClass.category,
Object.assign({}, serviceClass.defaultBadgeData, data)
);

try {
const namedParams = {};
if (serviceClass.uri.capture.length !== match.length - 1) {
throw new Error(
`Incorrect number of capture groups (expected `+
`${serviceClass.uri.capture.length}, got ${match.length - 1})`
);
}

serviceClass.uri.capture.forEach((name, index) => {
// The first capture group is the entire match, so every index is + 1 here
namedParams[name] = match[index + 1];
});

const serviceInstance = new serviceClass({
sendAndCacheRequest: request.asPromise,
});
const serviceData = await serviceInstance.handle(namedParams);
const text = badgeData.text;
if (serviceData.text) {
text[1] = serviceData.text;
}
Object.assign(badgeData, serviceData);
badgeData.text = text;
sendBadge(format, badgeData);

} catch (error) {
console.log(error);
const text = badgeData.text;
text[1] = 'error';
badgeData.text = text;
sendBadge(format, badgeData);
}
}));
}

// New-style services
glob.sync(`${__dirname}/services/*.js`)
.filter(path => !path.endsWith('BaseService.js'))
.map(path => require(path))
.forEach(registerService);

// JIRA issue integration
camp.route(/^\/jira\/issue\/(http(?:s)?)\/(.+)\/([^/]+)\.(svg|png|gif|jpg|json)$/,
cache(function (data, match, sendBadge, request) {
Expand Down Expand Up @@ -680,48 +741,6 @@ cache(function (data, match, sendBadge, request) {
});
}));

// AppVeyor CI integration.
camp.route(/^\/appveyor\/ci\/([^/]+\/[^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/,
cache(function(data, match, sendBadge, request) {
var repo = match[1]; // eg, `gruntjs/grunt`.
var branch = match[2];
var format = match[3];
var apiUrl = 'https://ci.appveyor.com/api/projects/' + repo;
if (branch != null) {
apiUrl += '/branch/' + branch;
}
var badgeData = getBadgeData('build', data);
request(apiUrl, { headers: { 'Accept': 'application/json' } }, function(err, res, buffer) {
if (err != null) {
badgeData.text[1] = 'inaccessible';
sendBadge(format, badgeData);
return;
}
try {
if (res.statusCode === 404) {
badgeData.text[1] = 'project not found or access denied';
sendBadge(format, badgeData);
return;
}
var data = JSON.parse(buffer);
var status = data.build.status;
if (status === 'success') {
badgeData.text[1] = 'passing';
badgeData.colorscheme = 'brightgreen';
} else if (status !== 'running' && status !== 'queued') {
badgeData.text[1] = 'failing';
badgeData.colorscheme = 'red';
} else {
badgeData.text[1] = status;
}
sendBadge(format, badgeData);
} catch(e) {
badgeData.text[1] = 'invalid';
sendBadge(format, badgeData);
}
});
}));

// AppVeyor test status integration.
camp.route(/^\/appveyor\/tests\/([^/]+\/[^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/,
cache(function(data, match, sendBadge, request) {
Expand Down
63 changes: 63 additions & 0 deletions services/AppVeyor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use strict';

const BaseService = require('./BaseService');
const loadLogos = require('../lib/load-logos');

/**
* AppVeyor CI integration.
*/
module.exports = class AppVeyor extends BaseService {
async handle({repo, branch}) {
let apiUrl = 'https://ci.appveyor.com/api/projects/' + repo;
if (branch != null) {
apiUrl += '/branch/' + branch;
}
const {buffer, res} = await this._sendAndCacheRequest(apiUrl, {
headers: { 'Accept': 'application/json' }
});

if (res.statusCode === 404) {
return {text: 'project not found or access denied'};
}

const data = JSON.parse(buffer);
const status = data.build.status;
if (status === 'success') {
return {text: 'passing', colorscheme: 'brightgreen'};
} else if (status !== 'running' && status !== 'queued') {
return {text: 'failing', colorscheme: 'red'};
} else {
return {text: status};
}
}

// Metadata
static get category() {
return 'build';
}

static get uri() {
return {
format: '/appveyor/ci/([^/]+/[^/]+)(?:/(.+))?',
capture: ['repo', 'branch']
};
}

static get defaultBadgeData() {
return {
logo: loadLogos().appveyor,
};
}

static getExamples() {
return [
{
uri: '/appveyor/ci/gruntjs/grunt',
},
{
name: 'Branch',
uri: '/appveyor/ci/gruntjs/grunt/master',
},
];
}
}
56 changes: 56 additions & 0 deletions services/BaseService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use strict';

module.exports = class BaseService {
constructor({sendAndCacheRequest}) {
this._sendAndCacheRequest = sendAndCacheRequest;
}

/**
* Asynchronous function to handle requests for this service. Takes the URI
* parameters (as defined in the `uri` property), performs a request using
* `this._sendAndCacheRequest`, and returns the badge data.
*/
async handle(namedParams) {
throw new Error(
`Handler not implemented for ${this.constructor.name}`
);
}

// Metadata

/**
* Name of the category to sort this badge into (eg. "build"). Used to sort
* the badges on the main shields.io website.
*/
static get category() {
return 'unknown';
}
/**
* Returns an object with two fields:
* - format: Regular expression to use for URIs for this service's badges
* - capture: Array of names for the capture groups in the regular
* expression. The handler will be passed an object containing
* the matches.
*/
static get uri() {
throw new Error(`URI not defined for ${this.name}`);
}

/**
* Default data for the badge. Can include things such as default logo, color,
* etc. These defaults will be used if the value is not explicitly overridden
* by either the handler or by the user via URL parameters.
*/
static get defaultBadgeData() {
return {};
}

/**
* Example URIs for this service. These should use the format
* specified in `uri`, and can be used to demonstrate how to use badges for
* this service.
*/
static getExamples() {
return [];
}
}

0 comments on commit 1593ea0

Please sign in to comment.