Skip to content

Commit 7792c0e

Browse files
authored
Merge pull request #10186 from Turbo87/utoipa
Use `utoipa` crates to generate and serve basic OpenAPI description
2 parents e0f2246 + a095082 commit 7792c0e

File tree

7 files changed

+169
-22
lines changed

7 files changed

+169
-22
lines changed

Cargo.lock

+38
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ tracing-subscriber = { version = "=0.3.19", features = ["env-filter", "json"] }
125125
typomania = { version = "=0.1.2", default-features = false }
126126
url = "=2.5.4"
127127
unicode-xid = "=0.2.6"
128+
utoipa = "=5.2.0"
129+
utoipa-axum = "=0.1.2"
128130

129131
[dev-dependencies]
130132
bytes = "=1.9.0"

src/controllers/krate/search.rs

+25-18
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,35 @@ use crate::models::krate::ALL_COLUMNS;
2424
use crate::sql::{array_agg, canon_crate_name, lower};
2525
use crate::util::RequestUtils;
2626

27-
/// Handles the `GET /crates` route.
28-
/// Returns a list of crates. Called in a variety of scenarios in the
29-
/// front end, including:
27+
/// Returns a list of crates.
28+
///
29+
/// Called in a variety of scenarios in the front end, including:
3030
/// - Alphabetical listing of crates
3131
/// - List of crates under a specific owner
3232
/// - Listing a user's followed crates
33-
///
34-
/// Notes:
35-
/// The different use cases this function covers is handled through passing
36-
/// in parameters in the GET request.
37-
///
38-
/// We would like to stop adding functionality in here. It was built like
39-
/// this to keep the number of database queries low, though given Rust's
40-
/// low performance overhead, this is a soft goal to have, and can afford
41-
/// more database transactions if it aids understandability.
42-
///
43-
/// All of the edge cases for this function are not currently covered
44-
/// in testing, and if they fail, it is difficult to determine what
45-
/// caused the break. In the future, we should look at splitting this
46-
/// function out to cover the different use cases, and create unit tests
47-
/// for them.
33+
#[utoipa::path(
34+
get,
35+
path = "/api/v1/crates",
36+
operation_id = "crates_list",
37+
tag = "crates",
38+
responses((status = 200, description = "Successful Response")),
39+
)]
4840
pub async fn search(app: AppState, req: Parts) -> AppResult<ErasedJson> {
41+
// Notes:
42+
// The different use cases this function covers is handled through passing
43+
// in parameters in the GET request.
44+
//
45+
// We would like to stop adding functionality in here. It was built like
46+
// this to keep the number of database queries low, though given Rust's
47+
// low performance overhead, this is a soft goal to have, and can afford
48+
// more database transactions if it aids understandability.
49+
//
50+
// All of the edge cases for this function are not currently covered
51+
// in testing, and if they fail, it is difficult to determine what
52+
// caused the break. In the future, we should look at splitting this
53+
// function out to cover the different use cases, and create unit tests
54+
// for them.
55+
4956
let mut conn = app.db_read().await?;
5057

5158
use diesel::sql_types::Float;

src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ mod licenses;
4242
pub mod metrics;
4343
pub mod middleware;
4444
pub mod models;
45+
pub mod openapi;
4546
pub mod rate_limiter;
4647
mod real_ip;
4748
mod router;

src/openapi.rs

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use utoipa::OpenApi;
2+
use utoipa_axum::router::OpenApiRouter;
3+
4+
#[derive(OpenApi)]
5+
#[openapi(
6+
info(
7+
title = "crates.io",
8+
description = "API documentation for the [crates.io](https://crates.io/) package registry",
9+
terms_of_service = "https://crates.io/policies",
10+
contact(name = "the crates.io team", email = "[email protected]"),
11+
license(),
12+
version = "0.0.0",
13+
),
14+
servers(
15+
(url = "https://crates.io"),
16+
(url = "https://staging.crates.io"),
17+
),
18+
)]
19+
pub struct BaseOpenApi;
20+
21+
impl BaseOpenApi {
22+
pub fn router<S>() -> OpenApiRouter<S>
23+
where
24+
S: Send + Sync + Clone + 'static,
25+
{
26+
OpenApiRouter::with_openapi(Self::openapi())
27+
}
28+
}
29+
30+
#[cfg(test)]
31+
mod tests {
32+
use crate::tests::util::{RequestHelper, TestApp};
33+
use http::StatusCode;
34+
use insta::assert_json_snapshot;
35+
36+
#[tokio::test(flavor = "multi_thread")]
37+
async fn test_openapi_snapshot() {
38+
let (_app, anon) = TestApp::init().empty().await;
39+
40+
let response = anon.get::<()>("/api/openapi.json").await;
41+
assert_eq!(response.status(), StatusCode::OK);
42+
assert_json_snapshot!(response.json());
43+
}
44+
}

