Skip to content

Commit

Permalink
[Thunderstore] Add Thunderstore Badges (#9782)
Browse files Browse the repository at this point in the history
* add base for thunderstore services

* badge service and corresponding tester for thunderstore download count

* badge service and corresponding tester for thunderstore latest package version

* fix HTML

* use stable package-metrics endpoint

* remove erroneous statement from docs

* remove `namedLogo` from default badge data on both services

* follow route naming conventions

* use `[x].json` for test assertions

* use existing version pattern

* document service `handle` return-type more narrowly

* use consistent test formatting

* add base for thunderstore services

* badge service and corresponding tester for thunderstore download count

* badge service and corresponding tester for thunderstore latest package version

* fix HTML

* use stable package-metrics endpoint

* remove erroneous statement from docs

* remove `namedLogo` from default badge data on both services

* follow route naming conventions

* use `[x].json` for test assertions

* use existing version pattern

* document service `handle` return-type more narrowly

* use consistent test formatting

* plural-ise base thunderstore docs

* don't require unused attributes

* declare BaseThunderstoreService abstract, add docstring

* add thunderstoreGreen static variable

* add thunderstore likes service

---------

Co-authored-by: chris48s <chris48s@users.noreply.github.com>
  • Loading branch information
Lordfirespeed and chris48s authored Dec 22, 2023
1 parent 2de7e8f commit d0bdb82
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 0 deletions.
93 changes: 93 additions & 0 deletions services/thunderstore/thunderstore-base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Joi from 'joi'
import { BaseJsonService } from '../index.js'
import { nonNegativeInteger } from '../validators.js'

const packageSchema = Joi.object({
latest: Joi.object({
version_number: Joi.string().required(),
}).required(),
}).required()

const packageMetricsSchema = Joi.object({
downloads: nonNegativeInteger,
rating_score: nonNegativeInteger,
})

const documentation = `
<p>
The Thunderstore badges require a package's <code>namespace</code> and <code>name</code>.
</p>
<p>
Everything can be discerned from your package's URL. Thunderstore package URLs have a mostly consistent
format:
</p>
<p>
<code>https://thunderstore.io/c/[community]/p/[namespace]/[packageName]</code>
</p>
<p>
For example: <code>https://thunderstore.io/c/lethal-company/p/notnotnotswipez/MoreCompany/</code>.
<ul>
<li><code>namespace = "notnotnotswipez"</code></li>
<li><code>packageName = "MoreCompany"</code></li>
</ul>
</p>
<details>
<summary>Risk Of Rain 2</summary>
<p>
The 'default community', Risk of Rain 2, has an alternative URL:
</p>
<p>
<code>https://thunderstore.io/package/[namespace]/[packageName]</code>
</p>
</details>
<details>
<summary>Subdomain Communities</summary>
<p>
Some communities use a 'subdomain' alternative URL, for example, Valheim:
</p>
<p>
<code>https://valheim.thunderstore.io/package/[namespace]/[packageName]</code>
</p>
</details>
`

/**
* Services which query Thunderstore endpoints should extend BaseThunderstoreService
*
* @abstract
*/
class BaseThunderstoreService extends BaseJsonService {
static thunderstoreGreen = '23FFB0'

/**
* Fetches package metadata from the Thunderstore API.
*
* @param {object} pkg - Package specifier
* @param {string} pkg.namespace - the package namespace
* @param {string} pkg.packageName - the package name
* @returns {Promise<object>} - Promise containing validated package information
*/
async fetchPackage({ namespace, packageName }) {
return this._requestJson({
schema: packageSchema,
url: `https://thunderstore.io/api/experimental/package/${namespace}/${packageName}`,
})
}

/**
* Fetches package metrics from the Thunderstore API.
*
* @param {object} pkg - Package specifier
* @param {string} pkg.namespace - the package namespace
* @param {string} pkg.packageName - the package name
* @returns {Promise<object>} - Promise containing validated package metrics
*/
async fetchPackageMetrics({ namespace, packageName }) {
return this._requestJson({
schema: packageMetricsSchema,
url: `https://thunderstore.io/api/v1/package-metrics/${namespace}/${packageName}`,
})
}
}

export { BaseThunderstoreService, documentation }
41 changes: 41 additions & 0 deletions services/thunderstore/thunderstore-downloads.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { renderDownloadsBadge } from '../downloads.js'
import { BaseThunderstoreService, documentation } from './thunderstore-base.js'

export default class ThunderstoreDownloads extends BaseThunderstoreService {
static category = 'downloads'

static route = {
base: 'thunderstore/dt',
pattern: ':namespace/:packageName',
}

static examples = [
{
title: 'Thunderstore Downloads',
namedParams: {
namespace: 'notnotnotswipez',
packageName: 'MoreCompany',
},
staticPreview: renderDownloadsBadge({ downloads: 120000 }),
documentation,
},
]

static defaultBadgeData = {
label: 'downloads',
}

/**
* @param {object} pkg - Package specifier
* @param {string} pkg.namespace - the package namespace
* @param {string} pkg.packageName - the package name
* @returns {Promise<object>} - Promise containing the rendered badge payload
*/
async handle({ namespace, packageName }) {
const { downloads } = await this.fetchPackageMetrics({
namespace,
packageName,
})
return renderDownloadsBadge({ downloads })
}
}
12 changes: 12 additions & 0 deletions services/thunderstore/thunderstore-downloads.tester.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createServiceTester } from '../tester.js'
import { isMetric } from '../test-validators.js'

