Skip to content

Commit 6a6017e

Browse files
authored
Merge pull request #10369 from Turbo87/canonical-names
Fix non-canonical crate name pages
2 parents d5dc56a + fbe6234 commit 6a6017e

File tree

6 files changed

+121
-8
lines changed

6 files changed

+121
-8
lines changed

app/adapters/crate.js

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,23 @@ export default class CrateAdapter extends ApplicationAdapter {
66
coalesceFindRequests = true;
77

88
findRecord(store, type, id, snapshot) {
9-
let { include } = snapshot;
10-
// This ensures `crate.versions` are always fetched from another request.
11-
if (include === undefined) {
12-
snapshot.include = 'keywords,categories,downloads';
9+
return super.findRecord(store, type, id, setDefaultInclude(snapshot));
10+
}
11+
12+
queryRecord(store, type, query, adapterOptions) {
13+
return super.queryRecord(store, type, setDefaultInclude(query), adapterOptions);
14+
}
15+
16+
/** Removes the `name` query parameter and turns it into a path parameter instead */
17+
urlForQueryRecord(query) {
18+
let baseUrl = super.urlForQueryRecord(...arguments);
19+
if (!query.name) {
20+
return baseUrl;
1321
}
14-
return super.findRecord(store, type, id, snapshot);
22+
23+
let crateName = query.name;
24+
delete query.name;
25+
return `${baseUrl}/${crateName}`;
1526
}
1627

1728
groupRecordsForFindMany(store, snapshots) {
@@ -22,3 +33,12 @@ export default class CrateAdapter extends ApplicationAdapter {
2233
return result;
2334
}
2435
}
36+
37+
function setDefaultInclude(query) {
38+
if (query.include === undefined) {
39+
// This ensures `crate.versions` are always fetched from another request.
40+
query.include = 'keywords,categories,downloads';
41+
}
42+
43+
return query;
44+
}

app/routes/crate.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default class CrateRoute extends Route {
1212
let crateName = params.crate_id;
1313

1414
try {
15-
return await this.store.findRecord('crate', crateName);
15+
return this.store.peekRecord('crate', crateName) || (await this.store.queryRecord('crate', { name: crateName }));
1616
} catch (error) {
1717
if (error instanceof NotFoundError) {
1818
let title = `${crateName}: Crate not found`;

e2e/acceptance/crate.spec.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { test, expect } from '@/e2e/helper';
1+
import { expect, test } from '@/e2e/helper';
22

33
test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => {
44
test('visiting a crate page from the front page', async ({ page, mirage }) => {
@@ -139,6 +139,20 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => {
139139
await expect(page.locator('[data-test-try-again]')).toBeVisible();
140140
});
141141

142+
test('works for non-canonical names', async ({ page, mirage }) => {
143+
await mirage.addHook(server => {
144+
let crate = server.create('crate', { name: 'foo-bar' });
145+
server.create('version', { crate });
146+
});
147+
148+
await page.goto('/crates/foo_bar');
149+
150+
await expect(page).toHaveURL('/crates/foo_bar');
151+
await expect(page).toHaveTitle('foo-bar - crates.io: Rust Package Registry');
152+
153+
await expect(page.locator('[data-test-heading] [data-test-crate-name]')).toHaveText('foo-bar');
154+
});
155+
142156
test('navigating to the all versions page', async ({ page, mirage }) => {
143157
await mirage.addHook(server => {
144158
server.loadFixtures();

mirage/route-handlers/crates.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { Response } from 'miragejs';
33
import { getSession } from '../utils/session';
44
import { compareIsoDates, compareStrings, notFound, pageParams, releaseTracks } from './-utils';
55

6+
function toCanonicalName(name) {
7+
return name.toLowerCase().replace(/-/g, '_');
8+
}
9+
610
export function list(schema, request) {
711
const { start, end } = pageParams(request);
812

@@ -56,7 +60,8 @@ export function register(server) {
5660

5761
server.get('/api/v1/crates/:name', function (schema, request) {
5862
let { name } = request.params;
59-
let crate = schema.crates.findBy({ name });
63+
let canonicalName = toCanonicalName(name);
64+
let crate = schema.crates.all().models.find(it => toCanonicalName(it.name) === canonicalName);
6065
if (!crate) return notFound();
6166
let serialized = this.serialize(crate);
6267
let includes = request.queryParams?.include ?? '';

tests/acceptance/crate-test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,19 @@ module('Acceptance | crate page', function (hooks) {
131131
assert.dom('[data-test-try-again]').exists();
132132
});
133133

134+
test('works for non-canonical names', async function (assert) {
135+
let crate = this.server.create('crate', { name: 'foo-bar' });
136+
this.server.create('version', { crate });
137+
138+
await visit('/crates/foo_bar');
139+
140+
assert.strictEqual(currentURL(), '/crates/foo_bar');
141+
assert.strictEqual(currentRouteName(), 'crate.index');
142+
assert.strictEqual(getPageTitle(), 'foo-bar - crates.io: Rust Package Registry');
143+
144+
assert.dom('[data-test-heading] [data-test-crate-name]').hasText('foo-bar');
145+
});
146+
134147
test('navigating to the all versions page', async function (assert) {
135148
this.server.loadFixtures();
136149

tests/mirage/crates/get-by-id-test.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,67 @@ module('Mirage | GET /api/v1/crates/:id', function (hooks) {
7676
});
7777
});
7878

79+
test('works for non-canonical names', async function (assert) {
80+
let crate = this.server.create('crate', { name: 'foo-bar' });
81+
this.server.create('version', { crate, num: '1.0.0-beta.1' });
82+
83+
let response = await fetch('/api/v1/crates/foo_bar');
84+
assert.strictEqual(response.status, 200);
85+
assert.deepEqual(await response.json(), {
86+
categories: [],
87+
crate: {
88+
badges: [],
89+
categories: [],
90+
created_at: '2010-06-16T21:30:45Z',
91+
default_version: '1.0.0-beta.1',
92+
description: 'This is the description for the crate called "foo-bar"',
93+
documentation: null,
94+
downloads: 0,
95+
homepage: null,
96+
id: 'foo-bar',
97+
keywords: [],
98+
links: {
99+
owner_team: '/api/v1/crates/foo-bar/owner_team',
100+
owner_user: '/api/v1/crates/foo-bar/owner_user',
101+
reverse_dependencies: '/api/v1/crates/foo-bar/reverse_dependencies',
102+
version_downloads: '/api/v1/crates/foo-bar/downloads',
103+
versions: '/api/v1/crates/foo-bar/versions',
104+
},
105+
max_version: '1.0.0-beta.1',
106+
max_stable_version: null,
107+
name: 'foo-bar',
108+
newest_version: '1.0.0-beta.1',
109+
repository: null,
110+
updated_at: '2017-02-24T12:34:56Z',
111+
versions: ['1'],
112+
yanked: false,
113+
},
114+
keywords: [],
115+
versions: [
116+
{
117+
id: '1',
118+
crate: 'foo-bar',
119+
crate_size: 0,
120+
created_at: '2010-06-16T21:30:45Z',
121+
dl_path: '/api/v1/crates/foo-bar/1.0.0-beta.1/download',
122+
downloads: 0,
123+
license: 'MIT/Apache-2.0',
124+
links: {
125+
dependencies: '/api/v1/crates/foo-bar/1.0.0-beta.1/dependencies',
126+
version_downloads: '/api/v1/crates/foo-bar/1.0.0-beta.1/downloads',
127+
},
128+
num: '1.0.0-beta.1',
129+
published_by: null,
130+
readme_path: '/api/v1/crates/foo-bar/1.0.0-beta.1/readme',
131+
rust_version: null,
132+
updated_at: '2017-02-24T12:34:56Z',
133+
yanked: false,
134+
yank_message: null,
135+
},
136+
],
137+
});
138+
});
139+
79140
test('includes related versions', async function (assert) {
80141
let crate = this.server.create('crate', { name: 'rand' });
81142
this.server.create('version', { crate, num: '1.0.0' });

0 commit comments

Comments
 (0)