Repository: badges/shields (js)

Issue:
Badge request for GH version of R packages from the DESCRIPTION file
:clipboard: **Description**

Thanks for the great work :)

It would be nice if R package developers could get public GH package version badges for their development versions from the DESCRIPTION file's line with structure: `Version: ${package_version} \n`

:link: **Data**

- The DESCRIPTION file's path is in https://github.com/${user}/${repo}/blob/${branch}/DESCRIPTION

- The policy that requires the aforementioned `Version: ` line structure can be found in http://r-pkgs.had.co.nz/description.html. 

- An example can be found [here](https://github.com/mixOmicsTeam/mixOmics/blob/RELEASE_3_10/DESCRIPTION).

:microphone: **Motivation**

It would be great if users could also have badges for their latest stable version not yet submitted on CRAN or Bioconductor so users can easily see if they need to update for the latest features or not.


Pull request description:
Added [GithubRPackageVersion] badge
Resolves #4368.

Patch:
diff --git a/services/github/github-go-mod.service.js b/services/github/github-go-mod.service.js
index 2351ec1e23..9e7db5c7fd 100644
--- a/services/github/github-go-mod.service.js
+++ b/services/github/github-go-mod.service.js
@@ -35,17 +35,13 @@ module.exports = class GithubGoModGoVersion extends ConditionalGithubAuthV3Servi
     {
       title: 'GitHub go.mod Go version (branch)',
       pattern: ':user/:repo/:branch',
-      namedParams: {
-        user: 'gohugoio',
-        repo: 'hugo',
-        branch: 'master',
-      },
+      namedParams: { user: 'gohugoio', repo: 'hugo', branch: 'master' },
       staticPreview: this.render({ version: '1.12', branch: 'master' }),
       documentation,
       keywords,
     },
     {
-      title: 'GitHub go.mod Go version (subfolder of monorepo)',
+      title: 'GitHub go.mod Go version (subdirectory of monorepo)',
       pattern: ':user/:repo',
       namedParams: { user: 'golang', repo: 'go' },
       queryParams: { filename: 'src/go.mod' },
@@ -54,7 +50,7 @@ module.exports = class GithubGoModGoVersion extends ConditionalGithubAuthV3Servi
       keywords,
     },
     {
-      title: 'GitHub go.mod Go version (branch & subfolder of monorepo)',
+      title: 'GitHub go.mod Go version (branch & subdirectory of monorepo)',
       pattern: ':user/:repo/:branch',
       namedParams: { user: 'golang', repo: 'go', branch: 'master' },
       queryParams: { filename: 'src/go.mod' },
diff --git a/services/github/github-r-package.service.js b/services/github/github-r-package.service.js
new file mode 100644
index 0000000000..2fe65d9fda
--- /dev/null
+++ b/services/github/github-r-package.service.js
@@ -0,0 +1,91 @@
+'use strict'
+
+const Joi = require('joi')
+const { renderVersionBadge } = require('../version')
+const { InvalidResponse } = require('..')
+const { ConditionalGithubAuthV3Service } = require('./github-auth-service')
+const { fetchRepoContent } = require('./github-common-fetch')
+const { documentation } = require('./github-helpers')
+
+const queryParamSchema = Joi.object({
+  filename: Joi.string(),
+}).required()
+
+const versionRegExp = new RegExp('^Version:[\\s]*(.+)$', 'm')
+
+module.exports = class GithubRPackageVersion extends ConditionalGithubAuthV3Service {
+  static category = 'version'
+
+  static route = {
+    base: 'github/r-package/v',
+    pattern: ':user/:repo/:branch*',
+    queryParamSchema,
+  }
+
+  static examples = [
+    {
+      title: 'GitHub R package version',
+      pattern: ':user/:repo',
+      namedParams: { user: 'mixOmicsTeam', repo: 'mixOmics' },
+      staticPreview: this.render({ version: '6.10.9' }),
+      documentation,
+    },
+    {
+      title: 'GitHub R package version (branch)',
+      pattern: ':user/:repo/:branch',
+      namedParams: { user: 'mixOmicsTeam', repo: 'mixOmics', branch: 'master' },
+      staticPreview: this.render({ version: '6.10.9', branch: 'master' }),
+      documentation,
+    },
+    {
+      title: 'GitHub R package version (subdirectory of monorepo)',
+      pattern: ':user/:repo',
+      namedParams: { user: 'mixOmicsTeam', repo: 'mixOmics' },
+      queryParams: { filename: 'subdirectory/DESCRIPTION' },
+      staticPreview: this.render({ version: '6.10.9' }),
+      documentation,
+    },
+    {
+      title: 'GitHub R package version (branch & subdirectory of monorepo)',
+      pattern: ':user/:repo:/:branch',
+      namedParams: { user: 'mixOmicsTeam', repo: 'mixOmics', branch: 'master' },
+      queryParams: { filename: 'subdirectory/DESCRIPTION' },
+      staticPreview: this.render({ version: '6.10.9', branch: 'master' }),
+      documentation,
+    },
+  ]
+
+  static defaultBadgeData = { label: 'R' }
+
+  static render({ version, branch }) {
+    return renderVersionBadge({
+      version,
+      tag: branch,
+      defaultLabel: 'R',
+    })
+  }
+
+  static transform(content, filename) {
+    const match = versionRegExp.exec(content)
+    if (!match) {
+      throw new InvalidResponse({
+        prettyMessage: `Version missing in ${filename}`,
+      })
+    }
+
+    return {
+      version: match[1],
+    }
+  }
+
+  async handle({ user, repo, branch }, { filename = 'DESCRIPTION' }) {
+    const content = await fetchRepoContent(this, {
+      user,
+      repo,
+      branch,
+      filename,
+    })
+    const { version } = this.constructor.transform(content, filename)
+    return this.constructor.render({ version, branch })
+  }
+}
diff --git a/services/github/github-r-package.spec.js b/services/github/github-r-package.spec.js
new file mode 100644
index 0000000000..8bf77d8728
--- /dev/null
+++ b/services/github/github-r-package.spec.js
@@ -0,0 +1,30 @@
+'use strict'
+
+const { expect } = require('chai')
+const { test, given } = require('sazerac')
+const { InvalidResponse } = require('..')
+const GithubRPackageVersion = require('./github-r-package.service')
+
+describe('GithubRPackageVersion', function () {
+  const content = versionLine =>
+    [
+      'Package: mypackage',
+      'Title: What The Package Does (one line, title case required)',
+      versionLine,
+      '',
+    ].join('\n')
+
+  test(GithubRPackageVersion.transform, () => {
+    given(content('Version: 6.10.9'), 'DESCRIPTION').expect({
+      version: '6.10.9',
+    })
+  })
+
+  it('throws InvalidResponse if a file does not contain version specification', function () {
+    expect(() =>
+      GithubRPackageVersion.transform(content('Versio: 6.10.9'), 'DESCRIPTION')
+    )
+      .to.throw(InvalidResponse)
+      .with.property('prettyMessage', 'Version missing in DESCRIPTION')
+  })
+})


Test patch:
diff --git a/services/github/github-go-mod.tester.js b/services/github/github-go-mod.tester.js
index 39d9865e66..e83e4a1d89 100644
--- a/services/github/github-go-mod.tester.js
+++ b/services/github/github-go-mod.tester.js
@@ -15,7 +15,7 @@ t.create('Go version (from branch)')
     message: isVPlusDottedVersionAtLeastOne,
   })
 
