diff --git a/CNAME b/CNAME new file mode 100644 index 0000000000..ebfa40fe78 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +contributing.shields.io diff --git a/badge-maker_lib_index.js.html b/badge-maker_lib_index.js.html new file mode 100644 index 0000000000..4d2f6185e7 --- /dev/null +++ b/badge-maker_lib_index.js.html @@ -0,0 +1,132 @@ + + + + + JSDoc: Source: badge-maker/lib/index.js + + + + + + + + + + +
+ +

Source: badge-maker/lib/index.js

+ + + + + + +
+
+
'use strict'
+/**
+ * @module badge-maker
+ */
+
+const _makeBadge = require('./make-badge')
+
+class ValidationError extends Error {}
+
+function _validate(format) {
+  if (format !== Object(format)) {
+    throw new ValidationError('makeBadge takes an argument of type object')
+  }
+
+  if (!('message' in format)) {
+    throw new ValidationError('Field `message` is required')
+  }
+
+  const stringFields = ['labelColor', 'color', 'message', 'label']
+  stringFields.forEach(function (field) {
+    if (field in format && typeof format[field] !== 'string') {
+      throw new ValidationError(`Field \`${field}\` must be of type string`)
+    }
+  })
+
+  const styleValues = [
+    'plastic',
+    'flat',
+    'flat-square',
+    'for-the-badge',
+    'social',
+  ]
+  if ('style' in format && !styleValues.includes(format.style)) {
+    throw new ValidationError(
+      `Field \`style\` must be one of (${styleValues.toString()})`
+    )
+  }
+}
+
+function _clean(format) {
+  const expectedKeys = ['label', 'message', 'labelColor', 'color', 'style']
+
+  const cleaned = {}
+  Object.keys(format).forEach(key => {
+    if (format[key] != null && expectedKeys.includes(key)) {
+      cleaned[key] = format[key]
+    } else {
+      throw new ValidationError(
+        `Unexpected field '${key}'. Allowed values are (${expectedKeys.toString()})`
+      )
+    }
+  })
+
+  // Legacy.
+  cleaned.label = cleaned.label || ''
+
+  return cleaned
+}
+
+/**
+ * Create a badge
+ *
+ * @param {object} format Object specifying badge data
+ * @param {string} format.label (Optional) Badge label (e.g: 'build')
+ * @param {string} format.message (Required) Badge message (e.g: 'passing')
+ * @param {string} format.labelColor (Optional) Label color
+ * @param {string} format.color (Optional) Message color
+ * @param {string} format.style (Optional) Visual style e.g: 'flat'
+ * @returns {string} Badge in SVG format
+ * @see https://github.com/badges/shields/tree/master/badge-maker/README.md
+ */
+function makeBadge(format) {
+  _validate(format)
+  const cleanedFormat = _clean(format)
+  return _makeBadge(cleanedFormat)
+}
+
+module.exports = {
+  makeBadge,
+  ValidationError,
+}
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/core_base-service_base-graphql.js.html b/core_base-service_base-graphql.js.html new file mode 100644 index 0000000000..96222c6637 --- /dev/null +++ b/core_base-service_base-graphql.js.html @@ -0,0 +1,147 @@ + + + + + JSDoc: Source: core/base-service/base-graphql.js + + + + + + + + + + +
+ +

Source: core/base-service/base-graphql.js

+ + + + + + +
+
+
/**
+ * @module
+ */
+
+'use strict'
+
+const { print } = require('graphql/language/printer')
+const BaseService = require('./base')
+const { InvalidResponse, ShieldsRuntimeError } = require('./errors')
+const { parseJson } = require('./json')
+
+function defaultTransformErrors(errors) {
+  return new InvalidResponse({ prettyMessage: errors[0].message })
+}
+
+/**
+ * Services which query a GraphQL endpoint should extend BaseGraphqlService
+ *
+ * @abstract
+ */
+class BaseGraphqlService extends BaseService {
+  /**
+   * Parse data from JSON endpoint
+   *
+   * @param {string} buffer JSON repsonse from upstream API
+   * @returns {object} Parsed response
+   */
+  _parseJson(buffer) {
+    return parseJson(buffer)
+  }
+
+  /**
+   * Request data from an upstream GraphQL API,
+   * parse it and validate against a schema
+   *
+   * @param {object} attrs Refer to individual attrs
+   * @param {Joi} attrs.schema Joi schema to validate the response against
+   * @param {string} attrs.url URL to request
+   * @param {object} attrs.query Parsed GraphQL object
+   *    representing the query clause of GraphQL POST body
+   *    e.g. gql`{ query { ... } }`
+   * @param {object} attrs.variables Variables clause of GraphQL POST body
+   * @param {object} [attrs.options={}] Options to pass to request. See
+   *    [documentation](https://github.com/request/request#requestoptions-callback)
+   * @param {object} [attrs.httpErrorMessages={}] Key-value map of HTTP status codes
+   *    and custom error messages e.g: `{ 404: 'package not found' }`.
+   *    This can be used to extend or override the
+   *    [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5)
+   * @param {Function} [attrs.transformJson=data => data] Function which takes the raw json and transforms it before
+   * further procesing. In case of multiple query in a single graphql call and few of them
+   * throw error, partial data might be used ignoring the error.
+   * @param {Function} [attrs.transformErrors=defaultTransformErrors]
+   *    Function which takes an errors object from a GraphQL
+   *    response and returns an instance of ShieldsRuntimeError.
+   *    The default is to return the first entry of the `errors` array as
+   *    an InvalidResponse.
+   * @returns {object} Parsed response
+   * @see https://github.com/request/request#requestoptions-callback
+   */
+  async _requestGraphql({
+    schema,
+    url,
+    query,
+    variables = {},
+    options = {},
+    httpErrorMessages = {},
+    transformJson = data => data,
+    transformErrors = defaultTransformErrors,
+  }) {
+    const mergedOptions = {
+      ...{ headers: { Accept: 'application/json' } },
+      ...options,
+    }
+    mergedOptions.method = 'POST'
+    mergedOptions.body = JSON.stringify({ query: print(query), variables })
+    const { buffer } = await this._request({
+      url,
+      options: mergedOptions,
+      errorMessages: httpErrorMessages,
+    })
+    const json = transformJson(this._parseJson(buffer))
+    if (json.errors) {
+      const exception = transformErrors(json.errors)
+      if (exception instanceof ShieldsRuntimeError) {
+        throw exception
+      } else {
+        throw Error(
+          `transformErrors() must return a ShieldsRuntimeError; got ${exception}`
+        )
+      }
+    }
+    return this.constructor._validate(json, schema)
+  }
+}
+
+module.exports = BaseGraphqlService
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/core_base-service_base-json.js.html b/core_base-service_base-json.js.html new file mode 100644 index 0000000000..5e3696fdcf --- /dev/null +++ b/core_base-service_base-json.js.html @@ -0,0 +1,108 @@ + + + + + JSDoc: Source: core/base-service/base-json.js + + + + + + + + + + +
+ +

Source: core/base-service/base-json.js

+ + + + + + +
+
+
/**
+ * @module
+ */
+
+'use strict'
+
+const BaseService = require('./base')
+const { parseJson } = require('./json')
+
+/**
+ * Services which query a JSON endpoint should extend BaseJsonService
+ *
+ * @abstract
+ */
+class BaseJsonService extends BaseService {
+  /**
+   * Parse data from JSON endpoint
+   *
+   * @param {string} buffer JSON repsonse from upstream API
+   * @returns {object} Parsed response
+   */
+  _parseJson(buffer) {
+    return parseJson(buffer)
+  }
+
+  /**
+   * Request data from an upstream API serving JSON,
+   * parse it and validate against a schema
+   *
+   * @param {object} attrs Refer to individual attrs
+   * @param {Joi} attrs.schema Joi schema to validate the response against
+   * @param {string} attrs.url URL to request
+   * @param {object} [attrs.options={}] Options to pass to request. See
+   *    [documentation](https://github.com/request/request#requestoptions-callback)
+   * @param {object} [attrs.errorMessages={}] Key-value map of status codes
+   *    and custom error messages e.g: `{ 404: 'package not found' }`.
+   *    This can be used to extend or override the
+   *    [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5)
+   * @returns {object} Parsed response
+   * @see https://github.com/request/request#requestoptions-callback
+   */
+  async _requestJson({ schema, url, options = {}, errorMessages = {} }) {
+    const mergedOptions = {
+      ...{ headers: { Accept: 'application/json' } },
+      ...options,
+    }
+    const { buffer } = await this._request({
+      url,
+      options: mergedOptions,
+      errorMessages,
+    })
+    const json = this._parseJson(buffer)
+    return this.constructor._validate(json, schema)
+  }
+}
+
+module.exports = BaseJsonService
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/core_base-service_base-svg-scraping.js.html b/core_base-service_base-svg-scraping.js.html new file mode 100644 index 0000000000..9f7c44308f --- /dev/null +++ b/core_base-service_base-svg-scraping.js.html @@ -0,0 +1,144 @@ + + + + + JSDoc: Source: core/base-service/base-svg-scraping.js + + + + + + + + + + +
+ +

Source: core/base-service/base-svg-scraping.js

+ + + + + + +
+
+
/**
+ * @module
+ */
+
+'use strict'
+
+// See available emoji at http://emoji.muan.co/
+const emojic = require('emojic')
+const BaseService = require('./base')
+const trace = require('./trace')
+const { InvalidResponse } = require('./errors')
+
+const defaultValueMatcher = />([^<>]+)<\/text><\/g>/
+const leadingWhitespace = /(?:\r\n\s*|\r\s*|\n\s*)/g
+
+/**
+ * Services which scrape data from another SVG badge
+ * should extend BaseSvgScrapingService
+ *
+ * @abstract
+ */
+class BaseSvgScrapingService extends BaseService {
+  /**
+   * Extract a value from SVG
+   *
+   * @param {string} svg SVG to parse
+   * @param {RegExp} [valueMatcher=defaultValueMatcher]
+   *    RegExp to match the value we want to parse from the SVG
+   * @returns {string} Matched value
+   */
+  static valueFromSvgBadge(svg, valueMatcher = defaultValueMatcher) {
+    if (typeof svg !== 'string') {
+      throw new TypeError('Parameter should be a string')
+    }
+    const stripped = svg.replace(leadingWhitespace, '')
+    const match = valueMatcher.exec(stripped)
+    if (match) {
+      return match[1]
+    } else {
+      throw new InvalidResponse({
+        prettyMessage: 'unparseable svg response',
+        underlyingError: Error(`Can't get value from SVG:\n${svg}`),
+      })
+    }
+  }
+
+  /**
+   * Request data from an endpoint serving SVG,
+   * parse a value from it and validate against a schema
+   *
+   * @param {object} attrs Refer to individual attrs
+   * @param {Joi} attrs.schema Joi schema to validate the response against
+   * @param {RegExp} attrs.valueMatcher
+   *    RegExp to match the value we want to parse from the SVG
+   * @param {string} attrs.url URL to request
+   * @param {object} [attrs.options={}] Options to pass to request. See
+   *    [documentation](https://github.com/request/request#requestoptions-callback)
+   * @param {object} [attrs.errorMessages={}] Key-value map of status codes
+   *    and custom error messages e.g: `{ 404: 'package not found' }`.
+   *    This can be used to extend or override the
+   *    [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5)
+   * @returns {object} Parsed response
+   * @see https://github.com/request/request#requestoptions-callback
+   */
+  async _requestSvg({
+    schema,
+    valueMatcher,
+    url,
+    options = {},
+    errorMessages = {},
+  }) {
+    const logTrace = (...args) => trace.logTrace('fetch', ...args)
+    const mergedOptions = {
+      ...{ headers: { Accept: 'image/svg+xml' } },
+      ...options,
+    }
+    const { buffer } = await this._request({
+      url,
+      options: mergedOptions,
+      errorMessages,
+    })
+    logTrace(emojic.dart, 'Response SVG', buffer)
+    const data = {
+      message: this.constructor.valueFromSvgBadge(buffer, valueMatcher),
+    }
+    logTrace(emojic.dart, 'Response SVG (before validation)', data, {
+      deep: true,
+    })
+    return this.constructor._validate(data, schema)
+  }
+}
+
+module.exports = BaseSvgScrapingService
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/core_base-service_base-xml.js.html b/core_base-service_base-xml.js.html new file mode 100644 index 0000000000..2e4581f16a --- /dev/null +++ b/core_base-service_base-xml.js.html @@ -0,0 +1,122 @@ + + + + + JSDoc: Source: core/base-service/base-xml.js + + + + + + + + + + +
+ +

Source: core/base-service/base-xml.js

+ + + + + + +
+
+
/**
+ * @module
+ */
+
+'use strict'
+
+// See available emoji at http://emoji.muan.co/
+const emojic = require('emojic')
+const fastXmlParser = require('fast-xml-parser')
+const BaseService = require('./base')
+const trace = require('./trace')
+const { InvalidResponse } = require('./errors')
+
+/**
+ * Services which query a XML endpoint should extend BaseXmlService
+ *
+ * @abstract
+ */
+class BaseXmlService extends BaseService {
+  /**
+   * Request data from an upstream API serving XML,
+   * parse it and validate against a schema
+   *
+   * @param {object} attrs Refer to individual attrs
+   * @param {Joi} attrs.schema Joi schema to validate the response against
+   * @param {string} attrs.url URL to request
+   * @param {object} [attrs.options={}] Options to pass to request. See
+   *    [documentation](https://github.com/request/request#requestoptions-callback)
+   * @param {object} [attrs.errorMessages={}] Key-value map of status codes
+   *    and custom error messages e.g: `{ 404: 'package not found' }`.
+   *    This can be used to extend or override the
+   *    [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5)
+   * @param {object} [attrs.parserOptions={}] Options to pass to fast-xml-parser. See
+   *    [documentation](https://github.com/NaturalIntelligence/fast-xml-parser#xml-to-json)
+   * @returns {object} Parsed response
+   * @see https://github.com/request/request#requestoptions-callback
+   * @see https://github.com/NaturalIntelligence/fast-xml-parser#xml-to-json
+   */
+  async _requestXml({
+    schema,
+    url,
+    options = {},
+    errorMessages = {},
+    parserOptions = {},
+  }) {
+    const logTrace = (...args) => trace.logTrace('fetch', ...args)
+    const mergedOptions = {
+      ...{ headers: { Accept: 'application/xml, text/xml' } },
+      ...options,
+    }
+    const { buffer } = await this._request({
+      url,
+      options: mergedOptions,
+      errorMessages,
+    })
+    const validateResult = fastXmlParser.validate(buffer)
+    if (validateResult !== true) {
+      throw new InvalidResponse({
+        prettyMessage: 'unparseable xml response',
+        underlyingError: validateResult.err,
+      })
+    }
+    const xml = fastXmlParser.parse(buffer, parserOptions)
+    logTrace(emojic.dart, 'Response XML (before validation)', xml, {
+      deep: true,
+    })
+    return this.constructor._validate(xml, schema)
+  }
+}
+
+module.exports = BaseXmlService
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/core_base-service_base-yaml.js.html b/core_base-service_base-yaml.js.html new file mode 100644 index 0000000000..21dcaa40b4 --- /dev/null +++ b/core_base-service_base-yaml.js.html @@ -0,0 +1,126 @@ + + + + + JSDoc: Source: core/base-service/base-yaml.js + + + + + + + + + + +
+ +

Source: core/base-service/base-yaml.js

+ + + + + + +
+
+
/**
+ * @module
+ */
+
+'use strict'
+
+const emojic = require('emojic')
+const yaml = require('js-yaml')
+const BaseService = require('./base')
+const { InvalidResponse } = require('./errors')
+const trace = require('./trace')
+
+/**
+ * Services which query a YAML endpoint should extend BaseYamlService
+ *
+ * @abstract
+ */
+class BaseYamlService extends BaseService {
+  /**
+   * Request data from an upstream API serving YAML,
+   * parse it and validate against a schema
+   *
+   * @param {object} attrs Refer to individual attrs
+   * @param {Joi} attrs.schema Joi schema to validate the response against
+   * @param {string} attrs.url URL to request
+   * @param {object} [attrs.options={}] Options to pass to request. See
+   *    [documentation](https://github.com/request/request#requestoptions-callback)
+   * @param {object} [attrs.errorMessages={}] Key-value map of status codes
+   *    and custom error messages e.g: `{ 404: 'package not found' }`.
+   *    This can be used to extend or override the
+   *    [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5)
+   * @param {object} [attrs.encoding='utf8'] Character encoding
+   * @returns {object} Parsed response
+   * @see https://github.com/request/request#requestoptions-callback
+   */
+  async _requestYaml({
+    schema,
+    url,
+    options = {},
+    errorMessages = {},
+    encoding = 'utf8',
+  }) {
+    const logTrace = (...args) => trace.logTrace('fetch', ...args)
+    const mergedOptions = {
+      ...{
+        headers: {
+          Accept:
+            'text/x-yaml, text/yaml, application/x-yaml, application/yaml, text/plain',
+        },
+      },
+      ...options,
+    }
+    const { buffer } = await this._request({
+      url,
+      options: mergedOptions,
+      errorMessages,
+    })
+    let parsed
+    try {
+      parsed = yaml.safeLoad(buffer.toString(), encoding)
+    } catch (err) {
+      logTrace(emojic.dart, 'Response YAML (unparseable)', buffer)
+      throw new InvalidResponse({
+        prettyMessage: 'unparseable yaml response',
+        underlyingError: err,
+      })
+    }
+    logTrace(emojic.dart, 'Response YAML (before validation)', parsed, {
+      deep: true,
+    })
+    return this.constructor._validate(parsed, schema)
+  }
+}
+
+module.exports = BaseYamlService
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/core_base-service_base.js.html b/core_base-service_base.js.html new file mode 100644 index 0000000000..92d93e0861 --- /dev/null +++ b/core_base-service_base.js.html @@ -0,0 +1,606 @@ + + + + + JSDoc: Source: core/base-service/base.js + + + + + + + + + + +
+ +

Source: core/base-service/base.js

+ + + + + + +
+
+
'use strict'
+/**
+ * @module
+ */
+
+// See available emoji at http://emoji.muan.co/
+const emojic = require('emojic')
+const Joi = require('joi')
+const log = require('../server/log')
+const { AuthHelper } = require('./auth-helper')
+const { MetricHelper, MetricNames } = require('./metric-helper')
+const { assertValidCategory } = require('./categories')
+const checkErrorResponse = require('./check-error-response')
+const coalesceBadge = require('./coalesce-badge')
+const {
+  NotFound,
+  InvalidResponse,
+  Inaccessible,
+  ImproperlyConfigured,
+  InvalidParameter,
+  Deprecated,
+} = require('./errors')
+const { validateExample, transformExample } = require('./examples')
+const {
+  makeFullUrl,
+  assertValidRoute,
+  prepareRoute,
+  namedParamsForMatch,
+  getQueryParamNames,
+} = require('./route')
+const { assertValidServiceDefinition } = require('./service-definitions')
+const trace = require('./trace')
+const validate = require('./validate')
+
+const defaultBadgeDataSchema = Joi.object({
+  label: Joi.string(),
+  color: Joi.string(),
+  labelColor: Joi.string(),
+  namedLogo: Joi.string(),
+}).required()
+
+const optionalStringWhenNamedLogoPresent = Joi.alternatives().conditional(
+  'namedLogo',
+  {
+    is: Joi.string().required(),
+    then: Joi.string(),
+  }
+)
+
+const optionalNumberWhenAnyLogoPresent = Joi.alternatives()
+  .conditional('namedLogo', { is: Joi.string().required(), then: Joi.number() })
+  .conditional('logoSvg', { is: Joi.string().required(), then: Joi.number() })
+
+const serviceDataSchema = Joi.object({
+  isError: Joi.boolean(),
+  label: Joi.string().allow(''),
+  // While a number of badges pass a number here, in the long run we may want
+  // `render()` to always return a string.
+  message: Joi.alternatives(Joi.string().allow(''), Joi.number()).required(),
+  color: Joi.string(),
+  link: Joi.array().items(Joi.string().uri()).single().max(2),
+  // Generally services should not use these options, which are provided to
+  // support the Endpoint badge.
+  labelColor: Joi.string(),
+  namedLogo: Joi.string(),
+  logoSvg: Joi.string(),
+  logoColor: optionalStringWhenNamedLogoPresent,
+  logoWidth: optionalNumberWhenAnyLogoPresent,
+  logoPosition: optionalNumberWhenAnyLogoPresent,
+  cacheSeconds: Joi.number().integer().min(0),
+  style: Joi.string(),
+})
+  .oxor('namedLogo', 'logoSvg')
+  .required()
+
+/**
+ * Abstract base class which all service classes inherit from.
+ * Concrete implementations of BaseService must implement the methods
+ * category(), route() and handle(namedParams, queryParams)
+ */
+class BaseService {
+  /**
+   * Name of the category to sort this badge into (eg. "build"). Used to sort
+   * the badges on the main shields.io website.
+   *
+   * @abstract
+   * @type {string}
+   */
+  static get category() {
+    throw new Error(`Category not set for ${this.name}`)
+  }
+
+  static isDeprecated = false
+
+  /**
+   * Route to mount this service on
+   *
+   * @abstract
+   * @type {module:core/base-service/base~Route}
+   */
+  static get route() {
+    throw new Error(`Route not defined for ${this.name}`)
+  }
+
+  /**
+   * Configuration for the authentication helper that prepares credentials
+   * for upstream requests.
+   *
+   * See also the config schema in `./server.js` and `doc/server-secrets.md`.
+   *
+   * To use the configured auth in the handler or fetch method, pass the
+   * credentials to the request. For example:
+   * - `{ options: { auth: this.authHelper.basicAuth } }`
+   * - `{ options: { headers: this.authHelper.bearerAuthHeader } }`
+   * - `{ options: { qs: { token: this.authHelper._pass } } }`
+   *
+   * @abstract
+   * @type {module:core/base-service/base~Auth}
+   */
+  static auth = undefined
+
+  /**
+   * Array of Example objects describing 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
+   * substituted into the service's compiled route pattern. The rendered badge
+   * is specified with `staticPreview`.
+   *
+   * For services which use a route `format`, the `pattern` can be specified as
+   * part of the example.
+   *
+   * @see {@link module:core/base-service/base~Example}
+   * @abstract
+   * @type {module:core/base-service/base~Example[]}
+   */
+  static examples = []
+
+  static get _cacheLength() {
+    const cacheLengths = {
+      build: 30,
+      license: 3600,
+      version: 300,
+      debug: 60,
+    }
+    return cacheLengths[this.category]
+  }
+
+  /**
+   * Default data for the badge.
+   * These defaults are used if the value is neither included in the service data
+   * from the handler nor overridden by the user via query parameters.
+   *
+   * @type {module:core/base-service/base~DefaultBadgeData}
+   */
+  static defaultBadgeData = {}
+
+  static render(props) {
+    throw new Error(`render() function not implemented for ${this.name}`)
+  }
+
+  static validateDefinition() {
+    assertValidCategory(this.category, `Category for ${this.name}`)
+
+    assertValidRoute(this.route, `Route for ${this.name}`)
+
+    Joi.assert(
+      this.defaultBadgeData,
+      defaultBadgeDataSchema,
+      `Default badge data for ${this.name}`
+    )
+
+    this.examples.forEach((example, index) =>
+      validateExample(example, index, this)
+    )
+  }
+
+  static getDefinition() {
+    const { category, name, isDeprecated } = this
+    const { base, format, pattern } = this.route
+    const queryParams = getQueryParamNames(this.route)
+
+    const examples = this.examples.map((example, index) =>
+      transformExample(example, index, this)
+    )
+
+    let route
+    if (pattern) {
+      route = { pattern: makeFullUrl(base, pattern), queryParams }
+    } else if (format) {
+      route = { format, queryParams }
+    } else {
+      route = undefined
+    }
+
+    const result = { category, name, isDeprecated, route, examples }
+
+    assertValidServiceDefinition(result, `getDefinition() for ${this.name}`)
+
+    return result
+  }
+
+  constructor(
+    { sendAndCacheRequest, authHelper, metricHelper },
+    { handleInternalErrors }
+  ) {
+    this._requestFetcher = sendAndCacheRequest
+    this.authHelper = authHelper
+    this._handleInternalErrors = handleInternalErrors
+    this._metricHelper = metricHelper
+  }
+
+  async _request({ url, options = {}, errorMessages = {} }) {
+    const logTrace = (...args) => trace.logTrace('fetch', ...args)
+    logTrace(emojic.bowAndArrow, 'Request', url, '\n', options)
+    const { res, buffer } = await this._requestFetcher(url, options)
+    await this._meterResponse(res, buffer)
+    logTrace(emojic.dart, 'Response status code', res.statusCode)
+    return checkErrorResponse(errorMessages)({ buffer, res })
+  }
+
+  static enabledMetrics = []
+
+  static isMetricEnabled(metricName) {
+    return this.enabledMetrics.includes(metricName)
+  }
+
+  async _meterResponse(res, buffer) {
+    if (
+      this._metricHelper &&
+      this.constructor.isMetricEnabled(MetricNames.SERVICE_RESPONSE_SIZE) &&
+      res.statusCode === 200
+    ) {
+      this._metricHelper.noteServiceResponseSize(buffer.length)
+    }
+  }
+
+  static _validate(
+    data,
+    schema,
+    {
+      prettyErrorMessage = 'invalid response data',
+      includeKeys = false,
+      allowAndStripUnknownKeys = true,
+    } = {}
+  ) {
+    return validate(
+      {
+        ErrorClass: InvalidResponse,
+        prettyErrorMessage,
+        includeKeys,
+        traceErrorMessage: 'Response did not match schema',
+        traceSuccessMessage: 'Response after validation',
+        allowAndStripUnknownKeys,
+      },
+      data,
+      schema
+    )
+  }
+
+  /**
+   * Asynchronous function to handle requests for this service. Take the route
+   * parameters (as defined in the `route` property), perform a request using
+   * `this._sendAndCacheRequest`, and return the badge data.
+   *
+   * @abstract
+   * @param {object} namedParams Params parsed from route pattern
+   *    defined in this.route.pattern or this.route.capture
+   * @param {object} queryParams Params parsed from the query string
+   * @returns {module:core/base-service/base~Badge}
+   *    badge Object validated against serviceDataSchema
+   */
+  async handle(namedParams, queryParams) {
+    throw new Error(`Handler not implemented for ${this.constructor.name}`)
+  }
+
+  // Making this an instance method ensures debuggability.
+  // https://github.com/badges/shields/issues/3784
+  _validateServiceData(serviceData) {
+    Joi.assert(serviceData, serviceDataSchema)
+  }
+
+  _handleError(error) {
+    if (error instanceof NotFound || error instanceof InvalidParameter) {
+      trace.logTrace('outbound', emojic.noGoodWoman, 'Handled error', error)
+      return {
+        isError: true,
+        message: error.prettyMessage,
+        color: 'red',
+      }
+    } else if (
+      error instanceof ImproperlyConfigured ||
+      error instanceof InvalidResponse ||
+      error instanceof Inaccessible ||
+      error instanceof Deprecated
+    ) {
+      trace.logTrace('outbound', emojic.noGoodWoman, 'Handled error', error)
+      return {
+        isError: true,
+        message: error.prettyMessage,
+        color: 'lightgray',
+      }
+    } else if (this._handleInternalErrors) {
+      if (
+        !trace.logTrace(
+          'unhandledError',
+          emojic.boom,
+          'Unhandled internal error',
+          error
+        )
+      ) {
+        // This is where we end up if an unhandled exception is thrown in
+        // production. Send the error to Sentry and the logs.
+        log.error(error)
+      }
+      return {
+        isError: true,
+        label: 'shields',
+        message: 'internal error',
+        color: 'lightgray',
+      }
+    } else {
+      trace.logTrace(
+        'unhandledError',
+        emojic.boom,
+        'Unhandled internal error',
+        error
+      )
+      throw error
+    }
+  }
+
+  static async invoke(
+    context = {},
+    config = {},
+    namedParams = {},
+    queryParams = {}
+  ) {
+    trace.logTrace('inbound', emojic.womanCook, 'Service class', this.name)
+    trace.logTrace('inbound', emojic.ticket, 'Named params', namedParams)
+    trace.logTrace('inbound', emojic.crayon, 'Query params', queryParams)
+
+    // Like the service instance, the auth helper could be reused for each request.
+    // However, moving its instantiation to `register()` makes `invoke()` harder
+    // to test.
+    const authHelper = this.auth ? new AuthHelper(this.auth, config) : undefined
+
+    const serviceInstance = new this({ ...context, authHelper }, config)
+
+    let serviceError
+    if (authHelper && !authHelper.isValid) {
+      const prettyMessage = authHelper.isRequired
+        ? 'credentials have not been configured'
+        : 'credentials are misconfigured'
+      serviceError = new ImproperlyConfigured({ prettyMessage })
+    }
+
+    const { queryParamSchema } = this.route
+    let transformedQueryParams
+    if (!serviceError && queryParamSchema) {
+      try {
+        transformedQueryParams = validate(
+          {
+            ErrorClass: InvalidParameter,
+            prettyErrorMessage: 'invalid query parameter',
+            includeKeys: true,
+            traceErrorMessage: 'Query params did not match schema',
+            traceSuccessMessage: 'Query params after validation',
+          },
+          queryParams,
+          queryParamSchema
+        )
+        trace.logTrace(
+          'inbound',
+          emojic.crayon,
+          'Query params after validation',
+          queryParams
+        )
+      } catch (error) {
+        serviceError = error
+      }
+    } else {
+      transformedQueryParams = {}
+    }
+
+    let serviceData
+    if (!serviceError) {
+      try {
+        serviceData = await serviceInstance.handle(
+          namedParams,
+          transformedQueryParams
+        )
+        serviceInstance._validateServiceData(serviceData)
+      } catch (error) {
+        serviceError = error
+      }
+    }
+
+    if (serviceError) {
+      serviceData = serviceInstance._handleError(serviceError)
+    }
+
+    trace.logTrace('outbound', emojic.shield, 'Service data', serviceData)
+
+    return serviceData
+  }
+
+  static register(
+    { camp, handleRequest, githubApiProvider, metricInstance },
+    serviceConfig
+  ) {
+    const { cacheHeaders: cacheHeaderConfig, fetchLimitBytes } = serviceConfig
+    const { regex, captureNames } = prepareRoute(this.route)
+    const queryParams = getQueryParamNames(this.route)
+
+    const metricHelper = MetricHelper.create({
+      metricInstance,
+      ServiceClass: this,
+    })
+
+    camp.route(
+      regex,
+      handleRequest(cacheHeaderConfig, {
+        queryParams,
+        handler: async (queryParams, match, sendBadge, request) => {
+          const metricHandle = metricHelper.startRequest()
+
+          const namedParams = namedParamsForMatch(captureNames, match, this)
+          const serviceData = await this.invoke(
+            {
+              sendAndCacheRequest: request.asPromise,
+              sendAndCacheRequestWithCallbacks: request,
+              githubApiProvider,
+              metricHelper,
+            },
+            serviceConfig,
+            namedParams,
+            queryParams
+          )
+
+          const badgeData = coalesceBadge(
+            queryParams,
+            serviceData,
+            this.defaultBadgeData,
+            this
+          )
+          // The final capture group is the extension.
+          const format = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
+          sendBadge(format, badgeData)
+
+          metricHandle.noteResponseSent()
+        },
+        cacheLength: this._cacheLength,
+        fetchLimitBytes,
+      })
+    )
+  }
+}
+
+/**
+ * Default badge properties, validated against defaultBadgeDataSchema
+ *
+ * @typedef {object} DefaultBadgeData
+ * @property {string} label (Optional)
+ * @property {string} color (Optional)
+ * @property {string} labelColor (Optional)
+ * @property {string} namedLogo (Optional)
+ */
+
+/**
+ * Badge Object, validated against serviceDataSchema
+ *
+ * @typedef {object} Badge
+ * @property {boolean} isError (Optional)
+ * @property {string} label (Optional)
+ * @property {(string|number)} message
+ * @property {string} color (Optional)
+ * @property {string[]} link (Optional)
+ */
+
+/**
+ * @typedef {object} Route
+ * @property {string} base
+ *    (Optional) The base path of the routes for this service.
+ *    This is used as a prefix.
+ * @property {string} pattern
+ *    A path-to-regexp pattern defining the route pattern and param names
+ *    See {@link https://www.npmjs.com/package/path-to-regexp}
+ * @property {RegExp} format
+ *    Deprecated: Regular expression to use for routes for this service's badges
+ *    Use `pattern` instead
+ * @property {string[]} capture
+ *    Deprecated: Array of names for the capture groups in the regular
+ *    expression. The handler will be passed an object containing
+ *    the matches.
+ *    Use `pattern` instead
+ * @property {Joi.object} queryParamSchema
+ *    (Optional) A Joi schema (`Joi.object({ ... }).required()`)
+ *    for the query param object. If you know a parameter
+ *    will never receive a numeric string, you can use
+ *    `Joi.string()`. Because of quirks in Scoutcamp and Joi,
+ *    alphanumeric strings should be declared using
+ *    `Joi.alternatives().try(Joi.string(), Joi.number())`,
+ *    otherwise a value like `?success_color=999` will fail.
+ *    A parameter requiring a numeric string can use
+ *    `Joi.number()`. A parameter that receives only non-numeric
+ *    strings can use `Joi.string()`. A parameter that never
+ *    receives numeric can use `Joi.string()`. A boolean
+ *    parameter should use `Joi.equal('')` and will receive an
+ *    empty string on e.g. `?compact_message` and undefined
+ *    when the parameter is absent. (Note that in,
+ *    `examples.queryParams` boolean query params should be given
+ *    `null` values.)
+ */
+
+/**
+ * @typedef {object} Auth
+ * @property {string} userKey
+ *    (Optional) The key from `privateConfig` to use as the username.
+ * @property {string} passKey
+ *    (Optional) The key from `privateConfig` to use as the password.
+ *    If auth is configured, either `userKey` or `passKey` is required.
+ * @property {string} isRequired
+ *    (Optional) If `true`, the service will return `NotFound` unless the
+ *    configured credentials are present.
+ */
+
+/**
+ * @typedef {object} Example
+ * @property {string} 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.
+ * @property {object} namedParams
+ *    An object containing the values of named parameters to
+ *    substitute into the compiled route pattern.
+ * @property {object} queryParams
+ *    An object containing query parameters to include in the
+ *    example URLs. For alphanumeric query parameters, specify a string value.
+ *    For boolean query parameters, specify `null`.
+ * @property {string} pattern
+ *    The route pattern to compile. Defaults to `this.route.pattern`.
+ * @property {object} staticPreview
+ *    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.
+ * @property {string[]} keywords
+ *    Additional keywords, other than words in the title. This helps
+ *    users locate relevant badges.
+ * @property {string} documentation
+ *    An HTML string that is included in the badge popup.
+ */
+
+module.exports = BaseService
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/core_base-service_errors.js.html b/core_base-service_errors.js.html new file mode 100644 index 0000000000..f4859285fe --- /dev/null +++ b/core_base-service_errors.js.html @@ -0,0 +1,272 @@ + + + + + JSDoc: Source: core/base-service/errors.js + + + + + + + + + + +
+ +

