Skip to content

Commit

Permalink
Rewrite the DynamicJson badge (#2399)
Browse files Browse the repository at this point in the history
This starts the rewrite of the dynamic badges. I've pulled into BaseService an initial version of the query param validation from #2325.

I've extended from BaseJsonService to avoid duplicating the deserialization logic, though it means there is a bit of duplicated code among the three dynamic services. The way to unravel this would be to move the logic from `_requestJson` and friends from the base classes into functions so DynamicJson can inherit from BaseDynamic. Would that be worth it?

This introduces a regression of #1446 for this badge.

Close #2345
  • Loading branch information
paulmelnikow committed Dec 6, 2018
1 parent b2d5d3e commit 6a737b7
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 65 deletions.
15 changes: 11 additions & 4 deletions lib/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ function validate(
{
ErrorClass,
prettyErrorMessage = 'data does not match schema',
includeKeys = false,
traceErrorMessage = 'Data did not match schema',
traceSuccessMessage = 'Data after validation',
},
Expand All @@ -28,10 +29,16 @@ function validate(
traceErrorMessage,
error.message
)
throw new ErrorClass({
prettyMessage: prettyErrorMessage,
underlyingError: error,
})

let prettyMessage = prettyErrorMessage
if (includeKeys) {
const keys = error.details.map(({ path }) => path)
if (keys) {
prettyMessage = `${prettyErrorMessage}: ${keys.join(',')}`
}
}

throw new ErrorClass({ prettyMessage, underlyingError: error })
} else {
trace.logTrace('validate', emojic.bathtub, traceSuccessMessage, value, {
deep: true,
Expand Down
23 changes: 22 additions & 1 deletion lib/validate.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('validate', function() {
})

context('data does not match schema', function() {
it('logs the data and throws the expected error', async function() {
it('logs the data and throws the expected error', function() {
try {
validate(
options,
Expand All @@ -83,5 +83,26 @@ describe('validate', function() {
'child "requiredString" fails because ["requiredString" must be a string]'
)
})

context('with includeKeys: true', function() {
it('includes keys in the error text', function() {
try {
validate(
{ ...options, includeKeys: true },
{ requiredString: ['this', "shouldn't", 'work'] },
schema
)
expect.fail('Expected to throw')
} catch (e) {
expect(e).to.be.an.instanceof(InvalidParameter)
expect(e.message).to.equal(
'Invalid Parameter: child "requiredString" fails because ["requiredString" must be a string]'
)
expect(e.prettyMessage).to.equal(
`${prettyErrorMessage}: requiredString`
)
}
})
})
})
})
18 changes: 1 addition & 17 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ loadServiceClasses().forEach(serviceClass =>

// User defined sources - JSON response
camp.route(
/^\/badge\/dynamic\/(json|xml|yaml)\.(svg|png|gif|jpg|json)$/,
/^\/badge\/dynamic\/(xml|yaml)\.(svg|png|gif|jpg|json)$/,
cache(config.cacheHeaders, {
queryParams: ['uri', 'url', 'query', 'prefix', 'suffix'],
handler: function(query, match, sendBadge, request) {
Expand Down Expand Up @@ -146,14 +146,6 @@ camp.route(
}

switch (type) {
case 'json':
requestOptions = {
headers: {
Accept: 'application/json',
},
json: true,
}
break
case 'xml':
requestOptions = {
headers: {
Expand Down Expand Up @@ -185,14 +177,6 @@ camp.route(

let innerText = []
switch (type) {
case 'json':
data = typeof data === 'object' ? data : JSON.parse(data)
data = jp.query(data, pathExpression)
if (!data.length) {
throw Error('no result')
}
innerText = data
break
case 'xml':
data = new DOMParser().parseFromString(data)
data = xpath.select(pathExpression, data)
Expand Down
14 changes: 14 additions & 0 deletions services/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,20 @@ class BaseService {
)
}

static _validateQueryParams(queryParams, queryParamSchema) {
return validate(
{
ErrorClass: InvalidParameter,
prettyErrorMessage: 'invalid query parameter',
includeKeys: true,
traceErrorMessage: 'Query params did not match schema',
traceSuccessMessage: 'Query params after validation',
},
queryParams,
queryParamSchema
)
}

async _request({ url, options = {}, errorMessages = {} }) {
const logTrace = (...args) => trace.logTrace('fetch', ...args)
logTrace(emojic.bowAndArrow, 'Request', url, '\n', options)
Expand Down
12 changes: 12 additions & 0 deletions services/base.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,18 @@ describe('BaseService', function() {
expect(e).to.be.an.instanceof(InvalidResponse)
}
})

it('throws error for invalid query params', async function() {
try {
DummyService._validateQueryParams(
{ requiredString: ['this', "shouldn't", 'work'] },
dummySchema
)
expect.fail('Expected to throw')
} catch (e) {
expect(e).to.be.an.instanceof(InvalidParameter)
}
})
})

describe('request', function() {
Expand Down
39 changes: 39 additions & 0 deletions services/dynamic/dynamic-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use strict'

const Joi = require('joi')
const { optionalUrl } = require('../validators')

function createRoute(which) {
return {
base: `badge/dynamic/${which}`,
pattern: '',
queryParams: ['uri', 'url', 'query', 'prefix', 'suffix'],
}
}

const queryParamSchema = Joi.object({
url: optionalUrl.required(),
query: Joi.string().required(),
prefix: Joi.alternatives().try(Joi.string(), Joi.number()),
suffix: Joi.alternatives().try(Joi.string(), Joi.number()),
})
.rename('uri', 'url', { ignoreUndefined: true, override: true })
.required()

const errorMessages = {
404: 'resource not found',
}

function renderDynamicBadge({ values, prefix = '', suffix = '' }) {
return {
message: `${prefix}${values.join(', ')}${suffix}`,
color: 'brightgreen',
}
}

module.exports = {
createRoute,
queryParamSchema,
errorMessages,
renderDynamicBadge,
}
51 changes: 51 additions & 0 deletions services/dynamic/dynamic-json.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use strict'

const Joi = require('joi')
const jp = require('jsonpath')
const BaseJsonService = require('../base-json')
const { InvalidResponse } = require('../errors')
const {
createRoute,
queryParamSchema,
errorMessages,
renderDynamicBadge,
} = require('./dynamic-helpers')

module.exports = class DynamicJson extends BaseJsonService {
static get category() {
return 'dynamic'
}

static get route() {
return createRoute('json')
}

static get defaultBadgeData() {
return {
label: 'custom badge',
}
}

async handle(namedParams, queryParams) {
const {
url,
query: pathExpression,
prefix,
suffix,
} = this.constructor._validateQueryParams(queryParams, queryParamSchema)

const data = await this._requestJson({
schema: Joi.any(),
url,
errorMessages,
})

const values = jp.query(data, pathExpression)

if (!values.length) {
throw new InvalidResponse({ prettyMessage: 'no result' })
}

return renderDynamicBadge({ values, prefix, suffix })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@

const Joi = require('joi')
const { expect } = require('chai')
const ServiceTester = require('../service-tester')
const { colorScheme: colorsB } = require('../test-helpers')

const t = new ServiceTester({
id: 'dynamic-json',
title: 'User Defined JSON Source Data',
pathPrefix: '/badge/dynamic/json',
})
const t = require('../create-service-tester')()
module.exports = t

t.create('Connection error')
Expand All @@ -20,14 +15,14 @@ t.create('Connection error')
.expectJSON({
name: 'Package Name',
value: 'inaccessible',
colorB: colorsB.red,
colorB: colorsB.lightgrey,
})

t.create('No URL specified')
.get('.json?query=$.name&label=Package Name&style=_shields_test')
.expectJSON({
name: 'Package Name',
value: 'no url specified',
value: 'invalid query parameter: url',
colorB: colorsB.red,
})

Expand All @@ -37,7 +32,7 @@ t.create('No query specified')
)
.expectJSON({
name: 'Package Name',
value: 'no query specified',
value: 'invalid query parameter: query',
colorB: colorsB.red,
})

Expand All @@ -47,8 +42,8 @@ t.create('Malformed url')
)
.expectJSON({
name: 'Package Name',
value: 'malformed url',
colorB: colorsB.red,
value: 'invalid',
colorB: colorsB.lightgrey,
})

t.create('JSON from url')
Expand All @@ -57,7 +52,7 @@ t.create('JSON from url')
)
.expectJSON({
name: 'custom badge',
value: 'gh-badges',
value: 'shields.io',
colorB: colorsB.brightgreen,
})

Expand All @@ -67,7 +62,7 @@ t.create('JSON from uri (support uri query paramater)')
)
.expectJSON({
name: 'custom badge',
value: 'gh-badges',
value: 'shields.io',
colorB: colorsB.brightgreen,
})

Expand Down Expand Up @@ -116,14 +111,14 @@ t.create('JSON from url | invalid url')
.expectJSON({
name: 'custom badge',
value: 'resource not found',
colorB: colorsB.lightgrey,
colorB: colorsB.red,
})

t.create('JSON from url | user color overrides default')
.get(
'.json?url=https://github.com/badges/shields/raw/master/package.json&query=$.name&colorB=10ADED&style=_shields_test'
)
.expectJSON({ name: 'custom badge', value: 'gh-badges', colorB: '#10ADED' })
.expectJSON({ name: 'custom badge', value: 'shields.io', colorB: '#10ADED' })

t.create('JSON from url | error color overrides default')
.get(
Expand All @@ -132,17 +127,18 @@ t.create('JSON from url | error color overrides default')
.expectJSON({
name: 'custom badge',
value: 'resource not found',
colorB: colorsB.lightgrey,
})

t.create('JSON from url | error color overrides user specified')
.get('.json?query=$.version&colorB=10ADED&style=_shields_test')
.expectJSON({
name: 'custom badge',
value: 'no url specified',
colorB: colorsB.red,
})

// FIXME This is a regression which should be fixed in BaseService.
// t.create('JSON from url | error color overrides user specified')
// .get('.json?query=$.version&colorB=10ADED&style=_shields_test')
// .expectJSON({
// name: 'custom badge',
// value: 'invalid query parameter: url',
// colorB: colorsB.red,
// })

let headers
t.create('JSON from url | request should set Accept header')
.get('.json?url=https://json-test/api.json&query=$.name')
Expand Down
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit 6a737b7

Please sign in to comment.