Skip to content

Commit

Permalink
[Crates] Only use non-yanked crate versions (ready for merge) (badges…
Browse files Browse the repository at this point in the history
…#9949)

* prototype fix msrv using yanked versions

* make it prettier

* deduplicate schema definition and add version info helper function

* use newest version only if it wasnt yanked

* fix variable name typo

* remove unused import

* try add new test

* satisfy linter

* don't import "describe()"

* fixup

---------

Co-authored-by: chris48s <[email protected]>
  • Loading branch information
johannesvollmer and chris48s authored Feb 5, 2024
1 parent 39ec093 commit e36d933
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 55 deletions.
52 changes: 31 additions & 21 deletions services/crates/crates-base.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,55 @@
import Joi from 'joi'
import { nonNegativeInteger } from '../validators.js'
import { BaseJsonService } from '../index.js'
import { BaseJsonService, InvalidResponse } from '../index.js'

const crateSchema = Joi.object({
const versionSchema = Joi.object({
downloads: nonNegativeInteger,
num: Joi.string().required(),
license: Joi.string().required().allow(null),
rust_version: Joi.string().allow(null),
})

const crateResponseSchema = Joi.object({
crate: Joi.object({
downloads: nonNegativeInteger,
recent_downloads: nonNegativeInteger.allow(null),
max_version: Joi.string().required(),
}).required(),
versions: Joi.array()
.items(
Joi.object({
downloads: nonNegativeInteger,
license: Joi.string().required().allow(null),
rust_version: Joi.string().allow(null),
}),
)
.min(1)
.required(),
versions: Joi.array().items(versionSchema).min(1).required(),
}).required()

const versionSchema = Joi.object({
version: Joi.object({
downloads: nonNegativeInteger,
num: Joi.string().required(),
license: Joi.string().required().allow(null),
rust_version: Joi.string().allow(null),
}).required(),
const versionResponseSchema = Joi.object({
version: versionSchema.required(),
}).required()

const schema = Joi.alternatives(crateSchema, versionSchema)

class BaseCratesService extends BaseJsonService {
static defaultBadgeData = { label: 'crates.io' }

async fetch({ crate, version }) {
const url = version
? `https://crates.io/api/v1/crates/${crate}/${version}`
: `https://crates.io/api/v1/crates/${crate}?include=versions,downloads`
const schema = version ? versionResponseSchema : crateResponseSchema
return this._requestJson({ schema, url })
}

static getLatestVersion(response) {
return response.crate.max_stable_version
? response.crate.max_stable_version
: response.crate.max_version
}

static getVersionObj(response) {
if (response.crate) {
const version = this.getLatestVersion(response)
const versionObj = response.versions.find(v => v.num === version)
if (versionObj === undefined) {
throw new InvalidResponse({ prettyMessage: 'version not found' })
}
return versionObj
}
return response.version
}
}

const description =
Expand Down
47 changes: 47 additions & 0 deletions services/crates/crates-base.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { expect } from 'chai'
import { test, given } from 'sazerac'
import { BaseCratesService } from './crates-base.js'

describe('BaseCratesService', function () {
describe('getLatestVersion', function () {
test(BaseCratesService.getLatestVersion, () => {
given({ crate: { max_version: '1.1.0' } }).expect('1.1.0')
given({
crate: { max_stable_version: '1.1.0', max_version: '1.9.0-alpha' },
}).expect('1.1.0')
})
})

describe('getVersionObj', function () {
const versions = [
/*
These versions are more recent, but we're going to ignore them
That might be because they were yanked, or because they're pre-releases.
*/
{ num: '1.3.0-beta' },
{ num: '1.2.0' },
// this is the one we will select
{ num: '1.1.0' },
]

it('ignores more recent versions than max_stable_version', function () {
const response = {
crate: {
max_stable_version: '1.1.0',
},
versions,
}
expect(BaseCratesService.getVersionObj(response).num).to.equal('1.1.0')
})

it('ignores more recent versions than max_version', function () {
const response = {
crate: {
max_version: '1.1.0',
},
versions,
}
expect(BaseCratesService.getVersionObj(response).num).to.equal('1.1.0')
})
})
})
2 changes: 1 addition & 1 deletion services/crates/crates-downloads.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export default class CratesDownloads extends BaseCratesService {
transform({ variant, json }) {
switch (variant) {
case 'dv':
return json.crate ? json.versions[0].downloads : json.version.downloads
return this.constructor.getVersionObj(json).downloads
case 'dr':
return json.crate.recent_downloads || 0
default:
Expand Down
4 changes: 3 additions & 1 deletion services/crates/crates-downloads.tester.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ t.create('recent downloads (null)')
recent_downloads: null,
max_version: '0.2.71',
},
versions: [{ downloads: 42, license: 'MIT OR Apache-2.0' }],
versions: [
{ downloads: 42, license: 'MIT OR Apache-2.0', num: '0.2.71' },
],
}),
)
.expectBadge({ label: 'recent downloads', message: '0' })
Expand Down
4 changes: 2 additions & 2 deletions services/crates/crates-license.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export default class CratesLicense extends BaseCratesService {

static defaultBadgeData = { label: 'license', color: 'blue' }

static transform({ version, versions }) {
const license = version ? version.license : versions[0].license
static transform(response) {
const license = this.getVersionObj(response).license
if (!license) {
throw new InvalidResponse({ prettyMessage: 'invalid null license' })
}
Expand Down
29 changes: 19 additions & 10 deletions services/crates/crates-license.spec.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { expect } from 'chai'
import { test, given } from 'sazerac'
import { InvalidResponse } from '../index.js'
import CratesLicense from './crates-license.service.js'

describe('CratesLicense', function () {
test(CratesLicense.transform, () => {
given({
version: { num: '1.0.0', license: 'MIT' },
versions: [{ license: 'MIT/Apache 2.0' }],
}).expect({ license: 'MIT' })
given({
versions: [{ license: 'MIT/Apache 2.0' }],
}).expect({ license: 'MIT/Apache 2.0' })
it('extracts expected license given valid inputs', function () {
expect(
CratesLicense.transform({
version: { num: '1.0.0', license: 'MIT' },
}),
).to.deep.equal({ license: 'MIT' })

expect(
CratesLicense.transform({
crate: { max_stable_version: '1.2.3' },
versions: [{ num: '1.2.3', license: 'MIT/Apache 2.0' }],
}),
).to.deep.equal({ license: 'MIT/Apache 2.0' })
})

it('throws InvalidResponse on null license with specific version', function () {
Expand All @@ -23,7 +27,12 @@ describe('CratesLicense', function () {
})

it('throws InvalidResponse on null license with latest version', function () {
expect(() => CratesLicense.transform({ versions: [{ license: null }] }))
expect(() =>
CratesLicense.transform({
crate: { max_stable_version: '1.2.3' },
versions: [{ num: '1.2.3', license: null }],
}),
)
.to.throw(InvalidResponse)
.with.property('prettyMessage', 'invalid null license')
})
Expand Down
4 changes: 2 additions & 2 deletions services/crates/crates-msrv.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ export default class CratesMSRV extends BaseCratesService {

static defaultBadgeData = { label: 'msrv', color: 'blue' }

static transform({ version, versions }) {
const msrv = version ? version.rust_version : versions[0].rust_version
static transform(response) {
const msrv = this.getVersionObj(response).rust_version
if (!msrv) {
throw new NotFound({ prettyMessage: 'unknown' })
}
Expand Down
8 changes: 1 addition & 7 deletions services/crates/crates-version.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,9 @@ export default class CratesVersion extends BaseCratesService {
},
}

transform(json) {
return json.crate.max_stable_version
? json.crate.max_stable_version
: json.crate.max_version
}

async handle({ crate }) {
const json = await this.fetch({ crate })
const version = this.transform(json)
const version = this.constructor.getLatestVersion(json)
return renderVersionBadge({ version })
}
}
11 changes: 0 additions & 11 deletions services/crates/crates-version.spec.js

This file was deleted.

0 comments on commit e36d933

Please sign in to comment.