diff --git a/services/jetbrains/jetbrains-base.js b/services/jetbrains/jetbrains-base.js index 877fb413fd..cba934dd42 100644 --- a/services/jetbrains/jetbrains-base.js +++ b/services/jetbrains/jetbrains-base.js @@ -1,11 +1,32 @@ 'use strict' const { BaseXmlService, NotFound } = require('..') +const { parseJson } = require('../../core/base-service/json') +/* +JetBrains is a bit awkward. Sometimes we want to call an XML API +and sometimes we want to call a JSON API so we need a mongrel base class. +When the legacy IntelliJ (XML) API is retired we can simplify all this and +switch JetbrainsDownloads, JetbrainsRating and JetbrainsVersion to just +inherit from BaseJsonService directly. +*/ module.exports = class JetbrainsBase extends BaseXmlService { + static _isLegacyPluginId(pluginId) { + return !pluginId.match(/^([0-9])+/) + } + + static _cleanPluginId(pluginId) { + const match = pluginId.match(/^([0-9])+/) + if (match) { + return match[0] + } + return pluginId + } + + // xml static _validate(data, schema) { if (data['plugin-repository'] === '') { - // Note the 'not found' response from JetBrains Plugins Repository is: + // Note the 'not found' response from JetBrains IntelliJ API is: // status code = 200, // body = // which is parsed to object = { 'plugin-repository': '' } @@ -14,7 +35,7 @@ module.exports = class JetbrainsBase extends BaseXmlService { return super._validate(data, schema) } - async fetchPackageData({ pluginId, schema }) { + async fetchIntelliJPluginData({ pluginId, schema }) { const parserOptions = { parseNodeValue: false, ignoreAttributes: false, @@ -25,4 +46,27 @@ module.exports = class JetbrainsBase extends BaseXmlService { parserOptions, }) } + + // json + _parseJson(buffer) { + return parseJson(buffer) + } + + static _validateJson(data, schema) { + return super._validate(data, schema) + } + + 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._validateJson(json, schema) + } } diff --git a/services/jetbrains/jetbrains-downloads.service.js b/services/jetbrains/jetbrains-downloads.service.js index 3a5019072c..c2d7f87904 100644 --- a/services/jetbrains/jetbrains-downloads.service.js +++ b/services/jetbrains/jetbrains-downloads.service.js @@ -6,7 +6,7 @@ const { downloadCount: downloadCountColor } = require('../color-formatters') const { nonNegativeInteger } = require('../validators') const JetbrainsBase = require('./jetbrains-base') -const schema = Joi.object({ +const intelliJschema = Joi.object({ 'plugin-repository': Joi.object({ category: Joi.object({ 'idea-plugin': Joi.array() @@ -22,6 +22,8 @@ const schema = Joi.object({ }).required(), }).required() +const jetbrainsSchema = Joi.object({ downloads: nonNegativeInteger }).required() + module.exports = class JetbrainsDownloads extends JetbrainsBase { static category = 'downloads' @@ -32,9 +34,9 @@ module.exports = class JetbrainsDownloads extends JetbrainsBase { static examples = [ { - title: 'JetBrains IntelliJ plugins', + title: 'JetBrains plugins', namedParams: { - pluginId: '1347-scala', + pluginId: '1347', }, staticPreview: this.render({ downloads: 10200000 }), }, @@ -48,9 +50,27 @@ module.exports = class JetbrainsDownloads extends JetbrainsBase { } async handle({ pluginId }) { - const pluginData = await this.fetchPackageData({ pluginId, schema }) - const downloads = - pluginData['plugin-repository'].category['idea-plugin'][0]['@_downloads'] + let downloads + if (this.constructor._isLegacyPluginId(pluginId)) { + const intelliJPluginData = await this.fetchIntelliJPluginData({ + pluginId, + schema: intelliJschema, + }) + downloads = + intelliJPluginData['plugin-repository'].category['idea-plugin'][0][ + '@_downloads' + ] + } else { + const jetbrainsPluginData = await this._requestJson({ + schema: jetbrainsSchema, + url: `https://plugins.jetbrains.com/api/plugins/${this.constructor._cleanPluginId( + pluginId + )}`, + errorMessages: { 400: 'not found' }, + }) + downloads = jetbrainsPluginData.downloads + } + return this.constructor.render({ downloads }) } } diff --git a/services/jetbrains/jetbrains-downloads.tester.js b/services/jetbrains/jetbrains-downloads.tester.js index 1a95d5dae5..946f43e517 100644 --- a/services/jetbrains/jetbrains-downloads.tester.js +++ b/services/jetbrains/jetbrains-downloads.tester.js @@ -15,12 +15,21 @@ t.create('downloads (user friendly plugin id)') .get('/1347-scala.json') .expectBadge({ label: 'downloads', message: isMetric }) -t.create('downloads') +t.create('downloads (numeric id)') .get('/9435.json') + .intercept(nock => + nock('https://plugins.jetbrains.com') + .get('/api/plugins/9435') + .reply(200, { downloads: 2 }) + ) + .expectBadge({ label: 'downloads', message: '2' }) + +t.create('downloads (string id)') + .get('/io.harply.plugin.json') .intercept( nock => nock('https://plugins.jetbrains.com') - .get('/plugins/list?pluginId=9435') + .get('/plugins/list?pluginId=io.harply.plugin') .reply( 200, ` @@ -36,6 +45,14 @@ t.create('downloads') ) .expectBadge({ label: 'downloads', message: '2' }) -t.create('unknown plugin') +t.create('unknown plugin (string id)') .get('/unknown-plugin.json') .expectBadge({ label: 'downloads', message: 'not found' }) + +t.create('unknown plugin (numeric id)') + .get('/9999999999999.json') + .expectBadge({ label: 'downloads', message: 'not found' }) + +t.create('unknown plugin (mixed id)') + .get('/9999999999999-abc.json') + .expectBadge({ label: 'downloads', message: 'not found' }) diff --git a/services/jetbrains/jetbrains-rating.service.js b/services/jetbrains/jetbrains-rating.service.js index e90fbcfcd5..480d276b6b 100644 --- a/services/jetbrains/jetbrains-rating.service.js +++ b/services/jetbrains/jetbrains-rating.service.js @@ -7,7 +7,7 @@ const JetbrainsBase = require('./jetbrains-base') const pluginRatingColor = colorScale([2, 3, 4]) -const schema = Joi.object({ +const intelliJschema = Joi.object({ 'plugin-repository': Joi.object({ category: Joi.object({ 'idea-plugin': Joi.array() @@ -23,6 +23,10 @@ const schema = Joi.object({ }).required(), }).required() +const jetbrainsSchema = Joi.object({ + meanRating: Joi.number().min(0).required(), +}).required() + module.exports = class JetbrainsRating extends JetbrainsBase { static category = 'rating' @@ -33,10 +37,10 @@ module.exports = class JetbrainsRating extends JetbrainsBase { static examples = [ { - title: 'JetBrains IntelliJ Plugins', + title: 'JetBrains Plugins', pattern: 'rating/:pluginId', namedParams: { - pluginId: '11941-automatic-power-saver', + pluginId: '11941', }, staticPreview: this.render({ rating: '4.5', @@ -44,10 +48,10 @@ module.exports = class JetbrainsRating extends JetbrainsBase { }), }, { - title: 'JetBrains IntelliJ Plugins', + title: 'JetBrains Plugins', pattern: 'stars/:pluginId', namedParams: { - pluginId: '11941-automatic-power-saver', + pluginId: '11941', }, staticPreview: this.render({ rating: '4.5', @@ -70,9 +74,26 @@ module.exports = class JetbrainsRating extends JetbrainsBase { } async handle({ format, pluginId }) { - const pluginData = await this.fetchPackageData({ pluginId, schema }) - const pluginRating = - pluginData['plugin-repository'].category['idea-plugin'][0].rating - return this.constructor.render({ rating: pluginRating, format }) + let rating + if (this.constructor._isLegacyPluginId(pluginId)) { + const intelliJPluginData = await this.fetchIntelliJPluginData({ + pluginId, + schema: intelliJschema, + }) + rating = + intelliJPluginData['plugin-repository'].category['idea-plugin'][0] + .rating + } else { + const jetbrainsPluginData = await this._requestJson({ + schema: jetbrainsSchema, + url: `https://plugins.jetbrains.com/api/plugins/${this.constructor._cleanPluginId( + pluginId + )}/rating`, + errorMessages: { 400: 'not found' }, + }) + rating = jetbrainsPluginData.meanRating + } + + return this.constructor.render({ rating, format }) } } diff --git a/services/jetbrains/jetbrains-rating.tester.js b/services/jetbrains/jetbrains-rating.tester.js index 1388905f5e..be721b1c32 100644 --- a/services/jetbrains/jetbrains-rating.tester.js +++ b/services/jetbrains/jetbrains-rating.tester.js @@ -17,10 +17,18 @@ t.create('rating number (number as a plugin id)') .get('/rating/11941.json') .expectBadge({ label: 'rating', message: isRating }) -t.create('rating number for unknown plugin') +t.create('rating number for unknown plugin (string)') .get('/rating/unknown-plugin.json') .expectBadge({ label: 'rating', message: 'not found' }) +t.create('rating stars for unknown plugin (numeric)') + .get('/stars/9999999999999.json') + .expectBadge({ label: 'rating', message: 'not found' }) + +t.create('rating stars for unknown plugin (mixed)') + .get('/stars/9999999999999-abc.json') + .expectBadge({ label: 'rating', message: 'not found' }) + t.create('rating stars (user friendly plugin id)') .get('/stars/11941-automatic-power-saver.json') .expectBadge({ label: 'rating', message: isStarRating }) @@ -33,23 +41,42 @@ t.create('rating stars (number as a plugin id)') .get('/stars/11941.json') .expectBadge({ label: 'rating', message: isStarRating }) -t.create('rating stars for unknown plugin') +t.create('rating stars for unknown plugin (string id)') .get('/stars/unknown-plugin.json') .expectBadge({ label: 'rating', message: 'not found' }) -t.create('rating number') +t.create('rating stars for unknown plugin (numeric id)') + .get('/stars/9999999999999.json') + .expectBadge({ label: 'rating', message: 'not found' }) + +t.create('rating stars for unknown plugin (mixed id)') + .get('/stars/9999999999999-abc.json') + .expectBadge({ label: 'rating', message: 'not found' }) + +t.create('rating number (numeric id)') .get('/rating/11941.json') + .intercept(nock => + nock('https://plugins.jetbrains.com') + .get('/api/plugins/11941/rating') + .reply(200, { meanRating: 4.4848 }) + ) + .expectBadge({ label: 'rating', message: '4.5/5' }) + +t.create('rating number (string id)') + .get('/rating/com.chriscarini.jetbrains.jetbrains-auto-power-saver.json') .intercept( nock => nock('https://plugins.jetbrains.com') - .get('/plugins/list?pluginId=11941') + .get( + '/plugins/list?pluginId=com.chriscarini.jetbrains.jetbrains-auto-power-saver' + ) .reply( 200, ` - 4.5 + 4.4848 ` @@ -60,19 +87,30 @@ t.create('rating number') ) .expectBadge({ label: 'rating', message: '4.5/5' }) -t.create('rating stars') +t.create('rating stars (numeric id)') .get('/stars/11941.json') + .intercept(nock => + nock('https://plugins.jetbrains.com') + .get('/api/plugins/11941/rating') + .reply(200, { meanRating: 4.4848 }) + ) + .expectBadge({ label: 'rating', message: '★★★★½' }) + +t.create('rating stars (string id)') + .get('/stars/com.chriscarini.jetbrains.jetbrains-auto-power-saver.json') .intercept( nock => nock('https://plugins.jetbrains.com') - .get('/plugins/list?pluginId=11941') + .get( + '/plugins/list?pluginId=com.chriscarini.jetbrains.jetbrains-auto-power-saver' + ) .reply( 200, ` - 4.5 + 4.4848 ` diff --git a/services/jetbrains/jetbrains-version.service.js b/services/jetbrains/jetbrains-version.service.js index fa081664a8..09aa91bf04 100644 --- a/services/jetbrains/jetbrains-version.service.js +++ b/services/jetbrains/jetbrains-version.service.js @@ -4,7 +4,7 @@ const Joi = require('joi') const { renderVersionBadge } = require('../version') const JetbrainsBase = require('./jetbrains-base') -const schema = Joi.object({ +const intelliJschema = Joi.object({ 'plugin-repository': Joi.object({ category: Joi.object({ 'idea-plugin': Joi.array() @@ -20,6 +20,15 @@ const schema = Joi.object({ }).required(), }).required() +const jetbrainsSchema = Joi.array() + .min(1) + .items( + Joi.object({ + version: Joi.string().required(), + }).required() + ) + .required() + module.exports = class JetbrainsVersion extends JetbrainsBase { static category = 'version' @@ -30,9 +39,9 @@ module.exports = class JetbrainsVersion extends JetbrainsBase { static examples = [ { - title: 'JetBrains IntelliJ Plugins', + title: 'JetBrains Plugins', namedParams: { - pluginId: '9630-a8translate', + pluginId: '9630', }, staticPreview: this.render({ version: 'v1.7' }), }, @@ -45,9 +54,26 @@ module.exports = class JetbrainsVersion extends JetbrainsBase { } async handle({ pluginId }) { - const pluginData = await this.fetchPackageData({ pluginId, schema }) - const version = - pluginData['plugin-repository'].category['idea-plugin'][0].version + let version + if (this.constructor._isLegacyPluginId(pluginId)) { + const intelliJPluginData = await this.fetchIntelliJPluginData({ + pluginId, + schema: intelliJschema, + }) + version = + intelliJPluginData['plugin-repository'].category['idea-plugin'][0] + .version + } else { + const jetbrainsPluginData = await this._requestJson({ + schema: jetbrainsSchema, + url: `https://plugins.jetbrains.com/api/plugins/${this.constructor._cleanPluginId( + pluginId + )}/updates`, + errorMessages: { 400: 'not found' }, + }) + version = jetbrainsPluginData[0].version + } + return this.constructor.render({ version }) } } diff --git a/services/jetbrains/jetbrains-version.tester.js b/services/jetbrains/jetbrains-version.tester.js index cf472ddafe..0d5861c176 100644 --- a/services/jetbrains/jetbrains-version.tester.js +++ b/services/jetbrains/jetbrains-version.tester.js @@ -22,12 +22,21 @@ t.create('version (number as a plugin id)').get('/7495.json').expectBadge({ message: isVPlusDottedVersionNClauses, }) -t.create('version') +t.create('version (numeric id)') .get('/9435.json') + .intercept(nock => + nock('https://plugins.jetbrains.com') + .get('/api/plugins/9435/updates') + .reply(200, [{ version: '1.0' }]) + ) + .expectBadge({ label: 'jetbrains plugin', message: 'v1.0' }) + +t.create('version (strong id)') + .get('/io.harply.plugin.json') .intercept( nock => nock('https://plugins.jetbrains.com') - .get('/plugins/list?pluginId=9435') + .get('/plugins/list?pluginId=io.harply.plugin') .reply( 200, ` @@ -45,6 +54,14 @@ t.create('version') ) .expectBadge({ label: 'jetbrains plugin', message: 'v1.0' }) -t.create('version for unknown plugin') +t.create('version for unknown plugin (string id)') .get('/unknown-plugin.json') .expectBadge({ label: 'jetbrains plugin', message: 'not found' }) + +t.create('version for unknown plugin (numeric id)') + .get('/9999999999999.json') + .expectBadge({ label: 'jetbrains plugin', message: 'not found' }) + +t.create('unknown plugin (mixed id)') + .get('/9999999999999-abc.json') + .expectBadge({ label: 'jetbrains plugin', message: 'not found' })