This tutorial should help you add a service to shields.io in form of a badge. You will need to learn to use JavaScript, Git and GitHub. Please improve the tutorial while you read it.
You should read CONTRIBUTING.md
You can also read previous merged pull-requests with the 'service-badge' label to see how other people implemented their badges.
You should have git installed. If you do not, install git and learn about the Github workflow.
Node 8 or later is required. If you don't already have them, install node and npm: https://nodejs.org/en/download/
- Fork this repository.
- Clone the fork
git clone [email protected]:YOURGITHUBUSERNAME/shields.git
cd shields
- Install project dependencies
npm install
- Run the server
npm start
- Visit the website to check the front-end is loaded: http://localhost:3000/
You may also want to install ImageMagick. This is an optional dependency needed for generating badges in raster format, but you can get a dev copy running without it.
Before you want to implement your service, you may want to open an issue and describe what you have in mind:
- What is the badge for?
- Which API do you want to use?
You may additionally proceed to say what you want to work on. This information allows other humans to help and build on your work.
Service badge code is stored in the /services directory. Each service has a directory for its files:
-
In files ending with
.service.js
, you can find the code which handles incoming requests and generates the badges. Sometimes, code for a service can be re-used. This might be the case when you add a badge for an API which is already used by other badges.Imagine a service that lives at https://img.shields.io/example/some-param-here.svg.
-
For services with a single badge, the badge code will generally be stored in
/services/example/example.service.js
. If you add a badge for a new API, create a new directory.Example: wercker
-
For service families with multiple badges we usually store the code for each badge in its own file like this:
/services/example/example-downloads.service.js
/services/example/example-version.service.js
etc.
Example: ruby gems
-
-
In files ending with
.tester.js
, you can find the code which uses the shields server to test if the badges are generated correctly. There is a chapter on Tests.
All service badge classes inherit from BaseService or another class which extends it. Other classes implement useful behavior on top of BaseService.
- BaseJsonService implements methods for performing requests to a JSON API and schema validation.
- BaseXmlService implements methods for performing requests to an XML API and schema validation.
- If you are contributing to a service family, you may define a common super class for the badges or one may already exist.
As a first step we will look at the code for an example which generates a badge without contacting an API.
'use strict' // (1)
const BaseService = require('../base') // (2)
module.exports = class Example extends BaseService { // (3)
static get route() { // (4)
return {
base: 'example',
pattern: ':text',
}
}
async handle({ text }) { // (5)
return {
label: 'example',
message: text,
color: 'blue',
}
}
}
Description of the code:
- We declare strict mode at the start of each file. This prevents certain classes of error such as undeclared variables.
- Our service badge class will extend
BaseService
so we need to require it. We declare variables withconst
andlet
in preference tovar
. - Our module must export a class which extends
BaseService
. route()
declares the URL path at which the service operates. It also maps components of the URL path to handler parameters.base
defines the first part of the URL that doesn't change, e.g./example/
.pattern
defines the variable part of the route, everything that comes after/example/
. It can include any number of named parameters. These are converted into regular expressions bypath-to-regexp
.
- Because a service instance won't be created until it's time to handle a request, the route and other metadata must be obtained by examining the classes themselves. That's why they're marked
static
. - All badges must implement the
async handle()
function that receives parameters to render the badge. Parameters ofhandle()
will match the name defined inroute()
Because we're capturing a single variable calledtext
our function signature isasync handle({ text })
.async
is needed to let JavaScript do other things while we are waiting for result from external API. Although in this simple case, we don't make any external calls. Ourhandle()
function should return an object with 3 properties:label
: the text on the left side of the badgemessage
: the text on the right side of the badge - here we are passing through the parameter we captured in the route regexcolor
: the background color of the right side of the badge
The process of turning this object into an image is handled automatically by the BaseService
class.
To try out this example badge:
- Copy and paste this code into a new file in
/services/example/example.service.js
- Quit the running server with
Control+C
. - Start the server again.
npm start
- Visit the badge at http://localhost:8080/example/foo.svg. It should look like this:
The example above was completely static. In order to make a useful service badge we will need to get some data from somewhere. The most common case is that we will query an API which serves up some JSON data, but other formats (e.g: XML) may be used.
This example is based on the Ruby Gems version badge:
'use strict' // (1)
const BaseJsonService = require('../base-json') // (2)
const { renderVersionBadge } = require('../../lib/version') // (3)
const Joi = require('joi') // (4)
const schema = Joi.object({ // (4)
version: Joi.string().required(), // (4)
}).required() // (4)
module.exports = class GemVersion extends BaseJsonService { // (5)
static get route() { // (6)
return {
base: 'gem/v',
pattern: ':gem',
}
}
static get defaultBadgeData() { // (7)
return { label: 'gem' }
}
async handle({ gem }) { // (8)
const { version } = await this.fetch({ gem })
return this.constructor.render({ version })
}
async fetch({ gem }) { // (9)
return this._requestJson({
schema,
url: `https://rubygems.org/api/v1/gems/${gem}.json`,
})
}
static render({ version }) { // (10)
return renderVersionBadge({ version })
}
}
Description of the code:
- As with the first example, we declare strict mode at the start of each file.
- Our badge will query a JSON API so we will extend
BaseJsonService
instead ofBaseService
. This contains some helpers to reduce the need for boilerplate when calling a JSON API. - In this case we are making a version badge, which is a common pattern. Instead of directly returning an object in this badge we will use a helper function to format our data consistently. There are a variety of helper functions to help with common tasks in
/lib
. Some useful generic helpers can be found in: - We perform input validation by defining a schema which we expect the JSON we receive to conform to. This is done using Joi. Defining a schema means we can ensure the JSON we receive meets our expectations and throw an error if we receive unexpected input without having to explicitly code validation checks. The schema also acts as a filter on the JSON object. Any properties we're going to reference need to be validated, otherwise they will be filtered out. In this case our schema declares that we expect to receive an object which must have a property called 'status', which is a string.
- Our module exports a class which extends
BaseJsonService
- As with our previous badge, we need to declare a route. This time we will capture a variable called
gem
. - We can use
defaultBadgeData()
to set a defaultcolor
,logo
and/orlabel
. Ifhandle()
doesn't return any of these keys, we'll use the default. Instead of explicitly setting the label text when we return a badge object, we'll usedefaultBadgeData()
here to define it declaratively. - Our badge must implement the
async handle()
function. Because our URL pattern captures a variable calledgem
, our function signature isasync handle({ gem })
. We usually separate the process of generating a badge into 2 stages or concerns: fetch and render. Thefetch()
function is responsible for calling an API endpoint to get data. Therender()
function formats the data for display. In a case where there is a lot of calculation or intermediate steps, this pattern may be thought of as fetch, transform, render and it might be necessary to define some helper functions to assist with the 'transform' step. - The
async fetch()
method is responsible for calling an API endpoint to get data. ExtendingBaseJsonService
gives us the helper function_requestJson()
. Note here that we pass the schema we defined in step 4 as an argument._requestJson()
will deal with validating the response against the schema and throwing an error if necessary._requestJson()
automatically adds an Accept header, checks the status code, parses the response as JSON, and returns the parsed response._requestJson()
uses request to perform the HTTP request. Options can be passed to request, including method, query string, and headers. If headers are provided they will override the ones automatically set by_requestJson()
. There is no need to specify json, as the JSON parsing is handled by_requestJson()
. See therequest
docs for supported options.- Error messages corresponding to each status code can be returned by passing a dictionary of status codes -> messages in
errorMessages
. - A more complex call to
_requestJson()
might look like this:return this._requestJson({ schema: mySchema, url, options: { qs: { branch: 'master' } }, errorMessages: { 401: 'private application not supported', 404: 'application not found', }, })
- The
static render()
method is responsible for formatting the data for display.render()
is a pure function so we can make it astatic
method. By convention we declare functions which don't referencethis
asstatic
. We could explicitly return an object here, as we did in the previous example. In this case, we will hand the version string off torenderVersionBadge()
which will format it consistently and set an appropriate color. BecauserenderVersionBadge()
doesn't return alabel
key, the default label we defined indefaultBadgeData()
will be used when we generate the badge.
This code allows us to call this URL https://img.shields.io/gem/v/formatador.svg to generate this badge:
It is also worth considering the code we haven't written here. Note that our example doesn't contain any explicit error handling code, but our badge handles errors gracefully. For example, if we call https://img.shields.io/gem/v/does-not-exist.svg we render a 'not found' badge because https://rubygems.org/api/v1/gems/this-package-does-not-exist.json returns a 404 Not Found
status code. When dealing with well-behaved APIs, some of our error handling will be handled implicitly in BaseJsonService
.
Specifically BaseJsonService
will handle the following errors for us:
- API does not respond
- API responds with a non-
200 OK
status code - API returns a response which can't be parsed as JSON
- API returns a response which doesn't validate against our schema
Sometimes it may be necessary to manually throw an exception to deal with a non-standard error condition. If so, standard exceptions can be imported from errors.js and thrown.
Once we have implemented our badge, we can add it to the index so that users can discover it. We will do this by adding a couple of additional methods to our class.
module.exports = class GemVersion extends BaseJsonService {
// ...
static get category() { // (1)
return 'version'
}
static get examples() { // (2)
return [
{ // (3)
title: 'Gem',
namedParams: { gem: 'formatador' },
staticPreview: this.render({ version: '2.1.0' }),
keywords: ['ruby'],
},
]
}
}
- The
category()
property defines which heading in the index our example will appear under. - The examples property defines an array of examples. In this case the array will contain a single object, but in some cases it is helpful to provide multiple usage examples.
- Our example object should contain the following properties:
title
: Descriptive text that will be shown next to the badgenamedParams
: Provide a valid example of params we can substitute into the pattern. In this case we need a valid ruby gem, so we've picked formatador.staticPreview
: On the index page we want to show an example badge, but for performance reasons we want that example to be generated without making an API call.staticPreview
should be populated by calling ourrender()
method with some valid data.keywords
: If we want to provide additional keywords other than the title, we can add them here. This helps users to search for relevant badges.
Save, run npm start
, and you can see it locally.
If you update examples
, you don't have to restart the server. Run npm run defs
in another terminal window and the frontend will update.
When creating a badge for a new service or changing a badge's behavior, tests should be included. They serve several purposes:
- They speed up future contributors when they are debugging or improving a badge.
- If a contributors like to change your badge, chances are, they forget edge cases and break your code. Tests may give hints in such cases.
- The contributor and reviewer can easily verify the code works as intended.
- When a badge stops working on the live server, maintainers can find out right away.
There is a dedicated tutorial for tests in the service-tests folder. Please follow it to include tests on your pull-request.
If your submission require an API token or authentication credentials, please update server-secrets.md. You should explain what the token or credentials are for and how to obtain them.
Once you have implemented a new badge:
- Before submitting your changes, please review the coding guidelines.
- Create a pull-request to propose your changes.
- CI will check the tests pass and that your code conforms to our coding standards.
- We also use Danger to check for some common problems. The first comment on your pull request will be posted by a bot. If there are any errors or warnings raised, please review them.
- One of the maintainers will review your contribution.
- We'll work with you to progress your contribution suggesting improvements if necessary. Although there are some occasions where a contribution is not appropriate, if your contribution conforms to our guidelines we'll aim to work towards merging it. The majority of pull requests adding a service badge are merged.
- If your contribution is merged, the final comment on the pull request will be an automated post which you can monitor to tell when your contribution has been deployed to production.