diff --git a/src/controllers/site_metadata.rs b/src/controllers/site_metadata.rs index 024033622b9..558e740d915 100644 --- a/src/controllers/site_metadata.rs +++ b/src/controllers/site_metadata.rs @@ -1,6 +1,20 @@ use crate::app::AppState; +use axum::Json; use axum::response::IntoResponse; -use axum_extra::json; + +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct MetadataResponse<'a> { + /// The SHA1 of the currently deployed commit. + #[schema(example = "0aebe2cdfacae1229b93853b1c58f9352195f081")] + pub deployed_sha: &'a str, + + /// The SHA1 of the currently deployed commit. + #[schema(example = "0aebe2cdfacae1229b93853b1c58f9352195f081")] + pub commit: &'a str, + + /// Whether the crates.io service is in read-only mode. + pub read_only: bool, +} /// Get crates.io metadata. /// @@ -10,7 +24,7 @@ use axum_extra::json; get, path = "/api/v1/site_metadata", tag = "other", - responses((status = 200, description = "Successful Response")), + responses((status = 200, description = "Successful Response", body = inline(MetadataResponse<'_>))), )] pub async fn get_site_metadata(state: AppState) -> impl IntoResponse { let read_only = state.config.db.are_all_read_only(); @@ -18,9 +32,10 @@ pub async fn get_site_metadata(state: AppState) -> impl IntoResponse { let deployed_sha = dotenvy::var("HEROKU_SLUG_COMMIT").unwrap_or_else(|_| String::from("unknown")); - json!({ - "deployed_sha": &deployed_sha[..], - "commit": &deployed_sha[..], - "read_only": read_only, + Json(MetadataResponse { + deployed_sha: &deployed_sha, + commit: &deployed_sha, + read_only, }) + .into_response() } diff --git a/src/controllers/summary.rs b/src/controllers/summary.rs index 1ea439c5bdd..1eab25083b4 100644 --- a/src/controllers/summary.rs +++ b/src/controllers/summary.rs @@ -5,13 +5,41 @@ use crate::schema::{ }; use crate::util::errors::AppResult; use crate::views::{EncodableCategory, EncodableCrate, EncodableKeyword}; -use axum_extra::json; -use axum_extra::response::ErasedJson; +use axum::Json; use diesel::prelude::*; use diesel_async::{AsyncPgConnection, RunQueryDsl}; use futures_util::FutureExt; use std::future::Future; +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct SummaryResponse { + /// The total number of downloads across all crates. + #[schema(example = 123_456_789)] + num_downloads: i64, + + /// The total number of crates on crates.io. + #[schema(example = 123_456)] + num_crates: i64, + + /// The 10 most recently created crates. + new_crates: Vec, + + /// The 10 crates with the highest total number of downloads. + most_downloaded: Vec, + + /// The 10 crates with the highest number of downloads within the last 90 days. + most_recently_downloaded: Vec, + + /// The 10 most recently updated crates. + just_updated: Vec, + + /// The 10 most popular keywords. + popular_keywords: Vec, + + /// The 10 most popular categories. + popular_categories: Vec, +} + /// Get front page data. /// /// This endpoint returns a summary of the most important data for the front @@ -20,9 +48,9 @@ use std::future::Future; get, path = "/api/v1/summary", tag = "other", - responses((status = 200, description = "Successful Response")), + responses((status = 200, description = "Successful Response", body = inline(SummaryResponse))), )] -pub async fn get_summary(state: AppState) -> AppResult { +pub async fn get_summary(state: AppState) -> AppResult> { let mut conn = state.db_read().await?; let config = &state.config; @@ -37,10 +65,10 @@ pub async fn get_summary(state: AppState) -> AppResult { popular_categories, popular_keywords, ) = tokio::try_join!( - crates::table.count().get_result::(&mut conn).boxed(), + crates::table.count().get_result(&mut conn).boxed(), metadata::table .select(metadata::total_downloads) - .get_result::(&mut conn) + .get_result(&mut conn) .boxed(), crates::table .inner_join(crate_downloads::table) @@ -100,25 +128,18 @@ pub async fn get_summary(state: AppState) -> AppResult { encode_crates(&mut conn, just_updated), )?; - let popular_categories = popular_categories - .into_iter() - .map(Category::into) - .collect::>(); - - let popular_keywords = popular_keywords - .into_iter() - .map(Keyword::into) - .collect::>(); - - Ok(json!({ - "num_downloads": num_downloads, - "num_crates": num_crates, - "new_crates": new_crates, - "most_downloaded": most_downloaded, - "most_recently_downloaded": most_recently_downloaded, - "just_updated": just_updated, - "popular_keywords": popular_keywords, - "popular_categories": popular_categories, + let popular_categories = popular_categories.into_iter().map(Category::into).collect(); + let popular_keywords = popular_keywords.into_iter().map(Keyword::into).collect(); + + Ok(Json(SummaryResponse { + num_downloads, + num_crates, + new_crates, + most_downloaded, + most_recently_downloaded, + just_updated, + popular_keywords, + popular_categories, })) } diff --git a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap index f1d2a7f545b..e286ce8df1e 100644 --- a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap +++ b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap @@ -3661,6 +3661,34 @@ expression: response.json() "operationId": "get_site_metadata", "responses": { "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "commit": { + "description": "The SHA1 of the currently deployed commit.", + "example": "0aebe2cdfacae1229b93853b1c58f9352195f081", + "type": "string" + }, + "deployed_sha": { + "description": "The SHA1 of the currently deployed commit.", + "example": "0aebe2cdfacae1229b93853b1c58f9352195f081", + "type": "string" + }, + "read_only": { + "description": "Whether the crates.io service is in read-only mode.", + "type": "boolean" + } + }, + "required": [ + "deployed_sha", + "commit", + "read_only" + ], + "type": "object" + } + } + }, "description": "Successful Response" } }, @@ -3676,6 +3704,79 @@ expression: response.json() "operationId": "get_summary", "responses": { "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "just_updated": { + "description": "The 10 most recently updated crates.", + "items": { + "$ref": "#/components/schemas/Crate" + }, + "type": "array" + }, + "most_downloaded": { + "description": "The 10 crates with the highest total number of downloads.", + "items": { + "$ref": "#/components/schemas/Crate" + }, + "type": "array" + }, + "most_recently_downloaded": { + "description": "The 10 crates with the highest number of downloads within the last 90 days.", + "items": { + "$ref": "#/components/schemas/Crate" + }, + "type": "array" + }, + "new_crates": { + "description": "The 10 most recently created crates.", + "items": { + "$ref": "#/components/schemas/Crate" + }, + "type": "array" + }, + "num_crates": { + "description": "The total number of crates on crates.io.", + "example": 123456, + "format": "int64", + "type": "integer" + }, + "num_downloads": { + "description": "The total number of downloads across all crates.", + "example": 123456789, + "format": "int64", + "type": "integer" + }, + "popular_categories": { + "description": "The 10 most popular categories.", + "items": { + "$ref": "#/components/schemas/Category" + }, + "type": "array" + }, + "popular_keywords": { + "description": "The 10 most popular keywords.", + "items": { + "$ref": "#/components/schemas/Keyword" + }, + "type": "array" + } + }, + "required": [ + "num_downloads", + "num_crates", + "new_crates", + "most_downloaded", + "most_recently_downloaded", + "just_updated", + "popular_keywords", + "popular_categories" + ], + "type": "object" + } + } + }, "description": "Successful Response" } },