From ca9f183a95cec6c36b52cf6070f05740d2c2de47 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 25 Feb 2025 10:51:03 +0100 Subject: [PATCH 1/3] Implement `utoipa::ToSchema` for `EncodableKeyword` --- src/openapi.rs | 5 +++ ..._io__openapi__tests__openapi_snapshot.snap | 35 +++++++++++++++++++ src/views.rs | 14 +++++++- 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/openapi.rs b/src/openapi.rs index 105c2280084..b4da3704684 100644 --- a/src/openapi.rs +++ b/src/openapi.rs @@ -40,6 +40,11 @@ other clients). license(name = "MIT OR Apache-2.0", url = "https://github.com/rust-lang/crates.io/blob/main/README.md#%EF%B8%8F-license"), version = "0.0.0", ), + components( + schemas( + crate::views::EncodableKeyword, + ), + ), modifiers(&SecurityAddon), servers( (url = "https://crates.io"), diff --git a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap index abf35a1c61e..bae5afbc055 100644 --- a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap +++ b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap @@ -4,6 +4,41 @@ expression: response.json() --- { "components": { + "schemas": { + "Keyword": { + "properties": { + "crates_cnt": { + "description": "The total number of crates that have this keyword.", + "example": 42, + "format": "int32", + "type": "integer" + }, + "created_at": { + "description": "The date and time this keyword was created.", + "example": "2017-01-06T14:23:11Z", + "format": "date-time", + "type": "string" + }, + "id": { + "description": "An opaque identifier for the keyword.", + "example": "http", + "type": "string" + }, + "keyword": { + "description": "The keyword itself.", + "example": "http", + "type": "string" + } + }, + "required": [ + "id", + "keyword", + "created_at", + "crates_cnt" + ], + "type": "object" + } + }, "securitySchemes": { "api_token": { "description": "The API token is used to authenticate requests from cargo and other clients.", diff --git a/src/views.rs b/src/views.rs index cf3b5838a32..03913aa1b75 100644 --- a/src/views.rs +++ b/src/views.rs @@ -158,11 +158,23 @@ impl From for EncodableVersionDownload { } } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, utoipa::ToSchema)] +#[schema(as = Keyword)] pub struct EncodableKeyword { + /// An opaque identifier for the keyword. + #[schema(example = "http")] pub id: String, + + /// The keyword itself. + #[schema(example = "http")] pub keyword: String, + + /// The date and time this keyword was created. + #[schema(example = "2017-01-06T14:23:11Z")] pub created_at: DateTime, + + /// The total number of crates that have this keyword. + #[schema(example = 42)] pub crates_cnt: i32, } From 1dc896cde898151b46f0e8465b2b45f72806c578 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 25 Feb 2025 10:18:25 +0100 Subject: [PATCH 2/3] controllers/keyword: Extract `ListResponse` struct --- src/controllers/keyword.rs | 32 +++++++++++------ ..._io__openapi__tests__openapi_snapshot.snap | 34 +++++++++++++++++++ 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/src/controllers/keyword.rs b/src/controllers/keyword.rs index 1f7937b99a2..5993248773c 100644 --- a/src/controllers/keyword.rs +++ b/src/controllers/keyword.rs @@ -4,6 +4,7 @@ use crate::controllers::helpers::{Paginate, pagination::Paginated}; use crate::models::Keyword; use crate::util::errors::AppResult; use crate::views::EncodableKeyword; +use axum::Json; use axum::extract::{FromRequestParts, Path, Query}; use axum_extra::json; use axum_extra::response::ErasedJson; @@ -22,19 +23,35 @@ pub struct ListQueryParams { sort: Option, } +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct ListResponse { + /// The list of keywords. + pub keywords: Vec, + + #[schema(inline)] + pub meta: ListMeta, +} + +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct ListMeta { + /// The total number of keywords. + #[schema(example = 123)] + pub total: i64, +} + /// List all keywords. #[utoipa::path( get, path = "/api/v1/keywords", params(ListQueryParams, PaginationQueryParams), tag = "keywords", - responses((status = 200, description = "Successful Response")), + responses((status = 200, description = "Successful Response", body = inline(ListResponse))), )] pub async fn list_keywords( state: AppState, params: ListQueryParams, req: Parts, -) -> AppResult { +) -> AppResult> { use crate::schema::keywords; let mut query = keywords::table.into_boxed(); @@ -49,15 +66,10 @@ pub async fn list_keywords( let mut conn = state.db_read().await?; let data: Paginated = query.load(&mut conn).await?; let total = data.total(); - let kws = data - .into_iter() - .map(Keyword::into) - .collect::>(); + let keywords = data.into_iter().map(Keyword::into).collect(); - Ok(json!({ - "keywords": kws, - "meta": { "total": total }, - })) + let meta = ListMeta { total }; + Ok(Json(ListResponse { keywords, meta })) } /// Get keyword metadata. diff --git a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap index bae5afbc055..32a215c1e12 100644 --- a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap +++ b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap @@ -1366,6 +1366,40 @@ expression: response.json() ], "responses": { "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "keywords": { + "description": "The list of keywords.", + "items": { + "$ref": "#/components/schemas/Keyword" + }, + "type": "array" + }, + "meta": { + "properties": { + "total": { + "description": "The total number of keywords.", + "example": 123, + "format": "int64", + "type": "integer" + } + }, + "required": [ + "total" + ], + "type": "object" + } + }, + "required": [ + "keywords", + "meta" + ], + "type": "object" + } + } + }, "description": "Successful Response" } }, From 54ba2bbabdbc46918bf226d83c412e14eaafb072 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Tue, 25 Feb 2025 10:57:46 +0100 Subject: [PATCH 3/3] controllers/keyword: Extract `GetResponse` struct --- src/controllers/keyword.rs | 18 ++++++++++++------ ...s_io__openapi__tests__openapi_snapshot.snap | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/controllers/keyword.rs b/src/controllers/keyword.rs index 5993248773c..467247b7797 100644 --- a/src/controllers/keyword.rs +++ b/src/controllers/keyword.rs @@ -6,8 +6,6 @@ use crate::util::errors::AppResult; use crate::views::EncodableKeyword; use axum::Json; use axum::extract::{FromRequestParts, Path, Query}; -use axum_extra::json; -use axum_extra::response::ErasedJson; use diesel::prelude::*; use http::request::Parts; @@ -72,6 +70,11 @@ pub async fn list_keywords( Ok(Json(ListResponse { keywords, meta })) } +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct GetResponse { + pub keyword: EncodableKeyword, +} + /// Get keyword metadata. #[utoipa::path( get, @@ -80,11 +83,14 @@ pub async fn list_keywords( ("keyword" = String, Path, description = "The keyword to find"), ), tag = "keywords", - responses((status = 200, description = "Successful Response")), + responses((status = 200, description = "Successful Response", body = inline(GetResponse))), )] -pub async fn find_keyword(Path(name): Path, state: AppState) -> AppResult { +pub async fn find_keyword( + Path(name): Path, + state: AppState, +) -> AppResult> { let mut conn = state.db_read().await?; let kw = Keyword::find_by_keyword(&mut conn, &name).await?; - - Ok(json!({ "keyword": EncodableKeyword::from(kw) })) + let keyword = EncodableKeyword::from(kw); + Ok(Json(GetResponse { keyword })) } diff --git a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap index 32a215c1e12..ce514090081 100644 --- a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap +++ b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap @@ -1425,6 +1425,21 @@ expression: response.json() ], "responses": { "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "keyword": { + "$ref": "#/components/schemas/Keyword" + } + }, + "required": [ + "keyword" + ], + "type": "object" + } + } + }, "description": "Successful Response" } },