Skip to content
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

Extract examples from new-style services #1582

Merged
merged 7 commits into from
Mar 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,4 @@ typings/
# Temporary build artifacts.
/build
.next
badge-examples.json
50 changes: 42 additions & 8 deletions lib/all-badge-examples.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict';

const { loadServiceClasses } = require('../services');

const visualStudioTeamServicesDoc = `
<p>
To obtain your own badge, you will first need to enable badges for your
Expand Down Expand Up @@ -144,14 +146,6 @@ const allBadgeExamples = [
],
exampleUri: '/teamcity/http/teamcity.jetbrains.com/e/bt345.svg'
},
{
title: 'AppVeyor',
previewUri: '/appveyor/ci/gruntjs/grunt.svg'
},
{
title: 'AppVeyor branch',
previewUri: '/appveyor/ci/gruntjs/grunt/master.svg'
},
{
title: 'AppVeyor tests',
previewUri: '/appveyor/tests/NZSmartie/coap-net-iu0to.svg'
Expand Down Expand Up @@ -2260,4 +2254,44 @@ const allBadgeExamples = [
}
];

function makeFullUrl(partialUrl, base) {
return '/' + [base, partialUrl].filter(Boolean).join('/');
}

function prepareExample({ title, previewUrl, exampleUrl, documentation }, ServiceClass) {
if (! previewUrl) {
throw Error(`Example for ${ServiceClass.name} is missing required previewUrl`);
}

const fullUrl = partial => makeFullUrl(partial, ServiceClass.url.base);

return {
title: title ? `${ServiceClass.name} ${title}` : ServiceClass.name,
previewUri: `${fullUrl(previewUrl)}.svg`,
exampleUri: exampleUrl ? `${fullUrl(exampleUrl)}.svg` : undefined,
documentation,
};
}

function getCategory(wantedCategory) {
return allBadgeExamples.find(thisCat => thisCat.category.id === wantedCategory);
}

function loadExamples() {
loadServiceClasses().forEach(ServiceClass => {
const { category: wantedCategory, examples: theseExamples } = ServiceClass;

const category = getCategory(wantedCategory);
if (category === undefined) {
throw Error(`Unknown category ${wantedCategory} referenced in ${ServiceClass.name}`);
}

const prepared = theseExamples.map(
inExample => prepareExample(inExample, ServiceClass));
category.examples = category.examples.concat(prepared);
});
}
loadExamples();

module.exports = allBadgeExamples;
module.exports.getCategory = getCategory;
29 changes: 29 additions & 0 deletions lib/all-badge-examples.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

const { expect } = require('chai');

const allBadgeExamples = require('./all-badge-examples');

describe('The badge examples', function () {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think its worth having another test here which just iterates over all examples (rather than just the appveyor ones) and runs prepareExample() on each one. It doesn't have to assert anything other than an error wasn't thrown. That way we'll get a failure on the server tests if an invalid example (without a previewUri) is added in a PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, that's a good idea, though I think this is already happening when this file is required, on account of the call to loadExamples().

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one step ahead of me :)

it('should include AppVeyor, which is added automatically', function () {
const { examples } = allBadgeExamples.getCategory('build');

const appVeyorBuildExamples = examples.filter(ex => ex.title.includes('AppVeyor'))
.filter(ex => ! ex.title.includes('tests'));

expect(appVeyorBuildExamples).to.deep.equal([
{
title: 'AppVeyor',
previewUri: '/appveyor/ci/gruntjs/grunt.svg',
exampleUri: undefined,
documentation: undefined,
},
{
title: 'AppVeyor branch',
previewUri: '/appveyor/ci/gruntjs/grunt/master.svg',
exampleUri: undefined,
documentation: undefined,
},
]);
});
});
5 changes: 5 additions & 0 deletions lib/export-badge-examples-cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const allBadgeExamples = require('./all-badge-examples');

process.stdout.write(JSON.stringify(allBadgeExamples));
4 changes: 2 additions & 2 deletions lib/service-test-runner/runner.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

const glob = require('glob');
const { loadTesters } = require('../../services');

/**
* Load a collection of ServiceTester objects and register them with Mocha.
Expand All @@ -16,7 +16,7 @@ class Runner {
* Prepare the runner by loading up all the ServiceTester objects.
*/
prepare () {
this.testers = glob.sync(`${__dirname}/../../services/**/*.tester.js`).map(name => require(name));
this.testers = loadTesters();
this.testers.forEach(tester => {
tester.beforeEach = () => { this.beforeEach(); };
});
Expand Down
5 changes: 5 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ module.exports = {
}));
}

config.module.loaders = (config.module.loaders || []).concat({
test: /\.json$/,
loader: 'json-loader',
});

return config;
},
exportPathMap: () => ({
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,13 @@
"depcheck": "check-node-version --node \">= 8.0\"",
"postinstall": "npm run depcheck",
"prebuild": "npm run depcheck",
"build": "next build && next export -o build/",
"examples": "node lib/export-badge-examples-cli.js > badge-examples.json",
"build": "npm run examples && next build && next export -o build/",
"heroku-postbuild": "npm run build",
"analyze": "ANALYZE=true LONG_CACHE=false BASE_URL=https://img.shields.io npm run build",
"start:server": "RATE_LIMIT=false node server 8080 ::",
"now-start": "node server",
"prestart": "npm run depcheck",
"prestart": "npm run depcheck && npm run examples",
"start": "concurrently --names server,frontend \"ALLOWED_ORIGIN=http://localhost:3000 npm run start:server\" \"BASE_URL=http://[::]:8080 next dev\""
},
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { BadgeExamples } from '../frontend/components/badge-examples';
import MarkupModal from '../frontend/components/markup-modal';
import Usage from '../frontend/components/usage';
import Footer from '../frontend/components/footer';
import badgeExampleData from '../lib/all-badge-examples';
import badgeExampleData from '../badge-examples.json';
import { prepareExamples, predicateFromQuery } from '../frontend/lib/prepare-examples';

const baseUri = process.env.BASE_URL;
Expand Down
12 changes: 3 additions & 9 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const dom = require('xmldom').DOMParser;
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 All @@ -16,6 +15,7 @@ const serverSecrets = require('./lib/server-secrets');
Raven.config(process.env.SENTRY_DSN || serverSecrets.sentry_dsn).install();
Raven.disableConsoleAlerts();

const { loadServiceClasses } = require('./services');
const { isDeprecated, getDeprecatedBadge } = require('./lib/deprecation-helpers');
const { checkErrorResponse } = require('./lib/error-helper');
const analytics = require('./lib/analytics');
Expand Down Expand Up @@ -200,14 +200,8 @@ camp.notfound(/.*/, function(query, match, end, request) {

// Vendors.

// Match modules with the same name as their containing directory.
// e.g. services/appveyor/appveyor.js
const serviceRegex = /\/services\/(.*)\/\1\.js$/;
// New-style services
glob.sync(`${__dirname}/services/**/*.js`)
.filter(path => serviceRegex.test(path))
.map(path => require(path))
.forEach(serviceClass => serviceClass.register(camp, cache));
loadServiceClasses().forEach(
serviceClass => serviceClass.register(camp, cache));

// JIRA issue integration
camp.route(/^\/jira\/issue\/(http(?:s)?)\/(.+)\/([^/]+)\.(svg|png|gif|jpg|json)$/,
Expand Down
13 changes: 7 additions & 6 deletions services/appveyor/appveyor.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,22 @@ module.exports = class AppVeyor extends BaseService {
return 'build';
}

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

static getExamples() {
static get examples() {
return [
{
uri: '/appveyor/ci/gruntjs/grunt',
previewUrl: 'gruntjs/grunt',
},
{
name: 'Branch',
uri: '/appveyor/ci/gruntjs/grunt/master',
title: 'branch',
previewUrl: 'gruntjs/grunt/master',
},
];
}
Expand Down
32 changes: 18 additions & 14 deletions services/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ module.exports = class BaseService {
}

/**
* Asynchronous function to handle requests for this service. Takes the URI
* parameters (as defined in the `uri` property), performs a request using
* Asynchronous function to handle requests for this service. Takes the URL
* parameters (as defined in the `url` property), performs a request using
* `this._sendAndCacheRequest`, and returns the badge data.
*/
async handle(namedParams) {
Expand All @@ -33,14 +33,16 @@ module.exports = class BaseService {
return 'unknown';
}
/**
* Returns an object with two fields:
* - format: Regular expression to use for URIs for this service's badges
* Returns an object:
* - base: (Optional) The base path of the URLs for this service. This is
* used as a prefix.
* - format: Regular expression to use for URLs 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}`);
static get url() {
throw new Error(`URL not defined for ${this.name}`);
}