export const t = await createServiceTester()

t.create('Downloads')
.get('/ebkr/r2modman.json')
.expectBadge({ label: 'downloads', message: isMetric })

t.create('Downloads (not found)')
.get('/not-a-namespace/not-a-package-name.json')
.expectBadge({ label: 'downloads', message: 'not found', color: 'red' })
53 changes: 53 additions & 0 deletions services/thunderstore/thunderstore-likes.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { metric } from '../text-formatters.js'
import { BaseThunderstoreService, documentation } from './thunderstore-base.js'

export default class ThunderstoreLikes extends BaseThunderstoreService {
static category = 'social'

static route = {
base: 'thunderstore/likes',
pattern: ':namespace/:packageName',
}

static examples = [
{
title: 'Thunderstore Likes',
namedParams: {
namespace: 'notnotnotswipez',
packageName: 'MoreCompany',
},
staticPreview: {
label: 'likes',
message: '150',
style: 'social',
},
documentation,
},
]

static defaultBadgeData = {
label: 'likes',
namedLogo: 'thunderstore',
}

static render({ likes }) {
return {
message: metric(likes),
color: `#${this.thunderstoreGreen}`,
}
}

/**
* @param {object} pkg - Package specifier
* @param {string} pkg.namespace - the package namespace
* @param {string} pkg.packageName - the package name
* @returns {Promise<object>} - Promise containing the rendered badge payload
*/
async handle({ namespace, packageName }) {
const { rating_score: likes } = await this.fetchPackageMetrics({
namespace,
packageName,
})
return this.constructor.render({ likes })
}
}
12 changes: 12 additions & 0 deletions services/thunderstore/thunderstore-likes.tester.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createServiceTester } from '../tester.js'
import { isMetric } from '../test-validators.js'

export const t = await createServiceTester()

t.create('Likes')
.get('/ebkr/r2modman.json')
.expectBadge({ label: 'likes', message: isMetric })

t.create('Likes (not found)')
.get('/not-a-namespace/not-a-package-name.json')
.expectBadge({ label: 'likes', message: 'not found', color: 'red' })
40 changes: 40 additions & 0 deletions services/thunderstore/thunderstore-version.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { renderVersionBadge } from '../version.js'
import { BaseThunderstoreService, documentation } from './thunderstore-base.js'

export default class ThunderstoreVersion extends BaseThunderstoreService {
static category = 'version'

static route = {
base: 'thunderstore/v',
pattern: ':namespace/:packageName',
}

static examples = [
{
title: 'Thunderstore Version',
namedParams: {
namespace: 'notnotnotswipez',
packageName: 'MoreCompany',
},
staticPreview: renderVersionBadge({ version: '1.4.5' }),
documentation,
},
]

static defaultBadgeData = {
label: 'thunderstore',
}

/**
* @param {object} pkg - Package specifier
* @param {string} pkg.namespace - the package namespace
* @param {string} pkg.packageName - the package name
* @returns {Promise<object>} - Promise containing the rendered badge payload
*/
async handle({ namespace, packageName }) {
const {
latest: { version_number: version },
} = await this.fetchPackage({ namespace, packageName })
return renderVersionBadge({ version })
}
}
12 changes: 12 additions & 0 deletions services/thunderstore/thunderstore-version.tester.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createServiceTester } from '../tester.js'
import { isSemver } from '../test-validators.js'

export const t = await createServiceTester()

t.create('Version')
.get('/ebkr/r2modman.json')
.expectBadge({ label: 'thunderstore', message: isSemver })

t.create('Version (not found)')
.get('/not-a-namespace/not-a-package-name.json')
.expectBadge({ label: 'thunderstore', message: 'not found', color: 'red' })

0 comments on commit d0bdb82

Please sign in to comment.