diff --git a/.eslintrc-frontend.yml b/.eslintrc-frontend.yml index 7599c017b67e7..9d5c47205aa8a 100644 --- a/.eslintrc-frontend.yml +++ b/.eslintrc-frontend.yml @@ -21,4 +21,4 @@ settings: rules: no-console: "error" - import/extensions: ["error", "never", { "json": "always" }] + import/extensions: ["error", "never", { "json": "always", "yml": "always" }] diff --git a/doc/TUTORIAL.md b/doc/TUTORIAL.md index 61ac5e13b0b1e..731df20f419f6 100644 --- a/doc/TUTORIAL.md +++ b/doc/TUTORIAL.md @@ -279,6 +279,9 @@ module.exports = class GemVersion extends BaseJsonService { Save, run `npm start`, and you can see it [locally](http://127.0.0.1:3000/). +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. + ### (4.5) Write Tests [write tests]: #45-write-tests diff --git a/frontend/components/badge-examples.js b/frontend/components/badge-examples.js index 049ea2b608ec5..30d8af1c48369 100644 --- a/frontend/components/badge-examples.js +++ b/frontend/components/badge-examples.js @@ -1,149 +1,92 @@ import React from 'react' -import { Link } from 'react-router-dom' import PropTypes from 'prop-types' -import classNames from 'classnames' -import resolveBadgeUrl from '../lib/badge-url' +import { badgeUrlFromPath, staticBadgeUrl } from '../../lib/make-badge-url' -const Badge = ({ - title, - exampleUrl, - previewUrl, - urlPattern, - documentation, - baseUrl, - longCache, - shouldDisplay = () => true, - onClick, -}) => { - const handleClick = onClick - ? () => - onClick({ - title, - exampleUrl, - previewUrl, - urlPattern, - documentation, - }) - : undefined +export default class BadgeExamples extends React.Component { + static propTypes = { + definitions: PropTypes.array.isRequired, + baseUrl: PropTypes.string, + longCache: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + } + + renderExample(exampleData) { + const { baseUrl, longCache, onClick } = this.props + const { title, example, preview } = exampleData + + let previewUrl + // There are two alternatives for `preview`. Refer to the schema in + // `services/service-definitions.js`. + if (preview.label !== undefined) { + const { label, message, color } = preview + previewUrl = staticBadgeUrl({ baseUrl, label, message, color }) + } else { + const { path, queryParams } = preview + previewUrl = badgeUrlFromPath({ baseUrl, path, queryParams, longCache }) + } + + // There are two alternatives for `example`. Refer to the schema in + // `services/service-definitions.js`. + let exampleUrl + if (example.pattern !== undefined) { + const { pattern, namedParams, queryParams } = example + exampleUrl = badgeUrlFromPath({ + baseUrl, + path: pattern, + namedParams, + queryParams, + }) + } else { + const { path, queryParams } = example + exampleUrl = badgeUrlFromPath({ baseUrl, path, queryParams }) + } + + const key = `${title} ${previewUrl} ${exampleUrl}` - const previewImage = previewUrl ? ( - - ) : ( - '\u00a0' - ) // non-breaking space - const resolvedExampleUrl = resolveBadgeUrl( - urlPattern || previewUrl, - baseUrl, - { longCache: false } - ) + const handleClick = () => onClick(exampleData) - if (shouldDisplay()) { return ( - - + + {title}: - {previewImage} - - {resolvedExampleUrl} + src={previewUrl} + alt="" + /> + + + + {exampleUrl} ) } - return null -} -Badge.propTypes = { - title: PropTypes.string.isRequired, - exampleUrl: PropTypes.string, - previewUrl: PropTypes.string, - urlPattern: PropTypes.string, - documentation: PropTypes.string, - baseUrl: PropTypes.string, - longCache: PropTypes.bool.isRequired, - shouldDisplay: PropTypes.func, - onClick: PropTypes.func.isRequired, -} -const Category = ({ category, examples, baseUrl, longCache, onClick }) => { - if (examples.filter(example => example.shouldDisplay()).length === 0) { - return null - } - return ( -
- -

{category.name}

- - - - {examples.map(badgeData => ( - - ))} - -
-
- ) -} -Category.propTypes = { - category: PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - }).isRequired, - examples: PropTypes.arrayOf( - PropTypes.shape({ - title: PropTypes.string.isRequired, - exampleUrl: PropTypes.string, - previewUrl: PropTypes.string, - urlPattern: PropTypes.string, - documentation: PropTypes.string, - }) - ).isRequired, - baseUrl: PropTypes.string, - longCache: PropTypes.bool.isRequired, - onClick: PropTypes.func.isRequired, -} + render() { + const { definitions } = this.props -const BadgeExamples = ({ categories, baseUrl, longCache, onClick }) => ( -
- {categories.map((categoryData, i) => ( - - ))} -
-) -BadgeExamples.propTypes = { - categories: PropTypes.arrayOf( - PropTypes.shape({ - category: Category.propTypes.category, - examples: Category.propTypes.examples, - }) - ), - baseUrl: PropTypes.string, - longCache: PropTypes.bool.isRequired, - onClick: PropTypes.func.isRequired, -} + if (!definitions) { + return null + } + + const flattened = definitions.reduce((accum, current) => { + const { examples } = current + return accum.concat(examples) + }, []) -export { Badge, BadgeExamples } + return ( +
+ + + {flattened.map(exampleData => this.renderExample(exampleData))} + +
+
+ ) + } +} diff --git a/frontend/components/category-headings.js b/frontend/components/category-headings.js new file mode 100644 index 0000000000000..f0ffb1552ba5b --- /dev/null +++ b/frontend/components/category-headings.js @@ -0,0 +1,32 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Link } from 'react-router-dom' + +const CategoryHeading = ({ category }) => { + const { id, name } = category + + return ( + +

{name}

+ + ) +} +CategoryHeading.propTypes = { + category: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + }).isRequired, +} + +const CategoryHeadings = ({ categories }) => ( +
+ {categories.map(category => ( + + ))} +
+) +CategoryHeadings.propTypes = { + categories: PropTypes.arrayOf(CategoryHeading.propTypes.category).isRequired, +} + +export { CategoryHeading, CategoryHeadings } diff --git a/frontend/components/examples-page.js b/frontend/components/examples-page.js deleted file mode 100644 index 1fe3bf4e2cdb9..0000000000000 --- a/frontend/components/examples-page.js +++ /dev/null @@ -1,109 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import Meta from './meta' -import Header from './header' -import SuggestionAndSearch from './suggestion-and-search' -import SearchResults from './search-results' -import MarkupModal from './markup-modal' -import Usage from './usage' -import Footer from './footer' -import { baseUrl, longCache } from '../constants' - -export default class ExamplesPage extends React.Component { - constructor(props) { - super(props) - this.state = { - category: props.match.params.id, - query: null, - example: null, - searchReady: true, - } - this.searchTimeout = 0 - this.renderSearchResults = this.renderSearchResults.bind(this) - this.searchQueryChanged = this.searchQueryChanged.bind(this) - } - - static propTypes = { - match: PropTypes.object.isRequired, - } - - searchQueryChanged(query) { - this.setState({ searchReady: false }) - /* - Add a small delay before showing search results - so that we wait until the user has stipped typing - before we start loading stuff. - - This - a) reduces the amount of badges we will load and - b) stops the page from 'flashing' as the user types, like this: - https://user-images.githubusercontent.com/7288322/42600206-9b278470-85b5-11e8-9f63-eb4a0c31cb4a.gif - */ - window.clearTimeout(this.searchTimeout) - this.searchTimeout = window.setTimeout(() => { - this.setState({ - searchReady: true, - query, - }) - }, 500) - } - - renderSearchResults() { - if (this.state.searchReady) { - if (this.state.query != null && this.state.query.length === 1) { - return
Search term must have 2 or more characters
- } else { - return ( - { - this.setState({ example }) - }} - /> - ) - } - } else { - return
searching...
- } - } - - render() { - return ( -
- -
- { - this.setState({ example: null }) - }} - baseUrl={baseUrl} - key={this.state.example} - /> -
- { - this.setState({ example }) - }} - baseUrl={baseUrl} - longCache={longCache} - /> - - donate - -
- {this.renderSearchResults()} - -
- -
- ) - } -} diff --git a/frontend/components/main.js b/frontend/components/main.js new file mode 100644 index 0000000000000..e132b6ee94b6a --- /dev/null +++ b/frontend/components/main.js @@ -0,0 +1,174 @@ +import React from 'react' +import PropTypes from 'prop-types' +import Meta from './meta' +import Header from './header' +import SuggestionAndSearch from './suggestion-and-search' +import MarkupModal from './markup-modal' +import Usage from './usage' +import Footer from './footer' +import { CategoryHeading, CategoryHeadings } from './category-headings' +import { + categories, + findCategory, + services, + getDefinitionsForCategory, +} from '../lib/service-definitions' +import BadgeExamples from './badge-examples' +import { baseUrl, longCache } from '../constants' +import ServiceDefinitionSetHelper from '../lib/service-definitions/service-definition-set-helper' +import groupBy from 'lodash.groupby' + +export default class Main extends React.Component { + constructor(props) { + super(props) + + this.state = { + isSearchInProgress: false, + isQueryTooShort: false, + searchResults: undefined, + selectedExample: undefined, + } + + this.searchTimeout = 0 + + this.handleExampleSelected = this.handleExampleSelected.bind(this) + this.dismissMarkupModal = this.dismissMarkupModal.bind(this) + this.searchQueryChanged = this.searchQueryChanged.bind(this) + } + + static propTypes = { + match: PropTypes.object.isRequired, + } + + get category() { + return this.props.match.params.category + } + + performSearch(query) { + const isQueryTooShort = query.length === 1 + + let searchResults + if (query.length >= 2) { + const flat = ServiceDefinitionSetHelper.create(services) + .notDeprecated() + .search(query) + .toArray() + searchResults = groupBy(flat, 'category') + } + + this.setState({ + isSearchInProgress: false, + isQueryTooShort, + searchResults, + }) + } + + searchQueryChanged(query) { + /* + Add a small delay before showing search results + so that we wait until the user has stopped typing + before we start loading stuff. + + This + a) reduces the amount of badges we will load and + b) stops the page from 'flashing' as the user types, like this: + https://user-images.githubusercontent.com/7288322/42600206-9b278470-85b5-11e8-9f63-eb4a0c31cb4a.gif + */ + this.setState({ isSearchInProgress: true }) + window.clearTimeout(this.searchTimeout) + this.searchTimeout = window.setTimeout(() => this.performSearch(query), 500) + } + + handleExampleSelected(example) { + this.setState({ selectedExample: example }) + } + + dismissMarkupModal() { + this.setState({ selectedExample: undefined }) + } + + renderCategory(category, definitions) { + const { id } = category + + return ( +
+ + +
+ ) + } + + renderMain() { + const { category: categoryId } = this + const { isSearchInProgress, isQueryTooShort, searchResults } = this.state + + const category = findCategory(categoryId) + + if (isSearchInProgress) { + return
searching...
+ } else if (isQueryTooShort) { + return
Search term must have 2 or more characters
+ } else if (searchResults) { + return Object.entries(searchResults).map(([categoryId, definitions]) => + this.renderCategory(findCategory(categoryId), definitions) + ) + } else if (category) { + const definitions = ServiceDefinitionSetHelper.create( + getDefinitionsForCategory(categoryId) + ) + .notDeprecated() + .toArray() + return this.renderCategory(category, definitions) + } else if (categoryId) { + return ( +
+ Unknown category {categoryId} +
+ ) + } else { + return + } + } + + render() { + const { selectedExample } = this.state + + return ( +
+ +
+ +
+ + + donate + +
+ {this.renderMain()} + +
+ +
+ ) + } +} diff --git a/frontend/components/markup-modal.js b/frontend/components/markup-modal.js index c89516a8941cd..7cf65ac073f52 100644 --- a/frontend/components/markup-modal.js +++ b/frontend/components/markup-modal.js @@ -2,86 +2,167 @@ import React from 'react' import PropTypes from 'prop-types' import Modal from 'react-modal' import ClickToSelect from '@mapbox/react-click-to-select' -import resolveBadgeUrl from '../lib/badge-url' +import { badgeUrlFromPath, badgeUrlFromPattern } from '../../lib/make-badge-url' import generateAllMarkup from '../lib/generate-image-markup' import { advertisedStyles } from '../../supported-features.json' +const nonBreakingSpace = '\u00a0' + export default class MarkupModal extends React.Component { static propTypes = { - example: PropTypes.shape({ - title: PropTypes.string.isRequired, - exampleUrl: PropTypes.string, - previewUrl: PropTypes.string, - urlPattern: PropTypes.string, - documentation: PropTypes.string, - link: PropTypes.string, - }), + // This is an item from the `examples` array within the + // `serviceDefinition` schema. + // https://github.com/badges/shields/blob/master/services/service-definitions.js + example: PropTypes.object, baseUrl: PropTypes.string.isRequired, onRequestClose: PropTypes.func.isRequired, } state = { - exampleUrl: null, - badgeUrl: null, + badgeUrl: '', + badgeUrlForProps: '', + exampleUrl: '', link: '', style: 'flat', } - constructor(props) { - super(props) - - // Transfer `badgeUrl` and `link` into state so they can be edited by the - // user. - const { example, baseUrl } = props - if (example) { - const { exampleUrl, urlPattern, previewUrl, link } = example - this.state = { - ...this.state, - exampleUrl: exampleUrl - ? resolveBadgeUrl(exampleUrl, baseUrl || window.location.href) - : null, - badgeUrl: resolveBadgeUrl( - urlPattern || previewUrl, - baseUrl || window.location.href - ), - link: !link ? '' : link, - } + get isOpen() { + return this.props.example !== undefined + } + + static urlsForProps(props) { + const { + example: { example }, + baseUrl, + } = props + + let badgeUrl + let exampleUrl + // There are two alternatives for `example`. Refer to the schema in + // `services/service-definitions.js`. + if (example.pattern !== undefined) { + const { pattern, namedParams, queryParams } = example + badgeUrl = badgeUrlFromPath({ + path: pattern, + queryParams, + }) + exampleUrl = badgeUrlFromPattern({ + baseUrl, + pattern, + namedParams, + queryParams, + }) + } else { + const { path, queryParams } = example + badgeUrl = badgeUrlFromPath({ + path, + queryParams, + }) + exampleUrl = '' } + + return { badgeUrl, exampleUrl } } - get isOpen() { - return this.props.example !== null + static getDerivedStateFromProps(props, state) { + let urlsForProps, link + if (props.example) { + urlsForProps = MarkupModal.urlsForProps(props) + link = props.example.example.link + } else { + urlsForProps = { badgeUrl: '', exampleUrl: '' } + link = '' + } + + if (urlsForProps.badgeUrl === state.badgeUrlForProps) { + return null + } else { + return { + ...urlsForProps, + badgeUrlForProps: urlsForProps.badgeUrl, + link, + } + } } - generateCompleteBadgeUrl() { + generateBuiltBadgeUrl() { const { baseUrl } = this.props const { badgeUrl, style } = this.state - return resolveBadgeUrl( - badgeUrl, - baseUrl || window.location.href, - // Default style doesn't need to be specified. - style === 'flat' ? undefined : { style } - ) + return badgeUrlFromPath({ + baseUrl, + path: badgeUrl, + format: '', // `badgeUrl` already contains `.svg`. + style: style === 'flat' ? undefined : style, + }) } - generateMarkup() { - if (!this.isOpen) { - return {} + renderLivePreview() { + const { badgeUrl } = this.state + const includesPlaceholders = badgeUrl.includes(':') + + if (includesPlaceholders) { + return nonBreakingSpace + } else { + const livePreviewUrl = this.generateBuiltBadgeUrl() + return } + } - const { title } = this.props.example + renderMarkup() { + const { + example: { + example: { title }, + }, + } = this.props const { link } = this.state - const completeBadgeUrl = this.generateCompleteBadgeUrl() - return generateAllMarkup(completeBadgeUrl, link, title) + + const builtBadgeUrl = this.generateBuiltBadgeUrl() + const { markdown, reStructuredText, asciiDoc } = generateAllMarkup( + builtBadgeUrl, + link, + title + ) + + return ( +
+

+ URL  + + + +

+

+ Markdown  + + + +

+

+ reStructuredText  + + + +

+

+ AsciiDoc  + + + +

+
+ ) } renderDocumentation() { - if (!this.isOpen) { - return null - } + const { + example: { documentation }, + } = this.props - const { documentation } = this.props.example return documentation ? (

Documentation

@@ -91,55 +172,57 @@ export default class MarkupModal extends React.Component { } render() { - const { markdown, reStructuredText, asciiDoc } = this.generateMarkup() + const { isOpen } = this + const { onRequestClose } = this.props + const { link, badgeUrl, exampleUrl, style } = this.state - const completeBadgeUrl = this.isOpen - ? this.generateCompleteBadgeUrl() - : undefined + const common = { + autoComplete: 'off', + autoCorrect: 'off', + autoCapitalize: 'off', + spellCheck: 'false', + } return (
-

- -

+

{isOpen && this.renderLivePreview()}

- {this.state.exampleUrl && ( + {exampleUrl && (

Example  - +

)} @@ -147,7 +230,7 @@ export default class MarkupModal extends React.Component {

-

- Markdown  - - - -

-

- reStructuredText  - - - -

-

- AsciiDoc  - - - -

- {this.renderDocumentation()} + {isOpen && this.renderMarkup()} + {isOpen && this.renderDocumentation()}
) diff --git a/frontend/components/search-results.js b/frontend/components/search-results.js deleted file mode 100644 index a1a601a978ad3..0000000000000 --- a/frontend/components/search-results.js +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react' -import { Link } from 'react-router-dom' -import PropTypes from 'prop-types' -import { BadgeExamples } from './badge-examples' -import badgeExampleData from '../../badge-examples.json' -import { prepareExamples, predicateFromQuery } from '../lib/prepare-examples' -import { baseUrl, longCache } from '../constants' - -export default class SearchResults extends React.Component { - static propTypes = { - category: PropTypes.string, - query: PropTypes.string, - clickHandler: PropTypes.func.isRequired, - } - - prepareExamples(category) { - const examples = category - ? badgeExampleData.filter(example => example.category.id === category) - : badgeExampleData - return prepareExamples(examples, () => predicateFromQuery(this.props.query)) - } - - renderExamples() { - return ( - - ) - } - - renderCategoryHeadings() { - return this.preparedExamples.map((category, i) => ( - -

{category.category.name}

- - )) - } - - render() { - this.preparedExamples = this.prepareExamples(this.props.category) - - if (this.props.category) { - return this.renderExamples() - } else if (this.props.query == null || this.props.query.length === 0) { - return this.renderCategoryHeadings() - } else { - return this.renderExamples() - } - } -} diff --git a/frontend/components/suggestion-and-search.js b/frontend/components/suggestion-and-search.js index afd1ed755c6c2..b8b59d76da11a 100644 --- a/frontend/components/suggestion-and-search.js +++ b/frontend/components/suggestion-and-search.js @@ -2,7 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import fetchPonyfill from 'fetch-ponyfill' import debounce from 'lodash.debounce' -import { Badge } from './badge-examples' +import BadgeExamples from './badge-examples' import resolveUrl from '../lib/resolve-url' export default class SuggestionAndSearch extends React.Component { @@ -51,7 +51,7 @@ export default class SuggestionAndSearch extends React.Component { const json = await res.json() // This doesn't validate the response. The default value here prevents // a crash if the server returns {"err":"Disallowed"}. - suggestions = json.badges || [] + suggestions = json.suggestions || [] } catch (e) { suggestions = [] } @@ -68,28 +68,24 @@ export default class SuggestionAndSearch extends React.Component { return null } + const transformed = [ + { + examples: suggestions.map(({ title, path, link, queryParams }) => ({ + title, + preview: { path, queryParams }, + example: { path, queryParams }, + link, + })), + }, + ] + return ( - - - {suggestions.map(({ name, link, badge }, i) => ( - // TODO We need to deal with `link`. - - this.props.onBadgeClick({ - title: name, - previewUrl: badge, - link, - }) - } - baseUrl={baseUrl} - longCache={longCache} - /> - ))} - -
+ ) } diff --git a/frontend/constants.js b/frontend/constants.js index 1a927a6971db5..1e81b0d708116 100644 --- a/frontend/constants.js +++ b/frontend/constants.js @@ -1,6 +1,6 @@ import envFlag from 'node-env-flag' -const baseUrl = process.env.BASE_URL +const baseUrl = process.env.BASE_URL || '' const longCache = envFlag(process.env.LONG_CACHE, false) export { baseUrl, longCache } diff --git a/frontend/lib/badge-url.js b/frontend/lib/badge-url.js index ceaee81a8dfc5..30e7d368f5202 100644 --- a/frontend/lib/badge-url.js +++ b/frontend/lib/badge-url.js @@ -4,7 +4,7 @@ import { staticBadgeUrl as makeStaticBadgeUrl } from '../../lib/make-badge-url' export default function resolveBadgeUrl( url, baseUrl, - { longCache, style, queryParams: inQueryParams } = {} + { longCache, style, queryParams: inQueryParams, format = 'svg' } = {} ) { const outQueryParams = Object.assign({}, inQueryParams) if (longCache) { @@ -13,7 +13,8 @@ export default function resolveBadgeUrl( if (style) { outQueryParams.style = style } - return resolveUrl(url, baseUrl, outQueryParams) + + return resolveUrl(`${url}.${format}`, baseUrl, outQueryParams) } export function staticBadgeUrl(baseUrl, label, message, color, options) { @@ -48,5 +49,5 @@ export function dynamicBadgeUrl( const outOptions = Object.assign({ queryParams }, rest) - return resolveBadgeUrl(`/badge/dynamic/${datatype}.svg`, baseUrl, outOptions) + return resolveBadgeUrl(`/badge/dynamic/${datatype}`, baseUrl, outOptions) } diff --git a/frontend/lib/badge-url.spec.js b/frontend/lib/badge-url.spec.js index 34fe55e8847d2..65ab08138efc6 100644 --- a/frontend/lib/badge-url.spec.js +++ b/frontend/lib/badge-url.spec.js @@ -6,19 +6,17 @@ const resolveBadgeUrlWithLongCache = (url, baseUrl) => describe('Badge URL functions', function() { test(resolveBadgeUrl, () => { - given('/badge/foo-bar-blue.svg', undefined).expect( - '/badge/foo-bar-blue.svg' - ) - given('/badge/foo-bar-blue.svg', 'http://example.com').expect( + given('/badge/foo-bar-blue', undefined).expect('/badge/foo-bar-blue.svg') + given('/badge/foo-bar-blue', 'http://example.com').expect( 'http://example.com/badge/foo-bar-blue.svg' ) }) test(resolveBadgeUrlWithLongCache, () => { - given('/badge/foo-bar-blue.svg', undefined).expect( + given('/badge/foo-bar-blue', undefined).expect( '/badge/foo-bar-blue.svg?maxAge=2592000' ) - given('/badge/foo-bar-blue.svg', 'http://example.com').expect( + given('/badge/foo-bar-blue', 'http://example.com').expect( 'http://example.com/badge/foo-bar-blue.svg?maxAge=2592000' ) }) diff --git a/frontend/lib/prepare-examples.js b/frontend/lib/prepare-examples.js deleted file mode 100644 index 4293d2aff7ebe..0000000000000 --- a/frontend/lib/prepare-examples.js +++ /dev/null @@ -1,45 +0,0 @@ -import escapeStringRegexp from 'escape-string-regexp' - -export function exampleMatchesRegex(example, regex) { - const { title, keywords } = example - const haystack = [title].concat(keywords).join(' ') - return regex.test(haystack) -} - -export function predicateFromQuery(query) { - if (query) { - const escaped = escapeStringRegexp(query) - const regex = new RegExp(escaped, 'i') // Case-insensitive. - return example => exampleMatchesRegex(example, regex) - } else { - return () => true - } -} - -export function mapExamples(categories, iteratee) { - return ( - categories - .map(({ category, examples }) => ({ - category, - examples: iteratee(examples), - })) - // Remove empty categories. - .filter(({ category, examples }) => examples.length > 0) - ) -} - -export function prepareExamples(categories, predicateProvider) { - let nextKey = 0 - return mapExamples(categories, examples => - examples.map(example => - Object.assign( - { - shouldDisplay: () => predicateProvider()(example), - // Assign each example a unique ID. - key: nextKey++, - }, - example - ) - ) - ) -} diff --git a/frontend/lib/prepare-examples.spec.js b/frontend/lib/prepare-examples.spec.js deleted file mode 100644 index 57b61caeaade9..0000000000000 --- a/frontend/lib/prepare-examples.spec.js +++ /dev/null @@ -1,18 +0,0 @@ -import { test, given, forCases } from 'sazerac' -import { predicateFromQuery } from './prepare-examples' - -describe('Badge example functions', function() { - const exampleMatchesQuery = (example, query) => - predicateFromQuery(query)(example) - - test(exampleMatchesQuery, () => { - forCases([given({ title: 'node version' }, 'npm')]).expect(false) - - forCases([ - given({ title: 'node version', keywords: ['npm'] }, 'node'), - given({ title: 'node version', keywords: ['npm'] }, 'npm'), - // https://github.com/badges/shields/issues/1578 - given({ title: 'c++ is the best language' }, 'c++'), - ]).expect(true) - }) -}) diff --git a/frontend/lib/service-definitions/index.js b/frontend/lib/service-definitions/index.js new file mode 100644 index 0000000000000..166843308d932 --- /dev/null +++ b/frontend/lib/service-definitions/index.js @@ -0,0 +1,13 @@ +import groupBy from 'lodash.groupby' + +import { services, categories } from '../../../service-definitions.yml' +export { services, categories } from '../../../service-definitions.yml' + +export function findCategory(category) { + return categories.find(({ id }) => id === category) +} + +const byCategory = groupBy(services, 'category') +export function getDefinitionsForCategory(category) { + return byCategory[category] +} diff --git a/frontend/lib/service-definitions/service-definition-set-helper.js b/frontend/lib/service-definitions/service-definition-set-helper.js new file mode 100644 index 0000000000000..105f560f20beb --- /dev/null +++ b/frontend/lib/service-definitions/service-definition-set-helper.js @@ -0,0 +1,47 @@ +import escapeStringRegexp from 'escape-string-regexp' + +export function exampleMatchesRegex(example, regex) { + const { title, keywords } = example + const haystack = [title].concat(keywords).join(' ') + return regex.test(haystack) +} + +export function predicateFromQuery(query) { + const escaped = escapeStringRegexp(query) + const regex = new RegExp(escaped, 'i') // Case-insensitive. + return ({ examples }) => + examples.some(example => exampleMatchesRegex(example, regex)) +} + +export default class ServiceDefinitionSetHelper { + constructor(definitionData) { + this.definitionData = definitionData + } + + static create(definitionData) { + return new ServiceDefinitionSetHelper(definitionData) + } + + getCategory(wantedCategory) { + return ServiceDefinitionSetHelper.create( + this.definitionData.filter(({ category }) => category === wantedCategory) + ) + } + + search(query) { + const predicate = predicateFromQuery(query) + return ServiceDefinitionSetHelper.create( + this.definitionData.filter(predicate) + ) + } + + notDeprecated() { + return ServiceDefinitionSetHelper.create( + this.definitionData.filter(({ isDeprecated }) => !isDeprecated) + ) + } + + toArray() { + return this.definitionData + } +} diff --git a/frontend/lib/service-definitions/service-definition-set-helper.spec.js b/frontend/lib/service-definitions/service-definition-set-helper.spec.js new file mode 100644 index 0000000000000..205c7631ec2ab --- /dev/null +++ b/frontend/lib/service-definitions/service-definition-set-helper.spec.js @@ -0,0 +1,26 @@ +import { test, given, forCases } from 'sazerac' +import { predicateFromQuery } from './service-definition-set-helper' + +describe('Badge example functions', function() { + const exampleMatchesQuery = (example, query) => + predicateFromQuery(query)(example) + + test(exampleMatchesQuery, () => { + forCases([given({ examples: [{ title: 'node version' }] }, 'npm')]).expect( + false + ) + + forCases([ + given( + { examples: [{ title: 'node version', keywords: ['npm'] }] }, + 'node' + ), + given( + { examples: [{ title: 'node version', keywords: ['npm'] }] }, + 'npm' + ), + // https://github.com/badges/shields/issues/1578 + given({ examples: [{ title: 'c++ is the best language' }] }, 'c++'), + ]).expect(true) + }) +}) diff --git a/lib/all-badge-examples.js b/lib/all-badge-examples.js deleted file mode 100644 index 93a1414918431..0000000000000 --- a/lib/all-badge-examples.js +++ /dev/null @@ -1,139 +0,0 @@ -'use strict' - -const { loadServiceClasses } = require('../services') - -const allBadgeExamples = [ - { - category: { - id: 'build', - name: 'Build', - }, - examples: [], - }, - { - category: { - id: 'chat', - name: 'Chat', - }, - examples: [], - }, - { - category: { - id: 'dependencies', - name: 'Dependencies', - }, - examples: [], - }, - { - category: { - id: 'size', - name: 'Size', - }, - examples: [], - }, - { - category: { - id: 'downloads', - name: 'Downloads', - }, - examples: [], - }, - { - category: { - id: 'funding', - name: 'Funding', - }, - examples: [], - }, - { - category: { - id: 'issue-tracking', - name: 'Issue Tracking', - }, - examples: [], - }, - { - category: { - id: 'license', - name: 'License', - }, - examples: [], - }, - { - category: { - id: 'rating', - name: 'Rating', - }, - examples: [], - }, - { - category: { - id: 'social', - name: 'Social', - }, - examples: [], - }, - { - category: { - id: 'version', - name: 'Version', - }, - examples: [], - }, - { - category: { - id: 'platform-support', - name: 'Platform & Version Support', - }, - examples: [], - }, - { - category: { - id: 'monitoring', - name: 'Monitoring', - }, - examples: [], - }, - { - category: { - id: 'activity', - name: 'Activity', - }, - examples: [], - }, - { - category: { - id: 'other', - name: 'Other', - }, - examples: [], - }, -] - -function findCategory(wantedCategory) { - return allBadgeExamples.find( - thisCat => thisCat.category.id === wantedCategory - ) -} - -function loadExamples() { - loadServiceClasses().forEach(ServiceClass => { - const prepared = ServiceClass.prepareExamples() - if (prepared.length === 0) { - return - } - const category = findCategory(ServiceClass.category) - if (category === undefined) { - throw Error( - `Unknown category ${ServiceClass.category} referenced in ${ - ServiceClass.name - }` - ) - } - category.examples = category.examples.concat(prepared) - }) -} -loadExamples() - -module.exports = allBadgeExamples -module.exports.findCategory = findCategory diff --git a/lib/all-badge-examples.spec.js b/lib/all-badge-examples.spec.js deleted file mode 100644 index d7b21cfcd1e2a..0000000000000 --- a/lib/all-badge-examples.spec.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict' - -const { expect } = require('chai') - -const allBadgeExamples = require('./all-badge-examples') - -describe('The badge examples', function() { - it('should include AppVeyor, which is added automatically', function() { - const { examples } = allBadgeExamples.findCategory('build') - - const appVeyorBuildExamples = examples - .filter(ex => ex.title.includes('AppVeyor')) - .filter(ex => !ex.title.includes('tests')) - - expect(appVeyorBuildExamples).to.deep.equal([ - { - title: 'AppVeyor', - exampleUrl: '/appveyor/ci/gruntjs/grunt.svg', - previewUrl: '/badge/build-passing-brightgreen.svg', - urlPattern: '/appveyor/ci/:user/:repo.svg', - documentation: undefined, - keywords: undefined, - }, - { - title: 'AppVeyor branch', - exampleUrl: '/appveyor/ci/gruntjs/grunt/master.svg', - previewUrl: '/badge/build-passing-brightgreen.svg', - urlPattern: '/appveyor/ci/:user/:repo/:branch.svg', - documentation: undefined, - keywords: undefined, - }, - ]) - }) -}) diff --git a/lib/make-badge-url.js b/lib/make-badge-url.js index 1b43f159d0538..aac423f45f191 100644 --- a/lib/make-badge-url.js +++ b/lib/make-badge-url.js @@ -1,13 +1,60 @@ 'use strict' const queryString = require('query-string') +const pathToRegexp = require('path-to-regexp') + +function badgeUrlFromPath({ + baseUrl = '', + path, + queryParams, + style, + format = 'svg', + longCache = false, +}) { + const outExt = format.length ? `.${format}` : '' + + const outQueryString = queryString.stringify({ + maxAge: longCache ? '2592000' : undefined, + style, + ...queryParams, + }) + const suffix = outQueryString ? `?${outQueryString}` : '' + + return `${baseUrl}${path}${outExt}${suffix}` +} + +function badgeUrlFromPattern({ + baseUrl = '', + pattern, + namedParams, + queryParams, + style, + format = 'svg', + longCache = false, +}) { + const toPath = pathToRegexp.compile(pattern, { + strict: true, + sensitive: true, + }) + + const path = toPath(namedParams) + + return badgeUrlFromPath({ + baseUrl, + path, + queryParams, + style, + format, + longCache, + }) +} function encodeField(s) { return encodeURIComponent(s.replace(/-/g, '--').replace(/_/g, '__')) } function staticBadgeUrl({ - baseUrl, + baseUrl = '', label, message, color = 'lightgray', @@ -22,10 +69,12 @@ function staticBadgeUrl({ style, }) const suffix = outQueryString ? `?${outQueryString}` : '' - return `/badge/${path}.${format}${suffix}` + return `${baseUrl}/badge/${path}.${format}${suffix}` } module.exports = { + badgeUrlFromPath, + badgeUrlFromPattern, encodeField, staticBadgeUrl, } diff --git a/lib/make-badge-url.spec.js b/lib/make-badge-url.spec.js index 595455c668bb5..28dbdb306d1c5 100644 --- a/lib/make-badge-url.spec.js +++ b/lib/make-badge-url.spec.js @@ -1,9 +1,37 @@ 'use strict' const { test, given } = require('sazerac') -const { encodeField, staticBadgeUrl } = require('./make-badge-url') +const { + badgeUrlFromPath, + badgeUrlFromPattern, + encodeField, + staticBadgeUrl, +} = require('./make-badge-url') describe('Badge URL generation functions', function() { + test(badgeUrlFromPath, () => { + given({ + baseUrl: 'http://example.com', + path: '/npm/v/gh-badges', + style: 'flat-square', + longCache: true, + }).expect( + 'http://example.com/npm/v/gh-badges.svg?maxAge=2592000&style=flat-square' + ) + }) + + test(badgeUrlFromPattern, () => { + given({ + baseUrl: 'http://example.com', + pattern: '/npm/v/:packageName', + namedParams: { packageName: 'gh-badges' }, + style: 'flat-square', + longCache: true, + }).expect( + 'http://example.com/npm/v/gh-badges.svg?maxAge=2592000&style=flat-square' + ) + }) + test(encodeField, () => { given('foo').expect('foo') given('').expect('') diff --git a/lib/suggest.js b/lib/suggest.js index 8c6bc3843553e..28fb70b3e1ac6 100644 --- a/lib/suggest.js +++ b/lib/suggest.js @@ -19,38 +19,39 @@ function twitterPage(url) { const host = url.host const path = url.pathname return { - name: 'Twitter', + title: 'Twitter', link: `https://twitter.com/intent/tweet?text=Wow:&url=${encodeURIComponent( url.href )}`, - badge: `https://img.shields.io/twitter/url/${schema}/${host}${path}.svg?style=social`, + path: `/twitter/url/${schema}/${host}${path}`, + queryParams: { style: 'social' }, } } function githubIssues(user, repo) { const repoSlug = `${user}/${repo}` return { - name: 'GitHub issues', + title: 'GitHub issues', link: `https://github.com/${repoSlug}/issues`, - badge: `https://img.shields.io/github/issues/${repoSlug}.svg`, + path: `/github/issues/${repoSlug}`, } } function githubForks(user, repo) { const repoSlug = `${user}/${repo}` return { - name: 'GitHub forks', + title: 'GitHub forks', link: `https://github.com/${repoSlug}/network`, - badge: `https://img.shields.io/github/forks/${repoSlug}.svg`, + path: `/github/forks/${repoSlug}`, } } function githubStars(user, repo) { const repoSlug = `${user}/${repo}` return { - name: 'GitHub stars', + title: 'GitHub stars', link: `https://github.com/${repoSlug}/stargazers`, - badge: `https://img.shields.io/github/stars/${repoSlug}.svg`, + path: `/github/stars/${repoSlug}`, } } @@ -71,8 +72,8 @@ async function githubLicense(githubApiProvider, user, repo) { } catch (e) {} return { - name: 'GitHub license', - badge: `https://img.shields.io/github/license/${repoSlug}.svg`, + title: 'GitHub license', + path: `/github/license/${repoSlug}`, link, } } @@ -99,10 +100,11 @@ async function findSuggestions(githubApiProvider, url) { // data: {url}, JSON-serializable object. // end: function(json), with json of the form: -// - badges: list of objects of the form: +// - suggestions: list of objects of the form: +// - title: string // - link: target as a string URL. -// - badge: shields image URL. -// - name: string +// - path: shields image URL path. +// - queryParams: Object containing query params (Optional) function setRoutes(allowedOrigin, githubApiProvider, server) { server.ajax.on('suggest/v1', (data, end, ask) => { // The typical dev and production setups are cross-origin. However, in @@ -145,11 +147,11 @@ function setRoutes(allowedOrigin, githubApiProvider, server) { findSuggestions(githubApiProvider, url) // This interacts with callback code and can't use async/await. // eslint-disable-next-line promise/prefer-await-to-then - .then(badges => { - end({ badges }) + .then(suggestions => { + end({ suggestions }) }) .catch(err => { - end({ badges: [], err }) + end({ suggestions: [], err }) }) }) } diff --git a/next.config.js b/next.config.js index e20cf711fe59b..9bf62b997ad0c 100644 --- a/next.config.js +++ b/next.config.js @@ -24,9 +24,11 @@ module.exports = { ) } - config.module.loaders = (config.module.loaders || []).concat({ - test: /\.json$/, - loader: 'json-loader', + config.module.rules.push({ + test: /\.yml$/, + use: { + loader: 'js-yaml-loader', + }, }) if (assetPrefix) { diff --git a/package-lock.json b/package-lock.json index cae90d8ed7210..904d4734ab6ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2758,12 +2758,6 @@ } } }, - "classnames": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==", - "dev": true - }, "cli-cursor": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", @@ -7541,6 +7535,17 @@ } } }, + "js-yaml-loader": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/js-yaml-loader/-/js-yaml-loader-1.0.1.tgz", + "integrity": "sha512-fts4y0A76YrlO+ARYue0dRy5CUUskF7hIoiFoffb2OlXGoUy3DPwNldDi/gwtwRhPyOBIAwXw9myqVLB1Mf17Q==", + "dev": true, + "requires": { + "js-yaml": "^3.9.1", + "loader-utils": "^1.1.0", + "un-eval": "^1.2.0" + } + }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", @@ -8142,6 +8147,12 @@ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, + "lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha1-Cwih3PaDl8OXhVwyOXg4Mt90A9E=", + "dev": true + }, "lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -15048,6 +15059,12 @@ "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" }, + "un-eval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/un-eval/-/un-eval-1.2.0.tgz", + "integrity": "sha512-Wlj/pum6dQtGTPD/lclDtoVPkSfpjPfy1dwnnKw/sZP5DpBH9fLhBgQfsqNhe5/gS1D+vkZUuB771NRMUPA5CA==", + "dev": true + }, "underscore": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", diff --git a/package.json b/package.json index a687216c80513..3d15424c27dc7 100644 --- a/package.json +++ b/package.json @@ -86,13 +86,12 @@ "prebuild": "npm run depcheck", "features": "node scripts/export-supported-features-cli.js > supported-features.json", "defs": "node scripts/export-service-definitions-cli.js > service-definitions.yml", - "examples": "node scripts/export-badge-examples-cli.js > badge-examples.json", - "build": "npm run examples && npm run defs && npm run features && next build && next export -o build/", + "build": "npm run defs && npm run features && 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": "HANDLE_INTERNAL_ERRORS=false RATE_LIMIT=false node server 8080 ::", "now-start": "node server", - "prestart": "npm run depcheck && npm run examples && npm run defs && npm run features", + "prestart": "npm run depcheck && npm run defs && npm run features", "start": "concurrently --names server,frontend \"ALLOWED_ORIGIN=http://localhost:3000 npm run start:server\" \"BASE_URL=http://[::]:8080 next dev\"", "refactoring-report": "node scripts/refactoring-cli.js" }, @@ -119,7 +118,6 @@ "chai-string": "^1.4.0", "chainsmoker": "^0.1.0", "child-process-promise": "^2.2.1", - "classnames": "^2.2.5", "concurrently": "^4.1.0", "danger": "^6.1.9", "danger-plugin-no-test-shortcuts": "^2.0.0", @@ -146,9 +144,11 @@ "is-png": "^1.1.0", "is-svg": "^3.0.0", "joi-extension-semver": "2.0.0", + "js-yaml-loader": "^1.0.1", "lint-staged": "^8.1.0", "lodash.debounce": "^4.0.8", "lodash.difference": "^4.5.0", + "lodash.groupby": "^4.6.0", "lodash.mapvalues": "^4.6.0", "minimist": "^1.2.0", "mkdirp": "^0.5.1", diff --git a/pages/index.js b/pages/index.js index c9444d2ce819f..90cd8635e6771 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,13 +1,13 @@ import React from 'react' import { HashRouter, StaticRouter, Route } from 'react-router-dom' -import ExamplesPage from '../frontend/components/examples-page' +import Main from '../frontend/components/main' export default class Router extends React.Component { render() { const router = (
- - + +
) diff --git a/scripts/export-badge-examples-cli.js b/scripts/export-badge-examples-cli.js deleted file mode 100644 index 9f1d966a703bf..0000000000000 --- a/scripts/export-badge-examples-cli.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict' - -const allBadgeExamples = require('../lib/all-badge-examples') - -process.stdout.write(JSON.stringify(allBadgeExamples)) diff --git a/services/base.js b/services/base.js index b7b03aea28259..5ff89f1dc35ad 100644 --- a/services/base.js +++ b/services/base.js @@ -2,7 +2,6 @@ // See available emoji at http://emoji.muan.co/ const emojic = require('emojic') -const queryString = require('query-string') const pathToRegexp = require('path-to-regexp') const { NotFound, @@ -20,9 +19,7 @@ const { makeColor, setBadgeColor, } = require('../lib/badge-data') -const { staticBadgeUrl } = require('../lib/make-badge-url') const trace = require('./trace') -const oldValidateExample = require('./validate-example') const { validateExample, transformExample } = require('./transform-example') const { assertValidCategory } = require('./categories') const { assertValidServiceDefinition } = require('./service-definitions') @@ -125,92 +122,6 @@ 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({ - label: badgeData.text[0], - message: `${badgeData.text[1]}`, - color: badgeData.colorscheme || badgeData.colorB, - }) - } - - static _dotSvg(url) { - if (url.includes('?')) { - return url.replace('?', '.svg?') - } else { - return `${url}.svg` - } - } - - /** - * Return an array of examples. Each example is prepared according to the - * schema in `lib/all-badge-examples.js`. - */ - static prepareExamples() { - return this.examples.map((example, index) => { - const { - title, - query, - namedParams, - exampleUrl, - previewUrl, - pattern, - staticExample, - documentation, - keywords, - } = oldValidateExample(example, index, this) - - const stringified = queryString.stringify(query) - const suffix = stringified ? `?${stringified}` : '' - - let outExampleUrl - let outPreviewUrl - let outPattern - if (namedParams) { - outPreviewUrl = this._makeStaticExampleUrl(staticExample) - outPattern = `${this._dotSvg(this._makeFullUrl(pattern))}${suffix}` - outExampleUrl = `${this._makeFullUrlFromParams( - pattern, - namedParams - )}${suffix}` - } else if (staticExample) { - outPreviewUrl = this._makeStaticExampleUrl(staticExample) - outPattern = `${this._dotSvg(this._makeFullUrl(pattern))}${suffix}` - outExampleUrl = `${this._dotSvg( - this._makeFullUrl(exampleUrl) - )}${suffix}` - } else { - outPreviewUrl = `${this._dotSvg( - this._makeFullUrl(previewUrl) - )}${suffix}` - outPattern = undefined - outExampleUrl = undefined - } - - return { - title: title ? `${title}` : this.name, - exampleUrl: outExampleUrl, - previewUrl: outPreviewUrl, - urlPattern: outPattern, - documentation, - keywords, - } - }) - } - static validateDefinition() { assertValidCategory(this.category, `Category for ${this.name}`) @@ -235,7 +146,7 @@ class BaseService { let route if (pattern) { - route = { pattern, queryParams } + route = { pattern: this._makeFullUrl(pattern), queryParams } } else if (format) { route = { format, queryParams } } else { diff --git a/services/base.spec.js b/services/base.spec.js index 855773dacf745..11d02f42e83ec 100644 --- a/services/base.spec.js +++ b/services/base.spec.js @@ -48,6 +48,11 @@ class DummyService extends BaseService { staticExample: this.render({ namedParamA: 'foo', queryParamA: 'bar' }), keywords: ['hello'], }, + { + namedParams: { namedParamA: 'World' }, + staticExample: this.render({ namedParamA: 'foo', queryParamA: 'bar' }), + keywords: ['hello'], + }, { pattern: ':world', namedParams: { world: 'World' }, @@ -494,78 +499,31 @@ describe('BaseService', function() { }) }) - describe('_makeStaticExampleUrl', function() { - test( - serviceData => DummyService._makeStaticExampleUrl(serviceData), - () => { - given({ - message: 'hello', - color: 'dcdc00', - }).expect('/badge/cat-hello-%23dcdc00.svg') - given({ - message: 'hello', - color: 'red', - }).expect('/badge/cat-hello-red.svg') - given({ - message: 'hello', - }).expect('/badge/cat-hello-lightgrey.svg') - } - ) - }) - - describe('prepareExamples', function() { - it('returns the expected result', function() { - const [ - first, - second, - third, - fourth, - fifth, - ] = DummyService.prepareExamples() - expect(first).to.deep.equal({ - title: 'DummyService', - exampleUrl: undefined, - previewUrl: '/foo/World.svg', - urlPattern: undefined, - documentation: undefined, - keywords: undefined, - }) - expect(second).to.deep.equal({ - title: 'DummyService', - exampleUrl: undefined, - previewUrl: '/foo/World.svg?queryParamA=%21%21%21', - urlPattern: undefined, - documentation: undefined, - keywords: undefined, - }) - const preparedStaticExample = { - title: 'DummyService', - exampleUrl: '/foo/World.svg', - previewUrl: - '/badge/cat-Hello%20namedParamA%3A%20foo%20with%20queryParamA%3A%20bar-lightgrey.svg', - 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({ - title: 'DummyService', - exampleUrl: '/foo/World.svg?queryParamA=%21%21%21', - previewUrl: - '/badge/cat-Hello%20namedParamA%3A%20foo%20with%20queryParamA%3A%20bar-lightgrey.svg', - urlPattern: '/foo/:world.svg?queryParamA=%21%21%21', - documentation: undefined, - keywords: ['hello'], - }) - }) - }) - describe('getDefinition', function() { it('returns the expected result', function() { const { - examples: [first, second, third, fourth, fifth], + category, + name, + isDeprecated, + route, + examples, } = DummyService.getDefinition() + expect({ + category, + name, + isDeprecated, + route, + }).to.deep.equal({ + category: 'cat', + name: 'DummyService', + isDeprecated: false, + route: { + pattern: '/foo/:namedParamA', + queryParams: [], + }, + }) + + const [first, second, third, fourth, fifth, sixth] = examples expect(first).to.deep.equal({ title: 'DummyService', example: { @@ -623,6 +581,21 @@ describe('BaseService', function() { documentation: undefined, }) expect(fifth).to.deep.equal({ + title: 'DummyService', + example: { + pattern: '/foo/:namedParamA', + namedParams: { namedParamA: 'World' }, + queryParams: {}, + }, + preview: { + label: 'cat', + message: 'Hello namedParamA: foo with queryParamA: bar', + color: 'lightgrey', + }, + keywords: ['hello'], + documentation: undefined, + }) + expect(sixth).to.deep.equal({ title: 'DummyService', example: { pattern: '/foo/:world', diff --git a/services/suggest/suggest.tester.js b/services/suggest/suggest.tester.js index 71d2177168c40..bece51b96219d 100644 --- a/services/suggest/suggest.tester.js +++ b/services/suggest/suggest.tester.js @@ -15,36 +15,37 @@ module.exports = t t.create('issues, forks, stars and twitter') .get(`/v1?url=${encodeURIComponent('https://github.com/atom/atom')}`) - // suggest resource requires this header value - .expectJSON('badges.?', { - name: 'GitHub issues', + .expectJSON('suggestions.?', { + title: 'GitHub issues', link: 'https://github.com/atom/atom/issues', - badge: 'https://img.shields.io/github/issues/atom/atom.svg', + path: '/github/issues/atom/atom', }) - .expectJSON('badges.?', { - name: 'GitHub forks', + .expectJSON('suggestions.?', { + title: 'GitHub forks', link: 'https://github.com/atom/atom/network', - badge: 'https://img.shields.io/github/forks/atom/atom.svg', + path: '/github/forks/atom/atom', }) - .expectJSON('badges.?', { - name: 'GitHub stars', + .expectJSON('suggestions.?', { + title: 'GitHub stars', link: 'https://github.com/atom/atom/stargazers', - badge: 'https://img.shields.io/github/stars/atom/atom.svg', + path: '/github/stars/atom/atom', }) - .expectJSON('badges.?', { - name: 'Twitter', + .expectJSON('suggestions.?', { + title: 'Twitter', link: 'https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fatom%2Fatom', - badge: - 'https://img.shields.io/twitter/url/https/github.com/atom/atom.svg?style=social', + path: '/twitter/url/https/github.com/atom/atom', + queryParams: { + style: 'social', + }, }) t.create('license') .get(`/v1?url=${encodeURIComponent('https://github.com/atom/atom')}`) - .expectJSON('badges.?', { - name: 'GitHub license', + .expectJSON('suggestions.?', { + title: 'GitHub license', link: 'https://github.com/atom/atom/blob/master/LICENSE.md', - badge: 'https://img.shields.io/github/license/atom/atom.svg', + path: '/github/license/atom/atom', }) t.create('license for non-existing project') @@ -54,10 +55,10 @@ t.create('license for non-existing project') .get(/\/repos\/atom\/atom\/license/) .reply(404) ) - .expectJSON('badges.?', { - name: 'GitHub license', + .expectJSON('suggestions.?', { + title: 'GitHub license', link: 'https://github.com/atom/atom', - badge: 'https://img.shields.io/github/license/atom/atom.svg', + path: '/github/license/atom/atom', }) t.create('license when json response is invalid') @@ -67,10 +68,10 @@ t.create('license when json response is invalid') .get(/\/repos\/atom\/atom\/license/) .reply(invalidJSON) ) - .expectJSON('badges.?', { - name: 'GitHub license', + .expectJSON('suggestions.?', { + title: 'GitHub license', link: 'https://github.com/atom/atom', - badge: 'https://img.shields.io/github/license/atom/atom.svg', + path: '/github/license/atom/atom', }) t.create('license when html_url not found in GitHub api response') @@ -82,8 +83,8 @@ t.create('license when html_url not found in GitHub api response') license: 'MIT', }) ) - .expectJSON('badges.?', { - name: 'GitHub license', + .expectJSON('suggestions.?', { + title: 'GitHub license', link: 'https://github.com/atom/atom', - badge: 'https://img.shields.io/github/license/atom/atom.svg', + path: '/github/license/atom/atom', }) diff --git a/services/transform-example.js b/services/transform-example.js index c25079d2a3a99..20187181d738d 100644 --- a/services/transform-example.js +++ b/services/transform-example.js @@ -104,7 +104,7 @@ function transformExample(inExample, index, ServiceClass) { let example if (namedParams) { example = { - pattern: ServiceClass._makeFullUrl(pattern), + pattern: ServiceClass._makeFullUrl(pattern || ServiceClass.route.pattern), namedParams, queryParams, } diff --git a/services/validate-example.js b/services/validate-example.js deleted file mode 100644 index 14e28b93b43ff..0000000000000 --- a/services/validate-example.js +++ /dev/null @@ -1,72 +0,0 @@ -'use strict' - -module.exports = function validateExample( - { - title, - query, - queryParams, - namedParams, - exampleUrl, - previewUrl, - pattern, - urlPattern, - staticExample, - staticPreview, - documentation, - keywords, - }, - index, - ServiceClass -) { - pattern = pattern || urlPattern || ServiceClass.route.pattern - staticExample = staticExample || staticPreview - query = query || queryParams - - 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 deleted file mode 100644 index 033785290e019..0000000000000 --- a/services/validate-example.spec.js +++ /dev/null @@ -1,45 +0,0 @@ -'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) - }) - }) -})