Validate response json in [apm appveyor cdnjs clojars gem] (#1883)

* split gem service into multiple files
* add validation to apm, appveyor, cdnjs, clojars & gem services
* fix the apm examples
This commit is contained in:
chris48s
2018-08-10 20:52:09 +01:00
committed by GitHub
parent 15a1449407
commit f05a3496ee
11 changed files with 406 additions and 332 deletions

View File

@@ -5,11 +5,22 @@ const { BaseJsonService } = require('../base')
const { InvalidResponse } = require('../errors')
const { version: versionColor } = require('../../lib/color-formatters')
const { metric, addv } = require('../../lib/text-formatters')
const { nonNegativeInteger } = require('../validators.js')
const apmSchema = Joi.object({
downloads: nonNegativeInteger,
releases: Joi.object({
latest: Joi.string().required(),
}),
metadata: Joi.object({
license: Joi.string().required(),
}),
})
class BaseAPMService extends BaseJsonService {
async fetch(repo) {
return this._requestJson({
schema: Joi.object(),
schema: apmSchema,
url: `https://atom.io/api/packages/${repo}`,
notFoundMessage: 'package not found',
})
@@ -18,6 +29,15 @@ class BaseAPMService extends BaseJsonService {
static get defaultBadgeData() {
return { label: 'apm' }
}
static get examples() {
return [
{
previewUrl: 'vim-mode',
keywords: ['atom'],
},
]
}
}
class APMDownloads extends BaseAPMService {
@@ -43,15 +63,6 @@ class APMDownloads extends BaseAPMService {
capture: ['repo'],
}
}
static get examples() {
return [
{
previewUrl: 'dm/vim-mode',
keywords: ['atom'],
},
]
}
}
class APMVersion extends BaseAPMService {
@@ -77,15 +88,6 @@ class APMVersion extends BaseAPMService {
capture: ['repo'],
}
}
static get examples() {
return [
{
previewUrl: 'v/vim-mode',
keywords: ['atom'],
},
]
}
}
class APMLicense extends BaseAPMService {
@@ -115,15 +117,6 @@ class APMLicense extends BaseAPMService {
capture: ['repo'],
}
}
static get examples() {
return [
{
previewUrl: 'l/vim-mode',
keywords: ['atom'],
},
]
}
}
module.exports = {

View File

@@ -3,6 +3,12 @@
const Joi = require('joi')
const { BaseJsonService } = require('../base')
const appVeyorSchema = Joi.object({
build: Joi.object({
status: Joi.string().required(),
}),
}).required()
module.exports = class AppVeyor extends BaseJsonService {
async handle({ repo, branch }) {
let url = `https://ci.appveyor.com/api/projects/${repo}`
@@ -12,7 +18,7 @@ module.exports = class AppVeyor extends BaseJsonService {
const {
build: { status },
} = await this._requestJson({
schema: Joi.object(),
schema: appVeyorSchema,
url,
notFoundMessage: 'project not found or access denied',
})

View File

@@ -6,12 +6,17 @@ const { NotFound } = require('../errors')
const { addv: versionText } = require('../../lib/text-formatters')
const { version: versionColor } = require('../../lib/color-formatters')
const cdnjsSchema = Joi.object({
// optional due to non-standard 'not found' condition
version: Joi.string(),
}).required()
module.exports = class Cdnjs extends BaseJsonService {
async handle({ library }) {
const url = `https://api.cdnjs.com/libraries/${library}?fields=version`
const json = await this._requestJson({
url,
schema: Joi.any(),
schema: cdnjsSchema,
})
if (Object.keys(json).length === 0) {
@@ -19,11 +24,10 @@ module.exports = class Cdnjs extends BaseJsonService {
status code = 200, body = {} */
throw new NotFound()
}
const version = json.version || 0
return {
message: versionText(version),
color: versionColor(version),
message: versionText(json.version),
color: versionColor(json.version),
}
}

View File

@@ -5,12 +5,17 @@ const { BaseJsonService } = require('../base')
const { NotFound } = require('../errors')
const { version: versionColor } = require('../../lib/color-formatters')
const clojarsSchema = Joi.object({
// optional due to non-standard 'not found' condition
version: Joi.string(),
}).required()
module.exports = class Clojars extends BaseJsonService {
async handle({ clojar }) {
const url = `https://clojars.org/${clojar}/latest-version.json`
const json = await this._requestJson({
url,
schema: Joi.any(),
schema: clojarsSchema,
})
if (Object.keys(json).length === 0) {

View File

@@ -0,0 +1,149 @@
'use strict'
const semver = require('semver')
const Joi = require('joi')
const { BaseJsonService } = require('../base')
const { InvalidResponse } = require('../errors')
const {
downloadCount: downloadCountColor,
} = require('../../lib/color-formatters')
const { metric } = require('../../lib/text-formatters')
const { latest: latestVersion } = require('../../lib/version')
const { nonNegativeInteger } = require('../validators.js')
const gemsSchema = Joi.object({
downloads: nonNegativeInteger,
version_downloads: nonNegativeInteger,
}).required()
const versionsSchema = Joi.array()
.items(
Joi.object({
prerelease: Joi.boolean().required(),
number: Joi.string().required(),
downloads_count: nonNegativeInteger,
})
)
.min(1)
.required()
module.exports = class GemDownloads extends BaseJsonService {
async fetch(repo, info) {
const endpoint = info === 'dv' ? 'versions/' : 'gems/'
const schema = info === 'dv' ? versionsSchema : gemsSchema
const url = `https://rubygems.org/api/v1/${endpoint}${repo}.json`
return this._requestJson({
url,
schema,
})
}
_getLabel(version, info) {
if (version) {
return 'downloads@' + version
} else {
if (info === 'dtv') {
return 'downloads@latest'
} else {
return 'downloads'
}
}
}
async handle({ info, rubygem }) {
const splitRubygem = rubygem.split('/')
const repo = splitRubygem[0]
let version =
splitRubygem.length > 1 ? splitRubygem[splitRubygem.length - 1] : null
version = version === 'stable' ? version : semver.valid(version)
const label = this._getLabel(version, info)
const json = await this.fetch(repo, info)
let downloads
if (info === 'dt') {
downloads = metric(json.downloads)
} else if (info === 'dtv') {
downloads = metric(json.version_downloads)
} else if (info === 'dv') {
let versionData
if (version !== null && version === 'stable') {
const versions = json
.filter(function(ver) {
return ver.prerelease === false
})
.map(function(ver) {
return ver.number
})
// Found latest stable version.
const stableVersion = latestVersion(versions)
versionData = json.filter(function(ver) {
return ver.number === stableVersion
})[0]
downloads = metric(versionData.downloads_count)
} else if (version !== null) {
versionData = json.filter(function(ver) {
return ver.number === version
})[0]
downloads = metric(versionData.downloads_count)
} else {
throw new InvalidResponse({
underlyingError: new Error('version is null'),
})
}
} else {
throw new InvalidResponse({
underlyingError: new Error('info is invalid'),
})
}
return {
label: label,
message: downloads,
color: downloadCountColor(downloads),
}
}
// Metadata
static get defaultBadgeData() {
return { label: 'downloads' }
}
static get category() {
return 'downloads'
}
static get url() {
return {
base: 'gem',
format: '(dt|dtv|dv)/(.+)',
capture: ['info', 'rubygem'],
}
}
static get examples() {
return [
{
title: 'Gem',
previewUrl: 'dv/rails/stable',
keywords: ['ruby'],
},
{
title: 'Gem',
previewUrl: 'dv/rails/4.1.0',
keywords: ['ruby'],
},
{
title: 'Gem',
previewUrl: 'dtv/rails',
keywords: ['ruby'],
},
{
title: 'Gem',
previewUrl: 'dt/rails',
keywords: ['ruby'],
},
]
}
}

View File

@@ -0,0 +1,51 @@
'use strict'
const Joi = require('joi')
const { BaseJsonService } = require('../base')
const { floorCount: floorCountColor } = require('../../lib/color-formatters')
const ownerSchema = Joi.array().required()
module.exports = class GemOwner extends BaseJsonService {
async handle({ user }) {
const url = `https://rubygems.org/api/v1/owners/${user}/gems.json`
const json = await this._requestJson({
url,
schema: ownerSchema,
})
const count = json.length
return {
message: count,
color: floorCountColor(count, 10, 50, 100),
}
}
// Metadata
static get defaultBadgeData() {
return { label: 'gems' }
}
static get category() {
return 'other'
}
static get url() {
return {
base: 'gem/u',
format: '(.+)',
capture: ['user'],
}
}
static get examples() {
return [
{
title: 'Gems',
previewUrl: 'raphink',
keywords: ['ruby'],
},
]
}
}

View File

@@ -0,0 +1,95 @@
'use strict'
const Joi = require('joi')
const { BaseJsonService } = require('../base')
const { floorCount: floorCountColor } = require('../../lib/color-formatters')
const { ordinalNumber } = require('../../lib/text-formatters')
const { nonNegativeInteger } = require('../validators.js')
const totalSchema = Joi.array()
.items(
Joi.object({
total_ranking: nonNegativeInteger,
})
)
.min(1)
.required()
const dailySchema = Joi.array()
.items(
Joi.object({
daily_ranking: nonNegativeInteger,
})
)
.min(1)
.required()
module.exports = class GemRank extends BaseJsonService {
_getApiUrl(repo, totalRank, dailyRank) {
let endpoint
if (totalRank) {
endpoint = '/total_ranking.json'
} else if (dailyRank) {
endpoint = '/daily_ranking.json'
}
return `http://bestgems.org/api/v1/gems/${repo}${endpoint}`
}
async handle({ info, repo }) {
const totalRank = info === 'rt'
const dailyRank = info === 'rd'
const schema = totalRank ? totalSchema : dailySchema
const url = this._getApiUrl(repo, totalRank, dailyRank)
const json = await this._requestJson({
url,
schema,
})
let rank
if (totalRank) {
rank = json[0].total_ranking
} else if (dailyRank) {
rank = json[0].daily_ranking
}
const count = Math.floor(100000 / rank)
let message = ordinalNumber(rank)
message += totalRank ? '' : ' daily'
return {
message: message,
color: floorCountColor(count, 10, 50, 100),
}
}
// Metadata
static get defaultBadgeData() {
return { label: 'rank' }
}
static get category() {
return 'rating'
}
static get url() {
return {
base: 'gem',
format: '(rt|rd)/(.+)',
capture: ['info', 'repo'],
}
}
static get examples() {
return [
{
title: 'Gems',
previewUrl: 'rt/puppet',
keywords: ['ruby'],
},
{
title: 'Gems',
previewUrl: 'rd/facter',
keywords: ['ruby'],
},
]
}
}

View File

@@ -0,0 +1,55 @@
'use strict'
const Joi = require('joi')
const { BaseJsonService } = require('../base')
const { addv: versionText } = require('../../lib/text-formatters')
const { version: versionColor } = require('../../lib/color-formatters')
// Response should contain a string key 'version'
// In most cases this will be a SemVer
// but the registry doesn't actually enforce this
const versionSchema = Joi.object({
version: Joi.string().required(),
}).required()
module.exports = class GemVersion extends BaseJsonService {
async handle({ repo }) {
const url = `https://rubygems.org/api/v1/gems/${repo}.json`
const { version } = await this._requestJson({
url,
schema: versionSchema,
})
return {
message: versionText(version),
color: versionColor(version),
}
}
// Metadata
static get defaultBadgeData() {
return { label: 'gem' }
}
static get category() {
return 'version'
}
static get url() {
return {
base: 'gem/v',
format: '(.+)',
capture: ['repo'],
}
}
static get examples() {
return [
{
title: 'Gem',
previewUrl: 'formatador',
keywords: ['ruby'],
},
]
}
}

View File

@@ -1,294 +0,0 @@
'use strict'
const semver = require('semver')
const Joi = require('joi')
const { BaseJsonService } = require('../base')
const { InvalidResponse } = require('../errors')
const { addv: versionText } = require('../../lib/text-formatters')
const { version: versionColor } = require('../../lib/color-formatters')
const {
floorCount: floorCountColor,
downloadCount: downloadCountColor,
} = require('../../lib/color-formatters')
const { metric, ordinalNumber } = require('../../lib/text-formatters')
const { latest: latestVersion } = require('../../lib/version')
class GemVersion extends BaseJsonService {
async handle({ repo }) {
const url = `https://rubygems.org/api/v1/gems/${repo}.json`
const { version } = await this._requestJson({
url,
schema: Joi.object(),
})
return {
message: versionText(version),
color: versionColor(version),
}
}
// Metadata
static get defaultBadgeData() {
return { label: 'gem' }
}
static get category() {
return 'version'
}
static get url() {
return {
base: 'gem/v',
format: '(.+)',
capture: ['repo'],
}
}
static get examples() {
return [
{
title: 'Gem',
previewUrl: 'formatador',
keywords: ['ruby'],
},
]
}
}
class GemDownloads extends BaseJsonService {
fetch(repo, info) {
const endpoint = info === 'dv' ? 'versions/' : 'gems/'
const url = `https://rubygems.org/api/v1/${endpoint}${repo}.json`
return this._requestJson({
url,
schema: Joi.any(),
})
}
_getLabel(version, info) {
if (version) {
return 'downloads@' + version
} else {
if (info === 'dtv') {
return 'downloads@latest'
} else {
return 'downloads'
}
}
}
async handle({ info, rubygem }) {
const splitRubygem = rubygem.split('/')
const repo = splitRubygem[0]
let version =
splitRubygem.length > 1 ? splitRubygem[splitRubygem.length - 1] : null
version = version === 'stable' ? version : semver.valid(version)
const label = this._getLabel(version, info)
const json = await this.fetch(repo, info)
let downloads
if (info === 'dt') {
downloads = metric(json.downloads)
} else if (info === 'dtv') {
downloads = metric(json.version_downloads)
} else if (info === 'dv') {
let versionData
if (version !== null && version === 'stable') {
const versions = json
.filter(function(ver) {
return ver.prerelease === false
})
.map(function(ver) {
return ver.number
})
// Found latest stable version.
const stableVersion = latestVersion(versions)
versionData = json.filter(function(ver) {
return ver.number === stableVersion
})[0]
downloads = metric(versionData.downloads_count)
} else if (version !== null) {
versionData = json.filter(function(ver) {
return ver.number === version
})[0]
downloads = metric(versionData.downloads_count)
} else {
throw new InvalidResponse({
underlyingError: new Error('version is null'),
})
}
} else {
throw new InvalidResponse({
underlyingError: new Error('info is invalid'),
})
}
return {
label: label,
message: downloads,
color: downloadCountColor(downloads),
}
}
// Metadata
static get defaultBadgeData() {
return { label: 'downloads' }
}
static get category() {
return 'downloads'
}
static get url() {
return {
base: 'gem',
format: '(dt|dtv|dv)/(.+)',
capture: ['info', 'rubygem'],
}
}
static get examples() {
return [
{
title: 'Gem',
previewUrl: 'dv/rails/stable',
keywords: ['ruby'],
},
{
title: 'Gem',
previewUrl: 'dv/rails/4.1.0',
keywords: ['ruby'],
},
{
title: 'Gem',
previewUrl: 'dtv/rails',
keywords: ['ruby'],
},
{
title: 'Gem',
previewUrl: 'dt/rails',
keywords: ['ruby'],
},
]
}
}
class GemOwner extends BaseJsonService {
async handle({ user }) {
const url = `https://rubygems.org/api/v1/owners/${user}/gems.json`
const json = await this._requestJson({
url,
schema: Joi.array(),
})
const count = json.length
return {
message: count,
color: floorCountColor(count, 10, 50, 100),
}
}
// Metadata
static get defaultBadgeData() {
return { label: 'gems' }
}
static get category() {
return 'other'
}
static get url() {
return {
base: 'gem/u',
format: '(.+)',
capture: ['user'],
}
}
static get examples() {
return [
{
title: 'Gems',
previewUrl: 'raphink',
keywords: ['ruby'],
},
]
}
}
class GemRank extends BaseJsonService {
_getApiUrl(repo, totalRank, dailyRank) {
let endpoint
if (totalRank) {
endpoint = '/total_ranking.json'
} else if (dailyRank) {
endpoint = '/daily_ranking.json'
}
return `http://bestgems.org/api/v1/gems/${repo}${endpoint}`
}
async handle({ info, repo }) {
const totalRank = info === 'rt'
const dailyRank = info === 'rd'
const url = this._getApiUrl(repo, totalRank, dailyRank)
const json = await this._requestJson({
url,
schema: Joi.array(),
})
let rank
if (totalRank) {
rank = json[0].total_ranking
} else if (dailyRank) {
rank = json[0].daily_ranking
}
const count = Math.floor(100000 / rank)
let message = ordinalNumber(rank)
message += totalRank ? '' : ' daily'
return {
message: message,
color: floorCountColor(count, 10, 50, 100),
}
}
// Metadata
static get defaultBadgeData() {
return { label: 'rank' }
}
static get category() {
return 'rating'
}
static get url() {
return {
base: 'gem',
format: '(rt|rd)/(.+)',
capture: ['info', 'repo'],
}
}
static get examples() {
return [
{
title: 'Gems',
previewUrl: 'rt/puppet',
keywords: ['ruby'],
},
{
title: 'Gems',
previewUrl: 'rd/facter',
keywords: ['ruby'],
},
]
}
}
module.exports = {
GemVersion,
GemDownloads,
GemOwner,
GemRank,
}

View File

@@ -3,13 +3,11 @@
const Joi = require('joi')
const { BaseJsonService } = require('../base')
const { metric } = require('../../lib/text-formatters')
const { nonNegativeInteger } = require('../validators.js')
// https://github.com/npm/registry/blob/master/docs/download-counts.md#output
const pointResponseSchema = Joi.object({
downloads: Joi.number()
.integer()
.min(0)
.required(),
downloads: nonNegativeInteger,
}).required()
// https://github.com/npm/registry/blob/master/docs/download-counts.md#output-1

12
services/validators.js Normal file
View File

@@ -0,0 +1,12 @@
'use strict'
const Joi = require('joi')
const nonNegativeInteger = Joi.number()
.integer()
.min(0)
.required()
module.exports = {
nonNegativeInteger,
}