diff --git a/src/controllers/krate/metadata.rs b/src/controllers/krate/metadata.rs index 2b897ef3f8e..5e4cf1cfa63 100644 --- a/src/controllers/krate/metadata.rs +++ b/src/controllers/krate/metadata.rs @@ -28,10 +28,12 @@ pub struct FindQueryParams { /// Additional data to include in the response. /// /// Valid values: `versions`, `keywords`, `categories`, `badges`, - /// `downloads`, or `full`. + /// `downloads`, `default_version`, or `full`. /// /// Defaults to `full` for backwards compatibility. /// + /// **Note**: `versions` and `default_version` share the same key `versions`, therefore `default_version` will be ignored if both are provided. + /// /// This parameter expects a comma-separated list of values. include: Option, } @@ -88,7 +90,7 @@ pub async fn find_crate( .optional()? .ok_or_else(|| crate_not_found(&path.name))?; - let versions_publishers_and_audit_actions = if include.versions { + let mut versions_publishers_and_audit_actions = if include.versions { let mut versions_and_publishers: Vec<(Version, Option)> = Version::belonging_to(&krate) .left_outer_join(users::table) @@ -118,6 +120,18 @@ pub async fn find_crate( .as_ref() .map(|vps| vps.iter().map(|v| v.0.id).collect()); + // Since `versions` and `default_version` share the same key (versions), we should only settle + // the `default_version` when `versions` is not included. + if let Some(default_version) = default_version + .as_ref() + .filter(|_| include.default_version && !include.versions) + { + let version = krate.find_version(&mut conn, default_version).await?; + let published_by = version.published_by(&mut conn).await?; + let actions = VersionOwnerAction::by_version(&mut conn, &version).await?; + versions_publishers_and_audit_actions = Some(vec![(version, published_by, actions)]); + }; + let kws = if include.keywords { Some( CrateKeyword::belonging_to(&krate) @@ -202,6 +216,7 @@ struct ShowIncludeMode { categories: bool, badges: bool, downloads: bool, + default_version: bool, } impl Default for ShowIncludeMode { @@ -213,13 +228,14 @@ impl Default for ShowIncludeMode { categories: true, badges: true, downloads: true, + default_version: true, } } } impl ShowIncludeMode { const INVALID_COMPONENT: &'static str = - "invalid component for ?include= (expected 'versions', 'keywords', 'categories', 'badges', 'downloads', or 'full')"; + "invalid component for ?include= (expected 'versions', 'keywords', 'categories', 'badges', 'downloads', 'default_version', or 'full')"; } impl FromStr for ShowIncludeMode { @@ -232,6 +248,7 @@ impl FromStr for ShowIncludeMode { categories: false, badges: false, downloads: false, + default_version: false, }; for component in s.split(',') { match component { @@ -243,6 +260,7 @@ impl FromStr for ShowIncludeMode { categories: true, badges: true, downloads: true, + default_version: true, } } "versions" => mode.versions = true, @@ -250,6 +268,7 @@ impl FromStr for ShowIncludeMode { "categories" => mode.categories = true, "badges" => mode.badges = true, "downloads" => mode.downloads = true, + "default_version" => mode.default_version = true, _ => return Err(bad_request(Self::INVALID_COMPONENT)), } } diff --git a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap index 71e86481062..8b74fdbdd30 100644 --- a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap +++ b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap @@ -523,7 +523,7 @@ snapshot_kind: text } }, { - "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.", + "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.", "in": "query", "name": "include", "required": false, diff --git a/src/tests/routes/crates/read.rs b/src/tests/routes/crates/read.rs index 7825c3b638d..727e5233ad6 100644 --- a/src/tests/routes/crates/read.rs +++ b/src/tests/routes/crates/read.rs @@ -178,3 +178,44 @@ async fn test_new_name() { ".crate.updated_at" => "[datetime]", }); } + +#[tokio::test(flavor = "multi_thread")] +async fn test_include_default_version() { + let (app, anon, user) = TestApp::init().with_user().await; + let mut conn = app.db_conn().await; + let user = user.as_model(); + + CrateBuilder::new("foo_default_version", user.id) + .description("description") + .documentation("https://example.com") + .homepage("http://example.com") + .version(VersionBuilder::new("1.0.0").yanked(true)) + .version(VersionBuilder::new("0.5.0")) + .version(VersionBuilder::new("0.5.1")) + .keyword("kw1") + .downloads(20) + .recent_downloads(10) + .expect_build(&mut conn) + .await; + + let response = anon + .get::<()>("/api/v1/crates/foo_default_version?include=default_version") + .await; + assert_eq!(response.status(), StatusCode::OK); + assert_json_snapshot!(response.json(), { + ".crate.created_at" => "[datetime]", + ".crate.updated_at" => "[datetime]", + ".versions[].created_at" => "[datetime]", + ".versions[].updated_at" => "[datetime]", + }); + + let resp_versions = anon + .get::<()>("/api/v1/crates/foo_default_version?include=versions") + .await; + let resp_both = anon + .get::<()>("/api/v1/crates/foo_default_version?include=versions,default_version") + .await; + assert_eq!(resp_versions.status(), StatusCode::OK); + assert_eq!(resp_both.status(), StatusCode::OK); + assert_eq!(resp_versions.json(), resp_both.json()); +} diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version.snap new file mode 100644 index 00000000000..40ece8b0963 --- /dev/null +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version.snap @@ -0,0 +1,79 @@ +--- +source: src/tests/routes/crates/read.rs +expression: response.json() +snapshot_kind: text +--- +{ + "categories": null, + "crate": { + "badges": [], + "categories": null, + "created_at": "[datetime]", + "default_version": "0.5.1", + "description": "description", + "documentation": "https://example.com", + "downloads": 20, + "exact_match": false, + "homepage": "http://example.com", + "id": "foo_default_version", + "keywords": null, + "links": { + "owner_team": "/api/v1/crates/foo_default_version/owner_team", + "owner_user": "/api/v1/crates/foo_default_version/owner_user", + "owners": "/api/v1/crates/foo_default_version/owners", + "reverse_dependencies": "/api/v1/crates/foo_default_version/reverse_dependencies", + "version_downloads": "/api/v1/crates/foo_default_version/downloads", + "versions": "/api/v1/crates/foo_default_version/versions" + }, + "max_stable_version": null, + "max_version": "0.0.0", + "name": "foo_default_version", + "newest_version": "0.0.0", + "recent_downloads": null, + "repository": null, + "updated_at": "[datetime]", + "versions": null, + "yanked": false + }, + "keywords": null, + "versions": [ + { + "audit_actions": [], + "bin_names": null, + "checksum": " ", + "crate": "foo_default_version", + "crate_size": 0, + "created_at": "[datetime]", + "description": null, + "dl_path": "/api/v1/crates/foo_default_version/0.5.1/download", + "documentation": null, + "downloads": 0, + "edition": null, + "features": {}, + "has_lib": null, + "homepage": null, + "id": 3, + "lib_links": null, + "license": null, + "links": { + "authors": "/api/v1/crates/foo_default_version/0.5.1/authors", + "dependencies": "/api/v1/crates/foo_default_version/0.5.1/dependencies", + "version_downloads": "/api/v1/crates/foo_default_version/0.5.1/downloads" + }, + "num": "0.5.1", + "published_by": { + "avatar": null, + "id": 1, + "login": "foo", + "name": null, + "url": "https://github.com/foo" + }, + "readme_path": "/api/v1/crates/foo_default_version/0.5.1/readme", + "repository": null, + "rust_version": null, + "updated_at": "[datetime]", + "yank_message": null, + "yanked": false + } + ] +}