src/router.rs

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
use axum::extract::DefaultBodyLimit;
22
use axum::response::IntoResponse;
33
use axum::routing::{delete, get, post, put};
4-
use axum::Router;
4+
use axum::{Json, Router};
55
use http::{Method, StatusCode};
6+
use utoipa_axum::routes;
67

78
use crate::app::AppState;
89
use crate::controllers::user::update_user;
910
use crate::controllers::*;
11+
use crate::openapi::BaseOpenApi;
1012
use crate::util::errors::not_found;
1113
use crate::Env;
1214

1315
const MAX_PUBLISH_CONTENT_LENGTH: usize = 128 * 1024 * 1024; // 128 MB
1416

1517
pub fn build_axum_router(state: AppState) -> Router<()> {
16-
let mut router = Router::new()
17-
// Route used by both `cargo search` and the frontend
18-
.route("/api/v1/crates", get(krate::search::search))
18+
let (router, openapi) = BaseOpenApi::router()
19+
.routes(routes!(
20+
// Route used by both `cargo search` and the frontend
21+
krate::search::search
22+
))
23+
.split_for_parts();
24+
25+
let mut router = router
1926
// Routes used by `cargo`
2027
.route(
2128
"/api/v1/crates/new",
@@ -174,6 +181,7 @@ pub fn build_axum_router(state: AppState) -> Router<()> {
174181
}
175182

176183
router
184+
.route("/api/openapi.json", get(|| async { Json(openapi) }))
177185
.fallback(|method: Method| async move {
178186
match method {
179187
Method::HEAD => StatusCode::NOT_FOUND.into_response(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
source: src/openapi.rs
3+
expression: response.json()
4+
snapshot_kind: text
5+
---
6+
{
7+
"components": {},
8+
"info": {
9+
"contact": {
10+
"email": "[email protected]",
11+
"name": "the crates.io team"
12+
},
13+
"description": "API documentation for the [crates.io](https://crates.io/) package registry",
14+
"license": {
15+
"name": ""
16+
},
17+
"termsOfService": "https://crates.io/policies",
18+
"title": "crates.io",
19+
"version": "0.0.0"
20+
},
21+
"openapi": "3.1.0",
22+
"paths": {
23+
"/api/v1/crates": {
24+
"get": {
25+
"description": "Called in a variety of scenarios in the front end, including:\n- Alphabetical listing of crates\n- List of crates under a specific owner\n- Listing a user's followed crates",
26+
"operationId": "crates_list",
27+
"responses": {
28+
"200": {
29+
"description": "Successful Response"
30+
}
31+
},
32+
"summary": "Returns a list of crates.",
33+
"tags": [
34+
"crates"
35+
]
36+
}
37+
}
38+
},
39+
"servers": [
40+
{
41+
"url": "https://crates.io"
42+
},
43+
{
44+
"url": "https://staging.crates.io"
45+
}
46+
]
47+
}

0 commit comments

Comments
 (0)