From 2ee1327eed9410142c13bc5afc607ddc11f62ef9 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Thu, 7 Feb 2019 21:14:04 -0500 Subject: [PATCH] Refactor the NuGet v2 badges; switch Resharper to XML (#2934) This closes #2921 by switching ReSharper to the XML API used by Powershell, and refactors the powershell code back into the common nuget v2 service class. It also removes mocked tests of the color logic, replacing them with smaller-bracket tests that accomplish the same thing more concisely. --- services/chocolatey/chocolatey.service.js | 1 + services/chocolatey/chocolatey.tester.js | 95 ----------- services/nuget-fixtures.js | 21 --- services/nuget/nuget-helpers.js | 14 ++ services/nuget/nuget-helpers.spec.js | 31 ++++ services/nuget/nuget-v2-service-family.js | 75 +++++++-- .../powershellgallery.service.js | 154 +++--------------- services/resharper/resharper.service.js | 1 + services/resharper/resharper.tester.js | 101 +----------- 9 files changed, 133 insertions(+), 360 deletions(-) create mode 100644 services/nuget/nuget-helpers.spec.js diff --git a/services/chocolatey/chocolatey.service.js b/services/chocolatey/chocolatey.service.js index 495c494a6b..c03f3d1a7c 100644 --- a/services/chocolatey/chocolatey.service.js +++ b/services/chocolatey/chocolatey.service.js @@ -6,6 +6,7 @@ module.exports = createServiceFamily({ defaultLabel: 'chocolatey', serviceBaseUrl: 'chocolatey', apiBaseUrl: 'https://www.chocolatey.org/api/v2', + odataFormat: 'json', title: 'Chocolatey', examplePackageName: 'git', exampleVersion: '2.19.2', diff --git a/services/chocolatey/chocolatey.tester.js b/services/chocolatey/chocolatey.tester.js index 693fba22a8..981924470d 100644 --- a/services/chocolatey/chocolatey.tester.js +++ b/services/chocolatey/chocolatey.tester.js @@ -6,11 +6,6 @@ const { isVPlusDottedVersionNClauses, isVPlusDottedVersionNClausesWithOptionalSuffix, } = require('../test-validators') -const { - nuGetV2VersionJsonWithDash, - nuGetV2VersionJsonFirstCharZero, - nuGetV2VersionJsonFirstCharNotZero, -} = require('../nuget-fixtures') const { ServiceTester } = require('../tester') const t = (module.exports = new ServiceTester({ @@ -44,51 +39,6 @@ t.create('version (valid)') }) ) -t.create('version (mocked, yellow badge)') - .get('/v/scriptcs.json?style=_shields_test') - .intercept(nock => - nock('https://www.chocolatey.org') - .get( - '/api/v2/Packages()?%24filter=Id%20eq%20%27scriptcs%27%20and%20IsLatestVersion%20eq%20true' - ) - .reply(200, nuGetV2VersionJsonWithDash) - ) - .expectJSON({ - name: 'chocolatey', - value: 'v1.2-beta', - color: 'yellow', - }) - -t.create('version (mocked, orange badge)') - .get('/v/scriptcs.json?style=_shields_test') - .intercept(nock => - nock('https://www.chocolatey.org') - .get( - '/api/v2/Packages()?%24filter=Id%20eq%20%27scriptcs%27%20and%20IsLatestVersion%20eq%20true' - ) - .reply(200, nuGetV2VersionJsonFirstCharZero) - ) - .expectJSON({ - name: 'chocolatey', - value: 'v0.35', - color: 'orange', - }) - -t.create('version (mocked, blue badge)') - .get('/v/scriptcs.json?style=_shields_test') - .intercept(nock => - nock('https://www.chocolatey.org') - .get( - '/api/v2/Packages()?%24filter=Id%20eq%20%27scriptcs%27%20and%20IsLatestVersion%20eq%20true' - ) - .reply(200, nuGetV2VersionJsonFirstCharNotZero) - ) - .expectJSON({ - name: 'chocolatey', - value: 'v1.2.7', - color: 'blue', - }) - t.create('version (not found)') .get('/v/not-a-real-package.json') .expectJSON({ name: 'chocolatey', value: 'not found' }) @@ -104,51 +54,6 @@ t.create('version (pre) (valid)') }) ) -t.create('version (pre) (mocked, yellow badge)') - .get('/vpre/scriptcs.json?style=_shields_test') - .intercept(nock => - nock('https://www.chocolatey.org') - .get( - '/api/v2/Packages()?%24filter=Id%20eq%20%27scriptcs%27%20and%20IsAbsoluteLatestVersion%20eq%20true' - ) - .reply(200, nuGetV2VersionJsonWithDash) - ) - .expectJSON({ - name: 'chocolatey', - value: 'v1.2-beta', - color: 'yellow', - }) - -t.create('version (pre) (mocked, orange badge)') - .get('/vpre/scriptcs.json?style=_shields_test') - .intercept(nock => - nock('https://www.chocolatey.org') - .get( - '/api/v2/Packages()?%24filter=Id%20eq%20%27scriptcs%27%20and%20IsAbsoluteLatestVersion%20eq%20true' - ) - .reply(200, nuGetV2VersionJsonFirstCharZero) - ) - .expectJSON({ - name: 'chocolatey', - value: 'v0.35', - color: 'orange', - }) - -t.create('version (pre) (mocked, blue badge)') - .get('/vpre/scriptcs.json?style=_shields_test') - .intercept(nock => - nock('https://www.chocolatey.org') - .get( - '/api/v2/Packages()?%24filter=Id%20eq%20%27scriptcs%27%20and%20IsAbsoluteLatestVersion%20eq%20true' - ) - .reply(200, nuGetV2VersionJsonFirstCharNotZero) - ) - .expectJSON({ - name: 'chocolatey', - value: 'v1.2.7', - color: 'blue', - }) - t.create('version (pre) (not found)') .get('/vpre/not-a-real-package.json') .expectJSON({ name: 'chocolatey', value: 'not found' }) diff --git a/services/nuget-fixtures.js b/services/nuget-fixtures.js index d5842ea5c7..ece84319c9 100644 --- a/services/nuget-fixtures.js +++ b/services/nuget-fixtures.js @@ -9,24 +9,6 @@ const queryIndex = JSON.stringify({ ], }) -const nuGetV2VersionJsonWithDash = JSON.stringify({ - d: { - results: [ - { NormalizedVersion: '1.2-beta', Version: 'xxx', DownloadCount: 0 }, - ], - }, -}) -const nuGetV2VersionJsonFirstCharZero = JSON.stringify({ - d: { - results: [{ NormalizedVersion: '0.35', Version: 'xxx', DownloadCount: 0 }], - }, -}) -const nuGetV2VersionJsonFirstCharNotZero = JSON.stringify({ - d: { - results: [{ NormalizedVersion: '1.2.7', Version: 'xxx', DownloadCount: 0 }], - }, -}) - const nuGetV3VersionJsonWithDash = JSON.stringify({ data: [ { @@ -54,9 +36,6 @@ const nuGetV3VersionJsonFirstCharNotZero = JSON.stringify({ module.exports = { queryIndex, - nuGetV2VersionJsonWithDash, - nuGetV2VersionJsonFirstCharZero, - nuGetV2VersionJsonFirstCharNotZero, nuGetV3VersionJsonWithDash, nuGetV3VersionJsonFirstCharZero, nuGetV3VersionJsonFirstCharNotZero, diff --git a/services/nuget/nuget-helpers.js b/services/nuget/nuget-helpers.js index 59731824bf..8fefd7291c 100644 --- a/services/nuget/nuget-helpers.js +++ b/services/nuget/nuget-helpers.js @@ -29,7 +29,21 @@ function renderDownloadBadge({ downloads }) { } } +function odataToObject(odata) { + if (odata === undefined) { + return undefined + } + + const result = {} + Object.entries(odata['m:properties']).forEach(([key, value]) => { + const newKey = key.replace(/^d:/, '') + result[newKey] = value + }) + return result +} + module.exports = { renderVersionBadge, renderDownloadBadge, + odataToObject, } diff --git a/services/nuget/nuget-helpers.spec.js b/services/nuget/nuget-helpers.spec.js new file mode 100644 index 0000000000..0026e6b51d --- /dev/null +++ b/services/nuget/nuget-helpers.spec.js @@ -0,0 +1,31 @@ +'use strict' + +const { renderVersionBadge, odataToObject } = require('./nuget-helpers') +const { test, given } = require('sazerac') + +describe('NuGet helpers', function() { + test(renderVersionBadge, () => { + given({ version: '1.2-beta' }).expect({ + label: undefined, + message: 'v1.2-beta', + color: 'yellow', + }) + given({ version: '0.35' }).expect({ + label: undefined, + message: 'v0.35', + color: 'orange', + }) + given({ version: '1.2.7' }).expect({ + label: undefined, + message: 'v1.2.7', + color: 'blue', + }) + }) + + test(odataToObject, () => { + given({ 'm:properties': { 'd:Version': '1.2.3' } }).expect({ + Version: '1.2.3', + }) + given(undefined).expect(undefined) + }) +}) diff --git a/services/nuget/nuget-v2-service-family.js b/services/nuget/nuget-v2-service-family.js index 523837e0af..1d7704c193 100644 --- a/services/nuget/nuget-v2-service-family.js +++ b/services/nuget/nuget-v2-service-family.js @@ -1,9 +1,13 @@ 'use strict' const Joi = require('joi') -const { BaseJsonService, NotFound } = require('..') +const { BaseJsonService, BaseXmlService, NotFound } = require('..') const { nonNegativeInteger } = require('../validators') -const { renderVersionBadge, renderDownloadBadge } = require('./nuget-helpers') +const { + renderVersionBadge, + renderDownloadBadge, + odataToObject, +} = require('./nuget-helpers') function createFilter({ packageName, includePrereleases }) { const releaseTypeFilter = includePrereleases @@ -12,7 +16,7 @@ function createFilter({ packageName, includePrereleases }) { return `Id eq '${packageName}' and ${releaseTypeFilter}` } -const schema = Joi.object({ +const jsonSchema = Joi.object({ d: Joi.object({ results: Joi.array() .items( @@ -27,25 +31,53 @@ const schema = Joi.object({ }).required(), }).required() +const xmlSchema = Joi.object({ + feed: Joi.object({ + entry: Joi.object({ + 'm:properties': Joi.object({ + 'd:Version': Joi.string(), + 'd:NormalizedVersion': Joi.string(), + 'd:DownloadCount': nonNegativeInteger, + 'd:Tags': Joi.string(), + }), + }), + }).required(), +}).required() + async function fetch( serviceInstance, - { baseUrl, packageName, includePrereleases = false } + { odataFormat, baseUrl, packageName, includePrereleases = false } ) { - const data = await serviceInstance._requestJson({ - schema, - url: `${baseUrl}/Packages()`, - options: { - headers: { Accept: 'application/atom+json,application/json' }, - qs: { $filter: createFilter({ packageName, includePrereleases }) }, - }, - }) + const url = `${baseUrl}/Packages()` + const qs = { $filter: createFilter({ packageName, includePrereleases }) } - const packageData = data.d.results[0] + let packageData + if (odataFormat === 'xml') { + const data = await serviceInstance._requestXml({ + schema: xmlSchema, + url, + options: { qs }, + }) + packageData = odataToObject(data.feed.entry) + } else if (odataFormat === 'json') { + const data = await serviceInstance._requestJson({ + schema: jsonSchema, + url, + options: { + headers: { Accept: 'application/atom+json,application/json' }, + qs, + }, + }) + packageData = data.d.results[0] + } else { + throw Error(`Unsupported Atom OData format: ${odataFormat}`) + } if (packageData) { return packageData } else if (!includePrereleases) { return fetch(serviceInstance, { + odataFormat, baseUrl, packageName, includePrereleases: true, @@ -67,13 +99,23 @@ function createServiceFamily({ defaultLabel, serviceBaseUrl, apiBaseUrl, + odataFormat, title, examplePackageName, exampleVersion, examplePrereleaseVersion, exampleDownloadCount, }) { - class NugetVersionService extends BaseJsonService { + let Base + if (odataFormat === 'xml') { + Base = BaseXmlService + } else if (odataFormat === 'json') { + Base = BaseJsonService + } else { + throw Error(`Unsupported Atom OData format: ${odataFormat}`) + } + + class NugetVersionService extends Base { static get category() { return 'version' } @@ -116,6 +158,7 @@ function createServiceFamily({ async handle({ which, packageName }) { const packageData = await fetch(this, { + odataFormat, baseUrl: apiBaseUrl, packageName, includePrereleases: which === 'vpre', @@ -125,7 +168,7 @@ function createServiceFamily({ } } - class NugetDownloadService extends BaseJsonService { + class NugetDownloadService extends Base { static get category() { return 'downloads' } @@ -155,6 +198,7 @@ function createServiceFamily({ async handle({ packageName }) { const packageData = await fetch(this, { + odataFormat, baseUrl: apiBaseUrl, packageName, }) @@ -168,5 +212,6 @@ function createServiceFamily({ module.exports = { createFilter, + fetch, createServiceFamily, } diff --git a/services/powershellgallery/powershellgallery.service.js b/services/powershellgallery/powershellgallery.service.js index 2b3ba59409..d053bd6bf7 100644 --- a/services/powershellgallery/powershellgallery.service.js +++ b/services/powershellgallery/powershellgallery.service.js @@ -1,142 +1,31 @@ 'use strict' -const Joi = require('joi') -const { createFilter } = require('../nuget/nuget-v2-service-family') const { - renderVersionBadge, - renderDownloadBadge, -} = require('../nuget/nuget-helpers') -const { BaseXmlService, NotFound } = require('..') -const { nonNegativeInteger } = require('../validators') + fetch, + createServiceFamily, +} = require('../nuget/nuget-v2-service-family') +const { BaseXmlService } = require('..') const WINDOWS_TAG_NAME = 'windows' const MACOS_TAG_NAME = 'macos' const LINUX_TAG_NAME = 'linux' -const schema = Joi.object({ - feed: Joi.object({ - entry: Joi.object({ - 'm:properties': Joi.object({ - 'd:Version': Joi.string(), - 'd:NormalizedVersion': Joi.string(), - 'd:DownloadCount': nonNegativeInteger, - 'd:Tags': Joi.string(), - }), - }), - }).required(), -}).required() +const apiBaseUrl = 'https://www.powershellgallery.com/api/v2' -async function fetch( - serviceInstance, - { packageName, includePrereleases = false } -) { - const data = await serviceInstance._requestXml({ - schema, - url: `https://www.powershellgallery.com/api/v2/Search()`, - options: { - qs: { $filter: createFilter({ packageName, includePrereleases }) }, - }, - }) - - const packageData = - 'entry' in data.feed ? data.feed.entry['m:properties'] : undefined - - if (packageData) { - return packageData - } else if (!includePrereleases) { - return fetch(serviceInstance, { - packageName, - includePrereleases: true, - }) - } else { - throw new NotFound() - } -} - -class PowershellGalleryVersion extends BaseXmlService { - static get category() { - return 'version' - } - - static get route() { - return { - base: 'powershellgallery', - pattern: ':which(v|vpre)/:packageName', - } - } - - static get examples() { - return [ - { - title: 'PowerShell Gallery', - pattern: 'v/:packageName', - namedParams: { packageName: 'Azure.Storage' }, - staticPreview: this.render({ version: '4.4.0' }), - }, - { - title: 'PowerShell Gallery (with prereleases)', - pattern: 'vpre/:packageName', - namedParams: { packageName: 'Azure.Storage' }, - staticPreview: this.render({ version: '4.4.1-preview' }), - }, - ] - } - - static get defaultBadgeData() { - return { - label: 'powershell gallery', - } - } - - static render(props) { - return renderVersionBadge(props) - } - - async handle({ which, packageName }) { - const packageData = await fetch(this, { - packageName, - includePrereleases: which === 'vpre', - }) - const version = - packageData['d:NormalizedVersion'] || packageData['d:Version'] - return this.constructor.render({ version }) - } -} - -class PowershellGalleryDownloads extends BaseXmlService { - static get category() { - return 'downloads' - } - - static get route() { - return { - base: 'powershellgallery/dt', - pattern: ':packageName', - } - } - - static get examples() { - return [ - { - title: 'PowerShell Gallery', - namedParams: { packageName: 'Azure.Storage' }, - staticPreview: this.render({ downloads: 1.2e7 }), - }, - ] - } - - static render(props) { - return renderDownloadBadge(props) - } - - async handle({ packageName }) { - const packageData = await fetch(this, { - packageName, - }) - const { 'd:DownloadCount': downloads } = packageData - return this.constructor.render({ downloads }) - } -} +const { + NugetVersionService: PowershellGalleryVersion, + NugetDownloadService: PowershellGalleryDownloads, +} = createServiceFamily({ + defaultLabel: 'powershell gallery', + serviceBaseUrl: 'powershellgallery', + apiBaseUrl, + odataFormat: 'xml', + title: 'PowerShell Gallery', + examplePackageName: 'Azure.Storage', + exampleVersion: '4.4.0', + examplePrereleaseVersion: '4.4.1-preview', + exampleDownloadCount: 1.2e7, +}) class PowershellGalleryPlatformSupport extends BaseXmlService { static get category() { @@ -175,10 +64,11 @@ class PowershellGalleryPlatformSupport extends BaseXmlService { } async handle({ packageName }) { - const packageData = await fetch(this, { + const { Tags: tagStr } = await fetch(this, { + baseUrl: apiBaseUrl, + odataFormat: 'xml', packageName, }) - const { 'd:Tags': tagStr } = packageData const platforms = new Set() const tagArr = tagStr.split(' ') diff --git a/services/resharper/resharper.service.js b/services/resharper/resharper.service.js index 0bd3f2117d..86fb7a96f0 100644 --- a/services/resharper/resharper.service.js +++ b/services/resharper/resharper.service.js @@ -6,6 +6,7 @@ module.exports = createServiceFamily({ defaultLabel: 'resharper', serviceBaseUrl: 'resharper', apiBaseUrl: 'https://resharper-plugins.jetbrains.com/api/v2', + odataFormat: 'xml', title: 'JetBrains ReSharper plugins', examplePackageName: 'StyleCop.StyleCop', exampleVersion: '2017.2.0', diff --git a/services/resharper/resharper.tester.js b/services/resharper/resharper.tester.js index 2ff327a186..9f0d446187 100644 --- a/services/resharper/resharper.tester.js +++ b/services/resharper/resharper.tester.js @@ -7,14 +7,11 @@ const { isVPlusDottedVersionNClauses, isVPlusDottedVersionNClausesWithOptionalSuffix, } = require('../test-validators') -const { - nuGetV2VersionJsonWithDash, - nuGetV2VersionJsonFirstCharZero, - nuGetV2VersionJsonFirstCharNotZero, -} = require('../nuget-fixtures') -const t = new ServiceTester({ id: 'resharper', title: 'ReSharper' }) -module.exports = t +const t = (module.exports = new ServiceTester({ + id: 'resharper', + title: 'ReSharper', +})) // downloads @@ -42,51 +39,6 @@ t.create('version (valid)') }) ) -t.create('version (mocked, yellow badge)') - .get('/v/ReSharper.Nuke.json?style=_shields_test') - .intercept(nock => - nock('https://resharper-plugins.jetbrains.com') - .get( - '/api/v2/Packages()?%24filter=Id%20eq%20%27ReSharper.Nuke%27%20and%20IsLatestVersion%20eq%20true' - ) - .reply(200, nuGetV2VersionJsonWithDash) - ) - .expectJSON({ - name: 'resharper', - value: 'v1.2-beta', - color: 'yellow', - }) - -t.create('version (mocked, orange badge)') - .get('/v/ReSharper.Nuke.json?style=_shields_test') - .intercept(nock => - nock('https://resharper-plugins.jetbrains.com') - .get( - '/api/v2/Packages()?%24filter=Id%20eq%20%27ReSharper.Nuke%27%20and%20IsLatestVersion%20eq%20true' - ) - .reply(200, nuGetV2VersionJsonFirstCharZero) - ) - .expectJSON({ - name: 'resharper', - value: 'v0.35', - color: 'orange', - }) - -t.create('version (mocked, blue badge)') - .get('/v/ReSharper.Nuke.json?style=_shields_test') - .intercept(nock => - nock('https://resharper-plugins.jetbrains.com') - .get( - '/api/v2/Packages()?%24filter=Id%20eq%20%27ReSharper.Nuke%27%20and%20IsLatestVersion%20eq%20true' - ) - .reply(200, nuGetV2VersionJsonFirstCharNotZero) - ) - .expectJSON({ - name: 'resharper', - value: 'v1.2.7', - color: 'blue', - }) - t.create('version (not found)') .get('/v/not-a-real-package.json') .expectJSON({ name: 'resharper', value: 'not found' }) @@ -102,51 +54,6 @@ t.create('version (pre) (valid)') }) ) -t.create('version (pre) (mocked, yellow badge)') - .get('/vpre/ReSharper.Nuke.json?style=_shields_test') - .intercept(nock => - nock('https://resharper-plugins.jetbrains.com') - .get( - '/api/v2/Packages()?%24filter=Id%20eq%20%27ReSharper.Nuke%27%20and%20IsAbsoluteLatestVersion%20eq%20true' - ) - .reply(200, nuGetV2VersionJsonWithDash) - ) - .expectJSON({ - name: 'resharper', - value: 'v1.2-beta', - color: 'yellow', - }) - -t.create('version (pre) (mocked, orange badge)') - .get('/vpre/ReSharper.Nuke.json?style=_shields_test') - .intercept(nock => - nock('https://resharper-plugins.jetbrains.com') - .get( - '/api/v2/Packages()?%24filter=Id%20eq%20%27ReSharper.Nuke%27%20and%20IsAbsoluteLatestVersion%20eq%20true' - ) - .reply(200, nuGetV2VersionJsonFirstCharZero) - ) - .expectJSON({ - name: 'resharper', - value: 'v0.35', - color: 'orange', - }) - -t.create('version (pre) (mocked, blue badge)') - .get('/vpre/ReSharper.Nuke.json?style=_shields_test') - .intercept(nock => - nock('https://resharper-plugins.jetbrains.com') - .get( - '/api/v2/Packages()?%24filter=Id%20eq%20%27ReSharper.Nuke%27%20and%20IsAbsoluteLatestVersion%20eq%20true' - ) - .reply(200, nuGetV2VersionJsonFirstCharNotZero) - ) - .expectJSON({ - name: 'resharper', - value: 'v1.2.7', - color: 'blue', - }) - t.create('version (pre) (not found)') .get('/vpre/not-a-real-package.json') .expectJSON({ name: 'resharper', value: 'not found' })