-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] New API for registering services #963
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
11e8f19
New API for registering services - v2
Daniel15 1faa976
Code review comments, and add basic unit test
Daniel15 1c2b3ce
Remove unneeded load-logos changes
Daniel15 2330181
Get lint passing with async/await
paulmelnikow dbf3c0f
Clear some more build errors
paulmelnikow 160a4a5
Oops, forgot server does not work in Node 9
paulmelnikow b23784c
Let’s make life simple and drop Node 9 for now
paulmelnikow File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,6 @@ | ||
parserOptions: | ||
ecmaVersion: 8 | ||
|
||
env: | ||
node: true | ||
# We use Promise, Map, and occasional ES6 syntax. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
'use strict'; | ||
|
||
const BaseService = require('./base'); | ||
|
||
/** | ||
* 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 getExamples() { | ||
return [ | ||
{ | ||
uri: '/appveyor/ci/gruntjs/grunt', | ||
}, | ||
{ | ||
name: 'Branch', | ||
uri: '/appveyor/ci/gruntjs/grunt/master', | ||
}, | ||
]; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
'use strict'; | ||
|
||
const { | ||
makeBadgeData: getBadgeData, | ||
} = require('../lib/badge-data'); | ||
|
||
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 []; | ||
} | ||
|
||
static register(camp, handleRequest) { | ||
const serviceClass = this; // In a static context, "this" is the class. | ||
|
||
// 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), | ||
handleRequest(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); | ||
} | ||
})); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
'use strict'; | ||
|
||
const assert = require('assert'); | ||
const sinon = require('sinon'); | ||
|
||
const BaseService = require('./base'); | ||
|
||
class DummyService extends BaseService { | ||
async handle({someArg}) { | ||
return { | ||
text: 'Hello ' + someArg, | ||
}; | ||
} | ||
|
||
static get category() { return 'cat'; } | ||
static get uri() { | ||
return { | ||
format: '/foo/([^/]+)', | ||
capture: ['someArg'] | ||
}; | ||
} | ||
} | ||
|
||
const expectedRouteRegex = /^\/foo\/([^/]+).(svg|png|gif|jpg|json)$/; | ||
|
||
describe('BaseService', () => { | ||
let mockCamp; | ||
let mockHandleRequest; | ||
|
||
beforeEach(() => { | ||
mockCamp = { | ||
route: sinon.spy(), | ||
}; | ||
mockHandleRequest = sinon.spy(); | ||
DummyService.register(mockCamp, mockHandleRequest); | ||
}); | ||
|
||
it('registers the service', () => { | ||
assert(mockCamp.route.calledOnce); | ||
assert.equal(mockCamp.route.getCall(0).args[0].toString(), expectedRouteRegex); | ||
}); | ||
|
||
it('handles the request', async () => { | ||
assert(mockHandleRequest.calledOnce); | ||
const requestHandler = mockHandleRequest.getCall(0).args[0]; | ||
|
||
const mockSendBadge = sinon.spy(); | ||
const mockRequest = { | ||
asPromise: sinon.spy(), | ||
}; | ||
await requestHandler( | ||
/*data*/ {}, | ||
/*match*/ '/foo/bar.svg'.match(expectedRouteRegex), | ||
mockSendBadge, | ||
mockRequest | ||
); | ||
|
||
assert(mockSendBadge.calledOnce); | ||
assert(mockSendBadge.calledWith( | ||
/*format*/ 'svg', | ||
{ | ||
text: ['cat', 'Hello bar'], | ||
colorscheme: 'lightgrey', | ||
template: 'default', | ||
logo: undefined, | ||
logoWidth: NaN, | ||
links: [], | ||
colorA: undefined, | ||
colorB: undefined, | ||
} | ||
)); | ||
}); | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aw, love these async tests with async/await!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I'm happy that Mocha allows test cases to return promises. Async/await is just a fancy syntax for promises.
I'm used to Jest, so I had to keep the Mocha docs open while writing these tests. They're similar but there's subtle differences (eg. Mocha doesn't have a mocking/stubbing framework built in and you need to use Sinon, whereas Jest includes mocking out-of-the-box).