From 0a5c9e7f82e48c2fd5f599d4dcf6537bc3cfc26d Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 27 Feb 2025 15:44:50 +0100 Subject: [PATCH 1/4] Add OpenAPI documentation for `GET /api/v1/crates/{name}/{version}/downloads` response payload --- src/controllers/version/downloads.rs | 17 ++++--- ..._io__openapi__tests__openapi_snapshot.snap | 45 +++++++++++++++++++ src/views.rs | 11 ++++- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/src/controllers/version/downloads.rs b/src/controllers/version/downloads.rs index 0ccd4a66fa9..86687945f27 100644 --- a/src/controllers/version/downloads.rs +++ b/src/controllers/version/downloads.rs @@ -9,10 +9,10 @@ use crate::schema::*; use crate::util::errors::AppResult; use crate::util::{RequestUtils, redirect}; use crate::views::EncodableVersionDownload; +use axum::Json; use axum::extract::{FromRequestParts, Query}; use axum::response::{IntoResponse, Response}; use axum_extra::json; -use axum_extra::response::ErasedJson; use chrono::{Duration, NaiveDate, Utc}; use diesel::prelude::*; use diesel_async::RunQueryDsl; @@ -51,6 +51,11 @@ pub struct DownloadsQueryParams { before_date: Option, } +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct DownloadsResponse { + pub version_downloads: Vec, +} + /// Get the download counts for a crate version. /// /// This includes the per-day downloads for the last 90 days. @@ -59,13 +64,13 @@ pub struct DownloadsQueryParams { path = "/api/v1/crates/{name}/{version}/downloads", params(CrateVersionPath, DownloadsQueryParams), tag = "versions", - responses((status = 200, description = "Successful Response")), + responses((status = 200, description = "Successful Response", body = inline(DownloadsResponse))), )] pub async fn get_version_downloads( app: AppState, path: CrateVersionPath, params: DownloadsQueryParams, -) -> AppResult { +) -> AppResult> { let mut conn = app.db_read().await?; let version = path.load_version(&mut conn).await?; @@ -75,14 +80,14 @@ pub async fn get_version_downloads( let cutoff_start_date = cutoff_end_date - Duration::days(89); - let downloads = VersionDownload::belonging_to(&version) + let version_downloads = VersionDownload::belonging_to(&version) .filter(version_downloads::date.between(cutoff_start_date, cutoff_end_date)) .order(version_downloads::date) .load(&mut conn) .await? .into_iter() .map(VersionDownload::into) - .collect::>(); + .collect(); - Ok(json!({ "version_downloads": downloads })) + Ok(Json(DownloadsResponse { version_downloads })) } diff --git a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap index 52991927aa4..1713a38b63a 100644 --- a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap +++ b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap @@ -645,6 +645,33 @@ expression: response.json() ], "type": "object" }, + "VersionDownload": { + "properties": { + "date": { + "description": "The date this download count is for.", + "example": "2019-12-13", + "type": "string" + }, + "downloads": { + "description": "The number of downloads for this version on the given date.", + "example": 123, + "format": "int32", + "type": "integer" + }, + "version": { + "description": "The ID of the version this download count is for.", + "example": 42, + "format": "int32", + "type": "integer" + } + }, + "required": [ + "version", + "downloads", + "date" + ], + "type": "object" + }, "VersionLinks": { "properties": { "authors": { @@ -2011,6 +2038,24 @@ expression: response.json() ], "responses": { "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "version_downloads": { + "items": { + "$ref": "#/components/schemas/VersionDownload" + }, + "type": "array" + } + }, + "required": [ + "version_downloads" + ], + "type": "object" + } + } + }, "description": "Successful Response" } }, diff --git a/src/views.rs b/src/views.rs index 0a099853bb8..2d4ecef3476 100644 --- a/src/views.rs +++ b/src/views.rs @@ -201,10 +201,19 @@ impl EncodableDependency { } } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, utoipa::ToSchema)] +#[schema(as = VersionDownload)] pub struct EncodableVersionDownload { + /// The ID of the version this download count is for. + #[schema(example = 42)] pub version: i32, + + /// The number of downloads for this version on the given date. + #[schema(example = 123)] pub downloads: i32, + + /// The date this download count is for. + #[schema(example = "2019-12-13")] pub date: String, } From 9c6a86d6d2cdea4154edcd21b0cd80926c2f3281 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 27 Feb 2025 15:56:58 +0100 Subject: [PATCH 2/4] Improve OpenAPI documentation for `GET /api/v1/crates/{name}/{version}/readme` endpoint --- src/controllers/version/readme.rs | 20 +++++++++---- ..._io__openapi__tests__openapi_snapshot.snap | 30 ++++++++++++++++++- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/controllers/version/readme.rs b/src/controllers/version/readme.rs index 27236fd2613..19747ff6aba 100644 --- a/src/controllers/version/readme.rs +++ b/src/controllers/version/readme.rs @@ -1,23 +1,33 @@ use crate::app::AppState; use crate::controllers::version::CrateVersionPath; use crate::util::{RequestUtils, redirect}; +use axum::Json; use axum::response::{IntoResponse, Response}; -use axum_extra::json; use http::request::Parts; +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct UrlResponse { + /// The URL to the readme file. + #[schema(example = "https://static.crates.io/readmes/serde/serde-1.0.0.html")] + pub url: String, +} + /// Get the readme of a crate version. #[utoipa::path( get, path = "/api/v1/crates/{name}/{version}/readme", params(CrateVersionPath), tag = "versions", - responses((status = 200, description = "Successful Response")), + responses( + (status = 302, description = "Successful Response (default)", headers(("location" = String, description = "The URL to the readme file."))), + (status = 200, description = "Successful Response (for `content-type: application/json`)", body = inline(UrlResponse)), + ), )] pub async fn get_version_readme(app: AppState, path: CrateVersionPath, req: Parts) -> Response { - let redirect_url = app.storage.readme_location(&path.name, &path.version); + let url = app.storage.readme_location(&path.name, &path.version); if req.wants_json() { - json!({ "url": redirect_url }).into_response() + Json(UrlResponse { url }).into_response() } else { - redirect(redirect_url) + redirect(url) } } diff --git a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap index 1713a38b63a..9efe75d5a9c 100644 --- a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap +++ b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap @@ -2091,7 +2091,35 @@ expression: response.json() ], "responses": { "200": { - "description": "Successful Response" + "content": { + "application/json": { + "schema": { + "properties": { + "url": { + "description": "The URL to the readme file.", + "example": "https://static.crates.io/readmes/serde/serde-1.0.0.html", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + } + } + }, + "description": "Successful Response (for `content-type: application/json`)" + }, + "302": { + "description": "Successful Response (default)", + "headers": { + "location": { + "description": "The URL to the readme file.", + "schema": { + "type": "string" + } + } + } } }, "summary": "Get the readme of a crate version.", From 316fa1b3694b57b21d37ef03e593e3c111af4316 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 27 Feb 2025 16:01:15 +0100 Subject: [PATCH 3/4] Improve OpenAPI documentation for `GET /api/v1/crates/{name}/{version}/download` endpoint --- src/controllers/version/downloads.rs | 12 +++++++- ..._io__openapi__tests__openapi_snapshot.snap | 30 ++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/controllers/version/downloads.rs b/src/controllers/version/downloads.rs index 86687945f27..631a29438e8 100644 --- a/src/controllers/version/downloads.rs +++ b/src/controllers/version/downloads.rs @@ -18,6 +18,13 @@ use diesel::prelude::*; use diesel_async::RunQueryDsl; use http::request::Parts; +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct UrlResponse { + /// The URL to the crate file. + #[schema(example = "https://static.crates.io/crates/serde/serde-1.0.0.crate")] + pub url: String, +} + /// Download a crate version. /// /// This returns a URL to the location where the crate is stored. @@ -26,7 +33,10 @@ use http::request::Parts; path = "/api/v1/crates/{name}/{version}/download", params(CrateVersionPath), tag = "versions", - responses((status = 200, description = "Successful Response")), + responses( + (status = 302, description = "Successful Response (default)", headers(("location" = String, description = "The URL to the crate file."))), + (status = 200, description = "Successful Response (for `content-type: application/json`)", body = inline(UrlResponse)), + ), )] pub async fn download_version( app: AppState, diff --git a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap index 9efe75d5a9c..1b8cbe78734 100644 --- a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap +++ b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap @@ -1991,7 +1991,35 @@ expression: response.json() ], "responses": { "200": { - "description": "Successful Response" + "content": { + "application/json": { + "schema": { + "properties": { + "url": { + "description": "The URL to the crate file.", + "example": "https://static.crates.io/crates/serde/serde-1.0.0.crate", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + } + } + }, + "description": "Successful Response (for `content-type: application/json`)" + }, + "302": { + "description": "Successful Response (default)", + "headers": { + "location": { + "description": "The URL to the crate file.", + "schema": { + "type": "string" + } + } + } } }, "summary": "Download a crate version.", From de89f9640ad00796bfdbdbdb8b420e04fdf0c5cd Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 27 Feb 2025 16:05:21 +0100 Subject: [PATCH 4/4] Improve OpenAPI documentation for `PATCH /api/v1/crates/{name}/{version}` endpoint --- src/controllers/version/update.rs | 15 +++++++++------ ...ates_io__openapi__tests__openapi_snapshot.snap | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/controllers/version/update.rs b/src/controllers/version/update.rs index f74c2f5969c..272bb0517d2 100644 --- a/src/controllers/version/update.rs +++ b/src/controllers/version/update.rs @@ -10,8 +10,6 @@ use crate::util::errors::{AppResult, bad_request, custom}; use crate::views::EncodableVersion; use crate::worker::jobs::{SyncToGitIndex, SyncToSparseIndex, UpdateDefaultVersion}; use axum::Json; -use axum_extra::json; -use axum_extra::response::ErasedJson; use crates_io_worker::BackgroundJob; use diesel::prelude::*; use diesel_async::{AsyncPgConnection, RunQueryDsl}; @@ -29,6 +27,11 @@ pub struct VersionUpdateRequest { version: VersionUpdate, } +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct UpdateResponse { + pub version: EncodableVersion, +} + /// Update a crate version. /// /// This endpoint allows updating the `yanked` state of a version, including a yank message. @@ -41,14 +44,14 @@ pub struct VersionUpdateRequest { ("cookie" = []), ), tag = "versions", - responses((status = 200, description = "Successful Response")), + responses((status = 200, description = "Successful Response", body = inline(UpdateResponse))), )] pub async fn update_version( state: AppState, path: CrateVersionPath, req: Parts, Json(update_request): Json, -) -> AppResult { +) -> AppResult> { let mut conn = state.db_write().await?; let (mut version, krate) = path.load_version_and_crate(&mut conn).await?; validate_yank_update(&update_request.version, &version)?; @@ -74,8 +77,8 @@ pub async fn update_version( VersionOwnerAction::by_version(&mut conn, &version), version.published_by(&mut conn), )?; - let updated_version = EncodableVersion::from(version, &krate.name, published_by, actions); - Ok(json!({ "version": updated_version })) + let version = EncodableVersion::from(version, &krate.name, published_by, actions); + Ok(Json(UpdateResponse { version })) } fn validate_yank_update(update_data: &VersionUpdate, version: &Version) -> AppResult<()> { diff --git a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap index 1b8cbe78734..9e73339eb4e 100644 --- a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap +++ b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap @@ -1856,6 +1856,21 @@ expression: response.json() ], "responses": { "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "version": { + "$ref": "#/components/schemas/Version" + } + }, + "required": [ + "version" + ], + "type": "object" + } + } + }, "description": "Successful Response" } },