Source: core/base-service/errors.js

+ + + + + + +
+
+
/**
+ * Standard exceptions for handling error cases
+ *
+ * @module
+ */
+
+'use strict'
+
+/**
+ * Base error class
+ *
+ * @abstract
+ */
+class ShieldsRuntimeError extends Error {
+  /**
+   * Name of the class. Implementations of ShieldsRuntimeError
+   * should override this method.
+   *
+   * @type {string}
+   */
+  get name() {
+    return 'ShieldsRuntimeError'
+  }
+
+  /**
+   * Default message for this exception if none is specified.
+   * Implementations of ShieldsRuntimeError should implement this method.
+   *
+   * @abstract
+   * @type {string}
+   */
+  get defaultPrettyMessage() {
+    throw new Error('Must implement abstract method')
+  }
+
+  /**
+   * @param {module:core/base-service/errors~RuntimeErrorProps} props
+   * Refer to individual attrs
+   * @param {string} message Exception message for debug purposes
+   */
+  constructor(props = {}, message) {
+    super(message)
+    this.prettyMessage = props.prettyMessage || this.defaultPrettyMessage
+    if (props.underlyingError) {
+      this.stack = props.underlyingError.stack
+    }
+  }
+}
+
+const defaultNotFoundError = 'not found'
+
+/**
+ * Throw this to wrap a 404 or other 'not found' response from an upstream API
+ */
+class NotFound extends ShieldsRuntimeError {
+  get name() {
+    return 'NotFound'
+  }
+
+  get defaultPrettyMessage() {
+    return defaultNotFoundError
+  }
+
+  /**
+   * @param {module:core/base-service/errors~RuntimeErrorProps} props
+   * Refer to individual attrs
+   */
+  constructor(props = {}) {
+    const prettyMessage = props.prettyMessage || defaultNotFoundError
+    const message =
+      prettyMessage === defaultNotFoundError
+        ? 'Not Found'
+        : `Not Found: ${prettyMessage}`
+    super(props, message)
+    this.response = props.response
+  }
+}
+
+/**
+ * Throw this to wrap an invalid or unexpected response from an upstream API
+ */
+class InvalidResponse extends ShieldsRuntimeError {
+  get name() {
+    return 'InvalidResponse'
+  }
+
+  get defaultPrettyMessage() {
+    return 'invalid'
+  }
+
+  /**
+   * @param {module:core/base-service/errors~RuntimeErrorProps} props
+   * Refer to individual attrs
+   */
+  constructor(props = {}) {
+    const message = props.underlyingError
+      ? `Invalid Response: ${props.underlyingError.message}`
+      : 'Invalid Response'
+    super(props, message)
+    this.response = props.response
+  }
+}
+
+/**
+ * Throw this if we can't contact an upstream API
+ * or to wrap a 5XX response
+ */
+class Inaccessible extends ShieldsRuntimeError {
+  get name() {
+    return 'Inaccessible'
+  }
+
+  get defaultPrettyMessage() {
+    return 'inaccessible'
+  }
+
+  /**
+   * @param {module:core/base-service/errors~RuntimeErrorProps} props
+   * Refer to individual attrs
+   */
+  constructor(props = {}) {
+    const message = props.underlyingError
+      ? `Inaccessible: ${props.underlyingError.message}`
+      : 'Inaccessible'
+    super(props, message)
+    this.response = props.response
+  }
+}
+
+/**
+ * Throw this error when required credentials are missing
+ */
+class ImproperlyConfigured extends ShieldsRuntimeError {
+  get name() {
+    return 'ImproperlyConfigured'
+  }
+
+  get defaultPrettyMessage() {
+    return 'improperly configured'
+  }
+
+  /**
+   * @param {module:core/base-service/errors~RuntimeErrorProps} props
+   * Refer to individual attrs
+   */
+  constructor(props = {}) {
+    const message = props.underlyingError
+      ? `ImproperlyConfigured: ${props.underlyingError.message}`
+      : 'ImproperlyConfigured'
+    super(props, message)
+    this.response = props.response
+  }
+}
+
+/**
+ * Throw this error when a user supplied input or parameter
+ * is invalid or unexpected
+ */
+class InvalidParameter extends ShieldsRuntimeError {
+  get name() {
+    return 'InvalidParameter'
+  }
+
+  get defaultPrettyMessage() {
+    return 'invalid parameter'
+  }
+
+  /**
+   * @param {module:core/base-service/errors~RuntimeErrorProps} props
+   * Refer to individual attrs
+   */
+  constructor(props = {}) {
+    const message = props.underlyingError
+      ? `Invalid Parameter: ${props.underlyingError.message}`
+      : 'Invalid Parameter'
+    super(props, message)
+    this.response = props.response
+  }
+}
+
+/**
+ * Throw this error to indicate that a service is deprecated or removed
+ */
+class Deprecated extends ShieldsRuntimeError {
+  get name() {
+    return 'Deprecated'
+  }
+
+  get defaultPrettyMessage() {
+    return 'no longer available'
+  }
+
+  /**
+   * @param {module:core/base-service/errors~RuntimeErrorProps} props
+   * Refer to individual attrs
+   */
+  constructor(props) {
+    const message = 'Deprecated'
+    super(props, message)
+  }
+}
+
+/**
+ * @typedef {object} RuntimeErrorProps
+ * @property {Error} underlyingError Exception we are wrapping (Optional)
+ * @property {object} response Response from an upstream API to provide
+ * context for the error (Optional)
+ * @property {string} prettyMessage User-facing error message to override the
+ * value of `defaultPrettyMessage()`. This is the text that will appear on the
+ * badge when we catch and render the exception (Optional)
+ */
+
+module.exports = {
+  ShieldsRuntimeError,
+  NotFound,
+  ImproperlyConfigured,
+  InvalidResponse,
+  Inaccessible,
+  InvalidParameter,
+  Deprecated,
+}
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/core_base-service_graphql.js.html b/core_base-service_graphql.js.html new file mode 100644 index 0000000000..9bd9bba258 --- /dev/null +++ b/core_base-service_graphql.js.html @@ -0,0 +1,103 @@ + + + + + JSDoc: Source: core/base-service/graphql.js + + + + + + + + + + +
+ +

Source: core/base-service/graphql.js

+ + + + + + +
+
+
'use strict'
+/**
+ * @module
+ */
+
+/**
+ * Utility function to merge two graphql queries together
+ * This is basically copied from
+ * [graphql-query-merge](https://www.npmjs.com/package/graphql-query-merge)
+ * but can't use that due to incorrect packaging.
+ *
+ * @param {...object} queries queries to merge
+ * @returns {object} merged query
+ */
+function mergeQueries(...queries) {
+  const merged = {
+    kind: 'Document',
+    definitions: [
+      {
+        directives: [],
+        operation: 'query',
+        variableDefinitions: [],
+        kind: 'OperationDefinition',
+        selectionSet: { kind: 'SelectionSet', selections: [] },
+      },
+    ],
+  }
+
+  queries.forEach(query => {
+    const parsedQuery = query
+    parsedQuery.definitions.forEach(definition => {
+      merged.definitions[0].directives = [
+        ...merged.definitions[0].directives,
+        ...definition.directives,
+      ]
+
+      merged.definitions[0].variableDefinitions = [
+        ...merged.definitions[0].variableDefinitions,
+        ...definition.variableDefinitions,
+      ]
+
+      merged.definitions[0].selectionSet.selections = [
+        ...merged.definitions[0].selectionSet.selections,
+        ...definition.selectionSet.selections,
+      ]
+    })
+  })
+
+  return merged
+}
+
+module.exports = { mergeQueries }
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/core_server_prometheus-metrics.js.html b/core_server_prometheus-metrics.js.html new file mode 100644 index 0000000000..6040f97b82 --- /dev/null +++ b/core_server_prometheus-metrics.js.html @@ -0,0 +1,176 @@ + + + + + JSDoc: Source: core/server/prometheus-metrics.js + + + + + + + + + + +
+ +

Source: core/server/prometheus-metrics.js

+ + + + + + +
+
+
'use strict'
+
+const decamelize = require('decamelize')
+const prometheus = require('prom-client')
+
+module.exports = class PrometheusMetrics {
+  constructor({ register } = {}) {
+    this.register = register || new prometheus.Registry()
+    this.counters = {
+      numRequests: new prometheus.Counter({
+        name: 'service_requests_total',
+        help: 'Total service requests',
+        labelNames: ['category', 'family', 'service'],
+        registers: [this.register],
+      }),
+      responseTime: new prometheus.Histogram({
+        name: 'service_response_millis',
+        help: 'Service response time in milliseconds',
+        // 250 ms increments up to 2 seconds, then 500 ms increments up to 8
+        // seconds, then 1 second increments up to 15 seconds.
+        buckets: [
+          250,
+          500,
+          750,
+          1000,
+          1250,
+          1500,
+          1750,
+          2000,
+          2250,
+          2500,
+          2750,
+          3000,
+          3250,
+          3500,
+          3750,
+          4000,
+          4500,
+          5000,
+          5500,
+          6000,
+          6500,
+          7000,
+          7500,
+          8000,
+          9000,
+          10000,
+          11000,
+          12000,
+          13000,
+          14000,
+          15000,
+        ],
+        registers: [this.register],
+      }),
+      rateLimitExceeded: new prometheus.Counter({
+        name: 'rate_limit_exceeded_total',
+        help: 'Count of rate limit exceeded by type',
+        labelNames: ['rate_limit_type'],
+        registers: [this.register],
+      }),
+      serviceResponseSize: new prometheus.Histogram({
+        name: 'service_response_bytes',
+        help: 'Service response size in bytes',
+        labelNames: ['category', 'family', 'service'],
+        // buckets: 64KiB, 128KiB, 256KiB, 512KiB, 1MiB, 2MiB, 4MiB, 8MiB
+        buckets: prometheus.exponentialBuckets(64 * 1024, 2, 8),
+        registers: [this.register],
+      }),
+    }
+    this.interval = prometheus.collectDefaultMetrics({
+      register: this.register,
+    })
+  }
+
+  async registerMetricsEndpoint(server) {
+    const { register } = this
+
+    server.route(/^\/metrics$/, (data, match, end, ask) => {
+      ask.res.setHeader('Content-Type', register.contentType)
+      ask.res.end(register.metrics())
+    })
+  }
+
+  stop() {
+    this.register.clear()
+    if (this.interval) {
+      clearInterval(this.interval)
+      this.interval = undefined
+    }
+  }
+
+  metrics() {
+    return this.register.getMetricsAsJSON()
+  }
+
+  /**
+   * @param {object} attrs Refer to individual attrs
+   * @param {string} attrs.category e.g: 'build'
+   * @param {string} attrs.serviceFamily e.g: 'npm'
+   * @param {string} attrs.name e.g: 'NpmVersion'
+   * @returns {object} `{ inc() {} }`.
+   */
+  createNumRequestCounter({ category, serviceFamily, name }) {
+    const service = decamelize(name)
+    return this.counters.numRequests.labels(category, serviceFamily, service)
+  }
+
+  noteResponseTime(responseTime) {
+    return this.counters.responseTime.observe(responseTime)
+  }
+
+  noteRateLimitExceeded(rateLimitType) {
+    return this.counters.rateLimitExceeded.labels(rateLimitType).inc()
+  }
+
+  createServiceResponseSizeHistogram({ category, serviceFamily, name }) {
+    const service = decamelize(name)
+    return this.counters.serviceResponseSize.labels(
+      category,
+      serviceFamily,
+      service
+    )
+  }
+}
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/core_server_server.js.html b/core_server_server.js.html new file mode 100644 index 0000000000..489ed86a47 --- /dev/null +++ b/core_server_server.js.html @@ -0,0 +1,575 @@ + + + + + JSDoc: Source: core/server/server.js + + + + + + + + + + +
+ +

Source: core/server/server.js

+ + + + + + +
+
+
'use strict'
+/**
+ * @module
+ */
+
+const path = require('path')
+const url = require('url')
+const { URL } = url
+const cloudflareMiddleware = require('cloudflare-middleware')
+const bytes = require('bytes')
+const Camp = require('@shields_io/camp')
+const originalJoi = require('joi')
+const makeBadge = require('../../badge-maker/lib/make-badge')
+const GithubConstellation = require('../../services/github/github-constellation')
+const suggest = require('../../services/suggest')
+const { loadServiceClasses } = require('../base-service/loader')
+const { makeSend } = require('../base-service/legacy-result-sender')
+const {
+  handleRequest,
+  clearRequestCache,
+} = require('../base-service/legacy-request-handler')
+const { clearRegularUpdateCache } = require('../legacy/regular-update')
+const { rasterRedirectUrl } = require('../badge-urls/make-badge-url')
+const log = require('./log')
+const sysMonitor = require('./monitor')
+const PrometheusMetrics = require('./prometheus-metrics')
+const InfluxMetrics = require('./influx-metrics')
+
+const Joi = originalJoi
+  .extend(base => ({
+    type: 'arrayFromString',
+    base: base.array(),
+    coerce: (value, state, options) => ({
+      value: typeof value === 'string' ? value.split(' ') : value,
+    }),
+  }))
+  .extend(base => ({
+    type: 'string',
+    base: base.string(),
+    messages: {
+      'string.origin':
+        'needs to be an origin string, e.g. https://host.domain with optional port and no trailing slash',
+    },
+    rules: {
+      origin: {
+        validate(value, helpers) {
+          let origin
+          try {
+            ;({ origin } = new URL(value))
+          } catch (e) {}
+          if (origin !== undefined && origin === value) {
+            return value
+          } else {
+            return helpers.error('string.origin')
+          }
+        },
+      },
+    },
+  }))
+
+const optionalUrl = Joi.string().uri({ scheme: ['http', 'https'] })
+const requiredUrl = optionalUrl.required()
+const origins = Joi.arrayFromString().items(Joi.string().origin())
+const defaultService = Joi.object({ authorizedOrigins: origins }).default({
+  authorizedOrigins: [],
+})
+
+const publicConfigSchema = Joi.object({
+  bind: {
+    port: Joi.alternatives().try(
+      Joi.number().port(),
+      Joi.string().pattern(/^\\\\\.\\pipe\\.+$/)
+    ),
+    address: Joi.alternatives().try(
+      Joi.string().ip().required(),
+      Joi.string().hostname().required()
+    ),
+  },
+  metrics: {
+    prometheus: {
+      enabled: Joi.boolean().required(),
+      endpointEnabled: Joi.boolean().required(),
+    },
+    influx: {
+      enabled: Joi.boolean().required(),
+      url: Joi.string()
+        .uri()
+        .when('enabled', { is: true, then: Joi.required() }),
+      timeoutMilliseconds: Joi.number()
+        .integer()
+        .min(1)
+        .when('enabled', { is: true, then: Joi.required() }),
+      intervalSeconds: Joi.number()
+        .integer()
+        .min(1)
+        .when('enabled', { is: true, then: Joi.required() }),
+      instanceIdFrom: Joi.string()
+        .equal('hostname', 'env-var', 'random')
+        .when('enabled', { is: true, then: Joi.required() }),
+      instanceIdEnvVarName: Joi.string().when('instanceIdFrom', {
+        is: 'env-var',
+        then: Joi.required(),
+      }),
+      envLabel: Joi.string().when('enabled', {
+        is: true,
+        then: Joi.required(),
+      }),
+      hostnameAliases: Joi.object(),
+    },
+  },
+  ssl: {
+    isSecure: Joi.boolean().required(),
+    key: Joi.string(),
+    cert: Joi.string(),
+  },
+  redirectUrl: optionalUrl,
+  rasterUrl: optionalUrl,
+  cors: {
+    allowedOrigin: Joi.array().items(optionalUrl).required(),
+  },
+  services: Joi.object({
+    bitbucketServer: defaultService,
+    drone: defaultService,
+    github: {
+      baseUri: requiredUrl,
+      debug: {
+        enabled: Joi.boolean().required(),
+        intervalSeconds: Joi.number().integer().min(1).required(),
+      },
+    },
+    jira: defaultService,
+    jenkins: Joi.object({
+      authorizedOrigins: origins,
+      requireStrictSsl: Joi.boolean(),
+      requireStrictSslToAuthenticate: Joi.boolean(),
+    }).default({ authorizedOrigins: [] }),
+    nexus: defaultService,
+    npm: defaultService,
+    sonar: defaultService,
+    teamcity: defaultService,
+    trace: Joi.boolean().required(),
+  }).required(),
+  cacheHeaders: {
+    defaultCacheLengthSeconds: Joi.number().integer().required(),
+  },
+  rateLimit: Joi.boolean().required(),
+  handleInternalErrors: Joi.boolean().required(),
+  fetchLimit: Joi.string().regex(/^[0-9]+(b|kb|mb|gb|tb)$/i),
+  documentRoot: Joi.string().default(
+    path.resolve(__dirname, '..', '..', 'public')
+  ),
+  requireCloudflare: Joi.boolean().required(),
+}).required()
+
+const privateConfigSchema = Joi.object({
+  azure_devops_token: Joi.string(),
+  bintray_user: Joi.string(),
+  bintray_apikey: Joi.string(),
+  discord_bot_token: Joi.string(),
+  drone_token: Joi.string(),
+  gh_client_id: Joi.string(),
+  gh_client_secret: Joi.string(),
+  gh_token: Joi.string(),
+  jenkins_user: Joi.string(),
+  jenkins_pass: Joi.string(),
+  jira_user: Joi.string(),
+  jira_pass: Joi.string(),
+  nexus_user: Joi.string(),
+  nexus_pass: Joi.string(),
+  npm_token: Joi.string(),
+  redis_url: Joi.string().uri({ scheme: ['redis', 'rediss'] }),
+  sentry_dsn: Joi.string(),
+  shields_secret: Joi.string(),
+  sl_insight_userUuid: Joi.string(),
+  sl_insight_apiToken: Joi.string(),
+  sonarqube_token: Joi.string(),
+  teamcity_user: Joi.string(),
+  teamcity_pass: Joi.string(),
+  twitch_client_id: Joi.string(),
+  twitch_client_secret: Joi.string(),
+  wheelmap_token: Joi.string(),
+  influx_username: Joi.string(),
+  influx_password: Joi.string(),
+  youtube_api_key: Joi.string(),
+}).required()
+const privateMetricsInfluxConfigSchema = privateConfigSchema.append({
+  influx_username: Joi.string().required(),
+  influx_password: Joi.string().required(),
+})
+
+function addHandlerAtIndex(camp, index, handlerFn) {
+  camp.stack.splice(index, 0, handlerFn)
+}
+
+/**
+ * The Server is based on the web framework Scoutcamp. It creates
+ * an http server, sets up helpers for token persistence and monitoring.
+ * Then it loads all the services, injecting dependencies as it
+ * asks each one to register its route with Scoutcamp.
+ */
+class Server {
+  /**
+   * Badge Server Constructor
+   *
+   * @param {object} config Configuration object read from config yaml files
+   * by https://www.npmjs.com/package/config and validated against
+   * publicConfigSchema and privateConfigSchema
+   * @see https://github.com/badges/shields/blob/master/doc/production-hosting.md#configuration
+   * @see https://github.com/badges/shields/blob/master/doc/server-secrets.md
+   */
+  constructor(config) {
+    const publicConfig = Joi.attempt(config.public, publicConfigSchema)
+    const privateConfig = this.validatePrivateConfig(
+      config.private,
+      privateConfigSchema
+    )
+    // We want to require an username and a password for the influx metrics
+    // only if the influx metrics are enabled. The private config schema
+    // and the public config schema are two separate schemas so we have to run
+    // validation manually.
+    if (publicConfig.metrics.influx && publicConfig.metrics.influx.enabled) {
+      this.validatePrivateConfig(
+        config.private,
+        privateMetricsInfluxConfigSchema
+      )
+    }
+    this.config = {
+      public: publicConfig,
+      private: privateConfig,
+    }
+
+    this.githubConstellation = new GithubConstellation({
+      service: publicConfig.services.github,
+      private: privateConfig,
+    })
+
+    if (publicConfig.metrics.prometheus.enabled) {
+      this.metricInstance = new PrometheusMetrics()
+      if (publicConfig.metrics.influx.enabled) {
+        this.influxMetrics = new InfluxMetrics(
+          this.metricInstance,
+          Object.assign({}, publicConfig.metrics.influx, {
+            username: privateConfig.influx_username,
+            password: privateConfig.influx_password,
+          })
+        )
+      }
+    }
+  }
+
+  validatePrivateConfig(privateConfig, privateConfigSchema) {
+    try {
+      return Joi.attempt(privateConfig, privateConfigSchema)
+    } catch (e) {
+      const badPaths = e.details.map(({ path }) => path)
+      throw Error(
+        `Private configuration is invalid. Check these paths: ${badPaths.join(
+          ','
+        )}`
+      )
+    }
+  }
+
+  get port() {
+    const {
+      port,
+      ssl: { isSecure },
+    } = this.config.public
+    return port || (isSecure ? 443 : 80)
+  }
+
+  get baseUrl() {
+    const {
+      bind: { address, port },
+      ssl: { isSecure },
+    } = this.config.public
+
+    return url.format({
+      protocol: isSecure ? 'https' : 'http',
+      hostname: address,
+      port,
+      pathname: '/',
+    })
+  }
+
+  // See https://www.viget.com/articles/heroku-cloudflare-the-right-way/
+  requireCloudflare() {
+    // Set `req.ip`, which is expected by `cloudflareMiddleware()`. This is set
+    // by Express but not Scoutcamp.
+    addHandlerAtIndex(this.camp, 0, function (req, res, next) {
+      // On Heroku, `req.socket.remoteAddress` is the Heroku router. However,
+      // the router ensures that the last item in the `X-Forwarded-For` header
+      // is the real origin.
+      // https://stackoverflow.com/a/18517550/893113
+      req.ip = process.env.DYNO
+        ? req.headers['x-forwarded-for'].split(', ').pop()
+        : req.socket.remoteAddress
+      next()
+    })
+    addHandlerAtIndex(this.camp, 1, cloudflareMiddleware())
+  }
+
+  /**
+   * Set up Scoutcamp routes for 404/not found responses
+   */
+  registerErrorHandlers() {
+    const { camp, config } = this
+    const {
+      public: { rasterUrl },
+    } = config
+
+    camp.route(/\.(gif|jpg)$/, (query, match, end, request) => {
+      const [, format] = match
+      makeSend(
+        'svg',
+        request.res,
+        end
+      )(
+        makeBadge({
+          label: '410',
+          message: `${format} no longer available`,
+          color: 'lightgray',
+          format: 'svg',
+        })
+      )
+    })
+
+    if (!rasterUrl) {
+      camp.route(/\.png$/, (query, match, end, request) => {
+        makeSend(
+          'svg',
+          request.res,
+          end
+        )(
+          makeBadge({
+            label: '404',
+            message: 'raster badges not available',
+            color: 'lightgray',
+            format: 'svg',
+          })
+        )
+      })
+    }
+
+    camp.notfound(/(\.svg|\.json|)$/, (query, match, end, request) => {
+      const [, extension] = match
+      const format = (extension || '.svg').replace(/^\./, '')
+
+      makeSend(
+        format,
+        request.res,
+        end
+      )(
+        makeBadge({
+          label: '404',
+          message: 'badge not found',
+          color: 'red',
+          format,
+        })
+      )
+    })
+  }
+
+  /**
+   * Set up a couple of redirects:
+   * One for the raster badges.
+   * Another to redirect the base URL /
+   * (we use this to redirect {@link https://img.shields.io/}
+   * to {@link https://shields.io/} )
+   */
+  registerRedirects() {
+    const { config, camp } = this
+    const {
+      public: { rasterUrl, redirectUrl },
+    } = config
+
+    if (rasterUrl) {
+      // Redirect to the raster server for raster versions of modern badges.
+      camp.route(/\.png$/, (queryParams, match, end, ask) => {
+        ask.res.statusCode = 301
+        ask.res.setHeader(
+          'Location',
+          rasterRedirectUrl({ rasterUrl }, ask.req.url)
+        )
+
+        const cacheDuration = (30 * 24 * 3600) | 0 // 30 days.
+        ask.res.setHeader('Cache-Control', `max-age=${cacheDuration}`)
+
+        ask.res.end()
+      })
+    }
+
+    if (redirectUrl) {
+      camp.route(/^\/$/, (data, match, end, ask) => {
+        ask.res.statusCode = 302
+        ask.res.setHeader('Location', redirectUrl)
+        ask.res.end()
+      })
+    }
+  }
+
+  /**
+   * Iterate all the service classes defined in /services,
+   * load each service and register a Scoutcamp route for each service.
+   */
+  registerServices() {
+    const { config, camp, metricInstance } = this
+    const { apiProvider: githubApiProvider } = this.githubConstellation
+
+    loadServiceClasses().forEach(serviceClass =>
+      serviceClass.register(
+        { camp, handleRequest, githubApiProvider, metricInstance },
+        {
+          handleInternalErrors: config.public.handleInternalErrors,
+          cacheHeaders: config.public.cacheHeaders,
+          fetchLimitBytes: bytes(config.public.fetchLimit),
+          rasterUrl: config.public.rasterUrl,
+          private: config.private,
+          public: config.public,
+        }
+      )
+    )
+  }
+
+  /**
+   * Start the HTTP server:
+   * Bootstrap Scoutcamp,
+   * Register handlers,
+   * Start listening for requests on this.baseUrl()
+   */
+  async start() {
+    const {
+      bind: { port, address: hostname },
+      ssl: { isSecure: secure, cert, key },
+      cors: { allowedOrigin },
+      rateLimit,
+      requireCloudflare,
+    } = this.config.public
+
+    log(`Server is starting up: ${this.baseUrl}`)
+
+    const camp = (this.camp = Camp.create({
+      documentRoot: this.config.public.documentRoot,
+      port,
+      hostname,
+      secure,
+      staticMaxAge: 300,
+      cert,
+      key,
+    }))
+
+    if (requireCloudflare) {
+      this.requireCloudflare()
+    }
+
+    const { metricInstance } = this
+    this.cleanupMonitor = sysMonitor.setRoutes(
+      { rateLimit },
+      { server: camp, metricInstance }
+    )
+
+    const { githubConstellation } = this
+    await githubConstellation.initialize(camp)
+    if (metricInstance) {
+      if (this.config.public.metrics.prometheus.endpointEnabled) {
+        metricInstance.registerMetricsEndpoint(camp)
+      }
+      if (this.influxMetrics) {
+        this.influxMetrics.startPushingMetrics()
+      }
+    }
+
+    const { apiProvider: githubApiProvider } = this.githubConstellation
+    suggest.setRoutes(allowedOrigin, githubApiProvider, camp)
+
+    this.registerErrorHandlers()
+    this.registerRedirects()
+    this.registerServices()
+
+    camp.listenAsConfigured()
+
+    await new Promise(resolve => camp.on('listening', () => resolve()))
+  }
+
+  static resetGlobalState() {
+    // This state should be migrated to instance state. When possible, do not add new
+    // global state.
+    clearRequestCache()
+    clearRegularUpdateCache()
+  }
+
+  reset() {
+    this.constructor.resetGlobalState()
+  }
+
+  /**
+   * Stop the HTTP server and clean up helpers
+   */
+  async stop() {
+    if (this.camp) {
+      await new Promise(resolve => this.camp.close(resolve))
+      this.camp = undefined
+    }
+
+    if (this.cleanupMonitor) {
+      this.cleanupMonitor()
+      this.cleanupMonitor = undefined
+    }
+
+    if (this.githubConstellation) {
+      await this.githubConstellation.stop()
+      this.githubConstellation = undefined
+    }
+
+    if (this.metricInstance) {
+      if (this.influxMetrics) {
+        this.influxMetrics.stopPushingMetrics()
+      }
+      this.metricInstance.stop()
+    }
+  }
+}
+
+module.exports = Server
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/core_service-test-runner_create-service-tester.js.html b/core_service-test-runner_create-service-tester.js.html new file mode 100644 index 0000000000..bb3336633f --- /dev/null +++ b/core_service-test-runner_create-service-tester.js.html @@ -0,0 +1,84 @@ + + + + + JSDoc: Source: core/service-test-runner/create-service-tester.js + + + + + + + + + + +
+ +

