Skip to content

Commit 9b89b10

Browse files
authored
controllers/krate/metadata: Add support for default_version include mode (#10288)
This allows us to respond with a version data of `default_version` included, which potentially benefits apps by eliminating the need to wait for the crate response to obtain the default version and then make a subsequent request to get the actual version data. This is particularly useful when we move towards not requesting with `versions` included.
1 parent 0602f95 commit 9b89b10

File tree

4 files changed

+143
-4
lines changed

4 files changed

+143
-4
lines changed

src/controllers/krate/metadata.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ pub struct FindQueryParams {
2828
/// Additional data to include in the response.
2929
///
3030
/// Valid values: `versions`, `keywords`, `categories`, `badges`,
31-
/// `downloads`, or `full`.
31+
/// `downloads`, `default_version`, or `full`.
3232
///
3333
/// Defaults to `full` for backwards compatibility.
3434
///
35+
/// **Note**: `versions` and `default_version` share the same key `versions`, therefore `default_version` will be ignored if both are provided.
36+
///
3537
/// This parameter expects a comma-separated list of values.
3638
include: Option<String>,
3739
}
@@ -88,7 +90,7 @@ pub async fn find_crate(
8890
.optional()?
8991
.ok_or_else(|| crate_not_found(&path.name))?;
9092

91-
let versions_publishers_and_audit_actions = if include.versions {
93+
let mut versions_publishers_and_audit_actions = if include.versions {
9294
let mut versions_and_publishers: Vec<(Version, Option<User>)> =
9395
Version::belonging_to(&krate)
9496
.left_outer_join(users::table)
@@ -118,6 +120,18 @@ pub async fn find_crate(
118120
.as_ref()
119121
.map(|vps| vps.iter().map(|v| v.0.id).collect());
120122

123+
// Since `versions` and `default_version` share the same key (versions), we should only settle
124+
// the `default_version` when `versions` is not included.
125+
if let Some(default_version) = default_version
126+
.as_ref()
127+
.filter(|_| include.default_version && !include.versions)
128+
{
129+
let version = krate.find_version(&mut conn, default_version).await?;
130+
let published_by = version.published_by(&mut conn).await?;
131+
let actions = VersionOwnerAction::by_version(&mut conn, &version).await?;
132+
versions_publishers_and_audit_actions = Some(vec![(version, published_by, actions)]);
133+
};
134+
121135
let kws = if include.keywords {
122136
Some(
123137
CrateKeyword::belonging_to(&krate)
@@ -202,6 +216,7 @@ struct ShowIncludeMode {
202216
categories: bool,
203217
badges: bool,
204218
downloads: bool,
219+
default_version: bool,
205220
}
206221

207222
impl Default for ShowIncludeMode {
@@ -213,13 +228,14 @@ impl Default for ShowIncludeMode {
213228
categories: true,
214229
badges: true,
215230
downloads: true,
231+
default_version: true,
216232
}
217233
}
218234
}
219235

220236
impl ShowIncludeMode {
221237
const INVALID_COMPONENT: &'static str =
222-
"invalid component for ?include= (expected 'versions', 'keywords', 'categories', 'badges', 'downloads', or 'full')";
238+
"invalid component for ?include= (expected 'versions', 'keywords', 'categories', 'badges', 'downloads', 'default_version', or 'full')";
223239
}
224240

225241
impl FromStr for ShowIncludeMode {
@@ -232,6 +248,7 @@ impl FromStr for ShowIncludeMode {
232248
categories: false,
233249
badges: false,
234250
downloads: false,
251+
default_version: false,
235252
};
236253
for component in s.split(',') {
237254
match component {
@@ -243,13 +260,15 @@ impl FromStr for ShowIncludeMode {
243260
categories: true,
244261
badges: true,
245262
downloads: true,
263+
default_version: true,
246264
}
247265
}
248266
"versions" => mode.versions = true,
249267
"keywords" => mode.keywords = true,
250268
"categories" => mode.categories = true,
251269
"badges" => mode.badges = true,
252270
"downloads" => mode.downloads = true,
271+
"default_version" => mode.default_version = true,
253272
_ => return Err(bad_request(Self::INVALID_COMPONENT)),
254273
}
255274
}

src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ snapshot_kind: text
523523
}
524524
},
525525
{
526-
"description": "Additional data to include in the response.\n\nValid values: `versions`, `keywords`, `categories`, `badges`,\n`downloads`, or `full`.\n\nDefaults to `full` for backwards compatibility.\n\nThis parameter expects a comma-separated list of values.",
526+
"description": "Additional data to include in the response.\n\nValid values: `versions`, `keywords`, `categories`, `badges`,\n`downloads`, `default_version`, or `full`.\n\nDefaults to `full` for backwards compatibility.\n\n**Note**: `versions` and `default_version` share the same key `versions`, therefore `default_version` will be ignored if both are provided.\n\nThis parameter expects a comma-separated list of values.",
527527
"in": "query",
528528
"name": "include",
529529
"required": false,

src/tests/routes/crates/read.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,44 @@ async fn test_new_name() {
178178
".crate.updated_at" => "[datetime]",
179179
});
180180
}
181+
182+
#[tokio::test(flavor = "multi_thread")]
183+
async fn test_include_default_version() {
184+
let (app, anon, user) = TestApp::init().with_user().await;
185+
let mut conn = app.db_conn().await;
186+
let user = user.as_model();
187+
188+
CrateBuilder::new("foo_default_version", user.id)
189+
.description("description")
190+
.documentation("https://example.com")
191+
.homepage("http://example.com")
192+
.version(VersionBuilder::new("1.0.0").yanked(true))
193+
.version(VersionBuilder::new("0.5.0"))
194+
.version(VersionBuilder::new("0.5.1"))
195+
.keyword("kw1")
196+
.downloads(20)
197+
.recent_downloads(10)
198+
.expect_build(&mut conn)
199+
.await;
200+
201+
let response = anon
202+
.get::<()>("/api/v1/crates/foo_default_version?include=default_version")
203+
.await;
204+
assert_eq!(response.status(), StatusCode::OK);
205+
assert_json_snapshot!(response.json(), {
206+
".crate.created_at" => "[datetime]",
207+
".crate.updated_at" => "[datetime]",
208+
".versions[].created_at" => "[datetime]",
209+
".versions[].updated_at" => "[datetime]",
210+
});
211+
212+
let resp_versions = anon
213+
.get::<()>("/api/v1/crates/foo_default_version?include=versions")
214+
.await;
215+
let resp_both = anon
216+
.get::<()>("/api/v1/crates/foo_default_version?include=versions,default_version")
217+
.await;
218+
assert_eq!(resp_versions.status(), StatusCode::OK);
219+
assert_eq!(resp_both.status(), StatusCode::OK);
220+
assert_eq!(resp_versions.json(), resp_both.json());
221+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
source: src/tests/routes/crates/read.rs
3+
expression: response.json()
4+
snapshot_kind: text
5+
---
6+
{
7+
"categories": null,
8+
"crate": {
9+
"badges": [],
10+
"categories": null,
11+
"created_at": "[datetime]",
12+
"default_version": "0.5.1",
13+
"description": "description",
14+
"documentation": "https://example.com",
15+
"downloads": 20,
16+
"exact_match": false,
17+
"homepage": "http://example.com",
18+
"id": "foo_default_version",
19+
"keywords": null,
20+
"links": {
21+
"owner_team": "/api/v1/crates/foo_default_version/owner_team",
22+
"owner_user": "/api/v1/crates/foo_default_version/owner_user",
23+
"owners": "/api/v1/crates/foo_default_version/owners",
24+
"reverse_dependencies": "/api/v1/crates/foo_default_version/reverse_dependencies",
25+
"version_downloads": "/api/v1/crates/foo_default_version/downloads",
26+
"versions": "/api/v1/crates/foo_default_version/versions"
27+
},
28+
"max_stable_version": null,
29+
"max_version": "0.0.0",
30+
"name": "foo_default_version",
31+
"newest_version": "0.0.0",
32+
"recent_downloads": null,
33+
"repository": null,
34+
"updated_at": "[datetime]",
35+
"versions": null,
36+
"yanked": false
37+
},
38+
"keywords": null,
39+
"versions": [
40+
{
41+
"audit_actions": [],
42+
"bin_names": null,
43+
"checksum": " ",
44+
"crate": "foo_default_version",
45+
"crate_size": 0,
46+
"created_at": "[datetime]",
47+
"description": null,
48+
"dl_path": "/api/v1/crates/foo_default_version/0.5.1/download",
49+
"documentation": null,
50+
"downloads": 0,
51+
"edition": null,
52+
"features": {},
53+
"has_lib": null,
54+
"homepage": null,
55+
"id": 3,
56+
"lib_links": null,
57+
"license": null,
58+
"links": {
59+
"authors": "/api/v1/crates/foo_default_version/0.5.1/authors",
60+
"dependencies": "/api/v1/crates/foo_default_version/0.5.1/dependencies",
61+
"version_downloads": "/api/v1/crates/foo_default_version/0.5.1/downloads"
62+
},
63+
"num": "0.5.1",
64+
"published_by": {
65+
"avatar": null,
66+
"id": 1,
67+
"login": "foo",
68+
"name": null,
69+
"url": "https://github.com/foo"
70+
},
71+
"readme_path": "/api/v1/crates/foo_default_version/0.5.1/readme",
72+
"repository": null,
73+
"rust_version": null,
74+
"updated_at": "[datetime]",
75+
"yank_message": null,
76+
"yanked": false
77+
}
78+
]
79+
}

0 commit comments

Comments
 (0)