-t.create('Go version (mongorepo)')
+t.create('Go version (monorepo)')
   .get(`/golang/go.json?filename=${encodeURIComponent('src/go.mod')}`)
   .expectBadge({
     label: 'Go',
diff --git a/services/github/github-r-package.tester.js b/services/github/github-r-package.tester.js
new file mode 100644
index 0000000000..01d1511674
--- /dev/null
+++ b/services/github/github-r-package.tester.js
@@ -0,0 +1,34 @@
+'use strict'
+
+const { isVPlusDottedVersionAtLeastOne } = require('../test-validators')
+const t = (module.exports = require('../tester').createServiceTester())
+
+t.create('R package version').get('/mixOmicsTeam/mixOmics.json').expectBadge({
+  label: 'R',
+  message: isVPlusDottedVersionAtLeastOne,
+})
+
+t.create('R package version (from branch)')
+  .get('/mixOmicsTeam/mixOmics/master.json')
+  .expectBadge({
+    label: 'R@master',
+    message: isVPlusDottedVersionAtLeastOne,
+  })
+
+t.create('R package version (monorepo)')
+  .get(
+    `/wch/r-source.json?filename=${encodeURIComponent(
+      'src/gnuwin32/windlgs/DESCRIPTION'
+    )}`
+  )
+  .expectBadge({
+    label: 'R',
+    message: isVPlusDottedVersionAtLeastOne,
+  })
+
+t.create('R package version (repo not found)')
+  .get('/badges/not-existing-repo.json')
+  .expectBadge({
+    label: 'R',
+    message: 'repo not found, branch not found, or DESCRIPTION missing',
+  })