Source: core/service-test-runner/create-service-tester.js

+ + + + + + +
+
+
'use strict'
+/**
+ * @module
+ */
+
+const caller = require('caller')
+const BaseService = require('../base-service/base')
+const ServiceTester = require('./service-tester')
+
+/**
+ * Automatically create a ServiceTester.
+ *
+ * When run from e.g. `gem-rank.tester.js`, this will create a tester that
+ * attaches to the service found in `gem-rank.service.js`.
+ *
+ * This can't be used for `.service.js` files which export more than one
+ * service.
+ *
+ * @returns {module:core/service-test-runner/service-tester~ServiceTester}
+ *    ServiceTester instance
+ */
+function createServiceTester() {
+  const servicePath = caller().replace('.tester.js', '.service.js')
+  const ServiceClass = require(servicePath)
+  if (!(ServiceClass.prototype instanceof BaseService)) {
+    throw Error(
+      `${servicePath} does not export a single service. Invoke new ServiceTester() directly.`
+    )
+  }
+  return ServiceTester.forServiceClass(ServiceClass)
+}
+
+module.exports = createServiceTester
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/core_service-test-runner_icedfrisby-shields.js.html b/core_service-test-runner_icedfrisby-shields.js.html new file mode 100644 index 0000000000..323435dae7 --- /dev/null +++ b/core_service-test-runner_icedfrisby-shields.js.html @@ -0,0 +1,139 @@ + + + + + JSDoc: Source: core/service-test-runner/icedfrisby-shields.js + + + + + + + + + + +
+ +

Source: core/service-test-runner/icedfrisby-shields.js

+ + + + + + +
+
+
'use strict'
+/**
+ * @module
+ */
+
+const Joi = require('joi')
+const { expect } = require('chai')
+
+/**
+ * Factory which wraps an "icedfrisby-nock" with some additional functionality:
+ * - check if a request was intercepted
+ * - set expectations on the badge JSON response
+ *
+ * @param {Function} superclass class to extend
+ * @see https://github.com/paulmelnikow/icedfrisby-nock/blob/master/icedfrisby-nock.js
+ * @returns {Function} wrapped class
+ */
+const factory = superclass =>
+  class IcedFrisbyNock extends superclass {
+    constructor(message) {
+      super(message)
+      this.intercepted = false
+    }
+
+    get(uri, options = { followRedirect: false }) {
+      if (!options.followRedirect) {
+        options.followRedirect = false
+      }
+      super.get(uri, options)
+      return this
+    }
+
+    intercept(setup) {
+      super.intercept(setup)
+      this.intercepted = true
+      return this
+    }
+
+    networkOff() {
+      super.networkOff()
+      this.intercepted = true
+      return this
+    }
+
+    networkOn() {
+      super.networkOn()
+      this.intercepted = true
+      return this
+    }
+
+    expectBadge({ label, message, logoWidth, labelColor, color, link }) {
+      return this.afterJSON(json => {
+        this.constructor._expectField(json, 'label', label)
+        this.constructor._expectField(json, 'message', message)
+        this.constructor._expectField(json, 'logoWidth', logoWidth)
+        this.constructor._expectField(json, 'labelColor', labelColor)
+        this.constructor._expectField(json, 'color', color)
+        this.constructor._expectField(json, 'link', link)
+      })
+    }
+
+    expectRedirect(location) {
+      return this.expectStatus(301).expectHeader('Location', location)
+    }
+
+    static _expectField(json, name, expected) {
+      if (typeof expected === 'undefined') return
+      if (typeof expected === 'string' || typeof expected === 'number') {
+        expect(json[name], `${name} mismatch`).to.equal(expected)
+      } else if (Array.isArray(expected)) {
+        expect(json[name], `${name} mismatch`).to.deep.equal(expected)
+      } else if (Joi.isSchema(expected)) {
+        Joi.attempt(json[name], expected, `${name} mismatch:`)
+      } else if (expected instanceof RegExp) {
+        Joi.attempt(
+          json[name],
+          Joi.string().regex(expected),
+          `${name} mismatch:`
+        )
+      } else {
+        throw new Error(
+          "'expected' must be a string, a number, a regex, an array or a Joi schema"
+        )
+      }
+    }
+  }
+
+module.exports = factory
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/core_service-test-runner_infer-pull-request.js.html b/core_service-test-runner_infer-pull-request.js.html new file mode 100644 index 0000000000..949db49b15 --- /dev/null +++ b/core_service-test-runner_infer-pull-request.js.html @@ -0,0 +1,156 @@ + + + + + JSDoc: Source: core/service-test-runner/infer-pull-request.js + + + + + + + + + + +
+ +

Source: core/service-test-runner/infer-pull-request.js

+ + + + + + +
+
+
'use strict'
+/**
+ * @module
+ */
+
+const { URL, format: urlFormat } = require('url')
+
+function formatSlug(owner, repo, pullRequest) {
+  return `${owner}/${repo}#${pullRequest}`
+}
+
+function parseGithubPullRequestUrl(url, options = {}) {
+  const { verifyBaseUrl } = options
+
+  const parsed = new URL(url)
+  const components = parsed.pathname.substr(1).split('/')
+  if (components[2] !== 'pull' || components.length !== 4) {
+    throw Error(`Invalid GitHub pull request URL: ${url}`)
+  }
+  const [owner, repo, , pullRequest] = components
+
+  parsed.pathname = ''
+  const baseUrl = urlFormat(parsed, {
+    auth: false,
+    fragment: false,
+    search: false,
+  }).replace(/\/$/, '')
+
+  if (verifyBaseUrl && baseUrl !== verifyBaseUrl) {
+    throw Error(`Expected base URL to be ${verifyBaseUrl} but got ${baseUrl}`)
+  }
+
+  return {
+    baseUrl,
+    owner,
+    repo,
+    pullRequest: +pullRequest,
+    slug: formatSlug(owner, repo, pullRequest),
+  }
+}
+
+function parseGithubRepoSlug(slug) {
+  const components = slug.split('/')
+  if (components.length !== 2) {
+    throw Error(`Invalid GitHub repo slug: ${slug}`)
+  }
+  const [owner, repo] = components
+  return { owner, repo }
+}
+
+function _inferPullRequestFromTravisEnv(env) {
+  const { owner, repo } = parseGithubRepoSlug(env.TRAVIS_REPO_SLUG)
+  const pullRequest = +env.TRAVIS_PULL_REQUEST
+  return {
+    owner,
+    repo,
+    pullRequest,
+    slug: formatSlug(owner, repo, pullRequest),
+  }
+}
+
+function _inferPullRequestFromCircleEnv(env) {
+  return parseGithubPullRequestUrl(env.CI_PULL_REQUEST)
+}
+
+/**
+ * When called inside a CI build, infer the details
+ * of a pull request from the environment variables.
+ *
+ * @param {object} [env=process.env] Environment variables
+ * @returns {module:core/service-test-runner/infer-pull-request~PullRequest}
+ *    Pull Request
+ */
+function inferPullRequest(env = process.env) {
+  if (env.TRAVIS) {
+    return _inferPullRequestFromTravisEnv(env)
+  } else if (env.CIRCLECI) {
+    return _inferPullRequestFromCircleEnv(env)
+  } else if (env.CI) {
+    throw Error(
+      'Unsupported CI system. Unable to obtain pull request information from the environment.'
+    )
+  } else {
+    throw Error(
+      'Unable to obtain pull request information from the environment. Is this running in CI?'
+    )
+  }
+}
+
+/**
+ * Pull Request
+ *
+ * @typedef PullRequest
+ * @property {string} pr.baseUrl (returned for travis CI only)
+ * @property {string} owner
+ * @property {string} repo
+ * @property {string} pullRequest PR/issue number
+ * @property {string} slug owner/repo/#pullRequest
+ */
+
+module.exports = {
+  parseGithubPullRequestUrl,
+  parseGithubRepoSlug,
+  inferPullRequest,
+}
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/core_service-test-runner_runner.js.html b/core_service-test-runner_runner.js.html new file mode 100644 index 0000000000..818f6d0184 --- /dev/null +++ b/core_service-test-runner_runner.js.html @@ -0,0 +1,126 @@ + + + + + JSDoc: Source: core/service-test-runner/runner.js + + + + + + + + + + +
+ +

Source: core/service-test-runner/runner.js

+ + + + + + +
+
+
'use strict'
+/**
+ * @module
+ */
+
+const { loadTesters } = require('../base-service/loader')
+
+/**
+ * Load a collection of ServiceTester objects and register them with Mocha.
+ */
+class Runner {
+  constructor({ baseUrl, skipIntercepted, retry }) {
+    this.baseUrl = baseUrl
+    this.skipIntercepted = skipIntercepted
+    this.retry = retry
+  }
+
+  /**
+   * Function to invoke before each test. This is a stub which can be
+   * overridden on instances.
+   */
+  beforeEach() {}
+
+  /**
+   * Prepare the runner by loading up all the ServiceTester objects.
+   */
+  prepare() {
+    this.testers = loadTesters()
+    this.testers.forEach(tester => {
+      tester.beforeEach = () => {
+        this.beforeEach()
+      }
+    })
+  }
+
+  _testersForService(service) {
+    return this.testers.filter(t => t.id.toLowerCase().startsWith(service))
+  }
+
+  /**
+   * Limit the test run to the specified services.
+   *
+   * @param {string[]} services An array of service id prefixes to run
+   */
+  only(services) {
+    const normalizedServices = new Set(services.map(v => v.toLowerCase()))
+
+    const missingServices = []
+    normalizedServices.forEach(service => {
+      const testers = this._testersForService(service)
+
+      if (testers.length === 0) {
+        missingServices.push(service)
+      }
+
+      testers.forEach(tester => {
+        tester.only()
+      })
+    })
+
+    // Throw at the end, to provide a better error message.
+    if (missingServices.length > 0) {
+      throw Error(`Unknown services: ${missingServices.join(', ')}`)
+    }
+  }
+
+  /**
+   * Register the tests with Mocha.
+   */
+  toss() {
+    const { testers, baseUrl, skipIntercepted, retry } = this
+    testers.forEach(tester => tester.toss({ baseUrl, skipIntercepted, retry }))
+  }
+}
+module.exports = Runner
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/core_service-test-runner_service-tester.js.html b/core_service-test-runner_service-tester.js.html new file mode 100644 index 0000000000..a6c9d1201d --- /dev/null +++ b/core_service-test-runner_service-tester.js.html @@ -0,0 +1,194 @@ + + + + + JSDoc: Source: core/service-test-runner/service-tester.js + + + + + + + + + + +
+ +

Source: core/service-test-runner/service-tester.js

+ + + + + + +
+
+
'use strict'
+/**
+ * @module
+ */
+
+const emojic = require('emojic')
+const trace = require('../base-service/trace')
+const frisby = require('./icedfrisby-shields')(
+  // eslint-disable-next-line import/order
+  require('icedfrisby-nock')(require('icedfrisby'))
+)
+
+/**
+ * Encapsulate a suite of tests. Create new tests using create() and register
+ * them with Mocha using toss().
+ *
+ * @see https://github.com/badges/shields/blob/master/doc/service-tests.md
+ */
+class ServiceTester {
+  /**
+   * Service Tester Constructor
+   *
+   * @param {object} attrs Refer to individual attrs
+   * @param {string} attrs.id
+   *    Specifies which tests to run from the CLI or pull requests
+   * @param {string} attrs.title
+   *    Prints in the Mocha output
+   * @param {string} attrs.pathPrefix
+   *    Prefix which is automatically prepended to each tested URI.
+   *    The default is `/${attrs.id}`.
+   */
+  constructor({ id, title, pathPrefix }) {
+    if (pathPrefix === undefined) {
+      pathPrefix = `/${id}`
+    }
+    Object.assign(this, {
+      id,
+      title,
+      pathPrefix,
+      specs: [],
+      _only: false,
+    })
+  }
+
+  /**
+   * Construct a ServiceTester instance for a single service class
+   *
+   * @param {Function} ServiceClass
+   *    A class that extends base-service/base.BaseService
+   * @returns {module:core/service-test-runner/service-tester~ServiceTester}
+   *    ServiceTester for ServiceClass
+   */
+  static forServiceClass(ServiceClass) {
+    const id = ServiceClass.name
+    const pathPrefix = ServiceClass.route.base
+      ? `/${ServiceClass.route.base}`
+      : ''
+    return new this({
+      id,
+      title: id,
+      pathPrefix,
+    })
+  }
+
+  /**
+   * Invoked before each test. This is a stub which can be overridden on
+   * instances.
+   */
+  beforeEach() {}
+
+  /**
+   * Create a new test. The hard work is delegated to IcedFrisby.
+   * {@link https://github.com/MarkHerhold/IcedFrisby/#show-me-some-code}
+   *
+   * Note: The caller should not invoke toss() on the Frisby chain, as it's
+   * invoked automatically by the tester.
+   *
+   * @param {string} msg The name of the test
+   * @returns {module:icedfrisby~IcedFrisby} IcedFrisby instance
+   */
+  create(msg) {
+    const spec = frisby
+      .create(msg)
+      .before(() => {
+        this.beforeEach()
+      })
+      // eslint-disable-next-line mocha/prefer-arrow-callback
+      .finally(function () {
+        // `this` is the IcedFrisby instance.
+        let responseBody
+        try {
+          responseBody = JSON.parse(this._response.body)
+        } catch (e) {
+          responseBody = this._response.body
+        }
+        trace.logTrace('outbound', emojic.shield, 'Response', responseBody)
+      })
+
+    this.specs.push(spec)
+
+    return spec
+  }
+
+  /**
+   * Run only this tester. This can be invoked using the --only argument to
+   * the CLI, or directly on the tester.
+   */
+  only() {
+    this._only = true
+  }
+
+  /**
+   * Register the tests with Mocha.
+   *
+   * @param {object} attrs Refer to individual attrs
+   * @param {string} attrs.baseUrl base URL for test server
+   * @param {boolean} attrs.skipIntercepted skip tests which intercept requests
+   * @param {object} attrs.retry retry configuration
+   * @param {number} attrs.retry.count number of times to retry test
+   * @param {number} attrs.retry.backoff number of milliseconds to add to the wait between each retry
+   */
+  toss({ baseUrl, skipIntercepted, retry: { count, backoff } }) {
+    const { specs, pathPrefix } = this
+    const testerBaseUrl = `${baseUrl}${pathPrefix}`
+
+    const fn = this._only ? describe.only : describe
+    // eslint-disable-next-line mocha/prefer-arrow-callback
+    fn(this.title, function () {
+      specs.forEach(spec => {
+        spec._message = `[${spec.hasIntercept ? 'mocked' : 'live'}] ${
+          spec._message
+        }`
+        if (!skipIntercepted || !spec.intercepted) {
+          spec.baseUri(testerBaseUrl)
+          spec.retry(count, backoff)
+          spec.toss()
+        }
+      })
+    })
+  }
+}
+
+module.exports = ServiceTester
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/core_service-test-runner_services-for-title.js.html b/core_service-test-runner_services-for-title.js.html new file mode 100644 index 0000000000..dd6427a9c3 --- /dev/null +++ b/core_service-test-runner_services-for-title.js.html @@ -0,0 +1,85 @@ + + + + + JSDoc: Source: core/service-test-runner/services-for-title.js + + + + + + + + + + +
+ +

Source: core/service-test-runner/services-for-title.js

+ + + + + + +
+
+
'use strict'
+/**
+ * @module
+ */
+
+const difference = require('lodash.difference')
+
+/**
+ * Given a pull request title like
+ * '[Travis Sonar] Support user token authentication'
+ * extract the list of service names in square brackets
+ * as an array of strings.
+ *
+ * @param {string} title Pull Request title
+ * @returns {string[]} Array of service names
+ */
+function servicesForTitle(title) {
+  const bracketed = /\[([^\]]+)\]/g
+
+  const preNormalized = title.toLowerCase()
+
+  let services = []
+  let match
+  while ((match = bracketed.exec(preNormalized))) {
+    const [, bracketed] = match
+    services = services.concat(bracketed.split(' '))
+  }
+  services = services.filter(Boolean).map(service => service.toLowerCase())
+
+  const ignored = ['wip', 'rfc', 'security']
+  return difference(services, ignored)
+}
+
+module.exports = servicesForTitle
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/core_token-pooling_token-pool.js.html b/core_token-pooling_token-pool.js.html new file mode 100644 index 0000000000..6f06e5ba41 --- /dev/null +++ b/core_token-pooling_token-pool.js.html @@ -0,0 +1,412 @@ + + + + + JSDoc: Source: core/token-pooling/token-pool.js + + + + + + + + + + +
+ +

Source: core/token-pooling/token-pool.js

+ + + + + + +
+
+
'use strict'
+/**
+ * @module
+ */
+
+const crypto = require('crypto')
+const PriorityQueue = require('priorityqueuejs')
+
+/**
+ * Compute a one-way hash of the input string.
+ *
+ * @param {string} id token
+ * @returns {string} hash
+ */
+function sanitizeToken(id) {
+  return crypto.createHash('sha256').update(id, 'utf-8').digest('hex')
+}
+
+function getUtcEpochSeconds() {
+  return (Date.now() / 1000) >>> 0
+}
+
+/**
+ * Encapsulate a rate-limited token, with a user-provided ID and user-provided data.
+ *
+ * Each token has a notion of the number of uses remaining until exhausted,
+ * and the next reset time, when it can be used again even if it's exhausted.
+ */
+class Token {
+  /**
+   * Token Constructor
+   *
+   * @param {string} id token string
+   * @param {*} data reserved for future use
+   * @param {number} usesRemaining
+   *    Number of uses remaining until the token is exhausted
+   * @param {number} nextReset
+   *    Time when the token can be used again even if it's exhausted (unix timestamp)
+   */
+  constructor(id, data, usesRemaining, nextReset) {
+    // Use underscores to avoid conflict with property accessors.
+    Object.assign(this, {
+      _id: id,
+      _data: data,
+      _usesRemaining: usesRemaining,
+      _nextReset: nextReset,
+      _isValid: true,
+      _isFrozen: false,
+    })
+  }
+
+  get id() {
+    return this._id
+  }
+
+  get data() {
+    return this._data
+  }
+
+  get usesRemaining() {
+    return this._usesRemaining
+  }
+
+  get nextReset() {
+    return this._nextReset
+  }
+
+  get isValid() {
+    return this._isValid
+  }
+
+  get isFrozen() {
+    return this._isFrozen
+  }
+
+  get hasReset() {
+    return getUtcEpochSeconds() >= this.nextReset
+  }
+
+  get isExhausted() {
+    return this.usesRemaining <= 0 && !this.hasReset
+  }
+
+  /**
+   * Update the uses remaining and next reset time for a token.
+   *
+   * @param {number} usesRemaining
+   *    Number of uses remaining until the token is exhausted
+   * @param {number} nextReset
+   *    Time when the token can be used again even if it's exhausted (unix timestamp)
+   */
+  update(usesRemaining, nextReset) {
+    if (!Number.isInteger(usesRemaining)) {
+      throw Error('usesRemaining must be an integer')
+    }
+    if (!Number.isInteger(nextReset)) {
+      throw Error('nextReset must be an integer')
+    }
+
+    if (this._isFrozen) {
+      return
+    }
+
+    // Since the token pool will typically return the same token for many uses
+    // before moving on to another, `update()` may be called many times. Since
+    // the sequence of responses may be indeterminate, keep the "worst case"
+    // value for uses remaining.
+    if (
+      this._nextReset === this.constructor.nextResetNever ||
+      nextReset > this._nextReset
+    ) {
+      this._nextReset = nextReset
+      this._usesRemaining = usesRemaining
+    } else if (nextReset === this._nextReset) {
+      this._usesRemaining = Math.min(this._usesRemaining, usesRemaining)
+    } else {
+      // Discard the new update; it's older than the values we have.
+    }
+  }
+
+  /**
+   * Indicate that the token should no longer be used.
+   */
+  invalidate() {
+    this._isValid = false
+  }
+
+  /**
+   * Freeze the uses remaining and next reset values. Helpful for keeping
+   * stable ordering for a valid priority queue.
+   */
+  freeze() {
+    this._isFrozen = true
+  }
+
+  /**
+   * Unfreeze the uses remaining and next reset values.
+   */
+  unfreeze() {
+    this._isFrozen = false
+  }
+
+  getDebugInfo({ sanitize = true } = {}) {
+    const { id, data, usesRemaining, nextReset, isValid, isFrozen } = this
+
+    if (sanitize) {
+      return {
+        id: sanitizeToken(id),
+        data: '[redacted]',
+        usesRemaining,
+        nextReset,
+        isValid,
+        isFrozen,
+      }
+    } else {
+      return { id, data, usesRemaining, nextReset, isValid, isFrozen }
+    }
+  }
+}
+
+// Large sentinel value which means "never reset".
+Token.nextResetNever = Number.MAX_SAFE_INTEGER
+
+/**
+ * Encapsulate a collection of rate-limited tokens and choose the next
+ * available token when one is needed.
+ *
+ * Designed for the Github API, though may be also useful with other rate-
+ * limited APIs.
+ */
+class TokenPool {
+  /**
+   * TokenPool Constructor
+   *
+   * @param {number} batchSize
+   *    The maximum number of times we use each token before moving
+   *    on to the next one.
+   */
+  constructor({ batchSize = 1 } = {}) {
+    this.batchSize = batchSize
+
+    this.currentBatch = { currentToken: null, remaining: 0 }
+
+    // A set of IDs used for deduplication.
+    this.tokenIds = new Set()
+
+    // See discussion on the FIFO and priority queues in `next()`.
+    this.fifoQueue = []
+    this.priorityQueue = new PriorityQueue(this.constructor.compareTokens)
+  }
+
+  /**
+   * compareTokens
+   *
+   * @param {module:core/token-pooling/token-pool~Token} first first token to compare
+   * @param {module:core/token-pooling/token-pool~Token} second second token to compare
+   * @returns {module:core/token-pooling/token-pool~Token} The token whose current rate allotment is expiring soonest.
+   */
+  static compareTokens(first, second) {
+    return second.nextReset - first.nextReset
+  }
+
+  /**
+   * Add a token with user-provided ID and data.
+   *
+   * @param {string} id token string
+   * @param {*} data reserved for future use
+   * @param {number} usesRemaining
+   *    Number of uses remaining until the token is exhausted
+   * @param {number} nextReset
+   *    Time when the token can be used again even if it's exhausted (unix timestamp)
+   *
+   * @returns {boolean} Was the token added to the pool?
+   */
+  add(id, data, usesRemaining, nextReset) {
+    if (this.tokenIds.has(id)) {
+      return false
+    }
+    this.tokenIds.add(id)
+
+    usesRemaining = usesRemaining === undefined ? this.batchSize : usesRemaining
+    nextReset = nextReset === undefined ? Token.nextResetNever : nextReset
+
+    const token = new Token(id, data, usesRemaining, nextReset)
+    this.fifoQueue.push(token)
+
+    return true
+  }
+
+  // Prepare to start a new batch by obtaining and returning the next usable
+  // token.
+  _nextBatch() {
+    let next
+
+    while ((next = this.fifoQueue.shift())) {
+      if (!next.isValid) {
+        // Discard, and
+        continue
+      } else if (next.isExhausted) {
+        next.freeze()
+        this.priorityQueue.enq(next)
+      } else {
+        return next
+      }
+    }
+
+    while (
+      !this.priorityQueue.isEmpty() &&
+      (next = this.priorityQueue.peek())
+    ) {
+      if (!next.isValid) {
+        // Discard, and
+        continue
+      } else if (next.isExhausted) {
+        // No need to check any more tokens, since they all reset after this
+        // one.
+        break
+      } else {
+        this.priorityQueue.deq() // deq next
+        next.unfreeze()
+        return next
+      }
+    }
+
+    throw Error('Token pool is exhausted')
+  }
+
+  /**
+   * Obtain the next available token, returning `null` if no tokens are
+   * available.
+   *
+   * Tokens are initially pulled from a FIFO queue. The first token is used
+   * for a batch of requests, then returned to the queue to give those
+   * requests the opportunity to complete. The next token is used for the next
+   * batch of requests.
+   *
+   * This strategy allows a token to be used for concurrent requests, not just
+   * sequential request, and simplifies token recovery after errored and timed
+   * out requests.
+   *
+   * By the time the original token re-emerges, its requests should have long
+   * since completed. Even if a couple them are still running, they can
+   * reasonably be ignored. The uses remaining won't be 100% correct, but
+   * that's fine, because Shields uses only 75%
+   *
+   * The process continues until an exhausted token is pulled from the FIFO
+   * queue. At that time it's placed in the priority queue based on its
+   * scheduled reset time. To ensure the priority queue works as intended,
+   * the scheduled reset time is frozen then.
+   *
+   * After obtaining a token using `next()`, invoke `update()` on it to set a
+   * new use-remaining count and next-reset time. Invoke `invalidate()` to
+   * indicate it should not be reused.
+   *
+   * @returns {module:core/token-pooling/token-pool~Token} token
+   */
+  next() {
+    let token = this.currentBatch.token
+    const remaining = this.currentBatch.remaining
+
+    if (remaining <= 0 || !token.isValid || token.isExhausted) {
+      token = this._nextBatch()
+      this.currentBatch = {
+        token,
+        remaining: token.hasReset
+          ? this.batchSize
+          : Math.min(this.batchSize, token.usesRemaining),
+      }
+      this.fifoQueue.push(token)
+    }
+
+    this.currentBatch.remaining -= 1
+
+    return token
+  }
+
+  /**
+   * Iterate over all valid tokens.
+   *
+   * @param {Function} callback function to execute on each valid token
+   */
+  forEach(callback) {
+    function visit(item) {
+      if (item.isValid) {
+        callback(item)
+      }
+    }
+
+    this.fifoQueue.forEach(visit)
+    this.priorityQueue.forEach(visit)
+  }
+
+  allValidTokenIds() {
+    const result = []
+    this.forEach(({ id }) => result.push(id))
+    return result
+  }
+
+  serializeDebugInfo({ sanitize = true } = {}) {
+    const maybeSanitize = sanitize ? id => sanitizeToken(id) : id => id
+
+    const priorityQueue = []
+    this.priorityQueue.forEach(t =>
+      priorityQueue.push(t.getDebugInfo({ sanitize }))
+    )
+
+    return {
+      utcEpochSeconds: getUtcEpochSeconds(),
+      allValidTokenIds: this.allValidTokenIds().map(maybeSanitize),
+      fifoQueue: this.fifoQueue.map(t => t.getDebugInfo({ sanitize })),
+      priorityQueue,
+      sanitized: sanitize,
+    }
+  }
+}
+
+module.exports = {
+  sanitizeToken,
+  Token,
+  TokenPool,
+}
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/fonts/OpenSans-Bold-webfont.eot b/fonts/OpenSans-Bold-webfont.eot new file mode 100644 index 0000000000..5d20d91633 Binary files /dev/null and b/fonts/OpenSans-Bold-webfont.eot differ diff --git a/fonts/OpenSans-Bold-webfont.svg b/fonts/OpenSans-Bold-webfont.svg new file mode 100644 index 0000000000..3ed7be4bc5 --- /dev/null +++ b/fonts/OpenSans-Bold-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/OpenSans-Bold-webfont.woff b/fonts/OpenSans-Bold-webfont.woff new file mode 100644 index 0000000000..1205787b0e Binary files /dev/null and b/fonts/OpenSans-Bold-webfont.woff differ diff --git a/fonts/OpenSans-BoldItalic-webfont.eot b/fonts/OpenSans-BoldItalic-webfont.eot new file mode 100644 index 0000000000..1f639a15ff Binary files /dev/null and b/fonts/OpenSans-BoldItalic-webfont.eot differ diff --git a/fonts/OpenSans-BoldItalic-webfont.svg b/fonts/OpenSans-BoldItalic-webfont.svg new file mode 100644 index 0000000000..6a2607b9da --- /dev/null +++ b/fonts/OpenSans-BoldItalic-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/OpenSans-BoldItalic-webfont.woff b/fonts/OpenSans-BoldItalic-webfont.woff new file mode 100644 index 0000000000..ed760c0628 Binary files /dev/null and b/fonts/OpenSans-BoldItalic-webfont.woff differ diff --git a/fonts/OpenSans-Italic-webfont.eot b/fonts/OpenSans-Italic-webfont.eot new file mode 100644 index 0000000000..0c8a0ae06e Binary files /dev/null and b/fonts/OpenSans-Italic-webfont.eot differ diff --git a/fonts/OpenSans-Italic-webfont.svg b/fonts/OpenSans-Italic-webfont.svg new file mode 100644 index 0000000000..e1075dcc24 --- /dev/null +++ b/fonts/OpenSans-Italic-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/OpenSans-Italic-webfont.woff b/fonts/OpenSans-Italic-webfont.woff new file mode 100644 index 0000000000..ff652e6435 Binary files /dev/null and b/fonts/OpenSans-Italic-webfont.woff differ diff --git a/fonts/OpenSans-Light-webfont.eot b/fonts/OpenSans-Light-webfont.eot new file mode 100644 index 0000000000..14868406aa Binary files /dev/null and b/fonts/OpenSans-Light-webfont.eot differ diff --git a/fonts/OpenSans-Light-webfont.svg b/fonts/OpenSans-Light-webfont.svg new file mode 100644 index 0000000000..11a472ca8a --- /dev/null +++ b/fonts/OpenSans-Light-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/OpenSans-Light-webfont.woff b/fonts/OpenSans-Light-webfont.woff new file mode 100644 index 0000000000..e786074813 Binary files /dev/null and b/fonts/OpenSans-Light-webfont.woff differ diff --git a/fonts/OpenSans-LightItalic-webfont.eot b/fonts/OpenSans-LightItalic-webfont.eot new file mode 100644 index 0000000000..8f445929ff Binary files /dev/null and b/fonts/OpenSans-LightItalic-webfont.eot differ diff --git a/fonts/OpenSans-LightItalic-webfont.svg b/fonts/OpenSans-LightItalic-webfont.svg new file mode 100644 index 0000000000..431d7e3546 --- /dev/null +++ b/fonts/OpenSans-LightItalic-webfont.svg @@ -0,0 +1,1835 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/OpenSans-LightItalic-webfont.woff b/fonts/OpenSans-LightItalic-webfont.woff new file mode 100644 index 0000000000..43e8b9e6cc Binary files /dev/null and b/fonts/OpenSans-LightItalic-webfont.woff differ diff --git a/fonts/OpenSans-Regular-webfont.eot b/fonts/OpenSans-Regular-webfont.eot new file mode 100644 index 0000000000..6bbc3cf58c Binary files /dev/null and b/fonts/OpenSans-Regular-webfont.eot differ diff --git a/fonts/OpenSans-Regular-webfont.svg b/fonts/OpenSans-Regular-webfont.svg new file mode 100644 index 0000000000..25a3952340 --- /dev/null +++ b/fonts/OpenSans-Regular-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/OpenSans-Regular-webfont.woff b/fonts/OpenSans-Regular-webfont.woff new file mode 100644 index 0000000000..e231183dce Binary files /dev/null and b/fonts/OpenSans-Regular-webfont.woff differ diff --git a/global.html b/global.html new file mode 100644 index 0000000000..e6c721b524 --- /dev/null +++ b/global.html @@ -0,0 +1,470 @@ + + + + + JSDoc: Global + + + + + + + + + + +
+ +

