From 3b925d014beb73c0f78c68cd1bf56b2966633275 Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Thu, 10 Oct 2024 16:49:04 +0800 Subject: [PATCH 1/4] Implement `ReleaseTracks` This will add `release_tracks` information to the meta field for paginated versions. This is necessary for pagination because it only loads a portion of versions per page, while `release_tracks` may require all sorted versions to determine their values. Therefore, we also need to calculate these on the server side. --- src/controllers/krate/versions.rs | 183 +++++++++++++++++++++++++++++- 1 file changed, 182 insertions(+), 1 deletion(-) diff --git a/src/controllers/krate/versions.rs b/src/controllers/krate/versions.rs index c6786f9b8ad..4e93d22a77b 100644 --- a/src/controllers/krate/versions.rs +++ b/src/controllers/krate/versions.rs @@ -133,7 +133,11 @@ fn list_by_date( Ok(PaginatedVersionsAndPublishers { data, - meta: ResponseMeta { total, next_page }, + meta: ResponseMeta { + total, + next_page, + release_tracks: None, + }, }) } @@ -233,6 +237,7 @@ fn list_by_semver( meta: ResponseMeta { total: total as i64, next_page, + release_tracks: None, }, }) } @@ -302,4 +307,180 @@ struct PaginatedVersionsAndPublishers { struct ResponseMeta { total: i64, next_page: Option, + #[serde(skip_serializing_if = "Option::is_none")] + release_tracks: Option, +} + +#[derive(Debug, Eq, PartialEq, Serialize)] +struct ReleaseTracks(IndexMap); + +impl ReleaseTracks { + // Return the release tracks based on a sorted semver versions iterator (in descending order). + // **Remember to** filter out yanked versions manually before calling this function. + pub fn from_sorted_semver_iter<'a, I>(versions: I) -> Self + where + I: Iterator, + { + let mut map = IndexMap::new(); + for num in versions.filter(|num| num.pre.is_empty()) { + let key = ReleaseTrackName::from_semver(num); + let prev = map.last(); + if prev.filter(|&(k, _)| *k == key).is_none() { + map.insert( + key, + ReleaseTrackDetails { + highest: num.clone(), + }, + ); + } + } + + Self(map) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +enum ReleaseTrackName { + Minor(u64), + Major(u64), +} + +impl ReleaseTrackName { + pub fn from_semver(version: &semver::Version) -> Self { + if version.major == 0 { + Self::Minor(version.minor) + } else { + Self::Major(version.major) + } + } +} + +impl std::fmt::Display for ReleaseTrackName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Minor(minor) => write!(f, "0.{minor}"), + Self::Major(major) => write!(f, "{major}"), + } + } +} + +impl serde::Serialize for ReleaseTrackName { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + Self: std::fmt::Display, + { + serializer.collect_str(self) + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Serialize)] +struct ReleaseTrackDetails { + highest: semver::Version, +} + +#[cfg(test)] +mod tests { + use super::{ReleaseTrackDetails, ReleaseTrackName, ReleaseTracks}; + use indexmap::IndexMap; + + #[track_caller] + fn version(str: &str) -> semver::Version { + semver::Version::parse(str).unwrap() + } + + #[test] + fn release_tracks_empty() { + let versions = []; + assert_eq!( + ReleaseTracks::from_sorted_semver_iter(versions.into_iter()), + ReleaseTracks(IndexMap::new()) + ); + } + + #[test] + fn release_tracks_prerelease() { + let versions = [version("1.0.0-beta.5")]; + assert_eq!( + ReleaseTracks::from_sorted_semver_iter(versions.iter()), + ReleaseTracks(IndexMap::new()) + ); + } + + #[test] + fn release_tracks_multiple() { + let versions = [ + "100.1.1", + "100.1.0", + "1.3.5", + "1.2.5", + "1.1.5", + "0.4.0-rc.1", + "0.3.23", + "0.3.22", + "0.3.21-pre.0", + "0.3.20", + "0.3.3", + "0.3.2", + "0.3.1", + "0.3.0", + "0.2.1", + "0.2.0", + "0.1.2", + "0.1.1", + ] + .map(version); + + let release_tracks = ReleaseTracks::from_sorted_semver_iter(versions.iter()); + assert_eq!( + release_tracks, + ReleaseTracks(IndexMap::from([ + ( + ReleaseTrackName::Major(100), + ReleaseTrackDetails { + highest: version("100.1.1") + } + ), + ( + ReleaseTrackName::Major(1), + ReleaseTrackDetails { + highest: version("1.3.5") + } + ), + ( + ReleaseTrackName::Minor(3), + ReleaseTrackDetails { + highest: version("0.3.23") + } + ), + ( + ReleaseTrackName::Minor(2), + ReleaseTrackDetails { + highest: version("0.2.1") + } + ), + ( + ReleaseTrackName::Minor(1), + ReleaseTrackDetails { + highest: version("0.1.2") + } + ), + ])) + ); + + let json = serde_json::from_str::( + &serde_json::to_string(&release_tracks).unwrap(), + ) + .unwrap(); + assert_eq!( + json, + json!({ + "100": { "highest": "100.1.1" }, + "1": { "highest": "1.3.5" }, + "0.3": { "highest": "0.3.23" }, + "0.2": { "highest": "0.2.1" }, + "0.1": { "highest": "0.1.2" } + }) + ); + } } From 687c3828837eb4f5c40bff23b7c72d2069aa4043 Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Thu, 10 Oct 2024 19:05:20 +0800 Subject: [PATCH 2/4] controllers/krate/versions: Implement `release_tracks` meta for pagination, sorted by semver --- src/controllers/krate/versions.rs | 35 ++++++++++++++++-------- src/tests/routes/crates/versions/list.rs | 16 ++++++++++- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/controllers/krate/versions.rs b/src/controllers/krate/versions.rs index 4e93d22a77b..8b7623fe66e 100644 --- a/src/controllers/krate/versions.rs +++ b/src/controllers/krate/versions.rs @@ -157,7 +157,7 @@ fn list_by_semver( ) -> AppResult { use seek::*; - let (data, total) = if let Some(options) = options { + let (data, total, release_tracks) = if let Some(options) = options { // Since versions will only increase in the future and both sorting and pagination need to // happen on the app server, implementing it with fetching only the data needed for sorting // and pagination, then making another query for the data to respond with, would minimize @@ -168,18 +168,26 @@ fn list_by_semver( let mut sorted_versions = IndexMap::new(); for result in versions::table .filter(versions::crate_id.eq(crate_id)) - .select((versions::id, versions::num)) - .load_iter::<(i32, String), DefaultLoadingMode>(conn)? + .select((versions::id, versions::num, versions::yanked)) + .load_iter::<(i32, String, bool), DefaultLoadingMode>(conn)? { - let (id, num) = result?; - sorted_versions.insert(id, (num, None)); + let (id, num, yanked) = result?; + let semver = semver::Version::parse(&num).ok(); + sorted_versions.insert(id, (semver, yanked, None)); } - sorted_versions.sort_by_cached_key(|_, (num, _)| Reverse(semver::Version::parse(num).ok())); + sorted_versions + .sort_unstable_by(|_, (semver_a, _, _), _, (semver_b, _, _)| semver_b.cmp(semver_a)); assert!( !matches!(&options.page, Page::Numeric(_)), "?page= is not supported" ); + let release_tracks = Some(ReleaseTracks::from_sorted_semver_iter( + sorted_versions + .values() + .filter(|(_, yanked, _)| !yanked) + .filter_map(|(semver, _, _)| semver.as_ref()), + )); let mut idx = Some(0); if let Some(SeekPayload::Semver(Semver { id })) = Seek::Semver.after(&options.page)? { idx = sorted_versions @@ -201,19 +209,24 @@ fn list_by_semver( .load_iter::<(Version, Option), DefaultLoadingMode>(conn)? { let row = result?; - sorted_versions.insert(row.0.id, (row.0.num.to_owned(), Some(row))); + // The versions are already sorted, and we only need to enrich the fetched rows into them. + // Therefore, other values can now be safely ignored. + sorted_versions + .entry(row.0.id) + .and_modify(|entry| *entry = (None, false, Some(row))); } let len = sorted_versions.len(); ( sorted_versions .into_values() - .filter_map(|(_, v)| v) + .filter_map(|(_, _, v)| v) .collect(), len, + release_tracks, ) } else { - (vec![], 0) + (vec![], 0, release_tracks) } } else { let mut data: Vec<(Version, Option)> = versions::table @@ -223,7 +236,7 @@ fn list_by_semver( .load(conn)?; data.sort_by_cached_key(|(version, _)| Reverse(semver::Version::parse(&version.num).ok())); let total = data.len(); - (data, total) + (data, total, None) }; let mut next_page = None; @@ -237,7 +250,7 @@ fn list_by_semver( meta: ResponseMeta { total: total as i64, next_page, - release_tracks: None, + release_tracks, }, }) } diff --git a/src/tests/routes/crates/versions/list.rs b/src/tests/routes/crates/versions/list.rs index 8e46a87fe41..c62ccb6d028 100644 --- a/src/tests/routes/crates/versions/list.rs +++ b/src/tests/routes/crates/versions/list.rs @@ -107,6 +107,10 @@ async fn test_sorting() { for (json, expect) in resp.iter().zip(expects) { assert_eq!(json.versions[0].num, expect); assert_eq!(json.meta.total as usize, expects.len()); + assert_eq!( + json.meta.release_tracks, + Some(json!({"1": {"highest": "1.0.0"}})) + ); } assert_eq!(calls as usize, expects.len() + 1); @@ -131,7 +135,7 @@ async fn test_seek_based_pagination_semver_sorting() { let user = user.as_model(); app.db(|conn| { CrateBuilder::new("foo_versions", user.id) - .version("0.5.1") + .version(VersionBuilder::new("0.5.1").yanked(true)) .version(VersionBuilder::new("1.0.0").rust_version("1.64")) .version("0.5.0") .expect_build(conn); @@ -147,6 +151,10 @@ async fn test_seek_based_pagination_semver_sorting() { let url = "/api/v1/crates/foo_versions/versions"; let expects = ["1.0.0", "0.5.1", "0.5.0"]; + let release_tracks = Some(json!({ + "1": {"highest": "1.0.0"}, + "0.5": {"highest": "0.5.0"} + })); // per_page larger than the number of versions let json: VersionList = anon @@ -155,6 +163,7 @@ async fn test_seek_based_pagination_semver_sorting() { .good(); assert_eq!(nums(&json.versions), expects); assert_eq!(json.meta.total as usize, expects.len()); + assert_eq!(json.meta.release_tracks, release_tracks); let json: VersionList = anon .get_with_query(url, "per_page=1&sort=semver") @@ -162,6 +171,7 @@ async fn test_seek_based_pagination_semver_sorting() { .good(); assert_eq!(nums(&json.versions), expects[0..1]); assert_eq!(json.meta.total as usize, expects.len()); + assert_eq!(json.meta.release_tracks, release_tracks); let seek = json .meta @@ -178,6 +188,7 @@ async fn test_seek_based_pagination_semver_sorting() { assert_eq!(nums(&json.versions), expects[1..]); assert!(json.meta.next_page.is_none()); assert_eq!(json.meta.total as usize, expects.len()); + assert_eq!(json.meta.release_tracks, release_tracks); // per_page euqal to the number of remain versions let json: VersionList = anon @@ -187,6 +198,7 @@ async fn test_seek_based_pagination_semver_sorting() { assert_eq!(nums(&json.versions), expects[1..]); assert!(json.meta.next_page.is_some()); assert_eq!(json.meta.total as usize, expects.len()); + assert_eq!(json.meta.release_tracks, release_tracks); // A decodable seek value, MTAwCg (100), but doesn't actually exist let json: VersionList = anon @@ -196,6 +208,7 @@ async fn test_seek_based_pagination_semver_sorting() { assert_eq!(json.versions.len(), 0); assert!(json.meta.next_page.is_none()); assert_eq!(json.meta.total, 0); + assert_eq!(json.meta.release_tracks, release_tracks); } #[tokio::test(flavor = "multi_thread")] @@ -242,6 +255,7 @@ pub struct VersionList { pub struct ResponseMeta { pub total: i64, pub next_page: Option, + pub release_tracks: Option, } fn nums(versions: &[EncodableVersion]) -> Vec { From a261c726be25c451ec5c6859604e66b50f3cc5c9 Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Thu, 10 Oct 2024 20:19:23 +0800 Subject: [PATCH 3/4] controllers/krate/versions: Implement `release_tracks` meta for pagination, sorted by date --- src/controllers/krate/versions.rs | 23 +++++- src/tests/routes/crates/versions/list.rs | 89 ++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/src/controllers/krate/versions.rs b/src/controllers/krate/versions.rs index 8b7623fe66e..8c18cde1778 100644 --- a/src/controllers/krate/versions.rs +++ b/src/controllers/krate/versions.rs @@ -3,10 +3,11 @@ use axum::extract::Path; use axum::Json; use diesel::connection::DefaultLoadingMode; +use diesel::dsl::not; use diesel::prelude::*; use diesel_async::async_connection_wrapper::AsyncConnectionWrapper; use http::request::Parts; -use indexmap::IndexMap; +use indexmap::{IndexMap, IndexSet}; use serde_json::Value; use std::cmp::Reverse; @@ -95,6 +96,7 @@ fn list_by_date( .select((versions::all_columns, users::all_columns.nullable())) .into_boxed(); + let mut release_tracks = None; if let Some(options) = options { assert!( !matches!(&options.page, Page::Numeric(_)), @@ -109,6 +111,23 @@ fn list_by_date( ) } query = query.limit(options.per_page); + + let mut sorted_versions = IndexSet::new(); + for result in versions::table + .filter(versions::crate_id.eq(crate_id)) + .filter(not(versions::yanked)) + .select(versions::num) + .load_iter::(conn)? + { + let Ok(semver) = semver::Version::parse(&result?) else { + continue; + }; + sorted_versions.insert(semver); + } + sorted_versions.sort_unstable_by(|a, b| b.cmp(a)); + release_tracks = Some(ReleaseTracks::from_sorted_semver_iter( + sorted_versions.iter(), + )); } query = query.order((versions::created_at.desc(), versions::id.desc())); @@ -136,7 +155,7 @@ fn list_by_date( meta: ResponseMeta { total, next_page, - release_tracks: None, + release_tracks, }, }) } diff --git a/src/tests/routes/crates/versions/list.rs b/src/tests/routes/crates/versions/list.rs index c62ccb6d028..c0d6a97589f 100644 --- a/src/tests/routes/crates/versions/list.rs +++ b/src/tests/routes/crates/versions/list.rs @@ -125,6 +125,10 @@ async fn test_sorting() { for (json, expect) in resp.iter().zip(&expects) { assert_eq!(json.versions[0].num, *expect); assert_eq!(json.meta.total as usize, expects.len()); + assert_eq!( + json.meta.release_tracks, + Some(json!({"1": {"highest": "1.0.0"}})) + ); } assert_eq!(calls as usize, expects.len() + 1); } @@ -211,6 +215,91 @@ async fn test_seek_based_pagination_semver_sorting() { assert_eq!(json.meta.release_tracks, release_tracks); } +#[tokio::test(flavor = "multi_thread")] +async fn test_seek_based_pagination_date_sorting() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + app.db(|conn| { + CrateBuilder::new("foo_versions", user.id) + .version(VersionBuilder::new("0.5.1").yanked(true)) + .version(VersionBuilder::new("1.0.0").rust_version("1.64")) + .version("0.5.0") + .expect_build(conn); + // Make version 1.0.0 mimic a version published before we started recording who published + // versions + let none: Option = None; + update(versions::table) + .filter(versions::num.eq("1.0.0")) + .set(versions::published_by.eq(none)) + .execute(conn) + .unwrap(); + }); + + let url = "/api/v1/crates/foo_versions/versions"; + let expects = ["0.5.0", "1.0.0", "0.5.1"]; + let release_tracks = Some(json!({ + "1": {"highest": "1.0.0"}, + "0.5": {"highest": "0.5.0"} + })); + + // per_page larger than the number of versions + let json: VersionList = anon + .get_with_query(url, "per_page=10&sort=date") + .await + .good(); + assert_eq!(nums(&json.versions), expects); + assert_eq!(json.meta.total as usize, expects.len()); + assert_eq!(json.meta.release_tracks, release_tracks); + + let json: VersionList = anon + .get_with_query(url, "per_page=1&sort=date") + .await + .good(); + assert_eq!(nums(&json.versions), expects[0..1]); + assert_eq!(json.meta.total as usize, expects.len()); + assert_eq!(json.meta.release_tracks, release_tracks); + + let seek = json + .meta + .next_page + .map(|s| s.split_once("seek=").unwrap().1.to_owned()) + .map(|p| p.split_once('&').map(|t| t.0.to_owned()).unwrap_or(p)) + .unwrap(); + + // per_page larger than the number of remain versions + let json: VersionList = anon + .get_with_query(url, &format!("per_page=5&sort=date&seek={seek}")) + .await + .good(); + assert_eq!(nums(&json.versions), expects[1..]); + assert!(json.meta.next_page.is_none()); + assert_eq!(json.meta.total as usize, expects.len()); + assert_eq!(json.meta.release_tracks, release_tracks); + + // per_page euqal to the number of remain versions + let json: VersionList = anon + .get_with_query(url, &format!("per_page=2&sort=date&seek={seek}")) + .await + .good(); + assert_eq!(nums(&json.versions), expects[1..]); + assert!(json.meta.next_page.is_some()); + assert_eq!(json.meta.total as usize, expects.len()); + assert_eq!(json.meta.release_tracks, release_tracks); + + // A decodable seek value, WzE3Mjg1NjE5OTI3MzQ2NzMsNV0K ([1728561992734673,5]), but doesn't actually exist + let json: VersionList = anon + .get_with_query( + url, + "per_page=10&sort=date&seek=WzE3Mjg1NjE5OTI3MzQ2NzMsNV0K", + ) + .await + .good(); + assert_eq!(json.versions.len(), 0); + assert!(json.meta.next_page.is_none()); + assert_eq!(json.meta.total, 0); + assert_eq!(json.meta.release_tracks, release_tracks); +} + #[tokio::test(flavor = "multi_thread")] async fn invalid_seek_parameter() { let (app, anon, user) = TestApp::init().with_user(); From ad747b620b0a0e53b6773f69f05dccb111cc5e34 Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Fri, 11 Oct 2024 04:34:52 +0800 Subject: [PATCH 4/4] controllers/krate/versions: Add `ShowIncludeMode` for pagination This enables clients to specify the `include` query param to include `release_tracks` meta. --- src/controllers/krate/versions.rs | 92 +++++++++++++++++------- src/tests/routes/crates/versions/list.rs | 74 +++++++++++-------- 2 files changed, 111 insertions(+), 55 deletions(-) diff --git a/src/controllers/krate/versions.rs b/src/controllers/krate/versions.rs index 8c18cde1778..8f76a25faf4 100644 --- a/src/controllers/krate/versions.rs +++ b/src/controllers/krate/versions.rs @@ -10,6 +10,7 @@ use http::request::Parts; use indexmap::{IndexMap, IndexSet}; use serde_json::Value; use std::cmp::Reverse; +use std::str::FromStr; use crate::app::AppState; use crate::controllers::helpers::pagination::{encode_seek, Page, PaginationOptions}; @@ -17,7 +18,7 @@ use crate::models::{Crate, User, Version, VersionOwnerAction}; use crate::schema::{crates, users, versions}; use crate::tasks::spawn_blocking; use crate::util::diesel::Conn; -use crate::util::errors::{crate_not_found, AppResult}; +use crate::util::errors::{bad_request, crate_not_found, AppResult, BoxedAppError}; use crate::util::RequestUtils; use crate::views::EncodableVersion; @@ -49,11 +50,18 @@ pub async fn versions( ); } + let include = req + .query() + .get("include") + .map(|mode| ShowIncludeMode::from_str(mode)) + .transpose()? + .unwrap_or_default(); + // Sort by semver by default let versions_and_publishers = match params.get("sort").map(|s| s.to_lowercase()).as_deref() { - Some("date") => list_by_date(crate_id, pagination.as_ref(), &req, conn)?, - _ => list_by_semver(crate_id, pagination.as_ref(), &req, conn)?, + Some("date") => list_by_date(crate_id, pagination.as_ref(), include, &req, conn)?, + _ => list_by_semver(crate_id, pagination.as_ref(), include, &req, conn)?, }; let versions = versions_and_publishers @@ -85,6 +93,7 @@ pub async fn versions( fn list_by_date( crate_id: i32, options: Option<&PaginationOptions>, + include: ShowIncludeMode, req: &Parts, conn: &mut impl Conn, ) -> AppResult { @@ -112,22 +121,24 @@ fn list_by_date( } query = query.limit(options.per_page); - let mut sorted_versions = IndexSet::new(); - for result in versions::table - .filter(versions::crate_id.eq(crate_id)) - .filter(not(versions::yanked)) - .select(versions::num) - .load_iter::(conn)? - { - let Ok(semver) = semver::Version::parse(&result?) else { - continue; - }; - sorted_versions.insert(semver); + if include.release_tracks { + let mut sorted_versions = IndexSet::new(); + for result in versions::table + .filter(versions::crate_id.eq(crate_id)) + .filter(not(versions::yanked)) + .select(versions::num) + .load_iter::(conn)? + { + let Ok(semver) = semver::Version::parse(&result?) else { + continue; + }; + sorted_versions.insert(semver); + } + sorted_versions.sort_unstable_by(|a, b| b.cmp(a)); + release_tracks = Some(ReleaseTracks::from_sorted_semver_iter( + sorted_versions.iter(), + )); } - sorted_versions.sort_unstable_by(|a, b| b.cmp(a)); - release_tracks = Some(ReleaseTracks::from_sorted_semver_iter( - sorted_versions.iter(), - )); } query = query.order((versions::created_at.desc(), versions::id.desc())); @@ -171,6 +182,7 @@ fn list_by_date( fn list_by_semver( crate_id: i32, options: Option<&PaginationOptions>, + include: ShowIncludeMode, req: &Parts, conn: &mut impl Conn, ) -> AppResult { @@ -201,12 +213,16 @@ fn list_by_semver( !matches!(&options.page, Page::Numeric(_)), "?page= is not supported" ); - let release_tracks = Some(ReleaseTracks::from_sorted_semver_iter( - sorted_versions - .values() - .filter(|(_, yanked, _)| !yanked) - .filter_map(|(semver, _, _)| semver.as_ref()), - )); + + let release_tracks = include.release_tracks.then(|| { + ReleaseTracks::from_sorted_semver_iter( + sorted_versions + .values() + .filter(|(_, yanked, _)| !yanked) + .filter_map(|(semver, _, _)| semver.as_ref()), + ) + }); + let mut idx = Some(0); if let Some(SeekPayload::Semver(Semver { id })) = Seek::Semver.after(&options.page)? { idx = sorted_versions @@ -411,6 +427,34 @@ struct ReleaseTrackDetails { highest: semver::Version, } +#[derive(Debug, Default)] +struct ShowIncludeMode { + release_tracks: bool, +} + +impl ShowIncludeMode { + const INVALID_COMPONENT: &'static str = + "invalid component for ?include= (expected 'release_tracks')"; +} + +impl FromStr for ShowIncludeMode { + type Err = BoxedAppError; + + fn from_str(s: &str) -> Result { + let mut mode = Self { + release_tracks: false, + }; + for component in s.split(',') { + match component { + "" => {} + "release_tracks" => mode.release_tracks = true, + _ => return Err(bad_request(Self::INVALID_COMPONENT)), + } + } + Ok(mode) + } +} + #[cfg(test)] mod tests { use super::{ReleaseTrackDetails, ReleaseTrackName, ReleaseTracks}; diff --git a/src/tests/routes/crates/versions/list.rs b/src/tests/routes/crates/versions/list.rs index c0d6a97589f..bc29c80a75d 100644 --- a/src/tests/routes/crates/versions/list.rs +++ b/src/tests/routes/crates/versions/list.rs @@ -86,9 +86,6 @@ async fn test_sorting() { .expect_build(conn); }); - // Sort by semver - let url = "/api/v1/crates/foo_versions/versions?sort=semver"; - let json: AllVersions = anon.get(url).await.good(); let expects = [ "2.0.0-alpha", "1.0.0", @@ -100,19 +97,29 @@ async fn test_sorting() { "1.0.0-alpha.1", "1.0.0-alpha", ]; + let release_tracks = Some(json!({"1": {"highest": "1.0.0"}})); + + // Sort by semver + let url = "/api/v1/crates/foo_versions/versions?sort=semver"; + let json: AllVersions = anon.get(url).await.good(); for (num, expect) in nums(&json.versions).iter().zip(expects) { assert_eq!(num, expect); } - let (resp, calls) = page_with_seek(&anon, url).await; - for (json, expect) in resp.iter().zip(expects) { - assert_eq!(json.versions[0].num, expect); - assert_eq!(json.meta.total as usize, expects.len()); - assert_eq!( - json.meta.release_tracks, - Some(json!({"1": {"highest": "1.0.0"}})) - ); + for (url, release_tracks) in [ + (url, None), + ( + &format!("{url}&include=release_tracks"), + release_tracks.as_ref(), + ), + ] { + let (resp, calls) = page_with_seek(&anon, url).await; + for (json, expect) in resp.iter().zip(expects) { + assert_eq!(json.versions[0].num, expect); + assert_eq!(json.meta.total as usize, expects.len()); + assert_eq!(json.meta.release_tracks.as_ref(), release_tracks); + } + assert_eq!(calls as usize, expects.len() + 1); } - assert_eq!(calls as usize, expects.len() + 1); // Sort by date let url = "/api/v1/crates/foo_versions/versions?sort=date"; @@ -121,16 +128,21 @@ async fn test_sorting() { for (num, expect) in nums(&json.versions).iter().zip(&expects) { assert_eq!(num, *expect); } - let (resp, calls) = page_with_seek(&anon, url).await; - for (json, expect) in resp.iter().zip(&expects) { - assert_eq!(json.versions[0].num, *expect); - assert_eq!(json.meta.total as usize, expects.len()); - assert_eq!( - json.meta.release_tracks, - Some(json!({"1": {"highest": "1.0.0"}})) - ); + for (url, release_tracks) in [ + (url, None), + ( + &format!("{url}&include=release_tracks"), + release_tracks.as_ref(), + ), + ] { + let (resp, calls) = page_with_seek(&anon, url).await; + for (json, expect) in resp.iter().zip(&expects) { + assert_eq!(json.versions[0].num, *expect); + assert_eq!(json.meta.total as usize, expects.len()); + assert_eq!(json.meta.release_tracks.as_ref(), release_tracks); + } + assert_eq!(calls as usize, expects.len() + 1); } - assert_eq!(calls as usize, expects.len() + 1); } #[tokio::test(flavor = "multi_thread")] @@ -167,10 +179,10 @@ async fn test_seek_based_pagination_semver_sorting() { .good(); assert_eq!(nums(&json.versions), expects); assert_eq!(json.meta.total as usize, expects.len()); - assert_eq!(json.meta.release_tracks, release_tracks); + assert_eq!(json.meta.release_tracks, None); let json: VersionList = anon - .get_with_query(url, "per_page=1&sort=semver") + .get_with_query(url, "per_page=1&sort=semver&include=release_tracks") .await .good(); assert_eq!(nums(&json.versions), expects[0..1]); @@ -192,7 +204,7 @@ async fn test_seek_based_pagination_semver_sorting() { assert_eq!(nums(&json.versions), expects[1..]); assert!(json.meta.next_page.is_none()); assert_eq!(json.meta.total as usize, expects.len()); - assert_eq!(json.meta.release_tracks, release_tracks); + assert_eq!(json.meta.release_tracks, None); // per_page euqal to the number of remain versions let json: VersionList = anon @@ -202,7 +214,7 @@ async fn test_seek_based_pagination_semver_sorting() { assert_eq!(nums(&json.versions), expects[1..]); assert!(json.meta.next_page.is_some()); assert_eq!(json.meta.total as usize, expects.len()); - assert_eq!(json.meta.release_tracks, release_tracks); + assert_eq!(json.meta.release_tracks, None); // A decodable seek value, MTAwCg (100), but doesn't actually exist let json: VersionList = anon @@ -212,7 +224,7 @@ async fn test_seek_based_pagination_semver_sorting() { assert_eq!(json.versions.len(), 0); assert!(json.meta.next_page.is_none()); assert_eq!(json.meta.total, 0); - assert_eq!(json.meta.release_tracks, release_tracks); + assert_eq!(json.meta.release_tracks, None); } #[tokio::test(flavor = "multi_thread")] @@ -249,10 +261,10 @@ async fn test_seek_based_pagination_date_sorting() { .good(); assert_eq!(nums(&json.versions), expects); assert_eq!(json.meta.total as usize, expects.len()); - assert_eq!(json.meta.release_tracks, release_tracks); + assert_eq!(json.meta.release_tracks, None); let json: VersionList = anon - .get_with_query(url, "per_page=1&sort=date") + .get_with_query(url, "per_page=1&sort=date&include=release_tracks") .await .good(); assert_eq!(nums(&json.versions), expects[0..1]); @@ -274,7 +286,7 @@ async fn test_seek_based_pagination_date_sorting() { assert_eq!(nums(&json.versions), expects[1..]); assert!(json.meta.next_page.is_none()); assert_eq!(json.meta.total as usize, expects.len()); - assert_eq!(json.meta.release_tracks, release_tracks); + assert_eq!(json.meta.release_tracks, None); // per_page euqal to the number of remain versions let json: VersionList = anon @@ -284,7 +296,7 @@ async fn test_seek_based_pagination_date_sorting() { assert_eq!(nums(&json.versions), expects[1..]); assert!(json.meta.next_page.is_some()); assert_eq!(json.meta.total as usize, expects.len()); - assert_eq!(json.meta.release_tracks, release_tracks); + assert_eq!(json.meta.release_tracks, None); // A decodable seek value, WzE3Mjg1NjE5OTI3MzQ2NzMsNV0K ([1728561992734673,5]), but doesn't actually exist let json: VersionList = anon @@ -297,7 +309,7 @@ async fn test_seek_based_pagination_date_sorting() { assert_eq!(json.versions.len(), 0); assert!(json.meta.next_page.is_none()); assert_eq!(json.meta.total, 0); - assert_eq!(json.meta.release_tracks, release_tracks); + assert_eq!(json.meta.release_tracks, None); } #[tokio::test(flavor = "multi_thread")]