Skip to content

Commit a6d1a11

Browse files
authored
Merge pull request #10693 from Turbo87/openapi
Add OpenAPI documentation for category API responses
2 parents 9423817 + 434a94d commit a6d1a11

File tree

3 files changed

+251
-27
lines changed

3 files changed

+251
-27
lines changed

src/controllers/category.rs

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ use crate::models::Category;
44
use crate::schema::categories;
55
use crate::util::errors::AppResult;
66
use crate::views::EncodableCategory;
7+
use axum::Json;
78
use axum::extract::{FromRequestParts, Path, Query};
8-
use axum_extra::json;
9-
use axum_extra::response::ErasedJson;
109
use diesel::QueryDsl;
1110
use diesel_async::RunQueryDsl;
1211
use http::request::Parts;
@@ -23,19 +22,35 @@ pub struct ListQueryParams {
2322
sort: Option<String>,
2423
}
2524

25+
#[derive(Debug, Serialize, utoipa::ToSchema)]
26+
pub struct ListResponse {
27+
/// The list of categories.
28+
pub categories: Vec<EncodableCategory>,
29+
30+
#[schema(inline)]
31+
pub meta: ListMeta,
32+
}
33+
34+
#[derive(Debug, Serialize, utoipa::ToSchema)]
35+
pub struct ListMeta {
36+
/// The total number of categories.
37+
#[schema(example = 123)]
38+
pub total: i64,
39+
}
40+
2641
/// List all categories.
2742
#[utoipa::path(
2843
get,
2944
path = "/api/v1/categories",
3045
params(ListQueryParams, PaginationQueryParams),
3146
tag = "categories",
32-
responses((status = 200, description = "Successful Response")),
47+
responses((status = 200, description = "Successful Response", body = inline(ListResponse))),
3348
)]
3449
pub async fn list_categories(
3550
app: AppState,
3651
params: ListQueryParams,
3752
req: Parts,
38-
) -> AppResult<ErasedJson> {
53+
) -> AppResult<Json<ListResponse>> {
3954
// FIXME: There are 69 categories, 47 top level. This isn't going to
4055
// grow by an OoM. We need a limit for /summary, but we don't need
4156
// to paginate this.
@@ -48,18 +63,18 @@ pub async fn list_categories(
4863
let offset = options.offset().unwrap_or_default();
4964

5065
let categories = Category::toplevel(&mut conn, sort, options.per_page, offset).await?;
51-
let categories = categories
52-
.into_iter()
53-
.map(Category::into)
54-
.collect::<Vec<EncodableCategory>>();
66+
let categories = categories.into_iter().map(Category::into).collect();
5567

5668
// Query for the total count of categories
5769
let total = Category::count_toplevel(&mut conn).await?;
5870

59-
Ok(json!({
60-
"categories": categories,
61-
"meta": { "total": total },
62-
}))
71+
let meta = ListMeta { total };
72+
Ok(Json(ListResponse { categories, meta }))
73+
}
74+
75+
#[derive(Debug, Serialize, utoipa::ToSchema)]
76+
pub struct GetResponse {
77+
pub category: EncodableCategory,
6378
}
6479

6580
/// Get category metadata.
@@ -70,9 +85,12 @@ pub async fn list_categories(
7085
("category" = String, Path, description = "Name of the category"),
7186
),
7287
tag = "categories",
73-
responses((status = 200, description = "Successful Response")),
88+
responses((status = 200, description = "Successful Response", body = inline(GetResponse))),
7489
)]
75-
pub async fn find_category(state: AppState, Path(slug): Path<String>) -> AppResult<ErasedJson> {
90+
pub async fn find_category(
91+
state: AppState,
92+
Path(slug): Path<String>,
93+
) -> AppResult<Json<GetResponse>> {
7694
let mut conn = state.db_read().await?;
7795

7896
let cat: Category = Category::by_slug(&slug).first(&mut conn).await?;
@@ -93,31 +111,47 @@ pub async fn find_category(state: AppState, Path(slug): Path<String>) -> AppResu
93111
category.subcategories = Some(subcats);
94112
category.parent_categories = Some(parents);
95113

96-
Ok(json!({ "category": category }))
114+
Ok(Json(GetResponse { category }))
115+
}
116+
117+
#[derive(Debug, Serialize, Queryable, utoipa::ToSchema)]
118+
pub struct Slug {
119+
/// An opaque identifier for the category.
120+
#[schema(example = "game-development")]
121+
id: String,
122+
123+
/// The "slug" of the category.
124+
///
125+
/// See <https://crates.io/category_slugs>.
126+
#[schema(example = "game-development")]
127+
slug: String,
128+
129+
/// A description of the category.
130+
#[schema(example = "Libraries for creating games.")]
131+
description: String,
132+
}
133+
134+
#[derive(Debug, Serialize, utoipa::ToSchema)]
135+
pub struct ListSlugsResponse {
136+
/// The list of category slugs.
137+
pub category_slugs: Vec<Slug>,
97138
}
98139

99140
/// List all available category slugs.
100141
#[utoipa::path(
101142
get,
102143
path = "/api/v1/category_slugs",
103144
tag = "categories",
104-
responses((status = 200, description = "Successful Response")),
145+
responses((status = 200, description = "Successful Response", body = inline(ListSlugsResponse))),
105146
)]
106-
pub async fn list_category_slugs(state: AppState) -> AppResult<ErasedJson> {
147+
pub async fn list_category_slugs(state: AppState) -> AppResult<Json<ListSlugsResponse>> {
107148
let mut conn = state.db_read().await?;
108149

109-
let slugs: Vec<Slug> = categories::table
150+
let category_slugs = categories::table
110151
.select((categories::slug, categories::slug, categories::description))
111152
.order(categories::slug)
112153
.load(&mut conn)
113154
.await?;
114155

115-
#[derive(Serialize, Queryable)]
116-
struct Slug {
117-
id: String,
118-
slug: String,
119-
description: String,
120-
}
121-
122-
Ok(json!({ "category_slugs": slugs }))
156+
Ok(Json(ListSlugsResponse { category_slugs }))
123157
}

src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,73 @@ expression: response.json()
55
{
66
"components": {
77
"schemas": {
8+
"Category": {
9+
"properties": {
10+
"category": {
11+
"description": "The name of the category.",
12+
"example": "Game development",
13+
"type": "string"
14+
},
15+
"crates_cnt": {
16+
"description": "The total number of crates that have this category.",
17+
"example": 42,
18+
"format": "int32",
19+
"type": "integer"
20+
},
21+
"created_at": {
22+
"description": "The date and time this category was created.",
23+
"example": "2019-12-13T13:46:41Z",
24+
"format": "date-time",
25+
"type": "string"
26+
},
27+
"description": {
28+
"description": "A description of the category.",
29+
"example": "Libraries for creating games.",
30+
"type": "string"
31+
},
32+
"id": {
33+
"description": "An opaque identifier for the category.",
34+
"example": "game-development",
35+
"type": "string"
36+
},
37+
"parent_categories": {
38+
"description": "The parent categories of this category.\n\nThis field is only present when the category details are queried,\nbut not when listing categories.",
39+
"example": [],
40+
"items": {
41+
"$ref": "#/components/schemas/Category"
42+
},
43+
"type": [
44+
"array",
45+
"null"
46+
]
47+
},
48+
"slug": {
49+
"description": "The \"slug\" of the category.\n\nSee <https://crates.io/category_slugs>.",
50+
"example": "game-development",
51+
"type": "string"
52+
},
53+
"subcategories": {
54+
"description": "The subcategories of this category.\n\nThis field is only present when the category details are queried,\nbut not when listing categories.",
55+
"example": [],
56+
"items": {
57+
"$ref": "#/components/schemas/Category"
58+
},
59+
"type": [
60+
"array",
61+
"null"
62+
]
63+
}
64+
},
65+
"required": [
66+
"id",
67+
"category",
68+
"slug",
69+
"description",
70+
"created_at",
71+
"crates_cnt"
72+
],
73+
"type": "object"
74+
},
875
"Keyword": {
976
"properties": {
1077
"crates_cnt": {
@@ -37,6 +104,31 @@ expression: response.json()
37104
"crates_cnt"
38105
],
39106
"type": "object"
107+
},
108+
"Slug": {
109+
"properties": {
110+
"description": {
111+
"description": "A description of the category.",
112+
"example": "Libraries for creating games.",
113+
"type": "string"
114+
},
115+
"id": {
116+
"description": "An opaque identifier for the category.",
117+
"example": "game-development",
118+
"type": "string"
119+
},
120+
"slug": {
121+
"description": "The \"slug\" of the category.\n\nSee <https://crates.io/category_slugs>.",
122+
"example": "game-development",
123+
"type": "string"
124+
}
125+
},
126+
"required": [
127+
"id",
128+
"slug",
129+
"description"
130+
],
131+
"type": "object"
40132
}
41133
},
42134
"securitySchemes": {
@@ -237,6 +329,40 @@ expression: response.json()
237329
],
238330
"responses": {
239331
"200": {
332+
"content": {
333+
"application/json": {
334+
"schema": {
335+
"properties": {
336+
"categories": {
337+
"description": "The list of categories.",
338+
"items": {
339+
"$ref": "#/components/schemas/Category"
340+
},
341+
"type": "array"
342+
},
343+
"meta": {
344+
"properties": {
345+
"total": {
346+
"description": "The total number of categories.",
347+
"example": 123,
348+
"format": "int64",
349+
"type": "integer"
350+
}
351+
},
352+
"required": [
353+
"total"
354+
],
355+
"type": "object"
356+
}
357+
},
358+
"required": [
359+
"categories",
360+
"meta"
361+
],
362+
"type": "object"
363+
}
364+
}
365+
},
240366
"description": "Successful Response"
241367
}
242368
},
@@ -262,6 +388,21 @@ expression: response.json()
262388
],
263389
"responses": {
264390
"200": {
391+
"content": {
392+
"application/json": {
393+
"schema": {
394+
"properties": {
395+
"category": {
396+
"$ref": "#/components/schemas/Category"
397+
}
398+
},
399+
"required": [
400+
"category"
401+
],
402+
"type": "object"
403+
}
404+
}
405+
},
265406
"description": "Successful Response"
266407
}
267408
},
@@ -276,6 +417,25 @@ expression: response.json()
276417
"operationId": "list_category_slugs",
277418
"responses": {
278419
"200": {
420+
"content": {
421+
"application/json": {
422+
"schema": {
423+
"properties": {
424+
"category_slugs": {
425+
"description": "The list of category slugs.",
426+
"items": {
427+
"$ref": "#/components/schemas/Slug"
428+
},
429+
"type": "array"
430+
}
431+
},
432+
"required": [
433+
"category_slugs"
434+
],
435+
"type": "object"
436+
}
437+
}
438+
},
279439
"description": "Successful Response"
280440
}
281441
},

src/views.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,49 @@ use crates_io_github as github;
1010
pub mod krate_publish;
1111
pub use self::krate_publish::{EncodableCrateDependency, PublishMetadata};
1212

13-
#[derive(Serialize, Deserialize, Debug)]
13+
#[derive(Serialize, Deserialize, Debug, utoipa::ToSchema)]
14+
#[schema(as = Category)]
1415
pub struct EncodableCategory {
16+
/// An opaque identifier for the category.
17+
#[schema(example = "game-development")]
1518
pub id: String,
19+
20+
/// The name of the category.
21+
#[schema(example = "Game development")]
1622
pub category: String,
23+
24+
/// The "slug" of the category.
25+
///
26+
/// See <https://crates.io/category_slugs>.
27+
#[schema(example = "game-development")]
1728
pub slug: String,
29+
30+
/// A description of the category.
31+
#[schema(example = "Libraries for creating games.")]
1832
pub description: String,
33+
34+
/// The date and time this category was created.
35+
#[schema(example = "2019-12-13T13:46:41Z")]
1936
pub created_at: DateTime<Utc>,
37+
38+
/// The total number of crates that have this category.
39+
#[schema(example = 42)]
2040
pub crates_cnt: i32,
2141

42+
/// The subcategories of this category.
43+
///
44+
/// This field is only present when the category details are queried,
45+
/// but not when listing categories.
2246
#[serde(skip_serializing_if = "Option::is_none")]
47+
#[schema(no_recursion, example = json!([]))]
2348
pub subcategories: Option<Vec<EncodableCategory>>,
2449

50+
/// The parent categories of this category.
51+
///
52+
/// This field is only present when the category details are queried,
53+
/// but not when listing categories.
2554
#[serde(skip_serializing_if = "Option::is_none")]
55+
#[schema(no_recursion, example = json!([]))]
2656
pub parent_categories: Option<Vec<EncodableCategory>>,
2757
}
2858

0 commit comments

Comments
 (0)