Repository: badges/shields (JavaScript)

Issue:
Custom bucket URLs for Scoop version shield.
:clipboard:
In addition to referencing Scoop's [bucket list](https://github.com/ScoopInstaller/Scoop/blob/master/buckets.json), shields could take in a URL as a bucket parameter for the Scoop version shield.

After `if (!bucketUrl)`, before throwing the error, shields could check if the bucket parameter is a valid GitHub URL, and attempt to extract the user & repo from that, using the same `gitHubRepoRegExp`, allowing users to reference buckets that are not in the official list. 

<!-- Love Shields? Please consider donating $10 to sustain our activities:
👉  https://opencollective.com/shields -->



Discussion:
Seems reasonable to me. Are you up for submitting a PR?
Sure, I'll see what I can do.



Patch:
diff --git a/services/scoop/scoop-version.service.js b/services/scoop/scoop-version.service.js
index 72fbf4671423b..7b4d806750815 100644
--- a/services/scoop/scoop-version.service.js
+++ b/services/scoop/scoop-version.service.js
@@ -1,3 +1,4 @@
+import { URL } from 'url'
 import Joi from 'joi'
 import { NotFound, pathParam, queryParam } from '../index.js'
 import { ConditionalGithubAuthV3Service } from '../github/github-auth-service.js'
@@ -37,7 +38,12 @@ export default class ScoopVersion extends ConditionalGithubAuthV3Service {
           '[Scoop](https://scoop.sh/) is a command-line installer for Windows',
         parameters: [
           pathParam({ name: 'app', example: 'ngrok' }),
-          queryParam({ name: 'bucket', example: 'extras' }),
+          queryParam({
+            name: 'bucket',
+            description:
+              "App's containing bucket. Can either be a name (e.g `extras`) or a URL to a GitHub Repo (e.g `https://github.com/jewlexx/personal-scoop`)",
+            example: 'extras',
+          }),
         ],
       },
     },
@@ -60,9 +66,30 @@ export default class ScoopVersion extends ConditionalGithubAuthV3Service {
       })
     }
     const bucket = queryParams.bucket || 'main'
-    const bucketUrl = this.buckets[bucket]
+    let bucketUrl = this.buckets[bucket]
     if (!bucketUrl) {
-      throw new NotFound({ prettyMessage: `bucket "${bucket}" not found` })
+      // Parsing URL here will throw an error if the url is invalid
+      try {
+        const url = new URL(decodeURIComponent(bucket))
+
+        // Throw errors to go to jump to catch statement
+        // The error messages here are purely for code readability, and will never reach the user.
+        if (url.hostname !== 'github.com') {
+          throw new Error('Not a GitHub URL')
+        }
+        const path = url.pathname.split('/').filter(value => value !== '')
+
+        if (path.length !== 2) {
+          throw new Error('Not a valid GitHub Repo')
+        }
+
+        const [user, repo] = path
+
+        // Reconstructing the url here ensures that the url will match the regex
+        bucketUrl = `https://github.com/${user}/${repo}`
+      } catch (e) {
+        throw new NotFound({ prettyMessage: `bucket "${bucket}" not found` })
+      }
     }
     const {
       groups: { user, repo },


Test patch:
diff --git a/services/scoop/scoop-version.tester.js b/services/scoop/scoop-version.tester.js
index 6e3950e8a58ef..8c25e03ccf0f7 100644
--- a/services/scoop/scoop-version.tester.js
+++ b/services/scoop/scoop-version.tester.js
@@ -40,3 +40,54 @@ t.create('version (wrong bucket)')
     label: 'scoop',
     message: 'bucket "not-a-real-bucket" not found',
   })
+
+// version (bucket url)
+const validBucketUrl = encodeURIComponent(
+  'https://github.com/jewlexx/personal-scoop',
+)
+
+t.create('version (valid bucket url)')
+  .get(`/v/sfsu.json?bucket=${validBucketUrl}`)
+  .expectBadge({
+    label: 'scoop',
+    message: isVPlusDottedVersionNClauses,
+  })
+
+const validBucketUrlTrailingSlash = encodeURIComponent(
+  'https://github.com/jewlexx/personal-scoop/',
+)
+
+t.create('version (valid bucket url)')
+  .get(`/v/sfsu.json?bucket=${validBucketUrlTrailingSlash}`)
+  .expectBadge({
+    label: 'scoop',
+    message: isVPlusDottedVersionNClauses,
+  })
+
+t.create('version (not found in custom bucket)')
+  .get(`/v/not-a-real-app.json?bucket=${validBucketUrl}`)
+  .expectBadge({
+    label: 'scoop',
+    message: `not-a-real-app not found in bucket "${decodeURIComponent(validBucketUrl)}"`,
+  })
+
+const nonGithubUrl = encodeURIComponent('https://example.com/')
+
+t.create('version (non-github url)')
+  .get(`/v/not-a-real-app.json?bucket=${nonGithubUrl}`)
+  .expectBadge({
+    label: 'scoop',
+    message: `bucket "${decodeURIComponent(nonGithubUrl)}" not found`,
+  })
+
+const nonBucketRepo = encodeURIComponent('https://github.com/jewlexx/sfsu')
+
+t.create('version (non-bucket repo)')
+  .get(`/v/sfsu.json?bucket=${nonBucketRepo}`)
+  .expectBadge({
+    label: 'scoop',
+    // !!! Important note here
+    // It is hard to tell if a repo is actually a scoop bucket, without getting the contents
+    // As such, a helpful error message here, which would require testing if the url is a valid scoop bucket, is difficult.
+    message: `sfsu not found in bucket "${decodeURIComponent(nonBucketRepo)}"`,
+  })