Global

+ + + + + + +
+ +
+ +

+ + +
+ +
+
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

module:services/dynamic/json-path(superclass) → {function}

+ + + + + + +
+

Dynamic service class factory which wraps module:core/base-service/base~BaseService with support of JSONPath.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
superclass + + +function + + + +

class to extend

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

wrapped class

+
+ + + +
+
+ Type +
+
+ +function + + +
+
+ + + + + + + + + + + + + +

validateAffiliations(value, helpers) → {string}

+ + + + + + +
+

Validates affiliations should contain combination of allowed values in any order.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
value + + +string + + + +

affiliation current value

helpers + + +* + + + +

object to construct custom error

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

valiadtion error or value unchanged

+
+ + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000000..d9a5a68680 --- /dev/null +++ b/index.html @@ -0,0 +1,226 @@ + + + + + JSDoc: Home + + + + + + + + + + +
+ +

Home

+ + + + + + + + +

+ + + + + + + + + + + + + + + +
+

+ +

+

+ + + + + + + + + + build status + + service-test status + + coverage + + Total alerts + + chat on Discord + + follow on Twitter +

+

This is home to Shields.io, a service for concise, consistent, +and legible badges in SVG and raster format, which can easily be included in +GitHub readmes or any other web page. The service supports dozens of +continuous integration services, package registries, distributions, app +stores, social networks, code coverage services, and code analysis services. +Every month it serves over 470 million images.

+

This repo hosts:

+ +

Examples

+
    +
  • code coverage percentage: coverage
  • +
  • stable release version: version
  • +
  • package manager release: gem
  • +
  • status of third-party dependencies: dependencies
  • +
  • static code analysis grade: codacy
  • +
  • SemVer version observance: semver
  • +
  • amount of Liberapay donations per week: receives
  • +
  • Python package downloads: downloads
  • +
  • Chrome Web Store extension rating: rating
  • +
  • Uptime Robot percentage: uptime
  • +
+

Make your own badges! +(Quick example: https://img.shields.io/badge/left-right-f39f37)

+

Quickstart

+

Browse a complete list of badges and locate a particular badge by using the search bar or by browsing the categories. Click on the badge to fill in required data elements for that badge type (like your username or repo) and optionally customize (label, colors etc.). And it's ready for use!

+

Use the button at the bottom to copy your badge url or snippet, which can then be added to places like your GitHub readme files or other web pages.

+

Contributing

+

Shields is a community project. We invite your participation through issues +and pull requests! You can peruse the contributing guidelines.

+

When adding or changing a service please add tests.

+

This project has quite a backlog of suggestions! If you're new to the project, +maybe you'd like to open a pull request to address one of them.

+

You can read a tutorial on how to add a badge.

+

GitHub issues by-label

+

Development

+
    +
  1. Install Node 12 or later. You can use the package manager of your choice. +Tests need to pass in Node 12 and 14.
  2. +
  3. Clone this repository.
  4. +
  5. Run npm ci to install the dependencies.
  6. +
  7. Run npm start to start the badge server and the frontend dev server.
  8. +
  9. Open http://localhost:3000/ to view the frontend.
  10. +
+

When server source files change, the badge server should automatically restart +itself (using nodemon). When the frontend files change, the frontend dev +server (gatsby dev) should also automatically reload. However the badge +definitions are built only before the server first starts. To regenerate those, +either run npm run defs or manually restart the server.

+

To debug a badge from the command line, run npm run badge -- /npm/v/nock. +It also works with full URLs like +npm run badge -- https://img.shields.io/npm/v/nock.

+

Use npm run debug:server to start server in debug mode. +This recipe shows how to debug Node.js application in VS Code.

+

Shields has experimental support for Gitpod, a pre-configured development +environment that runs in your browser. To use Gitpod, click the button below and +sign in with GitHub. Gitpod also offers a browser add-on, though it is not required. +Please report any Gitpod bugs, questions, or suggestions in issue +#2772.

+

Edit with Gitpod

+

Snapshot tests ensure we don't inadvertently make changes that affect the +SVG or JSON output. When deliberately changing the output, run +SNAPSHOT_DRY=1 npm run test:package to preview changes to the saved +snapshots, and SNAPSHOT_UPDATE=1 npm run test:package to update them.

+

The server can be configured to use Sentry (configuration) and Prometheus (configuration).

+

Daily tests, including a full run of the service tests and overall code coverage, are run via badges/daily-tests.

+

Hosting your own server

+

There is documentation about hosting your own server.

+

History

+

b.adge.me was the original website for this service. Heroku back then had a +thing which made it hard to use a toplevel domain with it, hence the odd +domain. It used code developed in 2013 from a library called +gh-badges, both developed by Thaddée Tyl. +The project merged with shields.io by making it use the b.adge.me code +and closed b.adge.me.

+

The original badge specification was developed in 2013 by +Olivier Lacan. It was inspired by the Travis CI and similar +badges (there were a lot fewer, back then). In 2014 Thaddée Tyl redesigned +it with help from a Travis CI employee and convinced everyone to switch to +it. The old design is what today is called the plastic style; the new one +is the flat style.

+

You can read more about the project's inception, +the motivation of the SVG badge specification, and +the specification itself.

+

Project leaders

+

Maintainers:

+ +

Operations:

+ +

Alumni:

+ +

Related projects

+ +

License

+

All assets and code are under the CC0 LICENSE and in the public +domain unless specified otherwise.

+

The assets in logo/ are trademarks of their respective companies and are +under their terms and license.

+

Community

+

Thanks to the people and companies who donate money, services or time to keep the project running. https://shields.io/community

+
+ + + + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-badge-maker.html b/module-badge-maker.html new file mode 100644 index 0000000000..7456327bc0 --- /dev/null +++ b/module-badge-maker.html @@ -0,0 +1,396 @@ + + + + + JSDoc: Module: badge-maker + + + + + + + + + + +
+ +

Module: badge-maker

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

(inner) makeBadge(format) → {string}

+ + + + + + +
+

Create a badge

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
format + + +object + + + +

Object specifying badge data

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
label + + +string + + + +

(Optional) Badge label (e.g: 'build')

message + + +string + + + +

(Required) Badge message (e.g: 'passing')

labelColor + + +string + + + +

(Optional) Label color

color + + +string + + + +

(Optional) Message color

style + + +string + + + +

(Optional) Visual style e.g: 'flat'

+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

Badge in SVG format

+
+ + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_base-BaseService.html b/module-core_base-service_base-BaseService.html new file mode 100644 index 0000000000..05849c2ab3 --- /dev/null +++ b/module-core_base-service_base-BaseService.html @@ -0,0 +1,752 @@ + + + + + JSDoc: Class: BaseService + + + + + + + + + + +
+ +

Class: BaseService

+ + + + + + +
+ +
+ +

+ core/base-service/base~BaseService()

+ +

Abstract base class which all service classes inherit from. +Concrete implementations of BaseService must implement the methods +category(), route() and handle(namedParams, queryParams)

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new BaseService()

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

(abstract, static) category :string

+ + + + +
+

Name of the category to sort this badge into (eg. "build"). Used to sort +the badges on the main shields.io website.

+
+ + + +
Type:
+
    +
  • + +string + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(abstract, static) route :module:core/base-service/base~Route

+ + + + +
+

Route to mount this service on

+
+ + + +
Type:
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(abstract) auth :module:core/base-service/base~Auth

+ + + + +
+

Configuration for the authentication helper that prepares credentials +for upstream requests.

+

See also the config schema in ./server.js and doc/server-secrets.md.

+

To use the configured auth in the handler or fetch method, pass the +credentials to the request. For example:

+
    +
  • { options: { auth: this.authHelper.basicAuth } }
  • +
  • { options: { headers: this.authHelper.bearerAuthHeader } }
  • +
  • { options: { qs: { token: this.authHelper._pass } } }
  • +
+
+ + + +
Type:
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

defaultBadgeData :module:core/base-service/base~DefaultBadgeData

+ + + + +
+

Default data for the badge. +These defaults are used if the value is neither included in the service data +from the handler nor overridden by the user via query parameters.

+
+ + + +
Type:
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(abstract) examples :Array.<module:core/base-service/base~Example>

+ + + + +
+

Array of Example objects describing 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 +substituted into the service's compiled route pattern. The rendered badge +is specified with staticPreview.

+

For services which use a route format, the pattern can be specified as +part of the example.

+
+ + + +
Type:
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

(async, abstract) handle(namedParams, queryParams) → {module:core/base-service/base~Badge}

+ + + + + + +
+

Asynchronous function to handle requests for this service. Take the route +parameters (as defined in the route property), perform a request using +this._sendAndCacheRequest, and return the badge data.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
namedParams + + +object + + + +

Params parsed from route pattern +defined in this.route.pattern or this.route.capture

queryParams + + +object + + + +

Params parsed from the query string

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

badge Object validated against serviceDataSchema

+
+ + + +
+
+ Type +
+
+ +module:core/base-service/base~Badge + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_base-graphql-BaseGraphqlService.html b/module-core_base-service_base-graphql-BaseGraphqlService.html new file mode 100644 index 0000000000..ad5cf25ede --- /dev/null +++ b/module-core_base-service_base-graphql-BaseGraphqlService.html @@ -0,0 +1,838 @@ + + + + + JSDoc: Class: BaseGraphqlService + + + + + + + + + + +
+ +

Class: BaseGraphqlService

+ + + + + + +
+ +
+ +

(abstract) + core/base-service/base-graphql~BaseGraphqlService()

+ +

Services which query a GraphQL endpoint should extend BaseGraphqlService

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

(abstract) new BaseGraphqlService()

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

_parseJson(buffer) → {object}

+ + + + + + +
+

Parse data from JSON endpoint

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
buffer + + +string + + + +

JSON repsonse from upstream API

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

Parsed response

+
+ + + +
+
+ Type +
+
+ +object + + +
+
+ + + + + + + + + + + + + +

(async) _requestGraphql(attrs) → {object}

+ + + + + + +
+

Request data from an upstream GraphQL API, +parse it and validate against a schema

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
attrs + + +object + + + +

Refer to individual attrs

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
schema + + +Joi + + + + + + + + + + + +

Joi schema to validate the response against

url + + +string + + + + + + + + + + + +

URL to request

query + + +object + + + + + + + + + + + +

Parsed GraphQL object +representing the query clause of GraphQL POST body +e.g. gql{ query { ... } }

variables + + +object + + + + + + + + + + + +

Variables clause of GraphQL POST body

options + + +object + + + + + + <optional>
+ + + + + +
+ + {} + +

Options to pass to request. See +documentation

httpErrorMessages + + +object + + + + + + <optional>
+ + + + + +
+ + {} + +

Key-value map of HTTP status codes +and custom error messages e.g: { 404: 'package not found' }. +This can be used to extend or override the +default

transformJson + + +function + + + + + + <optional>
+ + + + + +
+ + data => data + +

Function which takes the raw json and transforms it before +further procesing. In case of multiple query in a single graphql call and few of them +throw error, partial data might be used ignoring the error.

transformErrors + + +function + + + + + + <optional>
+ + + + + +
+ + defaultTransformErrors + +

Function which takes an errors object from a GraphQL +response and returns an instance of ShieldsRuntimeError. +The default is to return the first entry of the errors array as +an InvalidResponse.

+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

Parsed response

+
+ + + +
+
+ Type +
+
+ +object + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_base-graphql.html b/module-core_base-service_base-graphql.html new file mode 100644 index 0000000000..6e2c5abd11 --- /dev/null +++ b/module-core_base-service_base-graphql.html @@ -0,0 +1,92 @@ + + + + + JSDoc: Module: core/base-service/base-graphql + + + + + + + + + + +
+ +

Module: core/base-service/base-graphql

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + +

Classes

+ +
+
BaseGraphqlService
+
+
+ + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_base-json-BaseJsonService.html b/module-core_base-service_base-json-BaseJsonService.html new file mode 100644 index 0000000000..d84825b9d0 --- /dev/null +++ b/module-core_base-service_base-json-BaseJsonService.html @@ -0,0 +1,683 @@ + + + + + JSDoc: Class: BaseJsonService + + + + + + + + + + +
+ +

Class: BaseJsonService

+ + + + + + +
+ +
+ +

(abstract) + core/base-service/base-json~BaseJsonService()

+ +

Services which query a JSON endpoint should extend BaseJsonService

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

(abstract) new BaseJsonService()

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

_parseJson(buffer) → {object}

+ + + + + + +
+

Parse data from JSON endpoint

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
buffer + + +string + + + +

JSON repsonse from upstream API

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

Parsed response

+
+ + + +
+
+ Type +
+
+ +object + + +
+
+ + + + + + + + + + + + + +

(async) _requestJson(attrs) → {object}

+ + + + + + +
+

Request data from an upstream API serving JSON, +parse it and validate against a schema

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
attrs + + +object + + + +

Refer to individual attrs

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
schema + + +Joi + + + + + + + + + + + +

Joi schema to validate the response against

url + + +string + + + + + + + + + + + +

URL to request

options + + +object + + + + + + <optional>
+ + + + + +
+ + {} + +

Options to pass to request. See +documentation

errorMessages + + +object + + + + + + <optional>
+ + + + + +
+ + {} + +

Key-value map of status codes +and custom error messages e.g: { 404: 'package not found' }. +This can be used to extend or override the +default

+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

Parsed response

+
+ + + +
+
+ Type +
+
+ +object + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_base-json.html b/module-core_base-service_base-json.html new file mode 100644 index 0000000000..0c3e40343f --- /dev/null +++ b/module-core_base-service_base-json.html @@ -0,0 +1,92 @@ + + + + + JSDoc: Module: core/base-service/base-json + + + + + + + + + + +
+ +

Module: core/base-service/base-json

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + +

Classes

+ +
+
BaseJsonService
+
+
+ + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_base-svg-scraping-BaseSvgScrapingService.html b/module-core_base-service_base-svg-scraping-BaseSvgScrapingService.html new file mode 100644 index 0000000000..a114b8d660 --- /dev/null +++ b/module-core_base-service_base-svg-scraping-BaseSvgScrapingService.html @@ -0,0 +1,774 @@ + + + + + JSDoc: Class: BaseSvgScrapingService + + + + + + + + + + +
+ +

Class: BaseSvgScrapingService

+ + + + + + +
+ +
+ +

(abstract) + core/base-service/base-svg-scraping~BaseSvgScrapingService()

+ +

Services which scrape data from another SVG badge +should extend BaseSvgScrapingService

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

(abstract) new BaseSvgScrapingService()

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

(static) valueFromSvgBadge(svg, valueMatcheropt) → {string}

+ + + + + + +
+

Extract a value from SVG

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
svg + + +string + + + + + + + + + + + +

SVG to parse

valueMatcher + + +RegExp + + + + + + <optional>
+ + + + + +
+ + defaultValueMatcher + +

RegExp to match the value we want to parse from the SVG

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

Matched value

+
+ + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

(async) _requestSvg(attrs) → {object}

+ + + + + + +
+

Request data from an endpoint serving SVG, +parse a value from it and validate against a schema

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
attrs + + +object + + + +

Refer to individual attrs

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
schema + + +Joi + + + + + + + + + + + +

Joi schema to validate the response against

valueMatcher + + +RegExp + + + + + + + + + + + +

RegExp to match the value we want to parse from the SVG

url + + +string + + + + + + + + + + + +

URL to request

options + + +object + + + + + + <optional>
+ + + + + +
+ + {} + +

Options to pass to request. See +documentation

errorMessages + + +object + + + + + + <optional>
+ + + + + +
+ + {} + +

Key-value map of status codes +and custom error messages e.g: { 404: 'package not found' }. +This can be used to extend or override the +default

+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

Parsed response

+
+ + + +
+
+ Type +
+
+ +object + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_base-svg-scraping.html b/module-core_base-service_base-svg-scraping.html new file mode 100644 index 0000000000..610654aeb7 --- /dev/null +++ b/module-core_base-service_base-svg-scraping.html @@ -0,0 +1,92 @@ + + + + + JSDoc: Module: core/base-service/base-svg-scraping + + + + + + + + + + +
+ +

Module: core/base-service/base-svg-scraping

+ + + + + + +
+ +
+ + + +
+ + + +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_base-xml-BaseXmlService.html b/module-core_base-service_base-xml-BaseXmlService.html new file mode 100644 index 0000000000..8b3517bc6d --- /dev/null +++ b/module-core_base-service_base-xml-BaseXmlService.html @@ -0,0 +1,566 @@ + + + + + JSDoc: Class: BaseXmlService + + + + + + + + + + +
+ +

Class: BaseXmlService

+ + + + + + +
+ +
+ +

(abstract) + core/base-service/base-xml~BaseXmlService()

+ +

Services which query a XML endpoint should extend BaseXmlService

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

(abstract) new BaseXmlService()

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

(async) _requestXml(attrs) → {object}

+ + + + + + +
+

Request data from an upstream API serving XML, +parse it and validate against a schema

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
attrs + + +object + + + +

Refer to individual attrs

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
schema + + +Joi + + + + + + + + + + + +

Joi schema to validate the response against

url + + +string + + + + + + + + + + + +

URL to request

options + + +object + + + + + + <optional>
+ + + + + +
+ + {} + +

Options to pass to request. See +documentation

errorMessages + + +object + + + + + + <optional>
+ + + + + +
+ + {} + +

Key-value map of status codes +and custom error messages e.g: { 404: 'package not found' }. +This can be used to extend or override the +default

parserOptions + + +object + + + + + + <optional>
+ + + + + +
+ + {} + +

Options to pass to fast-xml-parser. See +documentation

+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

Parsed response

+
+ + + +
+
+ Type +
+
+ +object + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_base-xml.html b/module-core_base-service_base-xml.html new file mode 100644 index 0000000000..ba62737ccc --- /dev/null +++ b/module-core_base-service_base-xml.html @@ -0,0 +1,92 @@ + + + + + JSDoc: Module: core/base-service/base-xml + + + + + + + + + + +
+ +

Module: core/base-service/base-xml

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + +

Classes

+ +
+
BaseXmlService
+
+
+ + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_base-yaml-BaseYamlService.html b/module-core_base-service_base-yaml-BaseYamlService.html new file mode 100644 index 0000000000..bc0a9351e6 --- /dev/null +++ b/module-core_base-service_base-yaml-BaseYamlService.html @@ -0,0 +1,563 @@ + + + + + JSDoc: Class: BaseYamlService + + + + + + + + + + +
+ +

Class: BaseYamlService

+ + + + + + +
+ +
+ +

(abstract) + core/base-service/base-yaml~BaseYamlService()

+ +

Services which query a YAML endpoint should extend BaseYamlService

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

(abstract) new BaseYamlService()

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

(async) _requestYaml(attrs) → {object}

+ + + + + + +
+

Request data from an upstream API serving YAML, +parse it and validate against a schema

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
attrs + + +object + + + +

Refer to individual attrs

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
schema + + +Joi + + + + + + + + + + + +

Joi schema to validate the response against

url + + +string + + + + + + + + + + + +

URL to request

options + + +object + + + + + + <optional>
+ + + + + +
+ + {} + +

Options to pass to request. See +documentation

errorMessages + + +object + + + + + + <optional>
+ + + + + +
+ + {} + +

Key-value map of status codes +and custom error messages e.g: { 404: 'package not found' }. +This can be used to extend or override the +default

encoding + + +object + + + + + + <optional>
+ + + + + +
+ + 'utf8' + +

Character encoding

+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

Parsed response

+
+ + + +
+
+ Type +
+
+ +object + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_base-yaml.html b/module-core_base-service_base-yaml.html new file mode 100644 index 0000000000..c615fd1444 --- /dev/null +++ b/module-core_base-service_base-yaml.html @@ -0,0 +1,92 @@ + + + + + JSDoc: Module: core/base-service/base-yaml + + + + + + + + + + +
+ +

Module: core/base-service/base-yaml

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + +

Classes

+ +
+
BaseYamlService
+
+
+ + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_base.html b/module-core_base-service_base.html new file mode 100644 index 0000000000..d112f84d9c --- /dev/null +++ b/module-core_base-service_base.html @@ -0,0 +1,1175 @@ + + + + + JSDoc: Module: core/base-service/base + + + + + + + + + + +
+ +

Module: core/base-service/base

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + +

Classes

+ +
+
BaseService
+
+
+ + + + + + + + + + + + + +

Type Definitions

+ + + +

Auth

+ + + + + + +
Type:
+
    +
  • + +object + + +
  • +
+ + + + + +
Properties:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
userKey + + +string + + + +

(Optional) The key from privateConfig to use as the username.

passKey + + +string + + + +

(Optional) The key from privateConfig to use as the password. +If auth is configured, either userKey or passKey is required.

isRequired + + +string + + + +

(Optional) If true, the service will return NotFound unless the +configured credentials are present.

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

Badge

+ + + + +
+

Badge Object, validated against serviceDataSchema

+
+ + + +
Type:
+
    +
  • + +object + + +
  • +
+ + + + + +
Properties:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
isError + + +boolean + + + +

(Optional)

label + + +string + + + +

(Optional)

message + + +string +| + +number + + + +
color + + +string + + + +

(Optional)

link + + +Array.<string> + + + +

(Optional)

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

DefaultBadgeData

+ + + + +
+

Default badge properties, validated against defaultBadgeDataSchema

+
+ + + +
Type:
+
    +
  • + +object + + +
  • +
+ + + + + +
Properties:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
label + + +string + + + +

(Optional)

color + + +string + + + +

(Optional)

labelColor + + +string + + + +

(Optional)

namedLogo + + +string + + + +

(Optional)

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

Example

+ + + + + + +
Type:
+
    +
  • + +object + + +
  • +
+ + + + + +
Properties:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
title + + +string + + + +

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 + + +object + + + +

An object containing the values of named parameters to +substitute into the compiled route pattern.

queryParams + + +object + + + +

An object containing query parameters to include in the +example URLs. For alphanumeric query parameters, specify a string value. +For boolean query parameters, specify null.

pattern + + +string + + + +

The route pattern to compile. Defaults to this.route.pattern.

staticPreview + + +object + + + +

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.

keywords + + +Array.<string> + + + +

Additional keywords, other than words in the title. This helps +users locate relevant badges.

documentation + + +string + + + +

An HTML string that is included in the badge popup.

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

Route

+ + + + + + +
Type:
+
    +
  • + +object + + +
  • +
+ + + + + +
Properties:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
base + + +string + + + +

(Optional) The base path of the routes for this service. +This is used as a prefix.

pattern + + +string + + + +

A path-to-regexp pattern defining the route pattern and param names +See https://www.npmjs.com/package/path-to-regexp

format + + +RegExp + + + +

Deprecated: Regular expression to use for routes for this service's badges +Use pattern instead

capture + + +Array.<string> + + + +

Deprecated: Array of names for the capture groups in the regular +expression. The handler will be passed an object containing +the matches. +Use pattern instead

queryParamSchema + + +Joi.object + + + +

(Optional) A Joi schema (Joi.object({ ... }).required()) +for the query param object. If you know a parameter +will never receive a numeric string, you can use +Joi.string(). Because of quirks in Scoutcamp and Joi, +alphanumeric strings should be declared using +Joi.alternatives().try(Joi.string(), Joi.number()), +otherwise a value like ?success_color=999 will fail. +A parameter requiring a numeric string can use +Joi.number(). A parameter that receives only non-numeric +strings can use Joi.string(). A parameter that never +receives numeric can use Joi.string(). A boolean +parameter should use Joi.equal('') and will receive an +empty string on e.g. ?compact_message and undefined +when the parameter is absent. (Note that in, +examples.queryParams boolean query params should be given +null values.)

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_errors-Deprecated.html b/module-core_base-service_errors-Deprecated.html new file mode 100644 index 0000000000..c5fdb5229c --- /dev/null +++ b/module-core_base-service_errors-Deprecated.html @@ -0,0 +1,220 @@ + + + + + JSDoc: Class: Deprecated + + + + + + + + + + +
+ +

Class: Deprecated

+ + + + + + +
+ +
+ +

+ core/base-service/errors~Deprecated(props)

+ +

Throw this error to indicate that a service is deprecated or removed

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new Deprecated(props)

+ + + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +module:core/base-service/errors~RuntimeErrorProps + + + +

Refer to individual attrs

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_errors-ImproperlyConfigured.html b/module-core_base-service_errors-ImproperlyConfigured.html new file mode 100644 index 0000000000..5f513173cc --- /dev/null +++ b/module-core_base-service_errors-ImproperlyConfigured.html @@ -0,0 +1,220 @@ + + + + + JSDoc: Class: ImproperlyConfigured + + + + + + + + + + +
+ +

Class: ImproperlyConfigured

+ + + + + + +
+ +
+ +

+ core/base-service/errors~ImproperlyConfigured(props)

+ +

Throw this error when required credentials are missing

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new ImproperlyConfigured(props)

+ + + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +module:core/base-service/errors~RuntimeErrorProps + + + +

Refer to individual attrs

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_errors-Inaccessible.html b/module-core_base-service_errors-Inaccessible.html new file mode 100644 index 0000000000..4c9fdbfb0f --- /dev/null +++ b/module-core_base-service_errors-Inaccessible.html @@ -0,0 +1,221 @@ + + + + + JSDoc: Class: Inaccessible + + + + + + + + + + +
+ +

Class: Inaccessible

+ + + + + + +
+ +
+ +

+ core/base-service/errors~Inaccessible(props)

+ +

Throw this if we can't contact an upstream API +or to wrap a 5XX response

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new Inaccessible(props)

+ + + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +module:core/base-service/errors~RuntimeErrorProps + + + +

Refer to individual attrs

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_errors-InvalidParameter.html b/module-core_base-service_errors-InvalidParameter.html new file mode 100644 index 0000000000..e37bd1211a --- /dev/null +++ b/module-core_base-service_errors-InvalidParameter.html @@ -0,0 +1,221 @@ + + + + + JSDoc: Class: InvalidParameter + + + + + + + + + + +
+ +

Class: InvalidParameter

+ + + + + + +
+ +
+ +

+ core/base-service/errors~InvalidParameter(props)

+ +

Throw this error when a user supplied input or parameter +is invalid or unexpected

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new InvalidParameter(props)

+ + + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +module:core/base-service/errors~RuntimeErrorProps + + + +

Refer to individual attrs

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_errors-InvalidResponse.html b/module-core_base-service_errors-InvalidResponse.html new file mode 100644 index 0000000000..2228e6f518 --- /dev/null +++ b/module-core_base-service_errors-InvalidResponse.html @@ -0,0 +1,220 @@ + + + + + JSDoc: Class: InvalidResponse + + + + + + + + + + +
+ +

Class: InvalidResponse

+ + + + + + +
+ +
+ +

+ core/base-service/errors~InvalidResponse(props)

+ +

Throw this to wrap an invalid or unexpected response from an upstream API

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new InvalidResponse(props)

+ + + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +module:core/base-service/errors~RuntimeErrorProps + + + +

Refer to individual attrs

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_errors-NotFound.html b/module-core_base-service_errors-NotFound.html new file mode 100644 index 0000000000..6235a8ff83 --- /dev/null +++ b/module-core_base-service_errors-NotFound.html @@ -0,0 +1,220 @@ + + + + + JSDoc: Class: NotFound + + + + + + + + + + +
+ +

Class: NotFound

+ + + + + + +
+ +
+ +

+ core/base-service/errors~NotFound(props)

+ +

Throw this to wrap a 404 or other 'not found' response from an upstream API

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new NotFound(props)

+ + + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +module:core/base-service/errors~RuntimeErrorProps + + + +

Refer to individual attrs

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_errors-ShieldsRuntimeError.html b/module-core_base-service_errors-ShieldsRuntimeError.html new file mode 100644 index 0000000000..1a663a7e06 --- /dev/null +++ b/module-core_base-service_errors-ShieldsRuntimeError.html @@ -0,0 +1,393 @@ + + + + + JSDoc: Class: ShieldsRuntimeError + + + + + + + + + + +
+ +

Class: ShieldsRuntimeError

+ + + + + + +
+ +
+ +

(abstract) + core/base-service/errors~ShieldsRuntimeError(props, message)

+ +

Base error class

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

(abstract) new ShieldsRuntimeError(props, message)

+ + + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +module:core/base-service/errors~RuntimeErrorProps + + + +

Refer to individual attrs

message + + +string + + + +

Exception message for debug purposes

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

(abstract) defaultPrettyMessage :string

+ + + + +
+

Default message for this exception if none is specified. +Implementations of ShieldsRuntimeError should implement this method.

+
+ + + +
Type:
+
    +
  • + +string + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

name :string

+ + + + +
+

Name of the class. Implementations of ShieldsRuntimeError +should override this method.

+
+ + + +
Type:
+
    +
  • + +string + + +
  • +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_errors.html b/module-core_base-service_errors.html new file mode 100644 index 0000000000..30876bcaa1 --- /dev/null +++ b/module-core_base-service_errors.html @@ -0,0 +1,363 @@ + + + + + JSDoc: Module: core/base-service/errors + + + + + + + + + + +
+ +

Module: core/base-service/errors

+ + + + + + +
+ +
+ + + + + +
+ +
+
+ + +

Standard exceptions for handling error cases

+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +

Classes

+ +
+
Deprecated
+
+ +
ImproperlyConfigured
+
+ +
Inaccessible
+
+ +
InvalidParameter
+
+ +
InvalidResponse
+
+ +
NotFound
+
+ +
ShieldsRuntimeError
+
+
+ + + + + + + + + + + + + +

Type Definitions

+ + + +

RuntimeErrorProps

+ + + + + + +
Type:
+
    +
  • + +object + + +
  • +
+ + + + + +
Properties:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
underlyingError + + +Error + + + +

Exception we are wrapping (Optional)

response + + +object + + + +

Response from an upstream API to provide +context for the error (Optional)

prettyMessage + + +string + + + +

User-facing error message to override the +value of defaultPrettyMessage(). This is the text that will appear on the +badge when we catch and render the exception (Optional)

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_base-service_graphql.html b/module-core_base-service_graphql.html new file mode 100644 index 0000000000..92c5dd78f2 --- /dev/null +++ b/module-core_base-service_graphql.html @@ -0,0 +1,263 @@ + + + + + JSDoc: Module: core/base-service/graphql + + + + + + + + + + +
+ +

Module: core/base-service/graphql

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

(inner) mergeQueries(…queries) → {object}

+ + + + + + +
+

Utility function to merge two graphql queries together +This is basically copied from +graphql-query-merge +but can't use that due to incorrect packaging.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
queries + + +object + + + + + + + + + + <repeatable>
+ +

queries to merge

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

merged query

+
+ + + +
+
+ Type +
+
+ +object + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_server_server-Server.html b/module-core_server_server-Server.html new file mode 100644 index 0000000000..aa7fd6222c --- /dev/null +++ b/module-core_server_server-Server.html @@ -0,0 +1,690 @@ + + + + + JSDoc: Class: Server + + + + + + + + + + +
+ +

Class: Server

+ + + + + + +
+ +
+ +

+ core/server/server~Server(config)

+ +

The Server is based on the web framework Scoutcamp. It creates +an http server, sets up helpers for token persistence and monitoring. +Then it loads all the services, injecting dependencies as it +asks each one to register its route with Scoutcamp.

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new Server(config)

+ + + + + + +
+

Badge Server Constructor

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
config + + +object + + + +

Configuration object read from config yaml files +by https://www.npmjs.com/package/config and validated against +publicConfigSchema and privateConfigSchema

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

registerErrorHandlers()

+ + + + + + +
+

Set up Scoutcamp routes for 404/not found responses

+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

registerRedirects()

+ + + + + + +
+

Set up a couple of redirects: +One for the raster badges. +Another to redirect the base URL / +(we use this to redirect https://img.shields.io/ +to https://shields.io/ )

+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

registerServices()

+ + + + + + +
+

Iterate all the service classes defined in /services, +load each service and register a Scoutcamp route for each service.

+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) start()

+ + + + + + +
+

Start the HTTP server: +Bootstrap Scoutcamp, +Register handlers, +Start listening for requests on this.baseUrl()

+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(async) stop()

+ + + + + + +
+

Stop the HTTP server and clean up helpers

+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_server_server.html b/module-core_server_server.html new file mode 100644 index 0000000000..2487e73dfd --- /dev/null +++ b/module-core_server_server.html @@ -0,0 +1,92 @@ + + + + + JSDoc: Module: core/server/server + + + + + + + + + + +
+ +

Module: core/server/server

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + +

Classes

+ +
+
Server
+
+
+ + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_service-test-runner_create-service-tester.html b/module-core_service-test-runner_create-service-tester.html new file mode 100644 index 0000000000..dc31df2e6f --- /dev/null +++ b/module-core_service-test-runner_create-service-tester.html @@ -0,0 +1,203 @@ + + + + + JSDoc: Module: core/service-test-runner/create-service-tester + + + + + + + + + + +
+ +

Module: core/service-test-runner/create-service-tester

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

(inner) createServiceTester() → {module:core/service-test-runner/service-tester~ServiceTester}

+ + + + + + +
+

Automatically create a ServiceTester.

+

When run from e.g. gem-rank.tester.js, this will create a tester that +attaches to the service found in gem-rank.service.js.

+

This can't be used for .service.js files which export more than one +service.

+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

ServiceTester instance

+
+ + + +
+
+ Type +
+
+ +module:core/service-test-runner/service-tester~ServiceTester + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_service-test-runner_icedfrisby-shields.html b/module-core_service-test-runner_icedfrisby-shields.html new file mode 100644 index 0000000000..fb0deaa2e3 --- /dev/null +++ b/module-core_service-test-runner_icedfrisby-shields.html @@ -0,0 +1,259 @@ + + + + + JSDoc: Module: core/service-test-runner/icedfrisby-shields + + + + + + + + + + +
+ +

Module: core/service-test-runner/icedfrisby-shields

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

(inner) factory(superclass) → {function}

+ + + + + + +
+

Factory which wraps an "icedfrisby-nock" with some additional functionality:

+
    +
  • check if a request was intercepted
  • +
  • set expectations on the badge JSON response
  • +
+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
superclass + + +function + + + +

class to extend

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

wrapped class

+
+ + + +
+
+ Type +
+
+ +function + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_service-test-runner_infer-pull-request.html b/module-core_service-test-runner_infer-pull-request.html new file mode 100644 index 0000000000..36a9717299 --- /dev/null +++ b/module-core_service-test-runner_infer-pull-request.html @@ -0,0 +1,479 @@ + + + + + JSDoc: Module: core/service-test-runner/infer-pull-request + + + + + + + + + + +
+ +

Module: core/service-test-runner/infer-pull-request

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

(inner) inferPullRequest(envopt) → {module:core/service-test-runner/infer-pull-request~PullRequest}

+ + + + + + +
+

When called inside a CI build, infer the details +of a pull request from the environment variables.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
env + + +object + + + + + + <optional>
+ + + + + +
+ + process.env + +

Environment variables

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

Pull Request

+
+ + + +
+
+ Type +
+
+ +module:core/service-test-runner/infer-pull-request~PullRequest + + +
+
+ + + + + + + + + + + +

Type Definitions

+ + + +

PullRequest

+ + + + +
+

Pull Request

+
+ + + + + + + +
Properties:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
pr.baseUrl + + +string + + + +

(returned for travis CI only)

owner + + +string + + + +
repo + + +string + + + +
pullRequest + + +string + + + +

PR/issue number

slug + + +string + + + +

owner/repo/#pullRequest

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_service-test-runner_runner-Runner.html b/module-core_service-test-runner_runner-Runner.html new file mode 100644 index 0000000000..149b5e8f61 --- /dev/null +++ b/module-core_service-test-runner_runner-Runner.html @@ -0,0 +1,577 @@ + + + + + JSDoc: Class: Runner + + + + + + + + + + +
+ +

Class: Runner

+ + + + + + +
+ +
+ +

+ core/service-test-runner/runner~Runner()

+ +

Load a collection of ServiceTester objects and register them with Mocha.

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new Runner()

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

beforeEach()

+ + + + + + +
+

Function to invoke before each test. This is a stub which can be +overridden on instances.

+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

only(services)

+ + + + + + +
+

Limit the test run to the specified services.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
services + + +Array.<string> + + + +

An array of service id prefixes to run

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

prepare()

+ + + + + + +
+

Prepare the runner by loading up all the ServiceTester objects.

+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

toss()

+ + + + + + +
+

Register the tests with Mocha.

+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_service-test-runner_runner.html b/module-core_service-test-runner_runner.html new file mode 100644 index 0000000000..63bbe7b7f9 --- /dev/null +++ b/module-core_service-test-runner_runner.html @@ -0,0 +1,92 @@ + + + + + JSDoc: Module: core/service-test-runner/runner + + + + + + + + + + +
+ +

Module: core/service-test-runner/runner

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + +

Classes

+ +
+
Runner
+
+
+ + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_service-test-runner_service-tester-ServiceTester.html b/module-core_service-test-runner_service-tester-ServiceTester.html new file mode 100644 index 0000000000..bd6da5ea38 --- /dev/null +++ b/module-core_service-test-runner_service-tester-ServiceTester.html @@ -0,0 +1,1135 @@ + + + + + JSDoc: Class: ServiceTester + + + + + + + + + + +
+ +

Class: ServiceTester

+ + + + + + +
+ +
+ +

+ core/service-test-runner/service-tester~ServiceTester(attrs)

+ +

Encapsulate a suite of tests. Create new tests using create() and register +them with Mocha using toss().

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new ServiceTester(attrs)

+ + + + + + +
+

Service Tester Constructor

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
attrs + + +object + + + +

Refer to individual attrs

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
id + + +string + + + +

Specifies which tests to run from the CLI or pull requests

title + + +string + + + +

Prints in the Mocha output

pathPrefix + + +string + + + +

Prefix which is automatically prepended to each tested URI. +The default is /${attrs.id}.

+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

(static) forServiceClass(ServiceClass) → {module:core/service-test-runner/service-tester~ServiceTester}

+ + + + + + +
+

Construct a ServiceTester instance for a single service class

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
ServiceClass + + +function + + + +

A class that extends base-service/base.BaseService

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

ServiceTester for ServiceClass

+
+ + + +
+
+ Type +
+
+ +module:core/service-test-runner/service-tester~ServiceTester + + +
+
+ + + + + + + + + + + + + +

beforeEach()

+ + + + + + +
+

Invoked before each test. This is a stub which can be overridden on +instances.

+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

create(msg) → {module:icedfrisby~IcedFrisby}

+ + + + + + +
+

Create a new test. The hard work is delegated to IcedFrisby. +https://github.com/MarkHerhold/IcedFrisby/#show-me-some-code

+

Note: The caller should not invoke toss() on the Frisby chain, as it's +invoked automatically by the tester.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
msg + + +string + + + +

The name of the test

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

IcedFrisby instance

+
+ + + +
+
+ Type +
+
+ +module:icedfrisby~IcedFrisby + + +
+
+ + + + + + + + + + + + + +

only()

+ + + + + + +
+

Run only this tester. This can be invoked using the --only argument to +the CLI, or directly on the tester.

+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

toss(attrs)

+ + + + + + +
+

Register the tests with Mocha.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
attrs + + +object + + + +

Refer to individual attrs

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
baseUrl + + +string + + + +

base URL for test server

skipIntercepted + + +boolean + + + +

skip tests which intercept requests

retry + + +object + + + +

retry configuration

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
count + + +number + + + +

number of times to retry test

backoff + + +number + + + +

number of milliseconds to add to the wait between each retry

+ +
+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_service-test-runner_service-tester.html b/module-core_service-test-runner_service-tester.html new file mode 100644 index 0000000000..8cbf30b1af --- /dev/null +++ b/module-core_service-test-runner_service-tester.html @@ -0,0 +1,92 @@ + + + + + JSDoc: Module: core/service-test-runner/service-tester + + + + + + + + + + +
+ +

Module: core/service-test-runner/service-tester

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + +

Classes

+ +
+
ServiceTester
+
+
+ + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_service-test-runner_services-for-title.html b/module-core_service-test-runner_services-for-title.html new file mode 100644 index 0000000000..c0fb86e1b9 --- /dev/null +++ b/module-core_service-test-runner_services-for-title.html @@ -0,0 +1,251 @@ + + + + + JSDoc: Module: core/service-test-runner/services-for-title + + + + + + + + + + +
+ +

Module: core/service-test-runner/services-for-title

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

(inner) servicesForTitle(title) → {Array.<string>}

+ + + + + + +
+

Given a pull request title like +'[Travis Sonar] Support user token authentication' +extract the list of service names in square brackets +as an array of strings.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
title + + +string + + + +

Pull Request title

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

Array of service names

+
+ + + +
+
+ Type +
+
+ +Array.<string> + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_token-pooling_token-pool-Token.html b/module-core_token-pooling_token-pool-Token.html new file mode 100644 index 0000000000..4958995dac --- /dev/null +++ b/module-core_token-pooling_token-pool-Token.html @@ -0,0 +1,724 @@ + + + + + JSDoc: Class: Token + + + + + + + + + + +
+ +

Class: Token

+ + + + + + +
+ +
+ +

+ core/token-pooling/token-pool~Token(id, data, usesRemaining, nextReset)

+ +

Encapsulate a rate-limited token, with a user-provided ID and user-provided data.

+

Each token has a notion of the number of uses remaining until exhausted, +and the next reset time, when it can be used again even if it's exhausted.

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new Token(id, data, usesRemaining, nextReset)

+ + + + + + +
+

Token Constructor

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
id + + +string + + + +

token string

data + + +* + + + +

reserved for future use

usesRemaining + + +number + + + +

Number of uses remaining until the token is exhausted

nextReset + + +number + + + +

Time when the token can be used again even if it's exhausted (unix timestamp)

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

freeze()

+ + + + + + +
+

Freeze the uses remaining and next reset values. Helpful for keeping +stable ordering for a valid priority queue.

+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

invalidate()

+ + + + + + +
+

Indicate that the token should no longer be used.

+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

unfreeze()

+ + + + + + +
+

Unfreeze the uses remaining and next reset values.

+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

update(usesRemaining, nextReset)

+ + + + + + +
+

Update the uses remaining and next reset time for a token.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
usesRemaining + + +number + + + +

Number of uses remaining until the token is exhausted

nextReset + + +number + + + +

Time when the token can be used again even if it's exhausted (unix timestamp)

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_token-pooling_token-pool-TokenPool.html b/module-core_token-pooling_token-pool-TokenPool.html new file mode 100644 index 0000000000..32fcc099fb --- /dev/null +++ b/module-core_token-pooling_token-pool-TokenPool.html @@ -0,0 +1,908 @@ + + + + + JSDoc: Class: TokenPool + + + + + + + + + + +
+ +

Class: TokenPool

+ + + + + + +
+ +
+ +

+ core/token-pooling/token-pool~TokenPool(batchSize)

+ +

Encapsulate a collection of rate-limited tokens and choose the next +available token when one is needed.

+

Designed for the Github API, though may be also useful with other rate- +limited APIs.

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new TokenPool(batchSize)

+ + + + + + +
+

TokenPool Constructor

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
batchSize + + +number + + + +

The maximum number of times we use each token before moving +on to the next one.

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

(static) compareTokens(first, second) → {module:core/token-pooling/token-pool~Token}

+ + + + + + +
+

compareTokens

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
first + + +module:core/token-pooling/token-pool~Token + + + +

first token to compare

second + + +module:core/token-pooling/token-pool~Token + + + +

second token to compare

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

The token whose current rate allotment is expiring soonest.

+
+ + + +
+
+ Type +
+
+ +module:core/token-pooling/token-pool~Token + + +
+
+ + + + + + + + + + + + + +

add(id, data, usesRemaining, nextReset) → {boolean}

+ + + + + + +
+

Add a token with user-provided ID and data.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
id + + +string + + + +

token string

data + + +* + + + +

reserved for future use

usesRemaining + + +number + + + +

Number of uses remaining until the token is exhausted

nextReset + + +number + + + +

Time when the token can be used again even if it's exhausted (unix timestamp)

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

Was the token added to the pool?

+
+ + + +
+
+ Type +
+
+ +boolean + + +
+
+ + + + + + + + + + + + + +

forEach(callback)

+ + + + + + +
+

Iterate over all valid tokens.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
callback + + +function + + + +

function to execute on each valid token

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

next() → {module:core/token-pooling/token-pool~Token}

+ + + + + + +
+

Obtain the next available token, returning null if no tokens are +available.

+

Tokens are initially pulled from a FIFO queue. The first token is used +for a batch of requests, then returned to the queue to give those +requests the opportunity to complete. The next token is used for the next +batch of requests.

+

This strategy allows a token to be used for concurrent requests, not just +sequential request, and simplifies token recovery after errored and timed +out requests.

+

By the time the original token re-emerges, its requests should have long +since completed. Even if a couple them are still running, they can +reasonably be ignored. The uses remaining won't be 100% correct, but +that's fine, because Shields uses only 75%

+

The process continues until an exhausted token is pulled from the FIFO +queue. At that time it's placed in the priority queue based on its +scheduled reset time. To ensure the priority queue works as intended, +the scheduled reset time is frozen then.

+

After obtaining a token using next(), invoke update() on it to set a +new use-remaining count and next-reset time. Invoke invalidate() to +indicate it should not be reused.

+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

token

+
+ + + +
+
+ Type +
+
+ +module:core/token-pooling/token-pool~Token + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-core_token-pooling_token-pool.html b/module-core_token-pooling_token-pool.html new file mode 100644 index 0000000000..3af9dd0f81 --- /dev/null +++ b/module-core_token-pooling_token-pool.html @@ -0,0 +1,258 @@ + + + + + JSDoc: Module: core/token-pooling/token-pool + + + + + + + + + + +
+ +

Module: core/token-pooling/token-pool

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + +

Classes

+ +
+
Token
+
+ +
TokenPool
+
+
+ + + + + + + + + + + +

Methods

+ + + + + + + +

(inner) sanitizeToken(id) → {string}

+ + + + + + +
+

Compute a one-way hash of the input string.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
id + + +string + + + +

token

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

hash

+
+ + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-services_dynamic_json-path.html b/module-services_dynamic_json-path.html new file mode 100644 index 0000000000..c0a2bf2819 --- /dev/null +++ b/module-services_dynamic_json-path.html @@ -0,0 +1,551 @@ + + + + + JSDoc: Module: services/dynamic/json-path + + + + + + + + + + +
+ +

Module: services/dynamic/json-path

+ + + + + + +
+ +
+ + + + + +
+ +
+
+ + + + + + + + + +

(require("services/dynamic/json-path"))(superclass) → {function}

+ + + + + + +
+

Dynamic service class factory which wraps module:core/base-service/base~BaseService with support of JSONPath.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
superclass + + +function + + + +

class to extend

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

wrapped class

+
+ + + +
+
+ Type +
+
+ +function + + +
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

(async) fetch(attrs) → {object}

+ + + + + + +
+

Request data from an upstream API, transform it to JSON and validate against a schema

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
attrs + + +object + + + +

Refer to individual attrs

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
schema + + +Joi + + + + + + + + + + + +

Joi schema to validate the response transformed to JSON

url + + +string + + + + + + + + + + + +

URL to request

errorMessages + + +object + + + + + + <optional>
+ + + + + +
+ + {} + +

Key-value map of status codes +and custom error messages e.g: { 404: 'package not found' }. +This can be used to extend or override the +default

+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

Parsed response

+
+ + + +
+
+ Type +
+
+ +object + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-services_steam_steam-base-BaseSteamAPI.html b/module-services_steam_steam-base-BaseSteamAPI.html new file mode 100644 index 0000000000..e0517c2542 --- /dev/null +++ b/module-services_steam_steam-base-BaseSteamAPI.html @@ -0,0 +1,389 @@ + + + + + JSDoc: Class: BaseSteamAPI + + + + + + + + + + +
+ +

Class: BaseSteamAPI

+ + + + + + +
+ +
+ +

+ services/steam/steam-base~BaseSteamAPI()

+ +

The steam api is based like /{interface}/{method}/v{version}/

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new BaseSteamAPI()

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

(static) interf

+ + + + +
+

Steam API Interface

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + +

(static) method

+ + + + +
+

Steam API Method

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + +

(static) version

+ + + + +
+

Steam API Version

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module-services_steam_steam-base.html b/module-services_steam_steam-base.html new file mode 100644 index 0000000000..215147689d --- /dev/null +++ b/module-services_steam_steam-base.html @@ -0,0 +1,92 @@ + + + + + JSDoc: Module: services/steam/steam-base + + + + + + + + + + +
+ +

Module: services/steam/steam-base

+ + + + + + +
+ +
+ + + +
+ +
+
+ + + + + +
+ + + + + + +

Classes

+ +
+
BaseSteamAPI
+
+
+ + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/module.exports.html b/module.exports.html new file mode 100644 index 0000000000..ba935df87f --- /dev/null +++ b/module.exports.html @@ -0,0 +1,643 @@ + + + + + JSDoc: Class: exports + + + + + + + + + + +
+ +

Class: exports

+ + + + + + +
+ +
+ +

exports()

+ +

Criterion Badge Service

+

Support and Contact:

+
    +
  • https://github.com/chmoder/api.criterion.dev
  • +
+

API Documentation:

+
    +
  • https://app.swaggerhub.com/apis-docs/chmoder/Criterion.dev
  • +
+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new exports()

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +

Methods

+ + + + + + + +

createNumRequestCounter(attrs) → {object}

+ + + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
attrs + + +object + + + +

Refer to individual attrs

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
category + + +string + + + +

e.g: 'build'

serviceFamily + + +string + + + +

e.g: 'npm'

name + + +string + + + +

e.g: 'NpmVersion'

+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

{ inc() {} }.

+
+ + + +
+
+ Type +
+
+ +object + + +
+
+ + + + + + + + + + + + + +

getColor(attrs) → {string}

+ + + + + + +
+

Return color for active, maintenance and archived statuses, which were the three +example keywords used in Netflix's open-source meetup. +See https://slideshare.net/aspyker/netflix-open-source-meetup-season-4-episode-1 +Other keywords are possible, but will appear in grey.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
attrs + + +object + + + +

Refer to individual attrs

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
status + + +string + + + +

Specifies the current maintenance status

+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+

color

+
+ + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/scripts/linenumber.js b/scripts/linenumber.js new file mode 100644 index 0000000000..4354785cea --- /dev/null +++ b/scripts/linenumber.js @@ -0,0 +1,25 @@ +/*global document */ +(() => { + const source = document.getElementsByClassName('prettyprint source linenums'); + let i = 0; + let lineNumber = 0; + let lineId; + let lines; + let totalLines; + let anchorHash; + + if (source && source[0]) { + anchorHash = document.location.hash.substring(1); + lines = source[0].getElementsByTagName('li'); + totalLines = lines.length; + + for (; i < totalLines; i++) { + lineNumber++; + lineId = `line${lineNumber}`; + lines[i].id = lineId; + if (lineId === anchorHash) { + lines[i].className += ' selected'; + } + } + } +})(); diff --git a/scripts/prettify/Apache-License-2.0.txt b/scripts/prettify/Apache-License-2.0.txt new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/scripts/prettify/Apache-License-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/scripts/prettify/lang-css.js b/scripts/prettify/lang-css.js new file mode 100644 index 0000000000..041e1f5906 --- /dev/null +++ b/scripts/prettify/lang-css.js @@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", +/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); diff --git a/scripts/prettify/prettify.js b/scripts/prettify/prettify.js new file mode 100644 index 0000000000..eef5ad7e6a --- /dev/null +++ b/scripts/prettify/prettify.js @@ -0,0 +1,28 @@ +var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; +(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= +[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), +l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, +q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, +q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, +"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), +a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} +for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], +"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], +H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], +J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ +I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), +["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", +/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), +["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", +hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= +!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p + + + + JSDoc: Source: services/criterion/criterion.service.js + + + + + + + + + + +
+ +

