Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 60 additions & 26 deletions src/controllers/category.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,19 +22,35 @@ pub struct ListQueryParams {
sort: Option<String>,
}

#[derive(Debug, Serialize, utoipa::ToSchema)]
pub struct ListResponse {
/// The list of categories.
pub categories: Vec<EncodableCategory>,

#[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<ErasedJson> {
) -> AppResult<Json<ListResponse>> {
// 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.
Expand All @@ -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::<Vec<EncodableCategory>>();
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.
Expand All @@ -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<String>) -> AppResult<ErasedJson> {
pub async fn find_category(
state: AppState,
Path(slug): Path<String>,
) -> AppResult<Json<GetResponse>> {
let mut conn = state.db_read().await?;

let cat: Category = Category::by_slug(&slug).first(&mut conn).await?;
Expand All @@ -93,31 +111,47 @@ pub async fn find_category(state: AppState, Path(slug): Path<String>) -> 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 <https://crates.io/category_slugs>.
#[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<Slug>,
}

/// List all available category slugs.
#[utoipa::path(
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<ErasedJson> {
pub async fn list_category_slugs(state: AppState) -> AppResult<Json<ListSlugsResponse>> {
let mut conn = state.db_read().await?;

let slugs: Vec<Slug> = 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 }))
}
160 changes: 160 additions & 0 deletions src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://crates.io/category_slugs>.",
"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": {
Expand Down Expand Up @@ -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 <https://crates.io/category_slugs>.",
"example": "game-development",
"type": "string"
}
},
"required": [
"id",
"slug",
"description"
],
"type": "object"
}
},
"securitySchemes": {
Expand Down Expand Up @@ -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"
}
},
Expand All @@ -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"
}
},
Expand All @@ -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"
}
},
Expand Down
32 changes: 31 additions & 1 deletion src/views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://crates.io/category_slugs>.
#[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<Utc>,

/// 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<Vec<EncodableCategory>>,

/// 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<Vec<EncodableCategory>>,
}

Expand Down