diff --git a/src/controllers/category.rs b/src/controllers/category.rs index fe62f76a1c8..3c18b93c0fd 100644 --- a/src/controllers/category.rs +++ b/src/controllers/category.rs @@ -4,9 +4,8 @@ use crate::models::Category; use crate::schema::categories; use crate::util::errors::AppResult; use crate::views::EncodableCategory; +use axum::Json; use axum::extract::{FromRequestParts, Path, Query}; -use axum_extra::json; -use axum_extra::response::ErasedJson; use diesel::QueryDsl; use diesel_async::RunQueryDsl; use http::request::Parts; @@ -23,19 +22,35 @@ pub struct ListQueryParams { sort: Option, } +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct ListResponse { + /// The list of categories. + pub categories: Vec, + + #[schema(inline)] + pub meta: ListMeta, +} + +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct ListMeta { + /// The total number of categories. + #[schema(example = 123)] + pub total: i64, +} + /// List all categories. #[utoipa::path( get, path = "/api/v1/categories", params(ListQueryParams, PaginationQueryParams), tag = "categories", - responses((status = 200, description = "Successful Response")), + responses((status = 200, description = "Successful Response", body = inline(ListResponse))), )] pub async fn list_categories( app: AppState, params: ListQueryParams, req: Parts, -) -> AppResult { +) -> AppResult> { // FIXME: There are 69 categories, 47 top level. This isn't going to // grow by an OoM. We need a limit for /summary, but we don't need // to paginate this. @@ -48,18 +63,18 @@ pub async fn list_categories( let offset = options.offset().unwrap_or_default(); let categories = Category::toplevel(&mut conn, sort, options.per_page, offset).await?; - let categories = categories - .into_iter() - .map(Category::into) - .collect::>(); + let categories = categories.into_iter().map(Category::into).collect(); // Query for the total count of categories let total = Category::count_toplevel(&mut conn).await?; - Ok(json!({ - "categories": categories, - "meta": { "total": total }, - })) + let meta = ListMeta { total }; + Ok(Json(ListResponse { categories, meta })) +} + +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct GetResponse { + pub category: EncodableCategory, } /// Get category metadata. @@ -70,9 +85,12 @@ pub async fn list_categories( ("category" = String, Path, description = "Name of the category"), ), tag = "categories", - responses((status = 200, description = "Successful Response")), + responses((status = 200, description = "Successful Response", body = inline(GetResponse))), )] -pub async fn find_category(state: AppState, Path(slug): Path) -> AppResult { +pub async fn find_category( + state: AppState, + Path(slug): Path, +) -> AppResult> { let mut conn = state.db_read().await?; let cat: Category = Category::by_slug(&slug).first(&mut conn).await?; @@ -93,7 +111,30 @@ pub async fn find_category(state: AppState, Path(slug): Path) -> AppResu category.subcategories = Some(subcats); category.parent_categories = Some(parents); - Ok(json!({ "category": category })) + Ok(Json(GetResponse { category })) +} + +#[derive(Debug, Serialize, Queryable, utoipa::ToSchema)] +pub struct Slug { + /// An opaque identifier for the category. + #[schema(example = "game-development")] + id: String, + + /// The "slug" of the category. + /// + /// See . + #[schema(example = "game-development")] + slug: String, + + /// A description of the category. + #[schema(example = "Libraries for creating games.")] + description: String, +} + +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct ListSlugsResponse { + /// The list of category slugs. + pub category_slugs: Vec, } /// List all available category slugs. @@ -101,23 +142,16 @@ pub async fn find_category(state: AppState, Path(slug): Path) -> AppResu get, path = "/api/v1/category_slugs", tag = "categories", - responses((status = 200, description = "Successful Response")), + responses((status = 200, description = "Successful Response", body = inline(ListSlugsResponse))), )] -pub async fn list_category_slugs(state: AppState) -> AppResult { +pub async fn list_category_slugs(state: AppState) -> AppResult> { let mut conn = state.db_read().await?; - let slugs: Vec = categories::table + let category_slugs = categories::table .select((categories::slug, categories::slug, categories::description)) .order(categories::slug) .load(&mut conn) .await?; - #[derive(Serialize, Queryable)] - struct Slug { - id: String, - slug: String, - description: String, - } - - Ok(json!({ "category_slugs": slugs })) + Ok(Json(ListSlugsResponse { category_slugs })) } diff --git a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap index ce514090081..114679eff07 100644 --- a/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap +++ b/src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap @@ -5,6 +5,73 @@ expression: response.json() { "components": { "schemas": { + "Category": { + "properties": { + "category": { + "description": "The name of the category.", + "example": "Game development", + "type": "string" + }, + "crates_cnt": { + "description": "The total number of crates that have this category.", + "example": 42, + "format": "int32", + "type": "integer" + }, + "created_at": { + "description": "The date and time this category was created.", + "example": "2019-12-13T13:46:41Z", + "format": "date-time", + "type": "string" + }, + "description": { + "description": "A description of the category.", + "example": "Libraries for creating games.", + "type": "string" + }, + "id": { + "description": "An opaque identifier for the category.", + "example": "game-development", + "type": "string" + }, + "parent_categories": { + "description": "The parent categories of this category.\n\nThis field is only present when the category details are queried,\nbut not when listing categories.", + "example": [], + "items": { + "$ref": "#/components/schemas/Category" + }, + "type": [ + "array", + "null" + ] + }, + "slug": { + "description": "The \"slug\" of the category.\n\nSee .", + "example": "game-development", + "type": "string" + }, + "subcategories": { + "description": "The subcategories of this category.\n\nThis field is only present when the category details are queried,\nbut not when listing categories.", + "example": [], + "items": { + "$ref": "#/components/schemas/Category" + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "id", + "category", + "slug", + "description", + "created_at", + "crates_cnt" + ], + "type": "object" + }, "Keyword": { "properties": { "crates_cnt": { @@ -37,6 +104,31 @@ expression: response.json() "crates_cnt" ], "type": "object" + }, + "Slug": { + "properties": { + "description": { + "description": "A description of the category.", + "example": "Libraries for creating games.", + "type": "string" + }, + "id": { + "description": "An opaque identifier for the category.", + "example": "game-development", + "type": "string" + }, + "slug": { + "description": "The \"slug\" of the category.\n\nSee .", + "example": "game-development", + "type": "string" + } + }, + "required": [ + "id", + "slug", + "description" + ], + "type": "object" } }, "securitySchemes": { @@ -237,6 +329,40 @@ expression: response.json() ], "responses": { "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "categories": { + "description": "The list of categories.", + "items": { + "$ref": "#/components/schemas/Category" + }, + "type": "array" + }, + "meta": { + "properties": { + "total": { + "description": "The total number of categories.", + "example": 123, + "format": "int64", + "type": "integer" + } + }, + "required": [ + "total" + ], + "type": "object" + } + }, + "required": [ + "categories", + "meta" + ], + "type": "object" + } + } + }, "description": "Successful Response" } }, @@ -262,6 +388,21 @@ expression: response.json() ], "responses": { "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "category": { + "$ref": "#/components/schemas/Category" + } + }, + "required": [ + "category" + ], + "type": "object" + } + } + }, "description": "Successful Response" } }, @@ -276,6 +417,25 @@ expression: response.json() "operationId": "list_category_slugs", "responses": { "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "category_slugs": { + "description": "The list of category slugs.", + "items": { + "$ref": "#/components/schemas/Slug" + }, + "type": "array" + } + }, + "required": [ + "category_slugs" + ], + "type": "object" + } + } + }, "description": "Successful Response" } }, diff --git a/src/views.rs b/src/views.rs index d16179d1a82..5e0ccca3cd0 100644 --- a/src/views.rs +++ b/src/views.rs @@ -10,19 +10,49 @@ use crates_io_github as github; pub mod krate_publish; pub use self::krate_publish::{EncodableCrateDependency, PublishMetadata}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, utoipa::ToSchema)] +#[schema(as = Category)] pub struct EncodableCategory { + /// An opaque identifier for the category. + #[schema(example = "game-development")] pub id: String, + + /// The name of the category. + #[schema(example = "Game development")] pub category: String, + + /// The "slug" of the category. + /// + /// See . + #[schema(example = "game-development")] pub slug: String, + + /// A description of the category. + #[schema(example = "Libraries for creating games.")] pub description: String, + + /// The date and time this category was created. + #[schema(example = "2019-12-13T13:46:41Z")] pub created_at: DateTime, + + /// The total number of crates that have this category. + #[schema(example = 42)] pub crates_cnt: i32, + /// The subcategories of this category. + /// + /// This field is only present when the category details are queried, + /// but not when listing categories. #[serde(skip_serializing_if = "Option::is_none")] + #[schema(no_recursion, example = json!([]))] pub subcategories: Option>, + /// The parent categories of this category. + /// + /// This field is only present when the category details are queried, + /// but not when listing categories. #[serde(skip_serializing_if = "Option::is_none")] + #[schema(no_recursion, example = json!([]))] pub parent_categories: Option>, }