/**
Expand All @@ -53,18 +55,20 @@ module.exports = class BaseService {
}

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

static get _regex() {
const { base, format } = this.url;
// Regular expressions treat "/" specially, so we need to escape them
const escapedPath = this.uri.format.replace(/\//g, '\\/');
const fullRegex = '^' + escapedPath + '.(svg|png|gif|jpg|json)$';
const escapedPath = format.replace(/\//g, '\\/');
const joined = [base, escapedPath].filter(Boolean).join('/');
const fullRegex = `^/${joined}.(svg|png|gif|jpg|json)$`;
return new RegExp(fullRegex);
}

Expand All @@ -73,15 +77,15 @@ module.exports = class BaseService {
// entire match.
const captures = match.slice(1, -1);

if (this.uri.capture.length !== captures.length) {
if (this.url.capture.length !== captures.length) {
throw new Error(
`Service ${this.constructor.name} declares incorrect number of capture groups `+
`(expected ${this.uri.capture.length}, got ${captures.length})`
`(expected ${this.url.capture.length}, got ${captures.length})`
);
}

const result = {};
this.uri.capture.forEach((name, index) => {
this.url.capture.forEach((name, index) => {
result[name] = captures[index];
});
return result;
Expand Down
5 changes: 3 additions & 2 deletions services/base.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ class DummyService extends BaseService {
}

static get category() { return 'cat'; }
static get uri() {
static get url() {
return {
format: '/foo/([^/]+)',
base: 'foo',
format: '([^/]+)',
capture: ['someArg']
};
}
Expand Down
24 changes: 24 additions & 0 deletions services/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict';

const glob = require('glob');

// Match modules with the same name as their containing directory.
// e.g. services/appveyor/appveyor.js
const serviceRegex = /\/services\/(.*)\/\1\.js$/;

function loadServiceClasses() {
// New-style services
return glob.sync(`${__dirname}/**/*.js`)
.filter(path => serviceRegex.test(path))
.map(path => require(path))
}

function loadTesters() {
return glob.sync(`${__dirname}/**/*.tester.js`)
.map(name => require(name));
}

module.exports = {
loadServiceClasses,
loadTesters,
};