Skip to content

Commit 84a5be3

Browse files
authored
Declare static examples using namedParams (#2308)
This continues the work from #2279, by allowing example badges to be specified using `namedParams`. Using an object makes it possible for us to display these in form fields down the line. (#701) I've called this the "preferred" way, and labeled the other ways deprecated. I've also added some doc to the `examples` property in BaseService. Then I realized we had some doc in the tutorial, though I think it's fine to have a short version in the tutorial, and the gory detail in BaseService. I've also added a `pattern` keyword, and made `urlPattern` an alias. Closes #2050.
1 parent 00d5f87 commit 84a5be3

10 files changed

+232
-80
lines changed

doc/TUTORIAL.md

+3-4
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,8 @@ module.exports = class GemVersion extends BaseJsonService {
255255
return [
256256
{ // (3)
257257
title: 'Gem',
258-
urlPattern: ':package',
258+
namedParams: { gem: 'formatador' },
259259
staticExample: this.render({ version: '2.1.0' }),
260-
exampleUrl: 'formatador',
261260
keywords: ['ruby'],
262261
},
263262
]
@@ -270,9 +269,9 @@ module.exports = class GemVersion extends BaseJsonService {
270269
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.
271270
3. Our example object should contain the following properties:
272271
* `title`: Descriptive text that will be shown next to the badge
273-
* `urlPattern`: Describe the variable part of the route using `:param` syntax.
272+
* `namedParams`: Provide a valid example of params we can substitute into
273+
the pattern. In this case we need a valid ruby gem, so we've picked [formatador](https://rubygems.org/gems/formatador).
274274
* `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.
275-
* `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)
276275
* `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.
277276

278277
Save, run `npm start`, and you can see it [locally](http://127.0.0.1:3000/).

services/base.js

+85-62
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const {
2121
} = require('../lib/badge-data')
2222
const { staticBadgeUrl } = require('../lib/make-badge-url')
2323
const trace = require('./trace')
24+
const validateExample = require('./validate-example')
2425

2526
function coalesce(...candidates) {
2627
return candidates.find(c => c !== undefined)
@@ -84,6 +85,32 @@ class BaseService {
8485
* Example URLs for this service. These should use the format
8586
* specified in `route`, and can be used to demonstrate how to use badges for
8687
* this service.
88+
*
89+
* The preferred way to specify an example is with `namedParams` which are
90+
* substitued into the service's compiled route pattern. The rendered badge
91+
* is specified with `staticExample`.
92+
*
93+
* For services which use a route `format`, the `pattern` can be specified as
94+
* part of the example.
95+
*
96+
* title: Descriptive text that will be shown next to the badge. The default
97+
* is to use the service class name, which probably is not what you want.
98+
* namedParams: An object containing the values of named parameters to
99+
* substitute into the compiled route pattern.
100+
* query: An object containing query parameters to include in the example URLs.
101+
* pattern: The route pattern to compile. Defaults to `this.route.pattern`.
102+
* urlPattern: Deprecated. An alias for `pattern`.
103+
* staticExample: A rendered badge of the sort returned by `handle()` or
104+
* `render()`: an object containing `message` and optional `label` and
105+
* `color`. This is usually generated by invoking `this.render()` with some
106+
* explicit props.
107+
* previewUrl: Deprecated. An explicit example which is rendered as part of
108+
* the badge listing.
109+
* exampleUrl: Deprecated. An explicit example which will be displayed to
110+
* the user, but not rendered.
111+
* keywords: Additional keywords, other than words in the title. This helps
112+
* users locate relevant badges.
113+
* documentation: An HTML string that is included in the badge popup.
87114
*/
88115
static get examples() {
89116
return []
@@ -93,6 +120,19 @@ class BaseService {
93120
return `/${[this.route.base, partialUrl].filter(Boolean).join('/')}`
94121
}
95122

123+
static _makeFullUrlFromParams(pattern, namedParams, ext = 'svg') {
124+
const fullPattern = `${this._makeFullUrl(
125+
pattern
126+
)}.:ext(svg|png|gif|jpg|json)`
127+
128+
const toPath = pathToRegexp.compile(fullPattern, {
129+
strict: true,
130+
sensitive: true,
131+
})
132+
133+
return toPath({ ext, ...namedParams })
134+
}
135+
96136
static _makeStaticExampleUrl(serviceData) {
97137
const badgeData = this._makeBadgeData({}, serviceData)
98138
return staticBadgeUrl({
@@ -115,69 +155,52 @@ class BaseService {
115155
* schema in `lib/all-badge-examples.js`.
116156
*/
117157
static prepareExamples() {
118-
return this.examples.map(
119-
(
120-
{
121-
title,
122-
query,
123-
exampleUrl,
124-
previewUrl,
125-
urlPattern,
126-
staticExample,
127-
documentation,
128-
keywords,
129-
},
130-
index
131-
) => {
132-
if (staticExample) {
133-
if (!urlPattern) {
134-
throw new Error(
135-
`Static example for ${
136-
this.name
137-
} at index ${index} does not declare a urlPattern`
138-
)
139-
}
140-
if (!exampleUrl) {
141-
throw new Error(
142-
`Static example for ${
143-
this.name
144-
} at index ${index} does not declare an exampleUrl`
145-
)
146-
}
147-
if (previewUrl) {
148-
throw new Error(
149-
`Static example for ${
150-
this.name
151-
} at index ${index} also declares a dynamic previewUrl, which is not allowed`
152-
)
153-
}
154-
} else if (!previewUrl) {
155-
throw Error(
156-
`Example for ${
157-
this.name
158-
} at index ${index} is missing required previewUrl or staticExample`
159-
)
160-
}
161-
162-
const stringified = queryString.stringify(query)
163-
const suffix = stringified ? `?${stringified}` : ''
164-
165-
return {
166-
title: title ? `${title}` : this.name,
167-
exampleUrl: exampleUrl
168-
? `${this._dotSvg(this._makeFullUrl(exampleUrl))}${suffix}`
169-
: undefined,
170-
previewUrl: staticExample
171-
? this._makeStaticExampleUrl(staticExample)
172-
: `${this._dotSvg(this._makeFullUrl(previewUrl))}${suffix}`,
173-
urlPattern: urlPattern
174-
? `${this._dotSvg(this._makeFullUrl(urlPattern))}${suffix}`
175-
: undefined,
176-
documentation,
177-
keywords,
178-
}
158+
return this.examples.map((example, index) => {
159+
const {
160+
title,
161+
query,
162+
namedParams,
163+
exampleUrl,
164+
previewUrl,
165+
pattern,
166+
staticExample,
167+
documentation,
168+
keywords,
169+
} = validateExample(example, index, this)
170+
171+
const stringified = queryString.stringify(query)
172+
const suffix = stringified ? `?${stringified}` : ''
173+
174+
let outExampleUrl
175+
let outPreviewUrl
176+
let outPattern
177+
if (namedParams) {
178+
outExampleUrl = this._makeFullUrlFromParams(pattern, namedParams)
179+
outPreviewUrl = this._makeStaticExampleUrl(staticExample)
180+
outPattern = `${this._dotSvg(this._makeFullUrl(pattern))}${suffix}`
181+
} else if (staticExample) {
182+
outExampleUrl = `${this._dotSvg(
183+
this._makeFullUrl(exampleUrl)
184+
)}${suffix}`
185+
outPreviewUrl = this._makeStaticExampleUrl(staticExample)
186+
outPattern = `${this._dotSvg(this._makeFullUrl(pattern))}${suffix}`
187+
} else {
188+
outExampleUrl = undefined
189+
outPreviewUrl = `${this._dotSvg(
190+
this._makeFullUrl(previewUrl)
191+
)}${suffix}`
192+
outPattern = undefined
179193
}
180-
)
194+
195+
return {
196+
title: title ? `${title}` : this.name,
197+
exampleUrl: outExampleUrl,
198+
previewUrl: outPreviewUrl,
199+
urlPattern: outPattern,
200+
documentation,
201+
keywords,
202+
}
203+
})
181204
}
182205

183206
static get _regexFromPath() {

services/base.spec.js

+24-3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ class DummyService extends BaseService {
4242
staticExample: this.render({ namedParamA: 'foo', queryParamA: 'bar' }),
4343
keywords: ['hello'],
4444
},
45+
{
46+
pattern: ':world',
47+
exampleUrl: 'World',
48+
staticExample: this.render({ namedParamA: 'foo', queryParamA: 'bar' }),
49+
keywords: ['hello'],
50+
},
51+
{
52+
pattern: ':world',
53+
namedParams: { world: 'World' },
54+
staticExample: this.render({ namedParamA: 'foo', queryParamA: 'bar' }),
55+
keywords: ['hello'],
56+
},
4557
]
4658
}
4759
static get route() {
@@ -457,7 +469,13 @@ describe('BaseService', function() {
457469

458470
describe('prepareExamples', function() {
459471
it('returns the expected result', function() {
460-
const [first, second, third] = DummyService.prepareExamples()
472+
const [
473+
first,
474+
second,
475+
third,
476+
fourth,
477+
fifth,
478+
] = DummyService.prepareExamples()
461479
expect(first).to.deep.equal({
462480
title: 'DummyService',
463481
exampleUrl: undefined,
@@ -474,15 +492,18 @@ describe('BaseService', function() {
474492
documentation: undefined,
475493
keywords: undefined,
476494
})
477-
expect(third).to.deep.equal({
495+
const preparedStaticExample = {
478496
title: 'DummyService',
479497
exampleUrl: '/foo/World.svg',
480498
previewUrl:
481499
'/badge/cat-Hello%20namedParamA%3A%20foo%20with%20queryParamA%3A%20bar-lightgrey.svg',
482500
urlPattern: '/foo/:world.svg',
483501
documentation: undefined,
484502
keywords: ['hello'],
485-
})
503+
}
504+
expect(third).to.deep.equal(preparedStaticExample)
505+
expect(fourth).to.deep.equal(preparedStaticExample)
506+
expect(fifth).to.deep.equal(preparedStaticExample)
486507
})
487508
})
488509

services/gem/gem-version.service.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@ module.exports = class GemVersion extends BaseJsonService {
4848
return [
4949
{
5050
title: 'Gem',
51-
exampleUrl: 'formatador',
52-
urlPattern: ':package',
51+
namedParams: { gem: 'formatador' },
5352
staticExample: this.render({ version: '2.1.0' }),
5453
keywords: ['ruby'],
5554
},

services/validate-example.js

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use strict'
2+
3+
module.exports = function validateExample(
4+
{
5+
title,
6+
query,
7+
namedParams,
8+
exampleUrl,
9+
previewUrl,
10+
pattern,
11+
urlPattern,
12+
staticExample,
13+
documentation,
14+
keywords,
15+
},
16+
index,
17+
ServiceClass
18+
) {
19+
pattern = pattern || urlPattern || ServiceClass.route.pattern
20+
21+
if (staticExample) {
22+
if (!pattern) {
23+
throw new Error(
24+
`Static example for ${
25+
ServiceClass.name
26+
} at index ${index} does not declare a pattern`
27+
)
28+
}
29+
if (namedParams && exampleUrl) {
30+
throw new Error(
31+
`Static example for ${
32+
ServiceClass.name
33+
} at index ${index} declares both namedParams and exampleUrl`
34+
)
35+
} else if (!namedParams && !exampleUrl) {
36+
throw new Error(
37+
`Static example for ${
38+
ServiceClass.name
39+
} at index ${index} does not declare namedParams nor exampleUrl`
40+
)
41+
}
42+
if (previewUrl) {
43+
throw new Error(
44+
`Static example for ${
45+
ServiceClass.name
46+
} at index ${index} also declares a dynamic previewUrl, which is not allowed`
47+
)
48+
}
49+
} else if (!previewUrl) {
50+
throw Error(
51+
`Example for ${
52+
ServiceClass.name
53+
} at index ${index} is missing required previewUrl or staticExample`
54+
)
55+
}
56+
57+
return {
58+
title,
59+
query,
60+
namedParams,
61+
exampleUrl,
62+
previewUrl,
63+
pattern,
64+
staticExample,
65+
documentation,
66+
keywords,
67+
}
68+
}

services/validate-example.spec.js

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use strict'
2+
3+
const { expect } = require('chai')
4+
const validateExample = require('./validate-example')
5+
6+
describe('validateExample function', function() {
7+
it('passes valid examples', function() {
8+
const validExamples = [
9+
{ staticExample: {}, pattern: 'dt/:package', exampleUrl: 'dt/mypackage' },
10+
{
11+
staticExample: {},
12+
pattern: 'dt/:package',
13+
namedParams: { package: 'mypackage' },
14+
},
15+
{ previewUrl: 'dt/mypackage' },
16+
]
17+
18+
validExamples.forEach(example => {
19+
expect(() =>
20+
validateExample(example, 0, { route: {}, name: 'mockService' })
21+
).not.to.throw(Error)
22+
})
23+
})
24+
25+
it('rejects invalid examples', function() {
26+
const invalidExamples = [
27+
{},
28+
{ staticExample: {} },
29+
{
30+
staticExample: {},
31+
pattern: 'dt/:package',
32+
namedParams: { package: 'mypackage' },
33+
exampleUrl: 'dt/mypackage',
34+
},
35+
{ staticExample: {}, pattern: 'dt/:package' },
36+
{ staticExample: {}, pattern: 'dt/:package', previewUrl: 'dt/mypackage' },
37+
]
38+
39+
invalidExamples.forEach(example => {
40+
expect(() =>
41+
validateExample(example, 0, { route: {}, name: 'mockService' })
42+
).to.throw(Error)
43+
})
44+
})
45+
})

services/wordpress/wordpress-downloads.service.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ function DownloadsForExtensionType(extensionType) {
5555
return [
5656
{
5757
title: `Wordpress ${capt} Downloads`,
58-
exampleUrl: exampleSlug,
59-
urlPattern: ':slug',
58+
namedParams: { slug: exampleSlug },
6059
staticExample: this.render({ response: { downloaded: 200000 } }),
6160
keywords: ['wordpress'],
6261
},

services/wordpress/wordpress-platform.service.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ class WordpressPluginRequiresVersion extends BaseWordpressPlatform {
5555
return [
5656
{
5757
title: 'Wordpress Plugin: Required WP Version',
58-
exampleUrl: 'bbpress',
59-
urlPattern: ':slug',
58+
namedParams: { slug: 'bbpress' },
6059
staticExample: this.render({ response: { requires: '4.8' } }),
6160
keywords: ['wordpress'],
6261
},

0 commit comments

Comments
 (0)