feat: better api docs (#6586)

* feat: docs

* fix tombi

* chore: fix some of the routes rendering with missing /

* response schemas

* fix: restore labrinth docs routes

* Fix path parameter docs in routes

* remove utoipa-actix-web

* consistency

* improve version intros

* improve formatting, examples

* better hash examples, move openapi stuff to openapi.rs

* more utoipa param fixes

* request body docs

* chore: remove moderation route from v2, remove ingest & webhooks from v3 spec

* fixes

* chore: tweak sources titles

* fix

* fix test

* improve examples

* increase compiler spawned thread stack size

* remove unused tests & script

* test

* bro what

* fix

---------

Co-authored-by: aecsocket <43144841+aecsocket@users.noreply.github.com>
This commit is contained in:
François-Xavier Talbot
2026-07-04 14:19:21 +00:00
committed by GitHub
co-authored by aecsocket
parent b26d048a63
commit 4a6fa9fc3d
103 changed files with 4546 additions and 1387 deletions
+203 -39
View File
@@ -42,20 +42,17 @@ use eyre::eyre;
use futures::TryStreamExt;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use serde_json::json;
use validator::Validate;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.route("search", web::get().to(project_search));
cfg.service(project_search_post);
cfg.route("projects", web::get().to(projects_get));
cfg.route("projects", web::patch().to(projects_edit));
cfg.route("projects_random", web::get().to(random_projects_get));
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(project_search)
.service(project_search_post)
.service(projects_get_route)
.service(projects_edit_route)
.service(random_projects_get_route);
}
pub fn utoipa_config(
cfg: &mut utoipa_actix_web::service_config::ServiceConfig,
) {
pub fn project_config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(project_get)
.service(project_get_check)
.service(project_delete)
@@ -99,6 +96,20 @@ pub struct RandomProjects {
pub count: u32,
}
#[utoipa::path(
tag = "projects",
params(("count" = u32, Query)),
responses((status = OK))
)]
#[get("/projects_random")]
pub async fn random_projects_get_route(
count: web::Query<RandomProjects>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
random_projects_get(count, pool, redis).await
}
pub async fn random_projects_get(
web::Query(count): web::Query<RandomProjects>,
pool: web::Data<PgPool>,
@@ -140,11 +151,32 @@ pub async fn random_projects_get(
Ok(HttpResponse::Ok().json(projects_data))
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct ProjectIds {
pub ids: String,
}
#[derive(Serialize, utoipa::ToSchema)]
pub struct ProjectCheckResponse {
pub id: ProjectId,
}
#[utoipa::path(
tag = "projects",
params(("ids" = String, Query)),
responses((status = OK))
)]
#[get("/projects")]
pub async fn projects_get_route(
req: HttpRequest,
ids: web::Query<ProjectIds>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
projects_get(req, ids, pool, redis, session_queue).await
}
pub async fn projects_get(
req: HttpRequest,
web::Query(ids): web::Query<ProjectIds>,
@@ -174,9 +206,13 @@ pub async fn projects_get(
Ok(HttpResponse::Ok().json(projects))
}
#[utoipa::path]
/// Get a project.
#[utoipa::path(
context_path = "/project",
tag = "projects", responses((status = OK, body = Project))
)]
#[get("/{id}")]
async fn project_get(
pub async fn project_get(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
@@ -305,9 +341,13 @@ pub struct EditProject {
}
#[allow(clippy::too_many_arguments)]
#[utoipa::path]
/// Update a project.
#[utoipa::path(
context_path = "/project",
tag = "projects", responses((status = NO_CONTENT))
)]
#[patch("/{id}")]
async fn project_edit(
pub async fn project_edit(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
@@ -1205,6 +1245,27 @@ pub async fn edit_project_categories(
// pub total_hits: usize,
// }
/// Search projects.
#[utoipa::path(
tag = "search",
get,
operation_id = "v3SearchProjects",
params(
("query" = Option<String>, Query, description = "The query to search for"),
("facets" = Option<String>, Query, description = "Search facets JSON"),
("filters" = Option<String>, Query, description = "Search filters JSON"),
("new_filters" = Option<String>, Query, description = "Search filters JSON"),
("index" = Option<String>, Query, description = "Search index to use"),
("offset" = Option<String>, Query, description = "Search result offset"),
("limit" = Option<String>, Query, description = "Maximum number of search results"),
("version" = Option<String>, Query, description = "Game version to filter for")
),
responses(
(status = 200, description = "Expected response to a valid request", body = SearchResults),
(status = 400, description = "Request was invalid, see given error")
)
)]
#[get("/search")]
pub async fn project_search(
web::Query(info): web::Query<SearchQuery>,
search_backend: web::Data<dyn SearchBackend>,
@@ -1230,6 +1291,12 @@ pub async fn project_search(
}
// for more complicated search queries
/// Search projects.
#[utoipa::path(
tag = "search",
request_body = serde_json::Value,
responses((status = OK, body = SearchResults))
)]
#[post("/search")]
pub async fn project_search_post(
web::Json(info): web::Json<SearchRequest>,
@@ -1241,9 +1308,13 @@ pub async fn project_search_post(
}
//checks the validity of a project id or slug
#[utoipa::path]
/// Check project availability.
#[utoipa::path(
context_path = "/project",
tag = "projects", responses((status = OK, body = ProjectCheckResponse))
)]
#[get("/{id}/check")]
async fn project_get_check(
pub async fn project_get_check(
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
@@ -1262,21 +1333,25 @@ pub async fn project_get_check_internal(
db_models::DBProject::get(&slug, &**pool, &redis).await?;
if let Some(project) = project_data {
Ok(HttpResponse::Ok().json(json! ({
"id": models::ids::ProjectId::from(project.inner.id)
})))
Ok(HttpResponse::Ok().json(ProjectCheckResponse {
id: models::ids::ProjectId::from(project.inner.id),
}))
} else {
Err(ApiError::NotFound)
}
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
pub struct DependencyInfo {
pub projects: Vec<Project>,
pub versions: Vec<models::projects::Version>,
}
#[utoipa::path]
/// List project dependencies.
#[utoipa::path(
context_path = "/project",
tag = "projects", responses((status = OK, body = DependencyInfo))
)]
#[get("/{project_id}/dependencies")]
pub async fn dependency_list(
req: HttpRequest,
@@ -1400,7 +1475,7 @@ pub struct CategoryChanges<'a> {
pub remove_categories: &'a Option<Vec<String>>,
}
#[derive(Deserialize, Validate)]
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct BulkEditProject {
#[validate(length(max = 3))]
pub categories: Option<Vec<String>>,
@@ -1420,6 +1495,33 @@ pub struct BulkEditProject {
pub link_urls: Option<HashMap<String, Option<String>>>,
}
#[utoipa::path(
tag = "projects",
params(("ids" = String, Query)),
responses((status = NO_CONTENT))
)]
#[patch("/projects")]
pub async fn projects_edit_route(
req: HttpRequest,
ids: web::Query<ProjectIds>,
pool: web::Data<PgPool>,
bulk_edit_project: web::Json<BulkEditProject>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
search_state: web::Data<SearchState>,
) -> Result<HttpResponse, ApiError> {
projects_edit(
req,
ids,
pool,
bulk_edit_project,
redis,
session_queue,
search_state,
)
.await
}
pub async fn projects_edit(
req: HttpRequest,
web::Query(ids): web::Query<ProjectIds>,
@@ -1722,9 +1824,18 @@ pub struct Extension {
}
#[allow(clippy::too_many_arguments)]
#[utoipa::path]
/// Update a project icon.
#[utoipa::path(
context_path = "/project",
tag = "projects",
params(
("ext" = String, Query)
),
request_body(content = Vec<u8>, content_type = "application/octet-stream"),
responses((status = NO_CONTENT))
)]
#[patch("/{id}/icon")]
async fn project_icon_edit(
pub async fn project_icon_edit(
web::Query(ext): web::Query<Extension>,
req: HttpRequest,
info: web::Path<(String,)>,
@@ -1866,9 +1977,13 @@ pub async fn project_icon_edit_internal(
Ok(HttpResponse::NoContent().body(""))
}
#[utoipa::path]
/// Delete a project icon.
#[utoipa::path(
context_path = "/project",
tag = "projects", responses((status = NO_CONTENT))
)]
#[delete("/{id}/icon")]
async fn delete_project_icon(
pub async fn delete_project_icon(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
@@ -1992,7 +2107,20 @@ pub struct GalleryCreateQuery {
}
#[allow(clippy::too_many_arguments)]
#[utoipa::path]
/// Add a gallery item.
#[utoipa::path(
context_path = "/project",
tag = "projects",
params(
("ext" = String, Query),
("featured" = bool, Query),
("name" = Option<String>, Query),
("description" = Option<String>, Query),
("ordering" = Option<i64>, Query)
),
request_body(content = Vec<u8>, content_type = "application/octet-stream"),
responses((status = NO_CONTENT))
)]
#[post("/{id}/gallery")]
pub async fn add_gallery_item(
web::Query(ext): web::Query<Extension>,
@@ -2190,9 +2318,21 @@ pub struct GalleryEditQuery {
pub ordering: Option<i64>,
}
#[utoipa::path]
/// Update a gallery item.
#[utoipa::path(
context_path = "/project",
tag = "projects",
params(
("url" = String, Query),
("featured" = Option<bool>, Query),
("name" = Option<String>, Query),
("description" = Option<String>, Query),
("ordering" = Option<i64>, Query)
),
responses((status = NO_CONTENT))
)]
#[patch("/{id}/gallery")]
async fn edit_gallery_item(
pub async fn edit_gallery_item(
req: HttpRequest,
web::Query(item): web::Query<GalleryEditQuery>,
pool: web::Data<PgPool>,
@@ -2379,9 +2519,17 @@ pub struct GalleryDeleteQuery {
pub url: String,
}
#[utoipa::path]
/// Delete a gallery item.
#[utoipa::path(
context_path = "/project",
tag = "projects",
params(
("url" = String, Query)
),
responses((status = NO_CONTENT))
)]
#[delete("/{id}/gallery")]
async fn delete_gallery_item(
pub async fn delete_gallery_item(
req: HttpRequest,
web::Query(item): web::Query<GalleryDeleteQuery>,
pool: web::Data<PgPool>,
@@ -2514,9 +2662,13 @@ pub async fn delete_gallery_item_internal(
Ok(HttpResponse::NoContent().body(""))
}
#[utoipa::path]
/// Delete a project.
#[utoipa::path(
context_path = "/project",
tag = "projects", responses((status = NO_CONTENT))
)]
#[delete("/{id}")]
async fn project_delete(
pub async fn project_delete(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
@@ -2672,9 +2824,13 @@ pub async fn project_delete_internal(
}
}
#[utoipa::path]
/// Follow a project.
#[utoipa::path(
context_path = "/project",
tag = "projects", responses((status = NO_CONTENT))
)]
#[post("/{id}/follow")]
async fn project_follow(
pub async fn project_follow(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
@@ -2764,9 +2920,13 @@ pub async fn project_follow_internal(
}
}
#[utoipa::path]
/// Unfollow a project.
#[utoipa::path(
context_path = "/project",
tag = "projects", responses((status = NO_CONTENT))
)]
#[delete("/{id}/follow")]
async fn project_unfollow(
pub async fn project_unfollow(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
@@ -2852,7 +3012,11 @@ pub async fn project_unfollow_internal(
}
}
#[utoipa::path]
/// Get a project's organization.
#[utoipa::path(
context_path = "/project",
tag = "projects", responses((status = OK, body = models::organizations::Organization))
)]
#[get("/{id}/organization")]
pub async fn project_get_organization(
req: HttpRequest,