diff --git a/doc/TUTORIAL.md b/doc/TUTORIAL.md
index b1f37cc545..df3089e780 100644
--- a/doc/TUTORIAL.md
+++ b/doc/TUTORIAL.md
@@ -255,9 +255,8 @@ module.exports = class GemVersion extends BaseJsonService {
return [
{ // (3)
title: 'Gem',
- urlPattern: ':package',
+ namedParams: { gem: 'formatador' },
staticExample: this.render({ version: '2.1.0' }),
- exampleUrl: 'formatador',
keywords: ['ruby'],
},
]
@@ -270,9 +269,9 @@ module.exports = class GemVersion extends BaseJsonService {
2. 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.
3. Our example object should contain the following properties:
* `title`: Descriptive text that will be shown next to the badge
- * `urlPattern`: Describe the variable part of the route using `:param` syntax.
+ * `namedParams`: 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](https://rubygems.org/gems/formatador).
* `staticExample`: 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. `staticExample` should be populated by calling our `render()` method with some valid data.
- * `exampleUrl`: Provide a valid example of params we can call the badge with. In this case we need a valid ruby gem, so we've picked [formatador](https://rubygems.org/gems/formatador)
* `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](http://127.0.0.1:3000/).
diff --git a/services/base.js b/services/base.js
index f5fb789b32..96f44a7cf4 100644
--- a/services/base.js
+++ b/services/base.js
@@ -21,6 +21,7 @@ const {
} = require('../lib/badge-data')
const { staticBadgeUrl } = require('../lib/make-badge-url')
const trace = require('./trace')
+const validateExample = require('./validate-example')
function coalesce(...candidates) {
return candidates.find(c => c !== undefined)
@@ -84,6 +85,32 @@ class BaseService {
* Example URLs for this service. These should use the format
* specified in `route`, and can be used to demonstrate how to use badges for
* this service.
+ *
+ * The preferred way to specify an example is with `namedParams` which are
+ * substitued into the service's compiled route pattern. The rendered badge
+ * is specified with `staticExample`.
+ *
+ * For services which use a route `format`, the `pattern` can be specified as
+ * part of the example.
+ *
+ * title: Descriptive text that will be shown next to the badge. The default
+ * is to use the service class name, which probably is not what you want.
+ * namedParams: An object containing the values of named parameters to
+ * substitute into the compiled route pattern.
+ * query: An object containing query parameters to include in the example URLs.
+ * pattern: The route pattern to compile. Defaults to `this.route.pattern`.
+ * urlPattern: Deprecated. An alias for `pattern`.
+ * staticExample: A rendered badge of the sort returned by `handle()` or
+ * `render()`: an object containing `message` and optional `label` and
+ * `color`. This is usually generated by invoking `this.render()` with some
+ * explicit props.
+ * previewUrl: Deprecated. An explicit example which is rendered as part of
+ * the badge listing.
+ * exampleUrl: Deprecated. An explicit example which will be displayed to
+ * the user, but not rendered.
+ * keywords: Additional keywords, other than words in the title. This helps
+ * users locate relevant badges.
+ * documentation: An HTML string that is included in the badge popup.
*/
static get examples() {
return []
@@ -93,6 +120,19 @@ class BaseService {
return `/${[this.route.base, partialUrl].filter(Boolean).join('/')}`
}
+ static _makeFullUrlFromParams(pattern, namedParams, ext = 'svg') {
+ const fullPattern = `${this._makeFullUrl(
+ pattern
+ )}.:ext(svg|png|gif|jpg|json)`
+
+ const toPath = pathToRegexp.compile(fullPattern, {
+ strict: true,
+ sensitive: true,
+ })
+
+ return toPath({ ext, ...namedParams })
+ }
+
static _makeStaticExampleUrl(serviceData) {
const badgeData = this._makeBadgeData({}, serviceData)
return staticBadgeUrl({
@@ -115,69 +155,52 @@ class BaseService {
* schema in `lib/all-badge-examples.js`.
*/
static prepareExamples() {
- return this.examples.map(
- (
- {
- title,
- query,
- exampleUrl,
- previewUrl,
- urlPattern,
- staticExample,
- documentation,
- keywords,
- },
- index
- ) => {
- if (staticExample) {
- if (!urlPattern) {
- throw new Error(
- `Static example for ${
- this.name
- } at index ${index} does not declare a urlPattern`
- )
- }
- if (!exampleUrl) {
- throw new Error(
- `Static example for ${
- this.name
- } at index ${index} does not declare an exampleUrl`
- )
- }
- if (previewUrl) {
- throw new Error(
- `Static example for ${
- this.name
- } at index ${index} also declares a dynamic previewUrl, which is not allowed`
- )
- }
- } else if (!previewUrl) {
- throw Error(
- `Example for ${
- this.name
- } at index ${index} is missing required previewUrl or staticExample`
- )
- }
+ return this.examples.map((example, index) => {
+ const {
+ title,
+ query,
+ namedParams,
+ exampleUrl,
+ previewUrl,
+ pattern,
+ staticExample,
+ documentation,
+ keywords,
+ } = validateExample(example, index, this)
- const stringified = queryString.stringify(query)
- const suffix = stringified ? `?${stringified}` : ''
+ const stringified = queryString.stringify(query)
+ const suffix = stringified ? `?${stringified}` : ''
- return {
- title: title ? `${title}` : this.name,
- exampleUrl: exampleUrl
- ? `${this._dotSvg(this._makeFullUrl(exampleUrl))}${suffix}`
- : undefined,
- previewUrl: staticExample
- ? this._makeStaticExampleUrl(staticExample)
- : `${this._dotSvg(this._makeFullUrl(previewUrl))}${suffix}`,
- urlPattern: urlPattern
- ? `${this._dotSvg(this._makeFullUrl(urlPattern))}${suffix}`
- : undefined,
- documentation,
- keywords,
- }
+ let outExampleUrl
+ let outPreviewUrl
+ let outPattern
+ if (namedParams) {
+ outExampleUrl = this._makeFullUrlFromParams(pattern, namedParams)
+ outPreviewUrl = this._makeStaticExampleUrl(staticExample)
+ outPattern = `${this._dotSvg(this._makeFullUrl(pattern))}${suffix}`
+ } else if (staticExample) {
+ outExampleUrl = `${this._dotSvg(
+ this._makeFullUrl(exampleUrl)
+ )}${suffix}`
+ outPreviewUrl = this._makeStaticExampleUrl(staticExample)
+ outPattern = `${this._dotSvg(this._makeFullUrl(pattern))}${suffix}`
+ } else {
+ outExampleUrl = undefined
+ outPreviewUrl = `${this._dotSvg(
+ this._makeFullUrl(previewUrl)
+ )}${suffix}`
+ outPattern = undefined
}
- )
+
+ return {
+ title: title ? `${title}` : this.name,
+ exampleUrl: outExampleUrl,
+ previewUrl: outPreviewUrl,
+ urlPattern: outPattern,
+ documentation,
+ keywords,
+ }
+ })
}
static get _regexFromPath() {
diff --git a/services/base.spec.js b/services/base.spec.js
index 89c1be8f03..38b84624c7 100644
--- a/services/base.spec.js
+++ b/services/base.spec.js
@@ -42,6 +42,18 @@ class DummyService extends BaseService {
staticExample: this.render({ namedParamA: 'foo', queryParamA: 'bar' }),
keywords: ['hello'],
},
+ {
+ pattern: ':world',
+ exampleUrl: 'World',
+ staticExample: this.render({ namedParamA: 'foo', queryParamA: 'bar' }),
+ keywords: ['hello'],
+ },
+ {
+ pattern: ':world',
+ namedParams: { world: 'World' },
+ staticExample: this.render({ namedParamA: 'foo', queryParamA: 'bar' }),
+ keywords: ['hello'],
+ },
]
}
static get route() {
@@ -457,7 +469,13 @@ describe('BaseService', function() {
describe('prepareExamples', function() {
it('returns the expected result', function() {
- const [first, second, third] = DummyService.prepareExamples()
+ const [
+ first,
+ second,
+ third,
+ fourth,
+ fifth,
+ ] = DummyService.prepareExamples()
expect(first).to.deep.equal({
title: 'DummyService',
exampleUrl: undefined,
@@ -474,7 +492,7 @@ describe('BaseService', function() {
documentation: undefined,
keywords: undefined,
})
- expect(third).to.deep.equal({
+ const preparedStaticExample = {
title: 'DummyService',
exampleUrl: '/foo/World.svg',
previewUrl:
@@ -482,7 +500,10 @@ describe('BaseService', function() {
urlPattern: '/foo/:world.svg',
documentation: undefined,
keywords: ['hello'],
- })
+ }
+ expect(third).to.deep.equal(preparedStaticExample)
+ expect(fourth).to.deep.equal(preparedStaticExample)
+ expect(fifth).to.deep.equal(preparedStaticExample)
})
})
diff --git a/services/gem/gem-version.service.js b/services/gem/gem-version.service.js
index 35af33de4f..8a6ab40d7b 100644
--- a/services/gem/gem-version.service.js
+++ b/services/gem/gem-version.service.js
@@ -48,8 +48,7 @@ module.exports = class GemVersion extends BaseJsonService {
return [
{
title: 'Gem',
- exampleUrl: 'formatador',
- urlPattern: ':package',
+ namedParams: { gem: 'formatador' },
staticExample: this.render({ version: '2.1.0' }),
keywords: ['ruby'],
},
diff --git a/services/validate-example.js b/services/validate-example.js
new file mode 100644
index 0000000000..d2a6860f3b
--- /dev/null
+++ b/services/validate-example.js
@@ -0,0 +1,68 @@
+'use strict'
+
+module.exports = function validateExample(
+ {
+ title,
+ query,
+ namedParams,
+ exampleUrl,
+ previewUrl,
+ pattern,
+ urlPattern,
+ staticExample,
+ documentation,
+ keywords,
+ },
+ index,
+ ServiceClass
+) {
+ pattern = pattern || urlPattern || ServiceClass.route.pattern
+
+ if (staticExample) {
+ if (!pattern) {
+ throw new Error(
+ `Static example for ${
+ ServiceClass.name
+ } at index ${index} does not declare a pattern`
+ )
+ }
+ if (namedParams && exampleUrl) {
+ throw new Error(
+ `Static example for ${
+ ServiceClass.name
+ } at index ${index} declares both namedParams and exampleUrl`
+ )
+ } else if (!namedParams && !exampleUrl) {
+ throw new Error(
+ `Static example for ${
+ ServiceClass.name
+ } at index ${index} does not declare namedParams nor exampleUrl`
+ )
+ }
+ if (previewUrl) {
+ throw new Error(
+ `Static example for ${
+ ServiceClass.name
+ } at index ${index} also declares a dynamic previewUrl, which is not allowed`
+ )
+ }
+ } else if (!previewUrl) {
+ throw Error(
+ `Example for ${
+ ServiceClass.name
+ } at index ${index} is missing required previewUrl or staticExample`
+ )
+ }
+
+ return {
+ title,
+ query,
+ namedParams,
+ exampleUrl,
+ previewUrl,
+ pattern,
+ staticExample,
+ documentation,
+ keywords,
+ }
+}
diff --git a/services/validate-example.spec.js b/services/validate-example.spec.js
new file mode 100644
index 0000000000..033785290e
--- /dev/null
+++ b/services/validate-example.spec.js
@@ -0,0 +1,45 @@
+'use strict'
+
+const { expect } = require('chai')
+const validateExample = require('./validate-example')
+
+describe('validateExample function', function() {
+ it('passes valid examples', function() {
+ const validExamples = [
+ { staticExample: {}, pattern: 'dt/:package', exampleUrl: 'dt/mypackage' },
+ {
+ staticExample: {},
+ pattern: 'dt/:package',
+ namedParams: { package: 'mypackage' },
+ },
+ { previewUrl: 'dt/mypackage' },
+ ]
+
+ validExamples.forEach(example => {
+ expect(() =>
+ validateExample(example, 0, { route: {}, name: 'mockService' })
+ ).not.to.throw(Error)
+ })
+ })
+
+ it('rejects invalid examples', function() {
+ const invalidExamples = [
+ {},
+ { staticExample: {} },
+ {
+ staticExample: {},
+ pattern: 'dt/:package',
+ namedParams: { package: 'mypackage' },
+ exampleUrl: 'dt/mypackage',
+ },
+ { staticExample: {}, pattern: 'dt/:package' },
+ { staticExample: {}, pattern: 'dt/:package', previewUrl: 'dt/mypackage' },
+ ]
+
+ invalidExamples.forEach(example => {
+ expect(() =>
+ validateExample(example, 0, { route: {}, name: 'mockService' })
+ ).to.throw(Error)
+ })
+ })
+})
diff --git a/services/wordpress/wordpress-downloads.service.js b/services/wordpress/wordpress-downloads.service.js
index 26a4951449..596f567fa2 100644
--- a/services/wordpress/wordpress-downloads.service.js
+++ b/services/wordpress/wordpress-downloads.service.js
@@ -55,8 +55,7 @@ function DownloadsForExtensionType(extensionType) {
return [
{
title: `Wordpress ${capt} Downloads`,
- exampleUrl: exampleSlug,
- urlPattern: ':slug',
+ namedParams: { slug: exampleSlug },
staticExample: this.render({ response: { downloaded: 200000 } }),
keywords: ['wordpress'],
},
diff --git a/services/wordpress/wordpress-platform.service.js b/services/wordpress/wordpress-platform.service.js
index a8024b6283..6c061e7d01 100644
--- a/services/wordpress/wordpress-platform.service.js
+++ b/services/wordpress/wordpress-platform.service.js
@@ -55,8 +55,7 @@ class WordpressPluginRequiresVersion extends BaseWordpressPlatform {
return [
{
title: 'Wordpress Plugin: Required WP Version',
- exampleUrl: 'bbpress',
- urlPattern: ':slug',
+ namedParams: { slug: 'bbpress' },
staticExample: this.render({ response: { requires: '4.8' } }),
keywords: ['wordpress'],
},
diff --git a/services/wordpress/wordpress-rating.service.js b/services/wordpress/wordpress-rating.service.js
index 1d68441df4..0c17a40402 100644
--- a/services/wordpress/wordpress-rating.service.js
+++ b/services/wordpress/wordpress-rating.service.js
@@ -93,8 +93,8 @@ function StarsForExtensionType(extensionType) {
return [
{
title: `Wordpress ${capt} Rating`,
- exampleUrl: `stars/${exampleSlug}`,
- urlPattern: 'stars/:slug',
+ pattern: 'stars/:slug',
+ namedParams: { slug: exampleSlug },
staticExample: this.render({
response: {
rating: 80,
@@ -102,7 +102,7 @@ function StarsForExtensionType(extensionType) {
},
}),
keywords: ['wordpress'],
- documentation: 'There is an alias /r/:slug.svg aswell',
+ documentation: 'There is an alias /r/:slug.svg as well.',
},
]
}
diff --git a/services/wordpress/wordpress-version.service.js b/services/wordpress/wordpress-version.service.js
index f9e990f121..455934de39 100644
--- a/services/wordpress/wordpress-version.service.js
+++ b/services/wordpress/wordpress-version.service.js
@@ -47,8 +47,7 @@ function VersionForExtensionType(extensionType) {
return [
{
title: `Wordpress ${capt} Version`,
- exampleUrl: exampleSlug,
- urlPattern: ':slug',
+ namedParams: { slug: exampleSlug },
staticExample: this.render({ response: { version: 2.5 } }),
keywords: ['wordpress'],
},