Source: services/criterion/criterion.service.js

+ + + + + + +
+
+
'use strict'
+
+const Joi = require('joi')
+const { BaseJsonService } = require('..')
+const {
+  IMPROVED_STATUS,
+  NOT_FOUND_STATUS,
+  REGRESSED_STATUS,
+  NO_CHANGE_STATUS,
+} = require('./constants')
+
+const schema = Joi.string()
+  .allow(IMPROVED_STATUS, REGRESSED_STATUS, NO_CHANGE_STATUS)
+  .required()
+
+/**
+ * Criterion Badge Service
+ *
+ * Support and Contact:
+ * - https://github.com/chmoder/api.criterion.dev
+ *
+ * API Documentation:
+ * - https://app.swaggerhub.com/apis-docs/chmoder/Criterion.dev
+ */
+module.exports = class Criterion extends BaseJsonService {
+  static category = 'analysis'
+  static route = { base: 'criterion', pattern: ':user/:repo' }
+
+  static examples = [
+    {
+      title: 'Criterion',
+      namedParams: {
+        user: 'chmoder',
+        repo: 'data_vault',
+      },
+      staticPreview: this.render({ status: IMPROVED_STATUS }),
+    },
+  ]
+
+  static defaultBadgeData = { label: 'criterion' }
+
+  static render({ status }) {
+    let statusColor = 'lightgrey'
+
+    if (status === IMPROVED_STATUS) {
+      statusColor = 'brightgreen'
+    } else if (status === NO_CHANGE_STATUS) {
+      statusColor = 'green'
+    } else if (statusColor === REGRESSED_STATUS) {
+      statusColor = 'red'
+    }
+
+    return {
+      message: `${status}`,
+      color: statusColor,
+    }
+  }
+
+  async handle({ user, repo }) {
+    const status = await this._requestJson({
+      url: `https://api.criterion.dev/v1/${user}/${repo}/status`,
+      errorMessages: { 404: NOT_FOUND_STATUS },
+      schema,
+    })
+
+    return this.constructor.render({ status })
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Wed Dec 23 2020 18:45:19 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/services_dynamic_json-path.js.html b/services_dynamic_json-path.js.html new file mode 100644 index 0000000000..3014375440 --- /dev/null +++ b/services_dynamic_json-path.js.html @@ -0,0 +1,131 @@ + + + + + JSDoc: Source: services/dynamic/json-path.js + + + + + + + + + + +
+ +

Source: services/dynamic/json-path.js

+ + + + + + +
+
+
/**
+ * @module
+ */
+
+'use strict'
+
+const Joi = require('joi')
+const jp = require('jsonpath')
+const { renderDynamicBadge, errorMessages } = require('../dynamic-common')
+const { InvalidParameter, InvalidResponse } = require('..')
+
+/**
+ * Dynamic service class factory which wraps {@link module:core/base-service/base~BaseService} with support of {@link https://jsonpath.com/|JSONPath}.
+ *
+ * @param {Function} superclass class to extend
+ * @returns {Function} wrapped class
+ */
+module.exports = superclass =>
+  class extends superclass {
+    static category = 'dynamic'
+    static defaultBadgeData = { label: 'custom badge' }
+
+    /**
+     * Request data from an upstream API, transform it to JSON and validate against a schema
+     *
+     * @param {object} attrs Refer to individual attrs
+     * @param {Joi} attrs.schema Joi schema to validate the response transformed to JSON
+     * @param {string} attrs.url URL to request
+     * @param {object} [attrs.errorMessages={}] Key-value map of status codes
+     *    and custom error messages e.g: `{ 404: 'package not found' }`.
+     *    This can be used to extend or override the
+     *    [default](https://github.com/badges/shields/blob/master/services/dynamic-common.js#L8)
+     * @returns {object} Parsed response
+     */
+    async fetch({ schema, url, errorMessages }) {
+      throw new Error(
+        `fetch() function not implemented for ${this.constructor.name}`
+      )
+    }
+
+    async handle(namedParams, { url, query: pathExpression, prefix, suffix }) {
+      const data = await this.fetch({
+        schema: Joi.any(),
+        url,
+        errorMessages,
+      })
+
+      // JSONPath only works on objects and arrays.
+      // https://github.com/badges/shields/issues/4018
+      if (typeof data !== 'object') {
+        throw new InvalidResponse({
+          prettyMessage: 'resource must contain an object or array',
+        })
+      }
+
+      let values
+      try {
+        values = jp.query(data, pathExpression)
+      } catch (e) {
+        const { message } = e
+        if (
+          message.startsWith('Lexical error') ||
+          message.startsWith('Parse error') ||
+          message.includes('Unexpected token')
+        ) {
+          throw new InvalidParameter({
+            prettyMessage: 'unparseable jsonpath query',
+          })
+        } else {
+          throw e
+        }
+      }
+
+      if (!values.length) {
+        throw new InvalidResponse({ prettyMessage: 'no result' })
+      }
+
+      return renderDynamicBadge({ value: values, prefix, suffix })
+    }
+  }
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Wed Dec 23 2020 18:45:19 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/services_github_github-total-star.service.js.html b/services_github_github-total-star.service.js.html new file mode 100644 index 0000000000..a8f6c440dc --- /dev/null +++ b/services_github_github-total-star.service.js.html @@ -0,0 +1,297 @@ + + + + + JSDoc: Source: services/github/github-total-star.service.js + + + + + + + + + + +
+ +

Source: services/github/github-total-star.service.js

+ + + + + + +
+
+
'use strict'
+
+const Joi = require('joi')
+const gql = require('graphql-tag')
+const { nonNegativeInteger } = require('../validators')
+const { metric } = require('../text-formatters')
+const { GithubAuthV4Service } = require('./github-auth-service')
+const {
+  documentation: commonDocumentation,
+  transformErrors,
+} = require('./github-helpers')
+
+const MAX_REPO_LIMIT = 200
+
+const customDocumentation = `This badge takes into account up to <code>${MAX_REPO_LIMIT}</code> of the most starred repositories of given user / org.`
+
+const userDocumentation = `${commonDocumentation}
+<p>
+  <b>Note:</b><br>
+  1. ${customDocumentation}<br>
+  2. <code>affiliations</code> query param accepts three values (must be UPPER case) <code>OWNER</code>, <code>COLLABORATOR</code>, <code>ORGANIZATION_MEMBER</code>.
+  One can pass comma separated combinations of these values (no spaces) e.g. <code>OWNER,COLLABORATOR</code> or <code>OWNER,COLLABORATOR,ORGANIZATION_MEMBER</code>.
+  Default value is <code>OWNER</code>. See the explanation of these values <a href="https://docs.github.com/en/graphql/reference/enums#repositoryaffiliation">here</a>.
+</p>
+`
+const orgDocumentation = `${commonDocumentation}
+<p>
+  <b>Note:</b> ${customDocumentation}
+</p>`
+
+const pageInfoSchema = Joi.object({
+  hasNextPage: Joi.boolean().required(),
+  endCursor: Joi.string().allow(null).required(),
+}).required()
+
+const nodesSchema = Joi.array()
+  .items(
+    Joi.object({
+      stargazers: Joi.object({
+        totalCount: nonNegativeInteger,
+      }).required(),
+    })
+  )
+  .default([])
+
+const repositoriesSchema = Joi.object({
+  pageInfo: pageInfoSchema,
+  nodes: nodesSchema,
+}).required()
+
+const schema = Joi.object({
+  data: Joi.alternatives(
+    Joi.object({
+      user: Joi.object({
+        repositories: repositoriesSchema,
+      }).required(),
+    }).required(),
+    Joi.object({
+      organization: Joi.object({
+        repositories: repositoriesSchema,
+      }).required(),
+    }).required()
+  ).required(),
+}).required()
+
+const query = gql`
+  query fetchStars(
+    $user: String!
+    $nextCursor: String
+    $affiliations: [RepositoryAffiliation]!
+  ) {
+    user(login: $user) {
+      repositories(
+        first: 100
+        after: $nextCursor
+        ownerAffiliations: $affiliations
+        orderBy: { field: STARGAZERS, direction: DESC }
+      ) {
+        pageInfo {
+          hasNextPage
+          endCursor
+        }
+        nodes {
+          stargazers {
+            totalCount
+          }
+        }
+      }
+    }
+
+    organization(login: $user) {
+      repositories(
+        first: 100
+        after: $nextCursor
+        orderBy: { field: STARGAZERS, direction: DESC }
+      ) {
+        pageInfo {
+          hasNextPage
+          endCursor
+        }
+        nodes {
+          stargazers {
+            totalCount
+          }
+        }
+      }
+    }
+  }
+`
+
+const affiliationsAllowedValues = [
+  'OWNER',
+  `COLLABORATOR`,
+  'ORGANIZATION_MEMBER',
+]
+/**
+ * Validates affiliations should contain combination of allowed values in any order.
+ *
+ * @param {string} value affiliation current value
+ * @param {*} helpers object to construct custom error
+ *
+ * @returns {string} valiadtion error or value unchanged
+ */
+const validateAffiliations = (value, helpers) => {
+  const values = value.split(',')
+  if (values.some(e => !affiliationsAllowedValues.includes(e))) {
+    return helpers.error('any.invalid')
+  }
+  return value
+}
+
+const queryParamSchema = Joi.object({
+  affiliations: Joi.string().default('OWNER').custom(validateAffiliations),
+}).required()
+
+module.exports = class GithubTotalStarService extends GithubAuthV4Service {
+  static defaultLabel = 'stars'
+  static category = 'social'
+
+  static route = {
+    base: 'github/stars',
+    pattern: ':user',
+    queryParamSchema,
+  }
+
+  static examples = [
+    {
+      title: "GitHub User's stars",
+      namedParams: {
+        user: 'chris48s',
+      },
+      queryParams: { affiliations: 'OWNER,COLLABORATOR' },
+      staticPreview: {
+        label: this.defaultLabel,
+        message: 54,
+        style: 'social',
+      },
+      documentation: userDocumentation,
+    },
+    {
+      title: "GitHub Org's stars",
+      pattern: ':org',
+      namedParams: {
+        org: 'badges',
+      },
+      staticPreview: {
+        label: this.defaultLabel,
+        message: metric(7000),
+        style: 'social',
+      },
+      documentation: orgDocumentation,
+    },
+  ]
+
+  static defaultBadgeData = {
+    label: this.defaultLabel,
+    namedLogo: 'github',
+  }
+
+  static render({ totalStars, user }) {
+    return {
+      message: metric(totalStars),
+      color: 'blue',
+      link: [`https://github.com/${user}`],
+    }
+  }
+
+  async fetch({ user, affiliations, nextCursor }) {
+    const variables = { user, affiliations, nextCursor }
+    return await this._requestGraphql({
+      query,
+      variables,
+      schema,
+      transformJson: json =>
+        json.data.organization || json.data.user ? { data: json.data } : json,
+      transformErrors: e => transformErrors(e, 'user/org'),
+    })
+  }
+
+  transform(repos) {
+    const totalStars = repos
+      .map(element => element.stargazers.totalCount)
+      .reduce((accumulator, currentValue) => accumulator + currentValue, 0)
+    const lastRepo = repos.slice(-1).pop() // undefined when repos is empty
+    const hasStars = lastRepo ? lastRepo.stargazers.totalCount !== 0 : false
+    return {
+      totalStars,
+      hasStars,
+    }
+  }
+
+  async getTotalStars({ user }, { affiliations }) {
+    let grandTotalStars = 0
+    let fetchedReposCount = 0
+    let nextCursor = null
+    let hasNext
+
+    do {
+      const { data } = await this.fetch({
+        user,
+        affiliations: affiliations.split(','),
+        nextCursor,
+      })
+      const {
+        repositories: {
+          pageInfo: { hasNextPage, endCursor },
+          nodes: repos,
+        },
+      } = data.user || data.organization
+      const { totalStars, hasStars } = this.transform(repos)
+      // repos are sorted based on the stars. If last repo has zero star,
+      // no need to fire additional fetch call, as repos on next page will have zero stars only.
+      hasNext = hasNextPage && hasStars
+      nextCursor = endCursor
+      grandTotalStars += totalStars
+      fetchedReposCount += repos.length
+    } while (hasNext && fetchedReposCount < MAX_REPO_LIMIT)
+
+    return grandTotalStars
+  }
+
+  async handle({ user }, queryParams) {
+    const totalStars = await this.getTotalStars({ user }, queryParams)
+    return this.constructor.render({ totalStars, user })
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Wed Dec 23 2020 18:45:19 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/services_osslifecycle_osslifecycle.service.js.html b/services_osslifecycle_osslifecycle.service.js.html new file mode 100644 index 0000000000..10b7904050 --- /dev/null +++ b/services_osslifecycle_osslifecycle.service.js.html @@ -0,0 +1,150 @@ + + + + + JSDoc: Source: services/osslifecycle/osslifecycle.service.js + + + + + + + + + + +
+ +

Source: services/osslifecycle/osslifecycle.service.js

+ + + + + + +
+
+
'use strict'
+
+const { BaseService, InvalidResponse } = require('..')
+
+const documentation = `
+<p>
+  OSS Lifecycle is an initiative started by Netflix to classify open-source projects into lifecycles
+  and clearly identify which projects are active and which ones are retired. To enable this badge,
+  simply create an OSSMETADATA tagging file at the root of your GitHub repository containing a
+  single line similar to the following: <code>osslifecycle=active</code>. Other suggested values are
+  <code>osslifecycle=maintenance</code> and <code>osslifecycle=archived</code>. A working example
+  can be viewed on the <a href="https://github.com/Netflix/osstracker">OSS Tracker repository</a>.
+</p>
+`
+
+module.exports = class OssTracker extends BaseService {
+  static category = 'other'
+
+  static route = {
+    base: 'osslifecycle',
+    pattern: ':user/:repo/:branch*',
+  }
+
+  static examples = [
+    {
+      title: 'OSS Lifecycle',
+      pattern: ':user/:repo',
+      namedParams: { user: 'Teevity', repo: 'ice' },
+      staticPreview: this.render({ status: 'active' }),
+      keywords: ['Netflix'],
+      documentation,
+    },
+    {
+      title: 'OSS Lifecycle (branch)',
+      pattern: ':user/:repo/:branch',
+      namedParams: {
+        user: 'Netflix',
+        repo: 'osstracker',
+        branch: 'documentation',
+      },
+      staticPreview: this.render({ status: 'active' }),
+      keywords: ['Netflix'],
+      documentation,
+    },
+  ]
+
+  static defaultBadgeData = { label: 'oss lifecycle' }
+
+  /**
+   * Return color for active, maintenance and archived statuses, which were the three
+   * example keywords used in Netflix's open-source meetup.
+   * See https://slideshare.net/aspyker/netflix-open-source-meetup-season-4-episode-1
+   * Other keywords are possible, but will appear in grey.
+   *
+   * @param {object} attrs Refer to individual attrs
+   * @param {string} attrs.status Specifies the current maintenance status
+   * @returns {string} color
+   */
+  static getColor({ status }) {
+    if (status === 'active') {
+      return 'brightgreen'
+    } else if (status === 'maintenance') {
+      return 'yellow'
+    } else if (status === 'archived') {
+      return 'red'
+    }
+    return 'lightgrey'
+  }
+
+  static render({ status }) {
+    const color = this.getColor({ status })
+    return {
+      message: status,
+      color,
+    }
+  }
+
+  async fetch({ user, repo, branch }) {
+    return this._request({
+      url: `https://raw.githubusercontent.com/${user}/${repo}/${branch}/OSSMETADATA`,
+    })
+  }
+
+  async handle({ user, repo, branch }) {
+    const { buffer } = await this.fetch({
+      user,
+      repo,
+      branch: branch || 'HEAD',
+    })
+    try {
+      const status = buffer.match(/osslifecycle=([a-z]+)/im)[1]
+      return this.constructor.render({ status })
+    } catch (e) {
+      throw new InvalidResponse({
+        prettyMessage: 'metadata in unexpected format',
+      })
+    }
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Wed Dec 23 2020 18:45:19 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/services_packagist_packagist-base.js.html b/services_packagist_packagist-base.js.html new file mode 100644 index 0000000000..9928f5a060 --- /dev/null +++ b/services_packagist_packagist-base.js.html @@ -0,0 +1,161 @@ + + + + + JSDoc: Source: services/packagist/packagist-base.js + + + + + + + + + + +
+ +

Source: services/packagist/packagist-base.js

+ + + + + + +
+
+
'use strict'
+
+const Joi = require('joi')
+const { BaseJsonService } = require('..')
+
+const packageSchema = Joi.object()
+  .pattern(
+    /^/,
+    Joi.object({
+      'default-branch': Joi.bool(),
+      version: Joi.string(),
+      require: Joi.object({
+        php: Joi.string(),
+      }),
+    }).required()
+  )
+  .required()
+
+const allVersionsSchema = Joi.object({
+  packages: Joi.object().pattern(/^/, packageSchema).required(),
+}).required()
+const keywords = ['PHP']
+
+class BasePackagistService extends BaseJsonService {
+  /**
+   * Default fetch method.
+   *
+   * This method utilize composer metadata API which
+   * "... is the preferred way to access the data as it is always up to date,
+   * and dumped to static files so it is very efficient on our end." (comment from official documentation).
+   * For more information please refer to https://packagist.org/apidoc#get-package-data.
+   *
+   * @param {object} attrs Refer to individual attrs
+   * @param {string} attrs.user package user
+   * @param {string} attrs.repo package repository
+   * @param {Joi} attrs.schema Joi schema to validate the response transformed to JSON
+   * @param {string} attrs.server URL for the packagist registry server (Optional)
+   * @returns {object} Parsed response
+   */
+  async fetch({ user, repo, schema, server = 'https://packagist.org' }) {
+    const url = `${server}/p/${user.toLowerCase()}/${repo.toLowerCase()}.json`
+
+    return this._requestJson({
+      schema,
+      url,
+    })
+  }
+
+  /**
+   * It is highly recommended to use base fetch method!
+   *
+   * JSON API includes additional information about downloads, dependents count, github info, etc.
+   * However, responses from JSON API are cached for twelve hours by packagist servers,
+   * so data fetch from this method might be outdated.
+   * For more information please refer to https://packagist.org/apidoc#get-package-data.
+   *
+   * @param {object} attrs Refer to individual attrs
+   * @param {string} attrs.user package user
+   * @param {string} attrs.repo package repository
+   * @param {Joi} attrs.schema Joi schema to validate the response transformed to JSON
+   * @param {string} attrs.server URL for the packagist registry server (Optional)
+   * @returns {object} Parsed response
+   */
+  async fetchByJsonAPI({
+    user,
+    repo,
+    schema,
+    server = 'https://packagist.org',
+  }) {
+    const url = `${server}/packages/${user}/${repo}.json`
+
+    return this._requestJson({
+      schema,
+      url,
+    })
+  }
+
+  getDefaultBranch(json, user, repo) {
+    const packageName = this.getPackageName(user, repo)
+    return Object.values(json.packages[packageName]).find(
+      b => b['default-branch'] === true
+    )
+  }
+
+  getPackageName(user, repo) {
+    return `${user.toLowerCase()}/${repo.toLowerCase()}`
+  }
+}
+
+const customServerDocumentationFragment = `
+    <p>
+        Note that only network-accessible packagist.org and other self-hosted Packagist instances are supported.
+    </p>
+    `
+
+const cacheDocumentationFragment = `
+  <p>
+      Displayed data may be slightly outdated.
+      Due to performance reasons, data fetched from packagist JSON API is cached for twelve hours on packagist infrastructure.
+      For more information please refer to <a target="_blank" href="https://packagist.org/apidoc#get-package-data">official packagist documentation</a>.
+  </p>
+  `
+
+module.exports = {
+  allVersionsSchema,
+  keywords,
+  BasePackagistService,
+  customServerDocumentationFragment,
+  cacheDocumentationFragment,
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Wed Dec 23 2020 18:45:19 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/services_steam_steam-base.js.html b/services_steam_steam-base.js.html new file mode 100644 index 0000000000..2ca4bfdb59 --- /dev/null +++ b/services_steam_steam-base.js.html @@ -0,0 +1,108 @@ + + + + + JSDoc: Source: services/steam/steam-base.js + + + + + + + + + + +
+ +

Source: services/steam/steam-base.js

+ + + + + + +
+
+
'use strict'
+/**
+ * @module
+ */
+
+const { BaseJsonService } = require('..')
+
+/**
+ * The steam api is based like /{interface}/{method}/v{version}/
+ *
+ * @see https://partner.steamgames.com/doc/webapi_overview#2
+ */
+class BaseSteamAPI extends BaseJsonService {
+  /**
+   * Steam API Interface
+   *
+   * @see https://partner.steamgames.com/doc/webapi_overview#2
+   */
+  static get interf() {
+    throw new Error(`interf() was not implement for ${this.name}`)
+  }
+
+  /**
+   * Steam API Method
+   *
+   * @see https://partner.steamgames.com/doc/webapi_overview#2
+   */
+  static get method() {
+    throw new Error(`method() was not implement for ${this.name}`)
+  }
+
+  /**
+   * Steam API Version
+   *
+   * @see https://partner.steamgames.com/doc/webapi_overview#2
+   */
+  static get version() {
+    throw new Error(`version() was not implement for ${this.name}`)
+  }
+
+  async fetch({ schema, options }) {
+    const interf = this.constructor.interf
+    const method = this.constructor.method
+    const version = this.constructor.version
+    const url = `https://api.steampowered.com/${interf}/${method}/v${version}/?format=json`
+    return this._requestJson({
+      url,
+      schema,
+      errorMessages: {
+        400: 'bad request',
+      },
+      options,
+    })
+  }
+}
+
+module.exports = BaseSteamAPI
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Wed Dec 23 2020 18:45:19 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/styles/jsdoc-default.css b/styles/jsdoc-default.css new file mode 100644 index 0000000000..7d1729dc9b --- /dev/null +++ b/styles/jsdoc-default.css @@ -0,0 +1,358 @@ +@font-face { + font-family: 'Open Sans'; + font-weight: normal; + font-style: normal; + src: url('../fonts/OpenSans-Regular-webfont.eot'); + src: + local('Open Sans'), + local('OpenSans'), + url('../fonts/OpenSans-Regular-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/OpenSans-Regular-webfont.woff') format('woff'), + url('../fonts/OpenSans-Regular-webfont.svg#open_sansregular') format('svg'); +} + +@font-face { + font-family: 'Open Sans Light'; + font-weight: normal; + font-style: normal; + src: url('../fonts/OpenSans-Light-webfont.eot'); + src: + local('Open Sans Light'), + local('OpenSans Light'), + url('../fonts/OpenSans-Light-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/OpenSans-Light-webfont.woff') format('woff'), + url('../fonts/OpenSans-Light-webfont.svg#open_sanslight') format('svg'); +} + +html +{ + overflow: auto; + background-color: #fff; + font-size: 14px; +} + +body +{ + font-family: 'Open Sans', sans-serif; + line-height: 1.5; + color: #4d4e53; + background-color: white; +} + +a, a:visited, a:active { + color: #0095dd; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +header +{ + display: block; + padding: 0px 4px; +} + +tt, code, kbd, samp { + font-family: Consolas, Monaco, 'Andale Mono', monospace; +} + +.class-description { + font-size: 130%; + line-height: 140%; + margin-bottom: 1em; + margin-top: 1em; +} + +.class-description:empty { + margin: 0; +} + +#main { + float: left; + width: 70%; +} + +article dl { + margin-bottom: 40px; +} + +article img { + max-width: 100%; +} + +section +{ + display: block; + background-color: #fff; + padding: 12px 24px; + border-bottom: 1px solid #ccc; + margin-right: 30px; +} + +.variation { + display: none; +} + +.signature-attributes { + font-size: 60%; + color: #aaa; + font-style: italic; + font-weight: lighter; +} + +nav +{ + display: block; + float: right; + margin-top: 28px; + width: 30%; + box-sizing: border-box; + border-left: 1px solid #ccc; + padding-left: 16px; +} + +nav ul { + font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif; + font-size: 100%; + line-height: 17px; + padding: 0; + margin: 0; + list-style-type: none; +} + +nav ul a, nav ul a:visited, nav ul a:active { + font-family: Consolas, Monaco, 'Andale Mono', monospace; + line-height: 18px; + color: #4D4E53; +} + +nav h3 { + margin-top: 12px; +} + +nav li { + margin-top: 6px; +} + +footer { + display: block; + padding: 6px; + margin-top: 12px; + font-style: italic; + font-size: 90%; +} + +h1, h2, h3, h4 { + font-weight: 200; + margin: 0; +} + +h1 +{ + font-family: 'Open Sans Light', sans-serif; + font-size: 48px; + letter-spacing: -2px; + margin: 12px 24px 20px; +} + +h2, h3.subsection-title +{ + font-size: 30px; + font-weight: 700; + letter-spacing: -1px; + margin-bottom: 12px; +} + +h3 +{ + font-size: 24px; + letter-spacing: -0.5px; + margin-bottom: 12px; +} + +h4 +{ + font-size: 18px; + letter-spacing: -0.33px; + margin-bottom: 12px; + color: #4d4e53; +} + +h5, .container-overview .subsection-title +{ + font-size: 120%; + font-weight: bold; + letter-spacing: -0.01em; + margin: 8px 0 3px 0; +} + +h6 +{ + font-size: 100%; + letter-spacing: -0.01em; + margin: 6px 0 3px 0; + font-style: italic; +} + +table +{ + border-spacing: 0; + border: 0; + border-collapse: collapse; +} + +td, th +{ + border: 1px solid #ddd; + margin: 0px; + text-align: left; + vertical-align: top; + padding: 4px 6px; + display: table-cell; +} + +thead tr +{ + background-color: #ddd; + font-weight: bold; +} + +th { border-right: 1px solid #aaa; } +tr > th:last-child { border-right: 1px solid #ddd; } + +.ancestors, .attribs { color: #999; } +.ancestors a, .attribs a +{ + color: #999 !important; + text-decoration: none; +} + +.clear +{ + clear: both; +} + +.important +{ + font-weight: bold; + color: #950B02; +} + +.yes-def { + text-indent: -1000px; +} + +.type-signature { + color: #aaa; +} + +.name, .signature { + font-family: Consolas, Monaco, 'Andale Mono', monospace; +} + +.details { margin-top: 14px; border-left: 2px solid #DDD; } +.details dt { width: 120px; float: left; padding-left: 10px; padding-top: 6px; } +.details dd { margin-left: 70px; } +.details ul { margin: 0; } +.details ul { list-style-type: none; } +.details li { margin-left: 30px; padding-top: 6px; } +.details pre.prettyprint { margin: 0 } +.details .object-value { padding-top: 0; } + +.description { + margin-bottom: 1em; + margin-top: 1em; +} + +.code-caption +{ + font-style: italic; + font-size: 107%; + margin: 0; +} + +.source +{ + border: 1px solid #ddd; + width: 80%; + overflow: auto; +} + +.prettyprint.source { + width: inherit; +} + +.source code +{ + font-size: 100%; + line-height: 18px; + display: block; + padding: 4px 12px; + margin: 0; + background-color: #fff; + color: #4D4E53; +} + +.prettyprint code span.line +{ + display: inline-block; +} + +.prettyprint.linenums +{ + padding-left: 70px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.prettyprint.linenums ol +{ + padding-left: 0; +} + +.prettyprint.linenums li +{ + border-left: 3px #ddd solid; +} + +.prettyprint.linenums li.selected, +.prettyprint.linenums li.selected * +{ + background-color: lightyellow; +} + +.prettyprint.linenums li * +{ + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.params .name, .props .name, .name code { + color: #4D4E53; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + font-size: 100%; +} + +.params td.description > p:first-child, +.props td.description > p:first-child +{ + margin-top: 0; + padding-top: 0; +} + +.params td.description > p:last-child, +.props td.description > p:last-child +{ + margin-bottom: 0; + padding-bottom: 0; +} + +.disabled { + color: #454545; +} diff --git a/styles/prettify-jsdoc.css b/styles/prettify-jsdoc.css new file mode 100644 index 0000000000..5a2526e374 --- /dev/null +++ b/styles/prettify-jsdoc.css @@ -0,0 +1,111 @@ +/* JSDoc prettify.js theme */ + +/* plain text */ +.pln { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* string content */ +.str { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a keyword */ +.kwd { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a comment */ +.com { + font-weight: normal; + font-style: italic; +} + +/* a type name */ +.typ { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a literal value */ +.lit { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* punctuation */ +.pun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp open bracket */ +.opn { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp close bracket */ +.clo { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a markup tag name */ +.tag { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute name */ +.atn { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute value */ +.atv { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a declaration */ +.dec { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a variable name */ +.var { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a function name */ +.fun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; +} diff --git a/styles/prettify-tomorrow.css b/styles/prettify-tomorrow.css new file mode 100644 index 0000000000..b6f92a78db --- /dev/null +++ b/styles/prettify-tomorrow.css @@ -0,0 +1,132 @@ +/* Tomorrow Theme */ +/* Original theme - https://github.com/chriskempson/tomorrow-theme */ +/* Pretty printing styles. Used with prettify.js. */ +/* SPAN elements with the classes below are added by prettyprint. */ +/* plain text */ +.pln { + color: #4d4d4c; } + +@media screen { + /* string content */ + .str { + color: #718c00; } + + /* a keyword */ + .kwd { + color: #8959a8; } + + /* a comment */ + .com { + color: #8e908c; } + + /* a type name */ + .typ { + color: #4271ae; } + + /* a literal value */ + .lit { + color: #f5871f; } + + /* punctuation */ + .pun { + color: #4d4d4c; } + + /* lisp open bracket */ + .opn { + color: #4d4d4c; } + + /* lisp close bracket */ + .clo { + color: #4d4d4c; } + + /* a markup tag name */ + .tag { + color: #c82829; } + + /* a markup attribute name */ + .atn { + color: #f5871f; } + + /* a markup attribute value */ + .atv { + color: #3e999f; } + + /* a declaration */ + .dec { + color: #f5871f; } + + /* a variable name */ + .var { + color: #c82829; } + + /* a function name */ + .fun { + color: #4271ae; } } +/* Use higher contrast and text-weight for printable form. */ +@media print, projection { + .str { + color: #060; } + + .kwd { + color: #006; + font-weight: bold; } + + .com { + color: #600; + font-style: italic; } + + .typ { + color: #404; + font-weight: bold; } + + .lit { + color: #044; } + + .pun, .opn, .clo { + color: #440; } + + .tag { + color: #006; + font-weight: bold; } + + .atn { + color: #404; } + + .atv { + color: #060; } } +/* Style */ +/* +pre.prettyprint { + background: white; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + font-size: 12px; + line-height: 1.5; + border: 1px solid #ccc; + padding: 10px; } +*/ + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; } + +/* IE indents via margin-left */ +li.L0, +li.L1, +li.L2, +li.L3, +li.L4, +li.L5, +li.L6, +li.L7, +li.L8, +li.L9 { + /* */ } + +/* Alternate shading for lines */ +li.L1, +li.L3, +li.L5, +li.L7, +li.L9 { + /* */ } diff --git a/tutorial-TUTORIAL.html b/tutorial-TUTORIAL.html new file mode 100644 index 0000000000..7d051a5d9d --- /dev/null +++ b/tutorial-TUTORIAL.html @@ -0,0 +1,395 @@ + + + + + JSDoc: Tutorial: TUTORIAL + + + + + + + + + + +
+ +

Tutorial: TUTORIAL

+ +
+ +
+ + +

TUTORIAL

+
+ +
+

Tutorial on how to add a badge for a service

+

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, however, this document +will guide you through that journey if you are a beginner. +Please improve the tutorial while you read it.

+

(1) Reading

+ +

(2) Setup

+

Pre-requisites

+

Git

+

You should have git installed. +If you do not, install git +and learn about the Github workflow.

+

Node, NPM

+

Node 12 or later is required. If you don't already have them, +install node and npm: https://nodejs.org/en/download/

+

Setup a dev install

+
    +
  1. Fork this repository.
  2. +
  3. Clone the fork +git clone git@github.com:YOURGITHUBUSERNAME/shields.git
  4. +
  5. cd shields
  6. +
  7. Install project dependencies +npm ci
  8. +
  9. Run the badge server and the frontend dev server +npm start
  10. +
  11. Visit the website to check the front-end is loaded: http://localhost:3000/.
  12. +
+

In case you get the "getaddrinfo ENOTFOUND localhost" error, visit http://127.0.0.1:3000/ instead or take a look at this issue.

+

(3) Open an Issue

+

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.

+

(4) Implementing

+

(4.1) Structure and Layout

+

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.

    +
      +
    • +

      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.

    +
  • +
+

(4.2) Our First Badge

+

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.
  • +
  • BaseYamlService +implements methods for performing requests to a YAML API and schema validation.
  • +
  • BaseSvgScrapingService +implements methods for retrieving information from existing third-party badges.
  • +
  • BaseGraphqlService +implements methods for performing requests to a GraphQL 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.

+
// (1)
+'use strict'
+// (2)
+const { BaseService } = require('..')
+
+// (3)
+module.exports = class Example extends BaseService {
+  // (4)
+  static category = 'build'
+
+  // (5)
+  static route = { base: 'example', pattern: ':text' }
+
+  // (6)
+  async handle({ text }) {
+    return {
+      label: 'example',
+      message: text,
+      color: 'blue',
+    }
+  }
+}
+
+

Description of the code:

+
    +
  1. We declare strict mode at the start of each file. This prevents certain classes of error such as undeclared variables.
  2. +
  3. Our service badge class will extend BaseService so we need to require it. Variables are declared with const and let in preference to var.
  4. +
  5. Our module must export a class which extends BaseService.
  6. +
  7. Returns the name of the category to sort this badge into (eg. "build"). Used to sort the examples on the main shields.io website. Here is the list of the valid categories. See section 4.4 for more details on examples.
  8. +
  9. 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 by path-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.
    • +
    • There is additional documentation on conventions for designing badge URLs
    • +
    +
  10. +
  11. All badges must implement the async handle() function that receives parameters to render the badge. Parameters of handle() will match the name defined in route() Because we're capturing a single variable called text our function signature is async 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. Our handle() function should return an object with 3 properties: +
      +
    • label: the text on the left side of the badge
    • +
    • message: the text on the right side of the badge - here we are passing through the parameter we captured in the route regex
    • +
    • color: the background color of the right side of the badge
    • +
    +
  12. +
+

The process of turning this object into an image is handled automatically by the BaseService class.

+

To try out this example badge:

+
    +
  1. Copy and paste this code into a new file in /services/example/example.service.js
  2. +
  3. The server should restart on its own. (If it doesn't for some reason, quit +the running server with Control+C, then start it again with npm start.)
  4. +
  5. Visit the badge at http://localhost:8080/example/foo. +It should look like this:
  6. +
+

(4.3) Querying an API

+

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:

+
// (1)
+'use strict'
+
+// (2)
+const { renderVersionBadge } = require('..//version')
+// (3)
+const { BaseJsonService } = require('..')
+
+// (4)
+const Joi = require('joi')
+const schema = Joi.object({
+  version: Joi.string().required(),
+}).required()
+
+// (5)
+module.exports = class GemVersion extends BaseJsonService {
+  // (6)
+  static category = 'version'
+
+  // (7)
+  static route = { base: 'gem/v', pattern: ':gem' }
+
+  // (8)
+  static defaultBadgeData = { label: 'gem' }
+
+  // (11)
+  static render({ version }) {
+    return renderVersionBadge({ version })
+  }
+
+  // (10)
+  async fetch({ gem }) {
+    return this._requestJson({
+      schema,
+      url: `https://rubygems.org/api/v1/gems/${gem}.json`,
+    })
+  }
+
+  // (9)
+  async handle({ gem }) {
+    const { version } = await this.fetch({ gem })
+    return this.constructor.render({ version })
+  }
+}
+
+

Description of the code:

+
    +
  1. +

    As with the first example, we declare strict mode at the start of each file.

    +
  2. +
  3. +

    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 /services. Some useful generic helpers can be found in:

    + +
  4. +
  5. +

    Our badge will query a JSON API so we will extend BaseJsonService instead of BaseService. This contains some helpers to reduce the need for boilerplate when calling a JSON API.

    +
  6. +
  7. +

    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 'version', which is a string.

    +
  8. +
  9. +

    Our module exports a class which extends BaseJsonService

    +
  10. +
  11. +

    Returns the name of the category to sort this badge into (eg. "build"). Used to sort the examples on the main shields.io website. Here is the list of the valid categories. See section 4.4 for more details on examples.

    +
  12. +
  13. +

    As with our previous badge, we need to declare a route. This time we will capture a variable called gem.

    +
  14. +
  15. +

    We can use defaultBadgeData() to set a default color, logo and/or label. If handle() 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 use defaultBadgeData() here to define it declaratively.

    +
  16. +
  17. +

    We now jump to the bottom of the example code to the function all badges must implement: async handle(). This is the function the server will invoke to handle an incoming request. Because our URL pattern captures a variable called gem, our function signature is async handle({ gem }). We usually separate the process of generating a badge into 2 stages or concerns: fetch and render. The fetch() function is responsible for calling an API endpoint to get data. The render() 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.

    +
  18. +
  19. +

    Working our way upward, the async fetch() method is responsible for calling an API endpoint to get data. Extending BaseJsonService 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 the request 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',
      +  },
      +})
      +
      +
    • +
    +
  20. +
  21. +

    Upward still, the static render() method is responsible for formatting the data for display. render() is a pure function so we can make it a static method. By convention we declare functions which don't reference this as static. We could explicitly return an object here, as we did in the previous example. In this case, we will hand the version string off to renderVersionBadge() which will format it consistently and set an appropriate color. Because renderVersionBadge() doesn't return a label key, the default label we defined in defaultBadgeData() will be used when we generate the badge.

    +
  22. +
+

This code allows us to call this URL https://img.shields.io/gem/v/formatador 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 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, there are several standard exceptions that can be used. The errors are documented at +errors +and can be imported via the import shortcut and then thrown:

+
const { NotFound } = require('..')
+
+throw new NotFound({ prettyMessage: 'package not found' })
+
+

(4.4) Adding an Example to the Front Page

+

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 an additional method examples() to our class.

+
module.exports = class GemVersion extends BaseJsonService {
+  // ...
+
+  // (1)
+  static category = 'version'
+
+  // (2)
+  static examples = [
+    {
+      // (3)
+      title: 'Gem',
+      namedParams: { gem: 'formatador' },
+      staticPreview: this.render({ version: '2.1.0' }),
+      keywords: ['ruby'],
+    },
+  ]
+}
+
+
    +
  1. We defined category earlier in the tutorial. The category() property defines which heading in the index our example will appear under.
  2. +
  3. 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.
  4. +
  5. Our example object should contain the following properties: +
      +
    • title: Descriptive text that will be shown next to the badge
    • +
    • 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.
    • +
    • 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 our render() method with some valid data.
    • +
    • keywords: If we want to provide additional keywords other than the title and the category, we can add them here. This helps users to search for relevant badges.
    • +
    +
  6. +
+

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.

+

(4.5) Write Tests

+

When creating a badge for a new service or changing a badge's behavior, tests +should be included. They serve several purposes:

+
    +
  1. They speed up future contributors when they are debugging or improving a +badge.
  2. +
  3. If the contributors would like to change your badge, chances are, they forget +edge cases and break your code. +Tests may give hints in such cases.
  4. +
  5. The contributor and reviewer can easily verify the code works as +intended.
  6. +
  7. When a badge stops working on the live server, maintainers can find out +right away.
  8. +
+

There is a dedicated tutorial for tests in the service-tests folder. +Please follow it to include tests on your pull-request.

+

(4.6) Update the Docs

+

If your submission requires 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.

+

(5) Create a Pull Request

+

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.
  • +
+
+ +
+ +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Wed Dec 23 2020 18:45:19 GMT+0000 (Coordinated Universal Time) +
+ + + + + \ No newline at end of file diff --git a/tutorial-badge-urls.html b/tutorial-badge-urls.html new file mode 100644 index 0000000000..0d67fadde6 --- /dev/null +++ b/tutorial-badge-urls.html @@ -0,0 +1,86 @@ + + + + + JSDoc: Tutorial: badge-urls + + + + + + + + + + +
+ +

Tutorial: badge-urls

+ +
+ +
+ + +

badge-urls

+
+ +
+

Badge URL Conventions

+
    +
  • The format of new badges should be of the form /SERVICE/NOUN/PARAMETERS?QUERYSTRING e.g: +/github/issues/:user/:repo. The service is github, the +badge is for issues, and the parameters are :user/:repo.
  • +
  • Parameters should always be part of the route if they are required to display a badge e.g: :packageName.
  • +
  • Common optional params like, :branch or :tag should also be passed as part of the route.
  • +
  • Query string parameters should be used when: +
      +
    • The parameter is related to formatting. e.g: /appveyor/tests/:user/:repo?compact_message.
    • +
    • The parameter is for an uncommon optional attribute, like an alternate registry URL.
    • +
    • The parameter triggers application of alternative logic, like version semantics. e.g: /github/v/tag/:user/:repo?sort=semver.
    • +
    • Services which require a url/hostname parameter always should use a query string parameter to accept that value. e.g: /discourse/topics?server=https://meta.discourse.org.
    • +
    +
  • +
+

It is convention to use the following standard routes and abbreviations across services:

+
    +
  • Coverage: /coverage
  • +
  • Downloads or Installs: +
      +
    • Total: /dt - Use this even for services that only provide the total download/install data
    • +
    • Per month: /dm
    • +
    • Per week: /dw
    • +
    • Per day: /dd
    • +
    +
  • +
  • Rating: +
      +
    • Numeric: /rating
    • +
    • Stars: /stars
    • +
    +
  • +
  • License: /l
  • +
  • Version or Release: /v
  • +
+
+ +
+ +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Wed Dec 23 2020 18:45:19 GMT+0000 (Coordinated Universal Time) +
+ + + + + \ No newline at end of file diff --git a/tutorial-code-walkthrough.html b/tutorial-code-walkthrough.html new file mode 100644 index 0000000000..b6b22f3711 --- /dev/null +++ b/tutorial-code-walkthrough.html @@ -0,0 +1,250 @@ + + + + + JSDoc: Tutorial: code-walkthrough + + + + + + + + + + +
+ +

Tutorial: code-walkthrough

+ +
+ +
+ + +

code-walkthrough

+
+ +
+

High-level code walkthrough

+

Code inventory and testing strategy

+

The Shields codebase is divided into several parts:

+
    +
  1. The frontend (about 7% of the code) +
      +
    1. frontend
    2. +
    +
  2. +
  3. The badge renderer (which is available as an npm package) +
      +
    1. badge-maker
    2. +
    +
  4. +
  5. The base service classes (about 8% of the code, and probably the most important +code in the codebase) +
      +
    1. core/base-service
    2. +
    +
  6. +
  7. The server code and a few related odds and ends +
      +
    1. core/server
    2. +
    +
  8. +
  9. Helper code for token pooling and persistence (used to avoid GitHub rate limiting) +
      +
    1. core/token-pooling
    2. +
    +
  10. +
  11. Service common helper functions (about 7% of the code, and fairly important +since it’s shared across much of the service code) +
      +
    1. *.js in the root of services
    2. +
    +
  12. +
  13. The services themselves (about 80% of the code) +
      +
    1. *.js in the folders of services
    2. +
    +
  14. +
  15. The badge suggestion endpoint (Note: it's tested as if it’s a service.) +
      +
    1. lib/suggest.js
    2. +
    +
  16. +
+

The tests are also divided into several parts:

+
    +
  1. Unit and functional tests of the frontend +
      +
    1. frontend/**/*.spec.js
    2. +
    +
  2. +
  3. Unit and functional tests of the badge renderer +
      +
    1. badge-maker/**/*.spec.js
    2. +
    +
  4. +
  5. Unit and functional tests of the core code +
      +
    1. core/**/*.spec.js
    2. +
    +
  6. +
  7. Unit and functional tests of the service helper functions +
      +
    1. services/*.spec.js
    2. +
    +
  8. +
  9. Unit and functional tests of the service code (we have only a few of these) +
      +
    1. services/*/**/*.spec.js
    2. +
    +
  10. +
  11. The service tester and service test runner +
      +
    1. core/service-test-runner
    2. +
    +
  12. +
  13. The service tests themselves live integration tests of the +services, and some mocked tests +
      +
    1. *.tester.js in subfolders of services
    2. +
    +
  14. +
  15. Integration tests of Redis-backed persistence code +
      +
    1. core/token-pooling/redis-token-persistence.integration.js
    2. +
    +
  16. +
  17. Integration tests of the GitHub authorization code +
      +
    1. services/github/github-api-provider.integration.js
    2. +
    +
  18. +
+

Our goal is for the core code is to reach 100% coverage of the code in the +frontend, core, and service helper functions when the unit and functional +tests are run.

+

Our test strategy for the service code is a bit different. It’s primarily +based on live integration tests. That’s because service response formats can +change, and when they do the badges break. We want our tests to fail when this +happens. That way we can fix the problems proactively instead of waiting for +users to report them. There’s a good discussion about this decision in +#927. It’s acceptable to write mocked tests of logic that is +difficult to reach using live tests, however where possible, it’s preferred to +test this kind of logic through unit tests (e.g. of render() and +transform() functions).

+

Server initialization

+
    +
  1. +

    The server entrypoint is server.js which sets up error +reporting, loads config, and creates an instance of the server.

    +
  2. +
  3. +

    The Server, which is defined in +core/server/server.js, is based on the web +framework Scoutcamp. It creates an http server, sets up helpers for +token persistence and monitoring. Then it loads all the services, +injecting dependencies as it asks each one to register its route +with Scoutcamp.

    +
  4. +
  5. +

    The service registration continues in BaseService.register. From its +route property, it derives a regular expression to match the route +path, and invokes camp.route with this value.

    +
  6. +
  7. +

    At this point the situation gets gnarly and hard to follow. For the +purpose of initialization, suffice it to say that camp.route invokes a +callback with the four parameters ( queryParams, match, end, ask ) which +is created in a legacy helper function in +legacy-request-handler.js. This callback +delegates to a callback in BaseService.register with four different +parameters ( queryParams, match, sendBadge, request ), which +then runs BaseService.invoke. BaseService.invoke instantiates the +service and runs BaseService#handle.

    +
  8. +
+

Downstream caching

+
    +
  1. In production, the majority of requests are served from caches, including +the browser cache, GitHub’s camo proxy server, and other downstream caches.
  2. +
  3. The Shields servers sit behind the Cloudflare CDN. The CDN itself handles +about 40% of the HTTPS requests that come in.
  4. +
  5. The remaining requests are proxied to one of the servers.
  6. +
  7. See the production hosting documentation for a +fuller discussion of the production architecture.
  8. +
+

How the server makes a badge

+
    +
  1. An HTTPS request arrives. Scoutcamp inspects the URL path and matches it +against the regexes for all the registered routes until it finds one that +matches. (See Initialization above for an explanation of how routes are +registered.)
  2. +
  3. Scoutcamp invokes a callback with the four parameters: +( queryParams, match, end, ask ). This callback is defined in +legacy-request-handler. If the badge result +is found in a relatively small in-memory cache, the response is sent +immediately. Otherwise a timeout is set to handle unresponsive service +code and the next callback is invoked: the legacy handler function.
  4. +
  5. The legacy handler function receives +( queryParams, match, sendBadge, request ). Its job is to extract data +from the regex match and queryParams, invoke request to fetch +whatever data it needs, and then invoke sendBadge with the result.
  6. +
  7. The implementation of this function is in BaseService.register. It +works by running BaseService.invoke, which instantiates the service, +injects more dependencies, and invokes BaseService#handle which is +implemented by the service subclass.
  8. +
  9. The job of handle(), which should be implemented by each service +subclass, is to return an object which partially describes a badge or +throw one of the handled error classes. "Partially rendered" most +commonly means a non-empty message and an optional color. In the case +of the Endpoint badge, it could include many other parameters. At the +time of writing the handled error classes were NotFound, +InvalidResponse, Inaccessible, InvalidParameter, and Deprecated. +Throwing any other error is a programmer error which will be +reported and described to the user as a shields +internal error.
  10. +
  11. A typical handle() function delegates to one or more helpers to +handle stages of the request: +
      +
    1. fetch: load the needed data from the upstream service and +validate it
    2. +
    3. transform: pluck, convert, or summarize the response format +into a few properties which will be displayed on the badge
    4. +
    5. render: given a few properties, return a message, optional +color, and optional label.
    6. +
    +
  12. +
  13. When an error is thrown, BaseService steps in and converts the error +object to renderable properties: { isError, message, color }.
  14. +
  15. The service invokes coalesceBadge whose job is to +coalesce query string overrides with values from the service and the +service’s defaults to produce an object that fully describes the badge to +be rendered.
  16. +
  17. sendBadge is invoked with that object. It does some housekeeping on the +timeout and caches the result. Then it renders the badge to svg or raster +and pushes out the result over the HTTPS connection.
  18. +
+
+ +
+ +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Wed Dec 23 2020 18:45:19 GMT+0000 (Coordinated Universal Time) +
+ + + + + \ No newline at end of file diff --git a/tutorial-deprecating-badges.html b/tutorial-deprecating-badges.html new file mode 100644 index 0000000000..86c7dd4753 --- /dev/null +++ b/tutorial-deprecating-badges.html @@ -0,0 +1,125 @@ + + + + + JSDoc: Tutorial: deprecating-badges + + + + + + + + + + +
+ +

Tutorial: deprecating-badges

+ +
+ +
+ + +

deprecating-badges

+
+ +
+

Deprecating Badges

+

When a service that Shields integrates with shuts down, those badges will no longer work and need to be deprecated within Shields.

+

Deprecating a badge involves two steps:

+
    +
  1. Updating the service code to use the DeprecatedService class
  2. +
  3. Updating the service tests to reflect the new behavior of the deprecated service
  4. +
+

Update Service Implementation

+

Locate the source file(s) for the service, which can be found in *.service.js files located within the directory for the service (./services/:service-name/) such as ./services/imagelayers/imagelayers.service.js.

+

Replace the existing service class implementation with the DeprecatedService class from ./core/base-service/deprecated-service.js using the respective category, route, and label values for that service. For example:

+
'use strict'
+
+const { deprecatedService } = require('..')
+
+module.exports = deprecatedService({
+  category: 'size',
+  route: {
+    base: 'imagelayers',
+    format: '(?:.+?)',
+  },
+  label: 'imagelayers',
+  dateAdded: new Date('2019-xx-xx'), // Be sure to update this with today's date!
+})
+
+

Update Service Tests

+

Locate the test file(s) for the service, which can be found in *.tester.js files located in the service directory (./services/:service-name/), such as ./services/imagelayers/imagelayers.tester.js.

+

With DeprecatedService classes we cannot use createServiceTester() so you will need to create the ServiceTester class directly. For example:

+
const { ServiceTester } = require('../tester')
+
+const t = (module.exports = new ServiceTester({
+  id: 'imagelayers',
+  title: 'ImageLayers',
+}))
+
+

Next you will need to replace/refactor the existing tests to validate the new deprecated badge behavior for this service. Deprecated badges always return a message of no longer available (such as imagelayers | no longer available) so the tests need to be updated to reflect that message value. For example:

+
t.create('no longer available (previously image size)')
+  .get('/image-size/_/ubuntu/latest.json')
+  .expectBadge({
+    label: 'imagelayers',
+    message: 'no longer available',
+  })
+
+

Make sure to have a live (non-mocked) test for each badge the service provides that validates the each badge returns the no longer available message.

+

Here is an example of what the final result would look like for a test file:

+
'use strict'
+
+const { ServiceTester } = require('../tester')
+
+const t = (module.exports = new ServiceTester({
+  id: 'imagelayers',
+  title: 'ImageLayers',
+}))
+
+t.create('no longer available (previously image size)')
+  .get('/image-size/_/ubuntu/latest.json')
+  .expectBadge({
+    label: 'imagelayers',
+    message: 'no longer available',
+  })
+
+t.create('no longer available (previously number of layers)')
+  .get('/layers/_/ubuntu/latest.json')
+  .expectBadge({
+    label: 'imagelayers',
+    message: 'no longer available',
+  })
+
+

Additional Information

+

Some other information that may be useful:

+ +
+ +
+ +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Wed Dec 23 2020 18:45:19 GMT+0000 (Coordinated Universal Time) +
+ + + + + \ No newline at end of file diff --git a/tutorial-json-format.html b/tutorial-json-format.html new file mode 100644 index 0000000000..0b9cc8f8c5 --- /dev/null +++ b/tutorial-json-format.html @@ -0,0 +1,69 @@ + + + + + JSDoc: Tutorial: json-format + + + + + + + + + + +
+ +

Tutorial: json-format

+ +
+ +
+ + +

json-format

+
+ +
+

JSON Format

+

Even though Shields is probably best known for its SVG badges, you can also retrieve +a JSON payload by replacing the .svg extension with .json.

+

For instance, hitting this endpoint +will generate the following payload:

+
{
+  "name": "hello",
+  "label": "hello",
+  "value": "world",
+  "message": "world",
+  "color": "brightgreen"
+}
+
+

Note that the values of the name and value fields are duplicates of the label +and message ones, respectively. As of April 2019, name and value are deprecated +and will be removed in a future release, please consider migrating your application +to use label and message instead.

+

Feel free to open an issue +if you have any queries regarding the JSON format.

+
+ +
+ +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Wed Dec 23 2020 18:45:19 GMT+0000 (Coordinated Universal Time) +
+ + + + + \ No newline at end of file diff --git a/tutorial-logos.html b/tutorial-logos.html new file mode 100644 index 0000000000..5d6c9a4b3f --- /dev/null +++ b/tutorial-logos.html @@ -0,0 +1,104 @@ + + + + + JSDoc: Tutorial: logos + + + + + + + + + + +
+ +

Tutorial: logos

+ +
+ +
+ + +

logos

+
+ +
+

Logos

+

Using Logos

+

SimpleIcons

+

We support a wide range of logos via SimpleIcons. They can be referenced by name e.g:

+

- https://img.shields.io/npm/v/npm.svg?logo=javascript

+

Shields logos

+

We also maintain a small number of custom logos for some services: https://github.com/badges/shields/tree/master/logo They can also be referenced by name and take preference to SimpleIcons e.g:

+

- https://img.shields.io/npm/v/npm.svg?logo=npm

+

Custom Logos

+

Any custom logo can be passed in a URL parameter by base64 encoding it. e.g:

+

- https://img.shields.io/badge/play-station-blue.svg?logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEiIHdpZHRoPSI2MDAiIGhlaWdodD0iNjAwIj48cGF0aCBkPSJNMTI5IDExMWMtNTUgNC05MyA2Ni05MyA3OEwwIDM5OGMtMiA3MCAzNiA5MiA2OSA5MWgxYzc5IDAgODctNTcgMTMwLTEyOGgyMDFjNDMgNzEgNTAgMTI4IDEyOSAxMjhoMWMzMyAxIDcxLTIxIDY5LTkxbC0zNi0yMDljMC0xMi00MC03OC05OC03OGgtMTBjLTYzIDAtOTIgMzUtOTIgNDJIMjM2YzAtNy0yOS00Mi05Mi00MmgtMTV6IiBmaWxsPSIjZmZmIi8+PC9zdmc+

+

Contributing Logos

+

Our preferred way to consume icons is via the SimpleIcons logo. As a first port of call, we encourage you to contribute logos to the SimpleIcons project. Please review their guidance before contributing.

+

In some cases we may also accept logo submissions directly. In general, we do this only when:

+
    +
  • We have a corresponding badge on the homepage, (e.g. the Eclipse logo because we support service badges for the Eclipse Marketplace). We may also approve logos for other tools widely used by developers.
  • +
  • The logo provided in SimpleIcons is unclear when displayed at small size on a badge.
  • +
  • There is substantial benefit in using a multi-colored icon over a monochrome icon.
  • +
  • The logo doesn't meet the requirements to be included in the SimpleIcons set.
  • +
+

If you are submitting a pull request for a custom logo, please:

+
    +
  • Minimize SVG files through SVGO. This can be done in one of two ways +
      +
    • The SVGO Command Line Tool +
        +
      • Install SVGO +
          +
        • With npm: npm install -g svgo
        • +
        • With Homebrew: brew install svgo
        • +
        +
      • +
      • Run the following command svgo --precision=3 icon.svg icon.min.svg
      • +
      • Check if there is a loss of quality in the output, if so increase the precision.
      • +
      +
    • +
    • The SVGOMG Online Tool +
        +
      • Click "Open SVG" and select an SVG file.
      • +
      • Set the precision to about 3, depending on if there is a loss of quality.
      • +
      • Leave the remaining settings untouched (or reset them with the button at the bottom of the settings).
      • +
      • Click the download button.
      • +
      +
    • +
    +
  • +
  • Set a viewbox and ensure the logo is scaled to fit the viewbox, while preserving the logo's original proportions. This means the icon should be touching at least two sides of the viewbox.
  • +
  • Ensure the logo is vertically and horizontally centered.
  • +
  • Ensure the logo is minified to a single line with no formatting.
  • +
  • Ensure the SVG does not contain extraneous attributes.
  • +
  • Ensure your submission conforms to any relevant brand or logo guidelines.
  • +
+

Problems

+

We try to ensure our logos are compliant with brand guidelines. If one of our custom logos does not conform to the necessary brand guidelines, please open an issue on the shields.io tracker and we'll work with you to resolve it. If a logo from the simple-icons set does not conform to the relevant brand guidelines, please open an issue on the simple-icons tracker first.

+
+ +
+ +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Wed Dec 23 2020 18:45:19 GMT+0000 (Coordinated Universal Time) +
+ + + + + \ No newline at end of file diff --git a/tutorial-performance-testing.html b/tutorial-performance-testing.html new file mode 100644 index 0000000000..1185c1c83c --- /dev/null +++ b/tutorial-performance-testing.html @@ -0,0 +1,85 @@ + + + + + JSDoc: Tutorial: performance-testing + + + + + + + + + + +
+ +

Tutorial: performance-testing

+ +
+ +
+ + +

performance-testing

+
+ +
+

Performance testing

+

Shields has some basic tooling available to help you get started with +performance testing.

+

Benchmarking the badge generation

+

Want to micro-benchmark a section of the code responsible for generating the +static badges? Follow these two simple steps:

+
    +
  1. Surround the code you want to time with console.time and console.timeEnd +statements. For example:
  2. +
+
console.time('makeBadge')
+const svg = makeBadge(badgeData)
+console.timeEnd('makeBadge')
+
+
    +
  1. Run npm run benchmark:badge in your terminal. An average timing will +be displayed!
  2. +
+

If you want to change the number of iterations in the benchmark, you can modify +the values specified by the benchmark:badge script in package.json. If +you want to benchmark a specific code path not covered by the static badge, you +can modify the badge URL in scripts/benchmark-performance.js.

+

Profiling the full code

+

Want to have an overview of how the entire application is performing? Simply +run npm run profile:server in your terminal. This will start the +backend server (i.e. without the frontend) in profiling mode and any requests +you make on localhost:8080 will generate data in a file with a name +similar to isolate-00000244AB6ED3B0-11920-v8.log.

+

You can then make use of this profiling data in various tools, for example +flamebearer:

+
npm install -g flamebearer
+node --prof-process --preprocess -j isolate-00000244AB6ED3B0-11920-v8.log | flamebearer
+
+

An example output is the following: +

+
+ +
+ +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Wed Dec 23 2020 18:45:19 GMT+0000 (Coordinated Universal Time) +
+ + + + + \ No newline at end of file diff --git a/tutorial-production-hosting.html b/tutorial-production-hosting.html new file mode 100644 index 0000000000..46001c8710 --- /dev/null +++ b/tutorial-production-hosting.html @@ -0,0 +1,256 @@ + + + + + JSDoc: Tutorial: production-hosting + + + + + + + + + + +
+ +

Tutorial: production-hosting

+ +
+ +
+ + +

production-hosting

+
+ +
+

Production hosting

+

Production hosting is managed by the Shields ops team:

+ +

operations issues

+

#ops chat room

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ComponentSubcomponentPeople with access
shields-production-usAccount owner@paulmelnikow
shields-production-usFull access@calebcartwright, @chris48s, @paulmelnikow, @pyvesb
shields-production-usAccess management@calebcartwright, @chris48s, @paulmelnikow, @pyvesb
Compose.io RedisAccount owner@paulmelnikow
Compose.io RedisAccount access@paulmelnikow
Compose.io RedisDatabase connection credentials@calebcartwright, @chris48s, @paulmelnikow, @pyvesb
Zeit NowTeam owner@paulmelnikow
Zeit NowTeam members@paulmelnikow, @chris48s, @calebcartwright, @platan
Raster serverFull access as team members@paulmelnikow, @chris48s, @calebcartwright, @platan
shields-server.com redirectorFull access as team members@paulmelnikow, @chris48s, @calebcartwright, @platan
Cloudflare (CDN)Account owner@espadrine
Cloudflare (CDN)Access management@espadrine
Cloudflare (CDN)Admin access@calebcartwright, @chris48s, @espadrine, @paulmelnikow, @PyvesB
TwitchOAuth app@PyvesB
DiscordOAuth app@PyvesB
YouTubeAccount owner@PyvesB
OpenStreetMap (for Wheelmap)Account owner@paulmelnikow
DNSAccount owner@olivierlacan
DNSRead-only account access@espadrine, @paulmelnikow, @chris48s
SentryError reports@espadrine, @paulmelnikow
Metrics serverOwner@platan
UptimeRobotAccount owner@paulmelnikow
More metricsOwner@RedSparr0w
Netlify (documentation site)Owner@chris48s
+

There are too many bottlenecks!

+

Attached state

+

Shields has mercifully little persistent state:

+
    +
  1. The GitHub tokens we collect are saved on each server in a cloud Redis database. +They can also be fetched from the GitHub auth admin endpoint for debugging.
  2. +
  3. The server keeps a few caches in memory. These are neither persisted nor +inspectable. + +
  4. +
+

Configuration

+

To bootstrap the configuration process, +the script that starts the server sets a single +environment variable:

+
NODE_CONFIG_ENV=shields-io-production
+
+

With that variable set, the server (using config) reads these +files:

+ +

Badge CDN

+

Sitting in front of the three servers is a Cloudflare Free account which +provides several services:

+
    +
  • Global CDN, caching, and SSL gateway for img.shields.io and shields.io
  • +
  • Analytics through the Cloudflare dashboard
  • +
  • DNS resolution for shields.io (and subdomains)
  • +
+

Cloudflare is configured to respect the servers' cache headers.

+

Raster server

+

The raster server raster.shields.io (a.k.a. the rasterizing proxy) is +hosted on Zeit Now. It's managed in the +svg-to-image-proxy repo.

+

Heroku Deployment

+

Both the badge server and frontend are served from Heroku.

+

After merging a commit to master, heroku should create a staging deploy. Check this has deployed correctly in the shields-staging pipeline and review http://shields-staging.herokuapp.com/

+

If we're happy with it, "promote to production". This will deploy what's on staging to the shields-production-eu and shields-production-us pieplines.

+

DNS

+

DNS is registered with DNSimple.

+

Logs

+

Logs can be retrieved from heroku.

+

Error reporting

+

Error reporting is one of the most useful tools we have for monitoring +the server. It's generously donated by Sentry. We bundle +raven into the application, and the Sentry DSN is configured via +local-shields-io-production.yml (see documentation).

+

Monitoring

+

Overall server performance and requests by service are monitored using +Prometheus and Grafana.

+

Request performance is monitored in two places:

+ +
+ +
+ +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Wed Dec 23 2020 18:45:19 GMT+0000 (Coordinated Universal Time) +
+ + + + + \ No newline at end of file diff --git a/tutorial-rewriting-services.html b/tutorial-rewriting-services.html new file mode 100644 index 0000000000..f8297a4fa7 --- /dev/null +++ b/tutorial-rewriting-services.html @@ -0,0 +1,320 @@ + + + + + JSDoc: Tutorial: rewriting-services + + + + + + + + + + +
+ +

Tutorial: rewriting-services

+ +
+ +
+ + +

rewriting-services

+
+ +
+

WARNING: all legacy services have been rewritten, this document may contain outdated information.

+

Tips for rewriting legacy services

+

Background

+

The services are in the process of being rewritten to use our new service +framework (#1358). +Meanwhile, the legacy services extend from an abstract +adapter called LegacyService which provides a place to put the +camp.route() invocation. The wrapper extends from BaseService, so it +supports badge examples via category, examples, and route. Setting route +also enables createServiceTester() to infer a service's base path, reducing +boilerplate for creating the tester.

+

Legacy services look like:

+
module.exports = class ExampleService extends LegacyService {
+  static category = 'build'
+
+  static registerLegacyRouteHandler({ camp, cache }) {
+    camp.route(
+      /^\/example\/([^\/]+)\/([^\/]+)\.(svg|png|gif|jpg|json)$/,
+      cache(function (data, match, sendBadge, request) {
+        var first = match[1]
+        var second = match[2]
+        var format = match[3]
+        var badgeData = getBadgeData('X' + first + 'X', data)
+        badgeData.text[1] = second
+        badgeData.colorscheme = 'blue'
+        badgeData.colorB = '#008bb8'
+        sendBadge(format, badgeData)
+      })
+    )
+  }
+}
+
+

References:

+ +

First, write some tests

+

If service tests don’t exist for the legacy service, stop and write them first. +It’s recommended to PR these separately. If there’s some test coverage, it’s +probably fine to move right ahead and add more in the process. Make sure the +tests are passing, though.

+

Organization

+
    +
  1. When there’s a single legacy service that handles lots of different things +(e.g. version, license, and downloads), it should be split into three separate +service classes and placed in three separate files, e.g.:
  2. +
+
    +
  • example-version.service.js
  • +
  • example-license.service.js
  • +
  • example-downloads.service.js
  • +
+
    +
  1. +

    When a badge offers different variants of basically the same thing, it’s okay +to put them in the same service class. For example, daily/weekly/monthly/total +downloads can go in one badge, and star rating vs point rating vs rating count +can go in one badge, and same with various kinds of detail about a pull request. +The hard limit (as of now anyway) is one category per service class.

    +
  2. +
  3. +

    If the tests haven’t been split up, split them up too and make sure they +still pass.

    +
  4. +
+

Get the route working

+
    +
  1. +

    Disable the legacy service by adding a return at the top of +registerLegacyRouteHandler().

    +
  2. +
  3. +

    Set up the route for one of the badges. First determine if you can express +the route using a pattern. A pattern (e.g. pattern: ':param1/:param2') is +the simplest way to declare the route, also the most readable, and will be +useful for displaying a badge builder with fields in the front end and +generating badge URLs programmatically.

    +
  4. +
  5. +

    When creating the initial route, you can stub out the service. A minimal +service extends BaseJsonService (or BaseService, or one of the others), and +defines route() and handle(). defaultBadgeData is optional but suggested:

    +
  6. +
+
const BaseJsonService = require('../base-json')
+
+class ExampleDownloads extends BaseJsonService {
+  static route = { base: 'example/d', pattern: ':param1/:param2' }
+
+  static defaultBadgeData() {
+    return { label: 'downloads' } // or whatever
+  }
+
+  async handle({ param1, param2 }) {
+    return { message: 'hello' }
+  }
+}
+
+
    +
  1. We don’t have really good tools for debugging matches, so the best you can do +is run a subset of your tests. To run a single service test, add .only() +somewhere in the chain, and run npm run test:services:trace -- --only=example.
  2. +
+
t.create('build status')
+  .get('/pip.json')
+  .only() // Prevent this ServiceTester from running its other tests.
+  .expectBadge(
+    label: 'docs',
+    message: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')),
+  )
+
+
    +
  1. +

    Presumably the test will fail, though by examining the copious output, you +can confirm the route was matched and the named parameters mapped successfully. +Since you'll have just run the tests on the old code (right?) you'll know you +haven't inadvertently changed the route (an easy mistake to make).

    +
  2. +
  3. +

    If the legacy service had a base URL and you've changed it, you’ll need to +update the tests and the examples. Take care to do that.

    +
  4. +
+

Implement render() and handle()

+

Once the route is working, fill out render() and handle().

+
    +
  1. If there’s a single service, you can implement fetch as a method or a +function at the top of the file. If there are more than one service which share +fetching code, you can put the fetch function in example-common.js like this +one for github:
  2. +
+
+
const Joi = require('joi')
+const { errorMessagesFor } = require('./github-helpers')
+
+const issueSchema = Joi.object({
+  head: Joi.object({
+    sha: Joi.string().required(),
+  }).required(),
+}).required()
+
+async function fetchIssue(serviceInstance, { user, repo, number }) {
+  return serviceInstance._requestJson({
+    schema: issueSchema,
+    url: `/repos/${user}/${repo}/pulls/${number}`,
+    errorMessages: errorMessagesFor('pull request or repo not found'),
+  })
+}
+
+module.exports = {
+  fetchIssue,
+}
+
+
+

or create an abstract superclass like PypiBase:

+
+
const Joi = require('joi')
+const BaseJsonService = require('../base-json')
+
+const schema = Joi.object({
+  info: Joi.object({
+    ...
+  }).required()
+}).required()
+
+module.exports = class PypiBase extends BaseJsonService {
+  static buildRoute(base) {
+    return {
+      base,
+      pattern: ':egg*',
+    }
+  }
+
+  async fetch({ egg }) {
+    return this._requestJson({
+      schema,
+      url: `https://pypi.org/pypi/${egg}/json`,
+      errorMessages: { 404: 'package or version not found' },
+    })
+  }
+}
+
+
+
    +
  1. +

    Validation should be handled using Joi. Save this for last. While you're +getting things working, you can use const schema = Joi.any(), which matches +anything.

    +
  2. +
  3. +

    Substitution of default values should also be handled by Joi, using +.default().

    +
  4. +
  5. +

    To keep with the design pattern of render(), formatting concerns, including +concatenation and color computation, should be dealt with inside render(). +This helps avoid static examples falling out of sync with the implementation.

    +
  6. +
+

Error handling

+

BaseService includes built-in runtime error handling. Error classes are defined +in services/errors.js. Request code and validation code will throw a runtime +error, which will then bubble up to BaseService, which then renders an error +badge. The cases covered by built-in error handling need not be tested in each +service, and existing tests should be removed.

+
    +
  1. +

    If an external server can't be reached or returns a 5xx status code, +_requestJson() along with code in lib/error-helper.js will bubble up an +Inaccessible error.

    +
  2. +
  3. +

    If a response does not match the schema, validate() will bubble up an +InvalidResponse error which will display invalid response data.

    +
  4. +
+

Error handling can also be customized by the service. Alternate messages +corresponding to HTTP status codes can be specified in the errorMessages +parameter to _requestJson() etc.

+

For the not found case, a service test should establish that the API is doing +what we expect. If the API returns a 404 error, code in lib/error-helper.js +will automatically throw a NotFound error. The error message can, and +generally should be customized to display something more specific like +package not found or room not found.

+

Not all services return a 404 response in the not found case. Sometimes a +different status code is returned.

+

Sometimes a 200 response must be examined to distinguish the not found case from a success case. This can be handled in either of two ways:

+
    +
  • Write a schema which accommodates both the success and error cases.
  • +
  • Write the schema for the success case. Pass schema: Joi.any() to +_requestJson(). Manually check for the error case, then invoke +_validate() with the success-case schema.
  • +
+

In either case, the service should throw e.g +new NotFound({ prettyMessage: 'package not found' }).

+

Convert the examples

+
    +
  1. +

    Convert all the examples to pattern, namedParams, and staticExample. In some cases you can use the pattern inherited from route, though in other cases you may need to specify a pattern in the example. For example, when showing download badges for several periods, you may want to render the example with an explicit dt instead of :which. You will also need to specify a pattern for badges that use a format regex in the route.

    +
  2. +
  3. +

    Open the frontend and check that the static preview badges look good. +Remember, none of them are live.

    +
  4. +
  5. +

    Open up the prepared example URLs in their own tabs, and make sure they work correctly.

    +
  6. +
+

Validation

+

When it's time to add the schema, refer to the Joi API docs: +https://github.com/hapijs/joi/blob/master/API.md

+

Housekeeping

+

Switch to createServiceTester:

+
const t = (module.exports = require('../tester').createServiceTester())
+
+

This may require updating the URLs, which will be relative to the service's base +URL. When using createServiceTester, services need to be specified using +the non-case-sensitive service class name, or a leading substring (e.g. +AppveyorTests or appveyor).

+

Do this last. Since it involves changing test URLs, and you don't want to +accidentally change them.

+
+ +
+ +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Wed Dec 23 2020 18:45:19 GMT+0000 (Coordinated Universal Time) +
+ + + + + \ No newline at end of file diff --git a/tutorial-self-hosting.html b/tutorial-self-hosting.html new file mode 100644 index 0000000000..5441beff9e --- /dev/null +++ b/tutorial-self-hosting.html @@ -0,0 +1,180 @@ + + + + + JSDoc: Tutorial: self-hosting + + + + + + + + + + +
+ +

Tutorial: self-hosting

+ +
+ +
+ + +

self-hosting

+
+ +
+

Hosting your own Shields server

+

Installation

+

You will need Node 12 or later, which you can install using a +package manager.

+

On Ubuntu / Debian:

+
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -; sudo apt-get install -y nodejs
+
+
git clone https://github.com/badges/shields.git
+cd shields
+npm ci  # You may need sudo for this.
+
+

Build the frontend

+
npm run build
+
+

Start the server

+
sudo node server
+
+

The server uses port 80 by default, which requires sudo permissions.

+

There are two ways to provide an alternate port:

+
PORT=8080 node server
+node server 8080
+
+

The root gets redirected to https://shields.io.

+

For testing purposes, you can go to http://localhost/.

+

Heroku

+

Once you have installed the Heroku CLI

+
heroku login
+heroku create your-app-name
+git push heroku master
+heroku open
+
+

Docker

+

You can build and run the server locally using Docker. First build an image:

+
$ docker build -t shields .
+Sending build context to Docker daemon 3.923 MB
+…
+Successfully built 4471b442c220
+
+

Optionally, create a file called shields.env that contains the needed +configuration. See server-secrets.md and config/custom-environment-variables.yml for examples.

+

Then run the container:

+
$ docker run --rm -p 8080:80 --name shields shields
+# or if you have shields.env file, run the following instead
+$ docker run --rm -p 8080:80 --env-file shields.env --name shields shields
+
+> badge-maker@3.0.0 start /usr/src/app
+> node server.js
+
+http://[::1]/
+
+

Assuming Docker is running locally, you should be able to get to the +application at http://localhost:8080/.

+

If you run Docker in a virtual machine (such as boot2docker or Docker Machine) +then you will need to replace localhost with the IP address of that virtual +machine.

+

Raster server

+

If you want to host PNG badges, you can also self-host a raster server +which points to your badge server. It's designed as a web function which is +tested on Zeit Now, though you may be able to run it on AWS Lambda. It's +built on the micro framework, and comes with a start script that allows +it to run as a standalone Node service.

+
    +
  • In your raster instance, set BASE_URL to your Shields instance, e.g. +https://shields.example.co.
  • +
  • Optionally, in your Shields, instance, configure RASTER_URL to the base +URL, e.g. https://raster.example.co. This will send 301 redirects +for the legacy raster URLs instead of 404's.
  • +
+

If anyone has set this up, more documentation on how to do this would be +welcome! It would also be nice to ship a Docker image that includes a +preconfigured raster server.

+

Zeit Now

+

To deploy using Zeit Now:

+
npm run build  # Not sure why, but this needs to be run before deploying.
+now
+
+

Persistence

+

To enable Redis-backed GitHub token persistence, point REDIS_URL to your +Redis installation.

+

Server secrets

+

You can add your own server secrets in environment variables or config/local.yml.

+

These are documented in server-secrets.md

+

Separate frontend hosting

+

If you want to host the frontend on a separate server, such as cloud storage +or a CDN, you can do that.

+

First, build the frontend, pointing GATSBY_BASE_URL to your server.

+
GATSBY_BASE_URL=https://your-server.example.com npm run build
+
+

Then copy the contents of the build/ folder to your static hosting / CDN.

+

There are also a couple settings you should configure on the server.

+

If you want to use server suggestions, you should also set ALLOWED_ORIGIN:

+
ALLOWED_ORIGIN=http://my-custom-shields.s3.amazonaws.com,https://my-custom-shields.s3.amazonaws.com
+
+

This should be a comma-separated list of allowed origin headers. They should +not have paths or trailing slashes.

+

To help out users, you can make the Shields server redirect the server root. +Set the REDIRECT_URI environment variable:

+
REDIRECT_URI=http://my-custom-shields.s3.amazonaws.com/
+
+

Sentry

+

In order to enable integration with Sentry, you need your own Sentry DSN. It’s an URL in format https://{PUBLIC_KEY}:{SECRET_KEY}@sentry.io/{PROJECT_ID}.

+

How to obtain the Sentry DSN

+
    +
  1. Sign up for Sentry
  2. +
  3. Log in to Sentry
  4. +
  5. Create a new project for Node.js
  6. +
  7. You should see Sentry DSN for your project. Sentry DSN can be found by navigating to [Project Name] -> Project Settings -> Client Keys (DSN) as well.
  8. +
+

Start the server using the Sentry DSN. You can set it:

+
    +
  • by SENTRY_DSN environment variable
  • +
+
sudo SENTRY_DSN=https://xxx:yyy@sentry.io/zzz node server
+
+

Or via config as you would do with server secrets:

+
private:
+  sentry_dsn: ...
+
+
sudo node server
+
+

Prometheus

+

Shields uses prom-client to provide default metrics. These metrics are disabled by default. +You can enable them by METRICS_PROMETHEUS_ENABLED environment variable.

+
METRICS_PROMETHEUS_ENABLED=true npm start
+
+

Metrics are available at /metrics resource.

+

Cloudflare

+

Shields uses Cloudflare as a downstream CDN. If your installation does the same, +you can configure your server to only accept requests coming from Cloudflare's IPs. +Set public.requireCloudflare: true.

+
+ +
+ +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Wed Dec 23 2020 18:45:19 GMT+0000 (Coordinated Universal Time) +
+ + + + + \ No newline at end of file diff --git a/tutorial-server-secrets.html b/tutorial-server-secrets.html new file mode 100644 index 0000000000..2ebbc0c466 --- /dev/null +++ b/tutorial-server-secrets.html @@ -0,0 +1,253 @@ + + + + + JSDoc: Tutorial: server-secrets + + + + + + + + + + +
+ +

Tutorial: server-secrets

+ +
+ +
+ + +

server-secrets

+
+ +
+

Server Secrets

+

It is possible to provide a token or credentials for a number of external +services. These may be used to lift a rate limit or provide access to +private resources from a self-hosted instance.

+

There are two ways of setting secrets:

+
    +
  1. Via environment variables. This is a good way to set them in a PaaS +environment.
  2. +
+
DRONE_TOKEN=...
+DRONE_ORIGINS="https://drone.example.com"
+
+
    +
  1. Via checked-in config/local.yml:
  2. +
+
public:
+  services:
+    drone:
+      authorizedOrigins: ['https://drone.example.com']
+private:
+  drone_token: '...'
+
+

For more complex scenarios, configuration files can cascade. See the node-config documentation +for details.

+

Authorized origins

+

Several of the badges provided by Shields allow users to specify the target +URL/server of the upstream instance to use via a query parameter in the badge URL +(e.g. https://img.shields.io/nexus/s/com.google.guava/guava?server=https%3A%2F%2Foss.sonatype.org). +This supports scenarios where your users may need badges from multiple upstream +targets, for example if you have more than one Nexus server.

+

Accordingly, if you configure credentials for one of these services with your +self-hosted Shields instance, you must also specifically authorize the hosts +to which the credentials are allowed to be sent. If your self-hosted Shields +instance then receives a badge request for a target that does not match any +of the authorized origins, one of two things will happen:

+
    +
  • if credentials are required for the targeted service, Shields will render +an error badge.
  • +
  • if credentials are optional for the targeted service, Shields will attempt +the request, but without sending any credentials.
  • +
+

When setting authorized origins through an environment variable, use a space +to separate multiple origins. Note that failing to define authorized origins +for a service will default to an empty list, i.e. no authorized origins.

+

It is highly recommended to use https origins with valid SSL, to avoid the +possibility of exposing your credentials, for example through DNS-based attacks.

+

It is also recommended to use tokens for a service account having +the fewest privileges needed for fetching the relevant status +information.

+

Services

+

Azure DevOps

+
    +
  • AZURE_DEVOPS_TOKEN (yml: private.azure_devops_token)
  • +
+

An Azure DevOps Token (PAT) is required for accessing private Azure DevOps projects.

+

Create a PAT using an account that has access to your target Azure DevOps projects. Your PAT only needs the following scopes:

+
    +
  • Build (read)
  • +
  • Release (read)
  • +
  • Test Management (read)
  • +
+

Bintray

+
    +
  • BINTRAY_USER (yml: private.bintray_user)
  • +
  • BINTRAY_API_KEY (yml: private.bintray_apikey)
  • +
+

The bintray API requires authentication +Create an account and obtain a token from the user profile page.

+

Bitbucket (Cloud)

+
    +
  • BITBUCKET_USER (yml: private.bitbucket_username)
  • +
  • BITBUCKET_PASS (yml: private.bitbucket_password)
  • +
+

Bitbucket badges use basic auth. Provide a username and password to give your +self-hosted Shields installation access to private repositories hosted on bitbucket.org.

+

Bitbucket Server

+
    +
  • BITBUCKET_SERVER_ORIGINS (yml: public.services.bitbucketServer.authorizedOrigins)
  • +
  • BITBUCKET_SERVER_USER (yml: private.bitbucket_server_username)
  • +
  • BITBUCKET_SERVER_PASS (yml: private.bitbucket_server_password)
  • +
+

Bitbucket badges use basic auth. Provide a username and password to give your +self-hosted Shields installation access to a private Bitbucket Server instance.

+

Discord

+

Using a token for Dicsord is optional but will allow higher API rates.

+
    +
  • DISCORD_BOT_TOKEN (yml: discord_bot_token)
  • +
+

Register an application in the Discord developer console. +To obtain a token, simply create a bot for your application.

+

Drone

+
    +
  • DRONE_ORIGINS (yml: public.services.drone.authorizedOrigins)
  • +
  • DRONE_TOKEN (yml: private.drone_token)
  • +
+

The self-hosted Drone API requires authentication. Log in to your +Drone instance and obtain a token from the user profile page.

+

GitHub

+
    +
  • GITHUB_URL (yml: public.services.github.baseUri)
  • +
  • GH_TOKEN (yml: private.gh_token)
  • +
+

Because of Github rate limits, you will need to provide a token, or else badges +will stop working once you hit 60 requests per hour, the +unauthenticated rate limit.

+

You can create a personal access token through the +Github website. When you create the token, you can choose to give read access +to your repositories. If you do that, your self-hosted Shields installation +will have access to your private repositories.

+

When a gh_token is specified, it is used in place of the Shields token +rotation logic.

+

GITHUB_URL can be used to optionally send all the GitHub requests to a +GitHub Enterprise server. This can be done in conjunction with setting a +token, though it's not required.

+
    +
  • GH_CLIENT_ID (yml: private.gh_client_id)
  • +
  • GH_CLIENT_SECRET (yml: private.gh_client_secret)
  • +
+

These settings are used by shields.io for GitHub OAuth app authorization +but will not be necessary for most self-hosted installations. See +production-hosting.md.

+

Jenkins CI

+
    +
  • JENKINS_ORIGINS (yml: public.services.jenkins.authorizedOrigins)
  • +
  • JENKINS_USER (yml: private.jenkins_user)
  • +
  • JENKINS_PASS (yml: private.jenkins_pass)
  • +
+

Provide a username and password to give your self-hosted Shields installation +access to a private Jenkins CI instance.

+

Jira

+
    +
  • JIRA_ORIGINS (yml: public.services.jira.authorizedOrigins)
  • +
  • JIRA_USER (yml: private.jira_user)
  • +
  • JIRA_PASS (yml: private.jira_pass)
  • +
+

Provide a username and password to give your self-hosted Shields installation +access to a private JIRA instance.

+

Nexus

+
    +
  • NEXUS_ORIGINS (yml: public.services.nexus.authorizedOrigins)
  • +
  • NEXUS_USER (yml: private.nexus_user)
  • +
  • NEXUS_PASS (yml: private.nexus_pass)
  • +
+

Provide a username and password to give your self-hosted Shields installation +access to your private nexus repositories.

+

npm

+
    +
  • NPM_ORIGINS (yml: public.services.npm.authorizedOrigins)
  • +
  • NPM_TOKEN (yml: private.npm_token)
  • +
+

Generate an npm token to give your self-hosted Shields +installation access to private npm packages

+

SymfonyInsight (formerly Sensiolabs)

+
    +
  • SL_INSIGHT_USER_UUID (yml: private.sl_insight_userUuid)
  • +
  • SL_INSIGHT_API_TOKEN (yml: private.sl_insight_apiToken)
  • +
+

The SymfonyInsight API requires authentication. To obtain a token, +Create an account, sign in and obtain a uuid and token from your +account page.

+

SonarQube

+
    +
  • SONAR_ORIGINS (yml: public.services.sonar.authorizedOrigins)
  • +
  • SONARQUBE_TOKEN (yml: private.sonarqube_token)
  • +
+

Generate a token +to give your self-hosted Shields installation access to a +private SonarQube instance or private project on a public instance.

+

TeamCity

+
    +
  • TEAMCITY_ORIGINS (yml: public.services.teamcity.authorizedOrigins)
  • +
  • TEAMCITY_USER (yml: private.teamcity_user)
  • +
  • TEAMCITY_PASS (yml: private.teamcity_pass)
  • +
+

Provide a username and password to give your self-hosted Shields installation +access to your private nexus repositories.

+

Twitch

+
    +
  • TWITCH_CLIENT_ID (yml: twitch_client_id)
  • +
  • TWITCH_CLIENT_SECRET (yml: twitch_client_secret)
  • +
+

Register an application in the Twitch developer console +in order to obtain a client id and a client secret for making Twitch API calls.

+

Wheelmap

+
    +
  • WHEELMAP_TOKEN (yml: private.wheelmap_token)
  • +
+

The wheelmap API requires authentication. To obtain a token, +Create an account, sign in and use the Authentication Token +displayed on your profile page.

+

YouTube

+
    +
  • YOUTUBE_API_KEY (yml: private.youtube_api_key)
  • +
+

The YouTube API requires authentication. To obtain an API key, +log in to a Google account, go to the credentials page, +and create an API key for the YouTube Data API v3.

+

Error reporting

+
    +
  • SENTRY_DSN (yml: private.sentry_dsn)
  • +
+

A Sentry DSN may be used to send error reports from your installation to +Sentry.io. For more info, see the self hosting docs.

+
+ +
+ +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Wed Dec 23 2020 18:45:19 GMT+0000 (Coordinated Universal Time) +
+ + + + + \ No newline at end of file diff --git a/tutorial-service-tests.html b/tutorial-service-tests.html new file mode 100644 index 0000000000..95acab556f --- /dev/null +++ b/tutorial-service-tests.html @@ -0,0 +1,268 @@ + + + + + JSDoc: Tutorial: service-tests + + + + + + + + + + +
+ +

Tutorial: service-tests

+ +
+ +
+ + +

service-tests

+
+ +
+

Service tests

+

When creating a badge for a new service or changing a badge's behavior, +automated tests should be included. They serve three purposes:

+
    +
  1. +

    The contributor and reviewer can easily verify the code works as +intended.

    +
  2. +
  3. +

    When a badge stops working due to an upstream API, maintainers can find out +right away.

    +
  4. +
  5. +

    They speed up future contributors when they are debugging or improving a +badge.

    +
  6. +
+

Test should cover:

+
    +
  1. Valid behavior
  2. +
  3. Optional parameters like tags or branches
  4. +
  5. Any customized error handling
  6. +
  7. If a non-trivial validator is defined, include tests for malformed responses
  8. +
+

Tutorial

+

Before getting started, set up a development environment by following the +setup instructions

+

We will write some tests for the Wercker Build service

+

(1) Boilerplate

+

The code for our badge is in services/wercker/wercker.service.js. Tests for this badge should be stored in services/wercker/wercker.tester.js.

+

We'll start by adding some boilerplate to our file:

+
'use strict'
+
+const t = (module.exports = require('../tester').createServiceTester())
+
+

If our .service.js module exports a single class, we can +require('../tester').createServiceTester(), which uses convention to create a +ServiceTester object. Calling this inside +services/wercker/wercker.tester.js will create a ServiceTester object +configured for the service exported in services/wercker/wercker.service.js. +We will add our tests to this ServiceTester object t, which is exported +from the module.

+

(2) Our First Test Case

+

First we'll add a test for the typical case:

+
const { isBuildStatus } = require('../test-validators')
+
+t.create('Build status')
+  .get('/build/wercker/go-wercker-api.json')
+  .expectBadge({ label: 'build', message: isBuildStatus })
+
+
    +
  1. The create() method adds a new test to the tester object. +The chained-on calls come from the API testing framework IcedFrisby. +Here's a longer example and the complete API guide.
  2. +
  3. We use the get() method to request a badge. There are several points to consider here: +
      +
    • We need a real project to test against. In this case we have used wercker/go-wercker-api but we could have chosen any stable project.
    • +
    • Note that when we call our badge, we are allowing it to communicate with an external service without mocking the response. We write tests which interact with external services, which is unusual practice in unit testing. We do this because one of the purposes of service tests is to notify us if a badge has broken due to an upstream API change. For this reason it is important for at least one test to call the live API without mocking the interaction.
    • +
    • All badges on shields can be requested in a number of formats. As well as calling https://img.shields.io/wercker/build/wercker/go-wercker-api.svg to generate we can also call https://img.shields.io/wercker/build/wercker/go-wercker-api.json to request the same content as JSON. When writing service tests, we request the badge in JSON format so it is easier to make assertions about the content.
    • +
    • We don't need to explicitly call /wercker/build/wercker/go-wercker-api.json here, only /build/wercker/go-wercker-api.json. When we create a tester object with createServiceTester() the URL base defined in our service class (in this case /wercker) is used as the base URL for any requests made by the tester object.
    • +
    +
  4. +
  5. expectBadge() is a helper function which accepts either a string literal, a RegExp or a Joi schema for the different fields. +Joi is a validation library that is build into IcedFrisby which you can use to +match based on a set of allowed strings, regexes, or specific values. You can +refer to their API reference.
  6. +
  7. We expect label to be a string literal "build".
  8. +
  9. Because this test depends on a live service, we don't want our test to depend on our API call returning a particular build status. Instead we should perform a "picture check" to assert that the badge data conforms to an expected pattern. Our test should not depend on the status of the example project's build, but should fail if trying to generate the badge throws an error, or if there is a breaking change to the upstream API. In this case we will use a pre-defined regular expression to check that the badge value looks like a build status. services/test-validators.js defines a number of useful validators we can use. Many of the common badge types (version, downloads, rank, etc.) already have validators defined here.
  10. +
+

When defining an IcedFrisby test, typically you would invoke the toss() +method, to register the test. This is not necessary, because the Shields test +harness will call it for you.

+

(3) Running the Tests

+

Lets run the test we have written:

+
npm run test:services -- --only=wercker
+
+

The --only= option indicates which service or services you want to test. You +can provide a comma-separated list here.

+

The -- tells the NPM CLI to pass the remaining arguments through to the test +runner.

+

Here's the output:

+
Server is starting up: http://lib/service-test-runner/cli.js:80/
+  Wercker
+    Build status
+      ✓
+        [ GET /build/wercker/go-wercker-api.json ] (572ms)
+
+  1 passing (1s)
+
+

That's looking good!

+

Sometimes if we have a failing test, it is useful to be able to see some logging output to help work out why the test is failing. We can do that by calling npm run test:services:trace. Try running

+
npm run test:services:trace -- --only=wercker
+
+

to run the test with some additional debug output.

+

(4) Writing More Tests

+

We should write tests cases for valid paths through our code. The Wercker badge supports an optional branch parameter so we'll add a second test for a branch build.

+
t.create('Build status (with branch)')
+  .get('/build/wercker/go-wercker-api/master.json')
+  .expectBadge({ label: 'build', message: isBuildStatus })
+
+
Server is starting up: http://lib/service-test-runner/cli.js:80/
+  Wercker
+    Build status
+      ✓
+        [ GET /build/wercker/go-wercker-api.json ] (572ms)
+    Build status (with branch)
+      ✓
+        [ GET /build/wercker/go-wercker-api/master.json ] (368ms)
+
+  2 passing (1s)
+
+

Once we have multiple tests, sometimes it is useful to run only one test. We can do this using the --fgrep argument. For example:

+
npm run test:services -- --only="wercker" --fgrep="Build status (with branch)"
+
+

Having covered the typical and custom cases, we'll move on to errors. We should include a test for the 'not found' response and also tests for any other custom error handling. The Wercker integration defines a custom error condition for 401 as well as a custom 404 message:

+
errorMessages: {
+  401: 'private application not supported',
+  404: 'application not found',
+}
+
+

First we'll add a test for a project which will return a 404 error:

+
t.create('Build status (application not found)')
+  .get('/build/some-project/that-doesnt-exist.json')
+  .expectBadge({ label: 'build', message: 'application not found' })
+
+

In this case we are expecting a string literal instead of a pattern for message. This narrows down the expectation and gives us a more helpful error message if the test fails.

+

We also want to include a test for the 'private application not supported' case. One way to do this would be to find another example of a private project which is unlikely to change. For example:

+
t.create('Build status (private application)')
+  .get('/build/wercker/blueprint.json')
+  .expectBadge({ label: 'build', message: 'private application not supported' })
+
+

(5) Mocking Responses

+

If we didn't have a stable example of a private project, another approach would be to mock the response. An alternative test for the 'private application' case might look like:

+
t.create('Build status (private application)')
+  .get('/build/wercker/go-wercker-api.json')
+  .intercept(nock =>
+    nock('https://app.wercker.com/api/v3/applications/')
+      .get('/wercker/go-wercker-api/builds?limit=1')
+      .reply(401)
+  )
+  .expectBadge({ label: 'build', message: 'private application not supported' })
+
+

This will intercept the request and provide our own mock response. +We use the intercept() method provided by the +icedfrisby-nock plugin. It takes a setup function, +which returns an interceptor, and exposes the full API of the HTTP mocking +library Nock.

+

Nock is fussy. All parts of a request must match perfectly for the mock to +take effect, including the HTTP method (in this case GET), scheme (https), host, +and path.

+

Our test suite should also include service tests which receive a known value from the API. For example, in the render() method of our service, there is some logic which sets the badge color based on the build status:

+
static render({ status, result }) {
+  if (status === 'finished') {
+    if (result === 'passed') {
+      return { message: 'passing', color: 'brightgreen' }
+    } else {
+      return { message: result, color: 'red' }
+    }
+  }
+  return { message: status }
+}
+
+

We can also use nock to intercept API calls to return a known response body.

+
t.create('Build passed')
+  .get('/build/wercker/go-wercker-api.json')
+  .intercept(nock =>
+    nock('https://app.wercker.com/api/v3/applications/')
+      .get('/wercker/go-wercker-api/builds?limit=1')
+      .reply(200, [{ status: 'finished', result: 'passed' }])
+  )
+  .expectBadge({
+    label: 'build',
+    message: 'passing',
+    color: 'brightgreen',
+  })
+
+t.create('Build failed')
+  .get('/build/wercker/go-wercker-api.json')
+  .intercept(nock =>
+    nock('https://app.wercker.com/api/v3/applications/')
+      .get('/wercker/go-wercker-api/builds?limit=1')
+      .reply(200, [{ status: 'finished', result: 'failed' }])
+  )
+  .expectBadge({ label: 'build', message: 'failed', color: 'red' })
+
+

Note that in these tests, we have specified a color parameter in expectBadge. This is helpful in a case like this when we want to test custom color logic, but it is only necessary to explicitly test color values if our badge implements custom logic for setting the badge colors.

+

Code coverage

+

By checking code coverage, we can make sure we've covered all our bases.

+

We can generate a coverage report and open it:

+
npm run coverage:test:services -- --only=wercker
+npm run coverage:report:open
+
+

Pull requests

+

The affected service ids should be included in square brackets in the pull request +title. That way, Circle CI will run those service tests. When a pull request +affects multiple services, they should be separated with spaces. The test +runner is case-insensitive, so they should be capitalized for readability.

+

For example:

+
    +
  • [Travis] Fix timeout issues
  • +
  • [Travis Sonar] Support user token authentication
  • +
  • Add tests for [CRAN] and [CPAN]
  • +
+

In the rare case when it's necessary to see the output of a full service-test +run in a PR, include [*****] in the title. Unless all the tests pass, the build +will fail, so likely it will be necessary to remove it and re-run the tests +before merging.

+

Getting help

+

If you have questions about how to write your tests, please open an issue. If +there's already an issue open for the badge you're working on, you can post a +comment there instead.

+

Further reading

+ +
+ +
+ +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Wed Dec 23 2020 18:45:19 GMT+0000 (Coordinated Universal Time) +
+ + + + + \ No newline at end of file diff --git a/tutorial-users.html b/tutorial-users.html new file mode 100644 index 0000000000..f9ce067ea1 --- /dev/null +++ b/tutorial-users.html @@ -0,0 +1,101 @@ + + + + + JSDoc: Tutorial: users + + + + + + + + + + +
+ +

Tutorial: users

+ +
+ +
+ + +

users

+
+ +
+

Notable Projects Using Shields

+
    +
  • https://github.com/AFNetworking/AFNetworking
  • +
  • https://github.com/angular/angular.js
  • +
  • https://github.com/ansible/ansible
  • +
  • https://github.com/apple/swift
  • +
  • https://github.com/atom/atom
  • +
  • https://github.com/babel/babel
  • +
  • https://github.com/bevacqua/dragula
  • +
  • https://github.com/bower/bower
  • +
  • https://github.com/chartjs/Chart.js
  • +
  • https://github.com/creationix/nvm
  • +
  • https://github.com/discourse/discourse
  • +
  • https://github.com/docker/docker
  • +
  • https://github.com/electron/electron
  • +
  • https://github.com/elm-lang/core
  • +
  • https://github.com/emberjs/ember.js
  • +
  • https://github.com/expressjs/express
  • +
  • https://github.com/facebook/react
  • +
  • https://github.com/FortAwesome/Font-Awesome
  • +
  • https://github.com/gitlabhq/gitlabhq
  • +
  • https://github.com/gulpjs/gulp
  • +
  • https://github.com/h5bp/html5-boilerplate
  • +
  • https://github.com/jakubroztocil/httpie
  • +
  • https://github.com/jekyll/jekyll
  • +
  • https://github.com/kennethreitz/requests
  • +
  • https://github.com/kubernetes/kubernetes
  • +
  • https://github.com/laravel/laravel
  • +
  • https://github.com/less/less.js
  • +
  • https://github.com/Microsoft/TypeScript
  • +
  • https://github.com/Microsoft/vscode
  • +
  • https://github.com/mitchellh/vagrant
  • +
  • https://github.com/Modernizr/Modernizr
  • +
  • https://github.com/moment/moment
  • +
  • https://github.com/mrdoob/three.js
  • +
  • https://github.com/necolas/normalize.css
  • +
  • https://github.com/nodejs/node
  • +
  • https://github.com/plataformatec/devise
  • +
  • https://github.com/postcss/postcss
  • +
  • https://github.com/rails/rails
  • +
  • https://github.com/reactjs/redux
  • +
  • https://github.com/socketio/socket.io
  • +
  • https://github.com/tensorflow/tensorflow
  • +
  • https://github.com/TryGhost/Ghost
  • +
  • https://github.com/twbs/bootstrap
  • +
  • https://github.com/videojs/video.js
  • +
  • https://github.com/vuejs/vue
  • +
  • https://github.com/webpack/webpack
  • +
  • https://github.com/yarnpkg/yarn
  • +
  • https://github.com/zurb/foundation-sites
  • +
+
+ +
+ +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.6 on Wed Dec 23 2020 18:45:19 GMT+0000 (Coordinated Universal Time) +
+ + + + + \ No newline at end of file