Skip to content

Commit f3b5d99

Browse files
committed
Auto merge of #3175 - Turbo87:default-version, r=pichfl
Simplify `crate.version` route implementation This PR adds a `defaultVersion` property on the `crate` model and uses it in the `crate.version` (and `crate.index`) route to determine which version to display by default. This lets us get rid of the `semver` dependency in that route and puts the backend in charge of telling us what to display by default. This will allow us to address #654 in the near future, since the new `defaultVersion` property can be used in the search results too. This is probably best reviewed commit-by-commit :) r? `@pichfl`
2 parents 09af1d8 + 52f086f commit f3b5d99

File tree

5 files changed

+128
-49
lines changed

5 files changed

+128
-49
lines changed

app/models/crate.js

+16
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default class Crate extends Model {
99
@attr('date') created_at;
1010
@attr('date') updated_at;
1111
@attr max_version;
12+
@attr max_stable_version;
1213
@attr newest_version;
1314

1415
@attr description;
@@ -30,6 +31,21 @@ export default class Crate extends Model {
3031
@hasMany('categories', { async: true }) categories;
3132
@hasMany('dependency', { async: true }) reverse_dependencies;
3233

34+
/**
35+
* This is the default version that will be shown when visiting the crate
36+
* details page. Note that this can be `undefined` if all versions of the crate
37+
* have been yanked.
38+
* @return {string}
39+
*/
40+
get defaultVersion() {
41+
if (this.max_stable_version) {
42+
return this.max_stable_version;
43+
}
44+
if (this.max_version && this.max_version !== '0.0.0') {
45+
return this.max_version;
46+
}
47+
}
48+
3349
follow = memberAction({ type: 'PUT', path: 'follow' });
3450
unfollow = memberAction({ type: 'DELETE', path: 'follow' });
3551

app/routes/crate/version.js

+12-45
Original file line numberDiff line numberDiff line change
@@ -2,63 +2,30 @@ import Route from '@ember/routing/route';
22
import { inject as service } from '@ember/service';
33

44
import * as Sentry from '@sentry/browser';
5-
import prerelease from 'semver/functions/prerelease';
65

76
import { AjaxError } from '../../utils/ajax';
87

9-
function isUnstableVersion(version) {
10-
return !!prerelease(version);
11-
}
12-
138
export default class VersionRoute extends Route {
149
@service notifications;
1510

1611
async model(params) {
17-
const requestedVersion = params.version_num;
18-
const crate = this.modelFor('crate');
19-
const maxVersion = crate.max_version;
20-
12+
let crate = this.modelFor('crate');
2113
let versions = await crate.get('versions');
2214

23-
// Fallback to the crate's last stable version
24-
// If `max_version` is `0.0.0` then all versions have been yanked
25-
if (!params.version_num && maxVersion !== '0.0.0') {
26-
if (isUnstableVersion(maxVersion)) {
27-
// Find the latest version that is stable AND not-yanked.
28-
const latestStableVersion = versions.find(version => !isUnstableVersion(version.num) && !version.yanked);
29-
30-
if (latestStableVersion == null) {
31-
// Cannot find any version that is stable AND not-yanked.
32-
// The fact that "maxVersion" itself cannot be found means that
33-
// we have to fall back to the latest one that is unstable....
34-
35-
// Find the latest version that not yanked.
36-
const latestUnyankedVersion = versions.find(version => !version.yanked);
37-
38-
if (latestUnyankedVersion == null) {
39-
// There's not even any unyanked version...
40-
params.version_num = maxVersion;
41-
} else {
42-
params.version_num = latestUnyankedVersion.num;
43-
}
44-
} else {
45-
params.version_num = latestStableVersion.num;
46-
}
47-
} else {
48-
params.version_num = maxVersion;
15+
let version;
16+
let requestedVersion = params.version_num;
17+
if (requestedVersion) {
18+
version = versions.find(version => version.num === requestedVersion);
19+
if (!version) {
20+
this.notifications.error(`Version '${requestedVersion}' of crate '${crate.name}' does not exist`);
21+
this.replaceWith('crate.index');
4922
}
23+
} else {
24+
let { defaultVersion } = crate;
25+
version = versions.find(version => version.num === defaultVersion) ?? versions.lastObject;
5026
}
5127

52-
const version = versions.find(version => version.num === params.version_num);
53-
if (params.version_num && !version) {
54-
this.notifications.error(`Version '${params.version_num}' of crate '${crate.name}' does not exist`);
55-
}
56-
57-
return {
58-
crate,
59-
requestedVersion,
60-
version: version || versions.find(version => version.num === maxVersion) || versions.objectAt(0),
61-
};
28+
return { crate, requestedVersion, version };
6229
}
6330

6431
setupController(controller, model) {

mirage/serializers/crate.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,15 @@ export default BaseSerializer.extend({
5353
_adjust(hash) {
5454
let versions = this.schema.versions.where({ crateId: hash.id });
5555
assert(`crate \`${hash.id}\` has no associated versions`, versions.length !== 0);
56+
versions = versions.filter(it => !it.yanked);
5657

5758
let versionNums = versions.models.map(it => it.num);
5859
semverSort(versionNums);
59-
hash.max_version = versionNums[0];
60+
hash.max_version = versionNums[0] ?? '0.0.0';
6061
hash.max_stable_version = versionNums.find(it => !prerelease(it)) ?? null;
6162

62-
let newestVersions = versions.sort((a, b) => compareIsoDates(b.updated_at, a.updated_at));
63-
hash.newest_version = newestVersions.models[0].num;
63+
let newestVersions = versions.models.sort((a, b) => compareIsoDates(b.updated_at, a.updated_at));
64+
hash.newest_version = newestVersions[0]?.num ?? '0.0.0';
6465

6566
hash.categories = hash.category_ids;
6667
delete hash.category_ids;

tests/acceptance/crate-test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ module('Acceptance | crate page', function (hooks) {
8888

8989
await visit('/crates/nanomsg/0.7.0');
9090

91-
assert.equal(currentURL(), '/crates/nanomsg/0.7.0');
91+
assert.equal(currentURL(), '/crates/nanomsg');
9292
assert.dom('[data-test-heading] [data-test-crate-name]').hasText('nanomsg');
9393
assert.dom('[data-test-heading] [data-test-crate-version]').hasText('0.6.1');
9494
assert.dom('[data-test-notification-message]').hasText("Version '0.7.0' of crate 'nanomsg' does not exist");
+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { currentURL, visit } from '@ember/test-helpers';
2+
import { setupApplicationTest } from 'ember-qunit';
3+
import { module, test } from 'qunit';
4+
5+
import setupMirage from '../../../helpers/setup-mirage';
6+
7+
module('Route | crate.version | model() hook', function (hooks) {
8+
setupApplicationTest(hooks);
9+
setupMirage(hooks);
10+
11+
module('with explicit version number in the URL', function () {
12+
test('shows yanked versions', async function (assert) {
13+
let crate = this.server.create('crate', { name: 'foo' });
14+
this.server.create('version', { crate, num: '1.0.0' });
15+
this.server.create('version', { crate, num: '1.2.3', yanked: true });
16+
this.server.create('version', { crate, num: '2.0.0-beta.1' });
17+
18+
await visit('/crates/foo/1.2.3');
19+
assert.equal(currentURL(), `/crates/foo/1.2.3`);
20+
assert.dom('[data-test-crate-name]').hasText('foo');
21+
assert.dom('[data-test-crate-version]').hasText('1.2.3');
22+
assert.dom('[data-test-notification-message]').doesNotExist();
23+
});
24+
25+
test('redirects to unspecific version URL', async function (assert) {
26+
let crate = this.server.create('crate', { name: 'foo' });
27+
this.server.create('version', { crate, num: '1.0.0' });
28+
this.server.create('version', { crate, num: '1.2.3', yanked: true });
29+
this.server.create('version', { crate, num: '2.0.0-beta.1' });
30+
31+
await visit('/crates/foo/2.0.0');
32+
assert.equal(currentURL(), `/crates/foo`);
33+
assert.dom('[data-test-crate-name]').hasText('foo');
34+
assert.dom('[data-test-crate-version]').hasText('1.0.0');
35+
assert.dom('[data-test-notification-message="error"]').hasText("Version '2.0.0' of crate 'foo' does not exist");
36+
});
37+
});
38+
39+
module('without version number in the URL', function () {
40+
test('defaults to the highest stable version', async function (assert) {
41+
let crate = this.server.create('crate', { name: 'foo' });
42+
this.server.create('version', { crate, num: '1.0.0' });
43+
this.server.create('version', { crate, num: '1.2.3', yanked: true });
44+
this.server.create('version', { crate, num: '2.0.0-beta.1' });
45+
this.server.create('version', { crate, num: '2.0.0' });
46+
47+
await visit('/crates/foo');
48+
assert.equal(currentURL(), `/crates/foo`);
49+
assert.dom('[data-test-crate-name]').hasText('foo');
50+
assert.dom('[data-test-crate-version]').hasText('2.0.0');
51+
assert.dom('[data-test-notification-message]').doesNotExist();
52+
});
53+
54+
test('defaults to the highest stable version, even if there are higher prereleases', async function (assert) {
55+
let crate = this.server.create('crate', { name: 'foo' });
56+
this.server.create('version', { crate, num: '1.0.0' });
57+
this.server.create('version', { crate, num: '1.2.3', yanked: true });
58+
this.server.create('version', { crate, num: '2.0.0-beta.1' });
59+
60+
await visit('/crates/foo');
61+
assert.equal(currentURL(), `/crates/foo`);
62+
assert.dom('[data-test-crate-name]').hasText('foo');
63+
assert.dom('[data-test-crate-version]').hasText('1.0.0');
64+
assert.dom('[data-test-notification-message]').doesNotExist();
65+
});
66+
67+
test('defaults to the highest not-yanked version', async function (assert) {
68+
let crate = this.server.create('crate', { name: 'foo' });
69+
this.server.create('version', { crate, num: '1.0.0', yanked: true });
70+
this.server.create('version', { crate, num: '1.2.3', yanked: true });
71+
this.server.create('version', { crate, num: '2.0.0-beta.1' });
72+
this.server.create('version', { crate, num: '2.0.0-beta.2' });
73+
this.server.create('version', { crate, num: '2.0.0', yanked: true });
74+
75+
await visit('/crates/foo');
76+
assert.equal(currentURL(), `/crates/foo`);
77+
assert.dom('[data-test-crate-name]').hasText('foo');
78+
assert.dom('[data-test-crate-version]').hasText('2.0.0-beta.2');
79+
assert.dom('[data-test-notification-message]').doesNotExist();
80+
});
81+
82+
test('if there are only yanked versions, it defaults to the latest version', async function (assert) {
83+
let crate = this.server.create('crate', { name: 'foo' });
84+
this.server.create('version', { crate, num: '1.0.0', yanked: true });
85+
this.server.create('version', { crate, num: '1.2.3', yanked: true });
86+
this.server.create('version', { crate, num: '2.0.0-beta.1', yanked: true });
87+
88+
await visit('/crates/foo');
89+
assert.equal(currentURL(), `/crates/foo`);
90+
assert.dom('[data-test-crate-name]').hasText('foo');
91+
assert.dom('[data-test-crate-version]').hasText('2.0.0-beta.1');
92+
assert.dom('[data-test-notification-message]').doesNotExist();
93+
});
94+
});
95+
});

0 commit comments

Comments
 (0)