Skip to content

Commit 82ef82e

Browse files
committed
msw: Implement GET /api/v1/crates/:name request handler
1 parent 00dc81b commit 82ef82e

File tree

3 files changed

+387
-1
lines changed

3 files changed

+387
-1
lines changed
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import getCrate from './crates/get.js';
12
import listCrates from './crates/list.js';
23

3-
export default [listCrates];
4+
export default [listCrates, getCrate];
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { http, HttpResponse } from 'msw';
2+
3+
import { db } from '../../index.js';
4+
import { serializeCategory } from '../../serializers/category.js';
5+
import { serializeCrate } from '../../serializers/crate.js';
6+
import { serializeKeyword } from '../../serializers/keyword.js';
7+
import { serializeVersion } from '../../serializers/version.js';
8+
import { notFound } from '../../utils/handlers.js';
9+
10+
const DEFAULT_INCLUDES = ['versions', 'keywords', 'categories'];
11+
12+
export default http.get('/api/v1/crates/:name', async ({ request, params }) => {
13+
let { name } = params;
14+
let canonicalName = toCanonicalName(name);
15+
let crate = db.crate.findMany({}).find(it => toCanonicalName(it.name) === canonicalName);
16+
if (!crate) return notFound();
17+
18+
let versions = db.version.findMany({ where: { crate: { id: { equals: crate.id } } } });
19+
versions.sort((a, b) => b.id - a.id);
20+
21+
let url = new URL(request.url);
22+
let include = url.searchParams.get('include');
23+
let includes = include == null || include === 'full' ? DEFAULT_INCLUDES : include.split(',');
24+
25+
let includeCategories = includes.includes('categories');
26+
let includeKeywords = includes.includes('keywords');
27+
let includeVersions = includes.includes('versions');
28+
let includeDefaultVersion = includes.includes('default_version');
29+
30+
let serializedCrate = serializeCrate(crate, {
31+
calculateVersions: includeVersions,
32+
includeCategories,
33+
includeKeywords,
34+
includeVersions,
35+
});
36+
37+
let serializedVersions = null;
38+
if (includeVersions) {
39+
serializedVersions = versions.map(v => serializeVersion(v));
40+
} else if (includeDefaultVersion) {
41+
let defaultVersion = versions.find(v => v.num === serializedCrate.default_version);
42+
serializedVersions = [serializeVersion(defaultVersion)];
43+
}
44+
45+
return HttpResponse.json({
46+
crate: serializedCrate,
47+
categories: includeCategories ? crate.categories.map(c => serializeCategory(c)) : null,
48+
keywords: includeKeywords ? crate.keywords.map(k => serializeKeyword(k)) : null,
49+
versions: serializedVersions,
50+
});
51+
});
52+
53+
function toCanonicalName(name) {
54+
return name.toLowerCase().replace(/-/g, '_');
55+
}
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
import { assert, test } from 'vitest';
2+
3+
import { db } from '../../index.js';
4+
5+
test('returns 404 for unknown crates', async function () {
6+
let response = await fetch('/api/v1/crates/foo');
7+
assert.strictEqual(response.status, 404);
8+
assert.deepEqual(await response.json(), { errors: [{ detail: 'Not Found' }] });
9+
});
10+
11+
test('returns a crate object for known crates', async function () {
12+
let crate = db.crate.create({ name: 'rand' });
13+
db.version.create({ crate, num: '1.0.0-beta.1' });
14+
15+
let response = await fetch('/api/v1/crates/rand');
16+
assert.strictEqual(response.status, 200);
17+
assert.deepEqual(await response.json(), {
18+
categories: [],
19+
crate: {
20+
badges: [],
21+
categories: [],
22+
created_at: '2010-06-16T21:30:45Z',
23+
default_version: '1.0.0-beta.1',
24+
description: 'This is the description for the crate called "rand"',
25+
documentation: null,
26+
downloads: 37_035,
27+
homepage: null,
28+
id: 'rand',
29+
keywords: [],
30+
links: {
31+
owner_team: '/api/v1/crates/rand/owner_team',
32+
owner_user: '/api/v1/crates/rand/owner_user',
33+
reverse_dependencies: '/api/v1/crates/rand/reverse_dependencies',
34+
version_downloads: '/api/v1/crates/rand/downloads',
35+
versions: '/api/v1/crates/rand/versions',
36+
},
37+
max_version: '1.0.0-beta.1',
38+
max_stable_version: null,
39+
name: 'rand',
40+
newest_version: '1.0.0-beta.1',
41+
repository: null,
42+
recent_downloads: 321,
43+
updated_at: '2017-02-24T12:34:56Z',
44+
versions: [1],
45+
yanked: false,
46+
},
47+
keywords: [],
48+
versions: [
49+
{
50+
id: 1,
51+
crate: 'rand',
52+
crate_size: 162_963,
53+
created_at: '2010-06-16T21:30:45Z',
54+
dl_path: '/api/v1/crates/rand/1.0.0-beta.1/download',
55+
downloads: 3702,
56+
features: {},
57+
license: 'MIT',
58+
links: {
59+
dependencies: '/api/v1/crates/rand/1.0.0-beta.1/dependencies',
60+
version_downloads: '/api/v1/crates/rand/1.0.0-beta.1/downloads',
61+
},
62+
num: '1.0.0-beta.1',
63+
published_by: null,
64+
readme_path: '/api/v1/crates/rand/1.0.0-beta.1/readme',
65+
rust_version: null,
66+
updated_at: '2017-02-24T12:34:56Z',
67+
yanked: false,
68+
yank_message: null,
69+
},
70+
],
71+
});
72+
});
73+
74+
test('works for non-canonical names', async function () {
75+
let crate = db.crate.create({ name: 'foo-bar' });
76+
db.version.create({ crate, num: '1.0.0-beta.1' });
77+
78+
let response = await fetch('/api/v1/crates/foo_bar');
79+
assert.strictEqual(response.status, 200);
80+
assert.deepEqual(await response.json(), {
81+
categories: [],
82+
crate: {
83+
badges: [],
84+
categories: [],
85+
created_at: '2010-06-16T21:30:45Z',
86+
default_version: '1.0.0-beta.1',
87+
description: 'This is the description for the crate called "foo-bar"',
88+
documentation: null,
89+
downloads: 37_035,
90+
homepage: null,
91+
id: 'foo-bar',
92+
keywords: [],
93+
links: {
94+
owner_team: '/api/v1/crates/foo-bar/owner_team',
95+
owner_user: '/api/v1/crates/foo-bar/owner_user',
96+
reverse_dependencies: '/api/v1/crates/foo-bar/reverse_dependencies',
97+
version_downloads: '/api/v1/crates/foo-bar/downloads',
98+
versions: '/api/v1/crates/foo-bar/versions',
99+
},
100+
max_version: '1.0.0-beta.1',
101+
max_stable_version: null,
102+
name: 'foo-bar',
103+
newest_version: '1.0.0-beta.1',
104+
repository: null,
105+
recent_downloads: 321,
106+
updated_at: '2017-02-24T12:34:56Z',
107+
versions: [1],
108+
yanked: false,
109+
},
110+
keywords: [],
111+
versions: [
112+
{
113+
id: 1,
114+
crate: 'foo-bar',
115+
crate_size: 162_963,
116+
created_at: '2010-06-16T21:30:45Z',
117+
dl_path: '/api/v1/crates/foo-bar/1.0.0-beta.1/download',
118+
downloads: 3702,
119+
features: {},
120+
license: 'MIT',
121+
links: {
122+
dependencies: '/api/v1/crates/foo-bar/1.0.0-beta.1/dependencies',
123+
version_downloads: '/api/v1/crates/foo-bar/1.0.0-beta.1/downloads',
124+
},
125+
num: '1.0.0-beta.1',
126+
published_by: null,
127+
readme_path: '/api/v1/crates/foo-bar/1.0.0-beta.1/readme',
128+
rust_version: null,
129+
updated_at: '2017-02-24T12:34:56Z',
130+
yanked: false,
131+
yank_message: null,
132+
},
133+
],
134+
});
135+
});
136+
137+
test('includes related versions', async function () {
138+
let crate = db.crate.create({ name: 'rand' });
139+
db.version.create({ crate, num: '1.0.0' });
140+
db.version.create({ crate, num: '1.1.0' });
141+
db.version.create({ crate, num: '1.2.0' });
142+
143+
let response = await fetch('/api/v1/crates/rand');
144+
assert.strictEqual(response.status, 200);
145+
146+
let responsePayload = await response.json();
147+
assert.deepEqual(responsePayload.crate.versions, [1, 2, 3]);
148+
assert.deepEqual(responsePayload.versions, [
149+
{
150+
id: 3,
151+
crate: 'rand',
152+
crate_size: 488_889,
153+
created_at: '2010-06-16T21:30:45Z',
154+
dl_path: '/api/v1/crates/rand/1.2.0/download',
155+
downloads: 11_106,
156+
features: {},
157+
license: 'MIT/Apache-2.0',
158+
links: {
159+
dependencies: '/api/v1/crates/rand/1.2.0/dependencies',
160+
version_downloads: '/api/v1/crates/rand/1.2.0/downloads',
161+
},
162+
num: '1.2.0',
163+
published_by: null,
164+
readme_path: '/api/v1/crates/rand/1.2.0/readme',
165+
rust_version: null,
166+
updated_at: '2017-02-24T12:34:56Z',
167+
yanked: false,
168+
yank_message: null,
169+
},
170+
{
171+
id: 2,
172+
crate: 'rand',
173+
crate_size: 325_926,
174+
created_at: '2010-06-16T21:30:45Z',
175+
dl_path: '/api/v1/crates/rand/1.1.0/download',
176+
downloads: 7404,
177+
features: {},
178+
license: 'Apache-2.0',
179+
links: {
180+
dependencies: '/api/v1/crates/rand/1.1.0/dependencies',
181+
version_downloads: '/api/v1/crates/rand/1.1.0/downloads',
182+
},
183+
num: '1.1.0',
184+
published_by: null,
185+
readme_path: '/api/v1/crates/rand/1.1.0/readme',
186+
rust_version: null,
187+
updated_at: '2017-02-24T12:34:56Z',
188+
yanked: false,
189+
yank_message: null,
190+
},
191+
{
192+
id: 1,
193+
crate: 'rand',
194+
crate_size: 162_963,
195+
created_at: '2010-06-16T21:30:45Z',
196+
dl_path: '/api/v1/crates/rand/1.0.0/download',
197+
downloads: 3702,
198+
features: {},
199+
license: 'MIT',
200+
links: {
201+
dependencies: '/api/v1/crates/rand/1.0.0/dependencies',
202+
version_downloads: '/api/v1/crates/rand/1.0.0/downloads',
203+
},
204+
num: '1.0.0',
205+
published_by: null,
206+
readme_path: '/api/v1/crates/rand/1.0.0/readme',
207+
rust_version: null,
208+
updated_at: '2017-02-24T12:34:56Z',
209+
yanked: false,
210+
yank_message: null,
211+
},
212+
]);
213+
});
214+
215+
test('includes related categories', async function () {
216+
let noStd = db.category.create({ category: 'no-std' });
217+
db.category.create({ category: 'cli' });
218+
let crate = db.crate.create({ name: 'rand', categories: [noStd] });
219+
db.version.create({ crate });
220+
221+
let response = await fetch('/api/v1/crates/rand');
222+
assert.strictEqual(response.status, 200);
223+
224+
let responsePayload = await response.json();
225+
assert.deepEqual(responsePayload.crate.categories, ['no-std']);
226+
assert.deepEqual(responsePayload.categories, [
227+
{
228+
id: 'no-std',
229+
category: 'no-std',
230+
crates_cnt: 1,
231+
created_at: '2010-06-16T21:30:45Z',
232+
description: 'This is the description for the category called "no-std"',
233+
slug: 'no-std',
234+
},
235+
]);
236+
});
237+
238+
test('includes related keywords', async function () {
239+
let noStd = db.keyword.create({ keyword: 'no-std' });
240+
db.keyword.create({ keyword: 'cli' });
241+
let crate = db.crate.create({ name: 'rand', keywords: [noStd] });
242+
db.version.create({ crate });
243+
244+
let response = await fetch('/api/v1/crates/rand');
245+
assert.strictEqual(response.status, 200);
246+
247+
let responsePayload = await response.json();
248+
assert.deepEqual(responsePayload.crate.keywords, ['no-std']);
249+
assert.deepEqual(responsePayload.keywords, [
250+
{
251+
crates_cnt: 1,
252+
id: 'no-std',
253+
keyword: 'no-std',
254+
},
255+
]);
256+
});
257+
258+
test('without versions included', async function () {
259+
db.category.create({ category: 'no-std' });
260+
db.category.create({ category: 'cli' });
261+
db.keyword.create({ keyword: 'no-std' });
262+
db.keyword.create({ keyword: 'cli' });
263+
let crate = db.crate.create({ name: 'rand', categoryIds: ['no-std'], keywordIds: ['no-std'] });
264+
db.version.create({ crate, num: '1.0.0' });
265+
db.version.create({ crate, num: '1.1.0' });
266+
db.version.create({ crate, num: '1.2.0' });
267+
268+
let req = await fetch('/api/v1/crates/rand');
269+
let expected = await req.json();
270+
271+
let response = await fetch('/api/v1/crates/rand?include=keywords,categories');
272+
assert.strictEqual(response.status, 200);
273+
274+
let responsePayload = await response.json();
275+
assert.deepEqual(responsePayload, {
276+
...expected,
277+
crate: {
278+
...expected.crate,
279+
max_version: '0.0.0',
280+
newest_version: '0.0.0',
281+
max_stable_version: null,
282+
versions: null,
283+
},
284+
versions: null,
285+
});
286+
});
287+
288+
test('includes default_version', async function () {
289+
let crate = db.crate.create({ name: 'rand' });
290+
db.version.create({ crate, num: '1.0.0' });
291+
db.version.create({ crate, num: '1.1.0' });
292+
db.version.create({ crate, num: '1.2.0' });
293+
294+
let req = await fetch('/api/v1/crates/rand');
295+
let expected = await req.json();
296+
297+
let response = await fetch('/api/v1/crates/rand?include=default_version');
298+
assert.strictEqual(response.status, 200);
299+
300+
let responsePayload = await response.json();
301+
let default_version = expected.versions.find(it => it.num === responsePayload.crate.default_version);
302+
assert.deepEqual(responsePayload, {
303+
...expected,
304+
crate: {
305+
...expected.crate,
306+
categories: null,
307+
keywords: null,
308+
max_version: '0.0.0',
309+
newest_version: '0.0.0',
310+
max_stable_version: null,
311+
versions: null,
312+
},
313+
categories: null,
314+
keywords: null,
315+
versions: [default_version],
316+
});
317+
318+
let resp_both = await fetch('/api/v1/crates/rand?include=versions,default_version');
319+
assert.strictEqual(response.status, 200);
320+
assert.deepEqual(await resp_both.json(), {
321+
...expected,
322+
crate: {
323+
...expected.crate,
324+
categories: null,
325+
keywords: null,
326+
},
327+
categories: null,
328+
keywords: null,
329+
});
330+
});

0 commit comments

Comments
 (0)