Skip to content

Commit

Permalink
Merge pull request #10186 from Turbo87/utoipa
Browse files Browse the repository at this point in the history
Use `utoipa` crates to generate and serve basic OpenAPI description
  • Loading branch information
Turbo87 authored Dec 12, 2024
2 parents e0f2246 + a095082 commit 7792c0e
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 22 deletions.
38 changes: 38 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ tracing-subscriber = { version = "=0.3.19", features = ["env-filter", "json"] }
typomania = { version = "=0.1.2", default-features = false }
url = "=2.5.4"
unicode-xid = "=0.2.6"
utoipa = "=5.2.0"
utoipa-axum = "=0.1.2"

[dev-dependencies]
bytes = "=1.9.0"
Expand Down
43 changes: 25 additions & 18 deletions src/controllers/krate/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,35 @@ use crate::models::krate::ALL_COLUMNS;
use crate::sql::{array_agg, canon_crate_name, lower};
use crate::util::RequestUtils;

/// Handles the `GET /crates` route.
/// Returns a list of crates. Called in a variety of scenarios in the
/// front end, including:
/// Returns a list of crates.
///
/// Called in a variety of scenarios in the front end, including:
/// - Alphabetical listing of crates
/// - List of crates under a specific owner
/// - Listing a user's followed crates
///
/// Notes:
/// The different use cases this function covers is handled through passing
/// in parameters in the GET request.
///
/// We would like to stop adding functionality in here. It was built like
/// this to keep the number of database queries low, though given Rust's
/// low performance overhead, this is a soft goal to have, and can afford
/// more database transactions if it aids understandability.
///
/// All of the edge cases for this function are not currently covered
/// in testing, and if they fail, it is difficult to determine what
/// caused the break. In the future, we should look at splitting this
/// function out to cover the different use cases, and create unit tests
/// for them.
#[utoipa::path(
get,
path = "/api/v1/crates",
operation_id = "crates_list",
tag = "crates",
responses((status = 200, description = "Successful Response")),
)]
pub async fn search(app: AppState, req: Parts) -> AppResult<ErasedJson> {
// Notes:
// The different use cases this function covers is handled through passing
// in parameters in the GET request.
//
// We would like to stop adding functionality in here. It was built like
// this to keep the number of database queries low, though given Rust's
// low performance overhead, this is a soft goal to have, and can afford
// more database transactions if it aids understandability.
//
// All of the edge cases for this function are not currently covered
// in testing, and if they fail, it is difficult to determine what
// caused the break. In the future, we should look at splitting this
// function out to cover the different use cases, and create unit tests
// for them.

let mut conn = app.db_read().await?;

use diesel::sql_types::Float;
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ mod licenses;
pub mod metrics;
pub mod middleware;
pub mod models;
pub mod openapi;
pub mod rate_limiter;
mod real_ip;
mod router;
Expand Down
44 changes: 44 additions & 0 deletions src/openapi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use utoipa::OpenApi;
use utoipa_axum::router::OpenApiRouter;

#[derive(OpenApi)]
#[openapi(
info(
title = "crates.io",
description = "API documentation for the [crates.io](https://crates.io/) package registry",
terms_of_service = "https://crates.io/policies",
contact(name = "the crates.io team", email = "[email protected]"),
license(),
version = "0.0.0",
),
servers(
(url = "https://crates.io"),
(url = "https://staging.crates.io"),
),
)]
pub struct BaseOpenApi;

impl BaseOpenApi {
pub fn router<S>() -> OpenApiRouter<S>
where
S: Send + Sync + Clone + 'static,
{
OpenApiRouter::with_openapi(Self::openapi())
}
}

#[cfg(test)]
mod tests {
use crate::tests::util::{RequestHelper, TestApp};
use http::StatusCode;
use insta::assert_json_snapshot;

#[tokio::test(flavor = "multi_thread")]
async fn test_openapi_snapshot() {
let (_app, anon) = TestApp::init().empty().await;

let response = anon.get::<()>("/api/openapi.json").await;
assert_eq!(response.status(), StatusCode::OK);
assert_json_snapshot!(response.json());
}
}
16 changes: 12 additions & 4 deletions src/router.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
use axum::extract::DefaultBodyLimit;
use axum::response::IntoResponse;
use axum::routing::{delete, get, post, put};
use axum::Router;
use axum::{Json, Router};
use http::{Method, StatusCode};
use utoipa_axum::routes;

use crate::app::AppState;
use crate::controllers::user::update_user;
use crate::controllers::*;
use crate::openapi::BaseOpenApi;
use crate::util::errors::not_found;
use crate::Env;

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

pub fn build_axum_router(state: AppState) -> Router<()> {
let mut router = Router::new()
// Route used by both `cargo search` and the frontend
.route("/api/v1/crates", get(krate::search::search))
let (router, openapi) = BaseOpenApi::router()
.routes(routes!(
// Route used by both `cargo search` and the frontend
krate::search::search
))
.split_for_parts();

let mut router = router
// Routes used by `cargo`
.route(
"/api/v1/crates/new",
Expand Down Expand Up @@ -174,6 +181,7 @@ pub fn build_axum_router(state: AppState) -> Router<()> {
}

router
.route("/api/openapi.json", get(|| async { Json(openapi) }))
.fallback(|method: Method| async move {
match method {
Method::HEAD => StatusCode::NOT_FOUND.into_response(),
Expand Down
47 changes: 47 additions & 0 deletions src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
source: src/openapi.rs
expression: response.json()
snapshot_kind: text
---
{
"components": {},
"info": {
"contact": {
"email": "[email protected]",
"name": "the crates.io team"
},
"description": "API documentation for the [crates.io](https://crates.io/) package registry",
"license": {
"name": ""
},
"termsOfService": "https://crates.io/policies",
"title": "crates.io",
"version": "0.0.0"
},
"openapi": "3.1.0",
"paths": {
"/api/v1/crates": {
"get": {
"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",
"operationId": "crates_list",
"responses": {
"200": {
"description": "Successful Response"
}
},
"summary": "Returns a list of crates.",
"tags": [
"crates"
]
}
}
},
"servers": [
{
"url": "https://crates.io"
},
{
"url": "https://staging.crates.io"
}
]
}

0 comments on commit 7792c0e

Please sign in to comment.