1
0

Add utoipa Swagger UI support (#4602)

* Add utoipa Swagger UI support

* remove unused code

* remove unused code

* consistency with trailing slash
This commit is contained in:
aecsocket
2025-10-24 07:44:50 -07:00
committed by GitHub
parent 707ff2146b
commit 03b0eba695
13 changed files with 253 additions and 87 deletions

112
Cargo.lock generated
View File

@@ -478,6 +478,7 @@ dependencies = [
"serde_cbor",
"serde_json",
"thiserror 2.0.17",
"utoipa",
"uuid 1.18.1",
]
@@ -4678,6 +4679,9 @@ dependencies = [
"tracing-subscriber",
"url",
"urlencoding",
"utoipa",
"utoipa-actix-web",
"utoipa-swagger-ui",
"uuid 1.18.1",
"validator",
"webp",
@@ -7335,6 +7339,40 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rust-embed"
version = "8.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb44e1917075637ee8c7bcb865cf8830e3a92b5b1189e44e3a0ab5a0d5be314b"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "8.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "382499b49db77a7c19abd2a574f85ada7e9dbe125d5d1160fa5cad7c4cf71fc9"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.106",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "8.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21fcbee55c2458836bcdbfffb6ec9ba74bbc23ca7aa6816015a3dd2c4d8fc185"
dependencies = [
"sha2",
"walkdir",
]
[[package]]
name = "rust-ini"
version = "0.21.3"
@@ -10309,6 +10347,66 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "utoipa"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993"
dependencies = [
"indexmap 2.11.4",
"serde",
"serde_json",
"utoipa-gen",
]
[[package]]
name = "utoipa-actix-web"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7eda9c23c05af0fb812f6a177514047331dac4851a2c8e9c4b895d6d826967f"
dependencies = [
"actix-service",
"actix-web",
"utoipa",
]
[[package]]
name = "utoipa-gen"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b"
dependencies = [
"proc-macro2",
"quote",
"regex",
"syn 2.0.106",
]
[[package]]
name = "utoipa-swagger-ui"
version = "9.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55"
dependencies = [
"actix-web",
"base64 0.22.1",
"mime_guess",
"regex",
"rust-embed",
"serde",
"serde_json",
"url",
"utoipa",
"utoipa-swagger-ui-vendored",
"zip 3.0.0",
]
[[package]]
name = "utoipa-swagger-ui-vendored"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2eebbbfe4093922c2b6734d7c679ebfebd704a0d7e56dfcb0d05818ce28977d"
[[package]]
name = "uuid"
version = "0.8.2"
@@ -11687,6 +11785,20 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "zip"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308"
dependencies = [
"arbitrary",
"crc32fast",
"flate2",
"indexmap 2.11.4",
"memchr",
"zopfli",
]
[[package]]
name = "zip"
version = "4.6.1"

View File

@@ -192,6 +192,9 @@ tracing-subscriber = "0.3.20"
typed-path = "0.12.0"
url = "2.5.7"
urlencoding = "2.1.3"
utoipa = { version = "5.4.0", features = ["actix_extras", "chrono", "decimal"] }
utoipa-actix-web = { version = "0.1.2" }
utoipa-swagger-ui = { version = "9.0.2", features = ["actix-web", "vendored"] }
uuid = "1.18.1"
validator = "0.20.0"
webp = { version = "0.3.1", default-features = false }

View File

@@ -120,6 +120,9 @@ tracing-ecs = { workspace = true }
tracing-subscriber = { workspace = true }
url = { workspace = true }
urlencoding = { workspace = true }
utoipa = { workspace = true }
utoipa-actix-web = { workspace = true }
utoipa-swagger-ui = { workspace = true }
uuid = { workspace = true, features = ["fast-rng", "serde", "v4"] }
validator = { workspace = true, features = ["derive"] }
webp = { workspace = true }

View File

@@ -345,6 +345,13 @@ pub fn app_config(
.default_service(web::get().wrap(default_cors()).to(routes::not_found));
}
pub fn utoipa_app_config(
cfg: &mut utoipa_actix_web::service_config::ServiceConfig,
_labrinth_config: LabrinthConfig,
) {
cfg.configure(routes::v3::utoipa_config);
}
// This is so that env vars not used immediately don't panic at runtime
pub fn check_env_vars() -> bool {
let mut failed = false;

View File

@@ -14,6 +14,7 @@ use labrinth::util::anrok;
use labrinth::util::env::parse_var;
use labrinth::util::gotenberg::GotenbergClient;
use labrinth::util::ratelimit::rate_limit_middleware;
use labrinth::utoipa_app_config;
use labrinth::{check_env_vars, clickhouse, database, file_hosting};
use std::ffi::CStr;
use std::str::FromStr;
@@ -25,6 +26,9 @@ use tracing_ecs::ECSLayerBuilder;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use utoipa::OpenApi;
use utoipa_actix_web::AppExt;
use utoipa_swagger_ui::SwaggerUi;
#[cfg(target_os = "linux")]
#[global_allocator]
@@ -293,6 +297,12 @@ async fn main() -> std::io::Result<()> {
.wrap(from_fn(rate_limit_middleware))
.wrap(actix_web::middleware::Compress::default())
.wrap(sentry_actix::Sentry::new())
.into_utoipa_app()
.configure(|cfg| utoipa_app_config(cfg, labrinth_config.clone()))
.openapi_service(|api| SwaggerUi::new("/docs/swagger-ui/{_:.*}")
.config(utoipa_swagger_ui::Config::default().try_it_out_enabled(true))
.url("/docs/openapi.json", ApiDoc::openapi().merge_from(api)))
.into_app()
.configure(|cfg| app_config(cfg, labrinth_config.clone()))
})
.bind(dotenvy::var("BIND_ADDR").unwrap())?
@@ -300,6 +310,10 @@ async fn main() -> std::io::Result<()> {
.await
}
#[derive(utoipa::OpenApi)]
#[openapi(info(title = "Labrinth"))]
struct ApiDoc;
fn log_error(err: &actix_web::Error) {
if err.as_response_error().status_code().is_client_error() {
tracing::debug!(

View File

@@ -77,12 +77,8 @@ pub fn root_config(cfg: &mut web::ServiceConfig) {
}.boxed_local()
})
);
cfg.service(
web::scope("")
.wrap(default_cors())
.service(index::index_get)
.service(Files::new("/", "assets/")),
);
cfg.service(index::index_get);
cfg.service(Files::new("/", "assets/"));
}
#[derive(thiserror::Error, Debug)]

View File

@@ -7,9 +7,11 @@
//! requests, you have to zip together M arrays of N elements
//! - this makes it inconvenient to have separate endpoints
mod old;
use std::num::NonZeroU64;
use actix_web::{HttpRequest, web};
use actix_web::{HttpRequest, post, web};
use chrono::{DateTime, TimeDelta, Utc};
use futures::StreamExt;
use rust_decimal::Decimal;
@@ -32,10 +34,9 @@ use crate::{
routes::ApiError,
};
// TODO: this service `analytics` is shadowed by `analytics_get_old`'s
// see the TODO in `analytics_get_old.rs`
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(web::scope("analytics").route("", web::post().to(get)));
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(fetch_analytics);
cfg.configure(old::config);
}
// request
@@ -43,7 +44,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
/// Requests analytics data, aggregating over all possible analytics sources
/// like projects and affiliate codes, returning the data in a list of time
/// slices.
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct GetRequest {
/// What time range to return statistics for.
pub time_range: TimeRange,
@@ -52,7 +53,7 @@ pub struct GetRequest {
}
/// Time range for fetching analytics.
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct TimeRange {
/// When to start including data.
pub start: DateTime<Utc>,
@@ -68,20 +69,22 @@ pub struct TimeRange {
/// Determines how many time slices between the start and end will be
/// included, and how fine-grained those time slices will be.
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum TimeRangeResolution {
/// Use a set number of time slices, with the resolution being determined
/// automatically.
#[schema(value_type = u64)]
Slices(NonZeroU64),
/// Each time slice will be a set number of minutes long, and the number of
/// slices is determined automatically.
#[schema(value_type = u64)]
Minutes(NonZeroU64),
}
/// What metrics the caller would like to receive from this analytics get
/// request.
#[derive(Debug, Default, Serialize, Deserialize)]
#[derive(Debug, Default, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ReturnMetrics {
/// How many times a project page has been viewed.
pub project_views: Option<Metrics<ProjectViewsField>>,
@@ -90,11 +93,15 @@ pub struct ReturnMetrics {
/// How long users have been playing a project.
pub project_playtime: Option<Metrics<ProjectPlaytimeField>>,
/// How much payout revenue a project has generated.
pub project_revenue: Option<Metrics<()>>,
pub project_revenue: Option<Metrics<Unit>>,
}
/// Replacement for `()` because of a `utoipa` limitation.
#[derive(Debug, Default, Serialize, Deserialize, utoipa::ToSchema)]
pub struct Unit {}
/// See [`ReturnMetrics`].
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
pub struct Metrics<F> {
/// When collecting metrics, what fields do we want to group the results by?
///
@@ -114,7 +121,9 @@ pub struct Metrics<F> {
}
/// Fields for [`ReturnMetrics::project_views`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum ProjectViewsField {
/// Project ID.
@@ -132,7 +141,9 @@ pub enum ProjectViewsField {
}
/// Fields for [`ReturnMetrics::project_downloads`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum ProjectDownloadsField {
/// Project ID.
@@ -150,7 +161,9 @@ pub enum ProjectDownloadsField {
}
/// Fields for [`ReturnMetrics::project_playtime`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum ProjectPlaytimeField {
/// Project ID.
@@ -177,15 +190,15 @@ pub const MAX_TIME_SLICES: usize = 1024;
/// This is a list of N [`TimeSlice`]s, where each slice represents an equal
/// time interval of metrics collection. The number of slices is determined
/// by [`GetRequest::time_range`].
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct GetResponse(pub Vec<TimeSlice>);
#[derive(Debug, Default, Serialize, Deserialize, utoipa::ToSchema)]
pub struct FetchResponse(pub Vec<TimeSlice>);
/// Single time interval of metrics collection.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
pub struct TimeSlice(pub Vec<AnalyticsData>);
/// Metrics collected in a single [`TimeSlice`].
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(untagged)] // the presence of `source_project`, `source_affiliate_code` determines the kind
pub enum AnalyticsData {
/// Project metrics.
@@ -194,7 +207,7 @@ pub enum AnalyticsData {
}
/// Project metrics.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ProjectAnalytics {
/// What project these metrics are for.
source_project: ProjectId,
@@ -213,7 +226,7 @@ impl ProjectAnalytics {
/// Project metrics of a specific kind.
///
/// If a field is not included in [`Metrics::bucket_by`], it will be [`None`].
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "snake_case", tag = "metric_kind")]
pub enum ProjectMetrics {
/// [`ReturnMetrics::project_views`].
@@ -227,7 +240,7 @@ pub enum ProjectMetrics {
}
/// [`ReturnMetrics::project_views`].
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ProjectViews {
/// [`ProjectViewsField::Domain`].
#[serde(skip_serializing_if = "Option::is_none")]
@@ -246,7 +259,7 @@ pub struct ProjectViews {
}
/// [`ReturnMetrics::project_downloads`].
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ProjectDownloads {
/// [`ProjectDownloadsField::Domain`].
#[serde(skip_serializing_if = "Option::is_none")]
@@ -265,7 +278,7 @@ pub struct ProjectDownloads {
}
/// [`ReturnMetrics::project_playtime`].
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ProjectPlaytime {
/// [`ProjectPlaytimeField::VersionId`].
#[serde(skip_serializing_if = "Option::is_none")]
@@ -281,7 +294,7 @@ pub struct ProjectPlaytime {
}
/// [`ReturnMetrics::project_revenue`].
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ProjectRevenue {
/// Total revenue for this bucket.
revenue: Decimal,
@@ -414,14 +427,19 @@ mod query {
};
}
pub async fn get(
/// Fetches analytics data for the authorized user's projects.
#[utoipa::path(
responses((status = OK, body = inline(FetchResponse))),
)]
#[post("")]
pub async fn fetch_analytics(
http_req: HttpRequest,
req: web::Json<GetRequest>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
clickhouse: web::Data<clickhouse::Client>,
) -> Result<web::Json<GetResponse>, ApiError> {
) -> Result<web::Json<FetchResponse>, ApiError> {
let (scopes, user) = get_user_from_headers(
&http_req,
&**pool,
@@ -655,7 +673,7 @@ pub async fn get(
}
}
Ok(web::Json(GetResponse(time_slices)))
Ok(web::Json(FetchResponse(time_slices)))
}
fn none_if_empty(s: String) -> Option<String> {
@@ -824,7 +842,7 @@ mod tests {
let test_project_2 = ProjectId(456);
let test_project_3 = ProjectId(789);
let src = GetResponse(vec![
let src = FetchResponse(vec![
TimeSlice(vec![
AnalyticsData::Project(ProjectAnalytics {
source_project: test_project_1,

View File

@@ -7,13 +7,10 @@ use crate::models::teams::ProjectPermissions;
use crate::{
auth::get_user_from_headers,
database::models::user_item,
models::{
ids::{ProjectId, VersionId},
pats::Scopes,
},
models::{ids::ProjectId, pats::Scopes},
queue::session::AuthQueue,
};
use actix_web::{HttpRequest, HttpResponse, web};
use actix_web::{HttpRequest, HttpResponse, get, web};
use ariadne::ids::base62_impl::to_base62;
use chrono::{DateTime, Duration, Utc};
use eyre::eyre;
@@ -24,28 +21,21 @@ use std::collections::HashMap;
use std::convert::TryInto;
use std::num::NonZeroU32;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("analytics")
// TODO: since our service shadows analytics v2, we have to redirect here
.route("", web::post().to(super::analytics_get::get))
.route("playtime", web::get().to(playtimes_get))
.route("views", web::get().to(views_get))
.route("downloads", web::get().to(downloads_get))
.route("revenue", web::get().to(revenue_get))
.route(
"countries/downloads",
web::get().to(countries_downloads_get),
)
.route("countries/views", web::get().to(countries_views_get)),
);
pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
cfg.service(playtimes_get)
.service(views_get)
.service(downloads_get)
.service(revenue_get)
.service(countries_downloads_get)
.service(countries_views_get);
}
/// The json data to be passed to fetch analytic data
/// The json data to be passed to fetch analytic data.
///
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
/// start_date and end_date are optional, and default to two weeks ago, and the maximum date respectively.
/// resolution_minutes is optional. This refers to the window by which we are looking (every day, every minute, etc) and defaults to 1440 (1 day)
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, utoipa::ToSchema)]
pub struct GetData {
// only one of project_ids or version_ids should be used
// if neither are provided, all projects the user has access to will be used
@@ -54,26 +44,12 @@ pub struct GetData {
pub start_date: Option<DateTime<Utc>>, // defaults to 2 weeks ago
pub end_date: Option<DateTime<Utc>>, // defaults to now
#[schema(value_type = Option<u32>, minimum = 1)]
pub resolution_minutes: Option<NonZeroU32>, // defaults to 1 day. Ignored in routes that do not aggregate over a resolution (eg: /countries)
}
/// Get playtime data for a set of projects or versions
/// Data is returned as a hashmap of project/version ids to a hashmap of days to playtime data
/// eg:
/// {
/// "4N1tEhnO": {
/// "20230824": 23
/// }
///}
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
#[derive(Serialize, Deserialize, Clone)]
pub struct FetchedPlaytime {
pub time: u64,
pub total_seconds: u64,
pub loader_seconds: HashMap<String, u64>,
pub game_version_seconds: HashMap<String, u64>,
pub parent_seconds: HashMap<VersionId, u64>,
}
#[utoipa::path]
#[get("/playtime")]
pub async fn playtimes_get(
req: HttpRequest,
clickhouse: web::Data<clickhouse::Client>,
@@ -134,7 +110,8 @@ pub async fn playtimes_get(
Ok(HttpResponse::Ok().json(hm))
}
/// Get view data for a set of projects or versions
/// Get view data for a set of projects or versions.
///
/// Data is returned as a hashmap of project/version ids to a hashmap of days to views
/// eg:
/// {
@@ -143,6 +120,8 @@ pub async fn playtimes_get(
/// }
///}
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
#[utoipa::path]
#[get("/views")]
pub async fn views_get(
req: HttpRequest,
clickhouse: web::Data<clickhouse::Client>,
@@ -203,7 +182,8 @@ pub async fn views_get(
Ok(HttpResponse::Ok().json(hm))
}
/// Get download data for a set of projects or versions
/// Get download data for a set of projects or versions.
///
/// Data is returned as a hashmap of project/version ids to a hashmap of days to downloads
/// eg:
/// {
@@ -212,6 +192,8 @@ pub async fn views_get(
/// }
///}
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
#[utoipa::path]
#[get("/downloads")]
pub async fn downloads_get(
req: HttpRequest,
clickhouse: web::Data<clickhouse::Client>,
@@ -273,7 +255,8 @@ pub async fn downloads_get(
Ok(HttpResponse::Ok().json(hm))
}
/// Get payout data for a set of projects
/// Get payout data for a set of projects.
///
/// Data is returned as a hashmap of project ids to a hashmap of days to amount earned per day
/// eg:
/// {
@@ -282,6 +265,8 @@ pub async fn downloads_get(
/// }
///}
/// ONLY project IDs can be used. Unauthorized projects will be filtered out.
#[utoipa::path]
#[get("/revenue")]
pub async fn revenue_get(
req: HttpRequest,
data: web::Query<GetData>,
@@ -409,7 +394,8 @@ pub async fn revenue_get(
Ok(HttpResponse::Ok().json(hm))
}
/// Get country data for a set of projects or versions
/// Get country data for a set of projects or versions.
///
/// Data is returned as a hashmap of project/version ids to a hashmap of coutnry to downloads.
/// Unknown countries are labeled "".
/// This is usable to see significant performing countries per project
@@ -421,6 +407,8 @@ pub async fn revenue_get(
///}
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
/// For this endpoint, provided dates are a range to aggregate over, not specific days to fetch
#[utoipa::path]
#[get("/countries/downloads")]
pub async fn countries_downloads_get(
req: HttpRequest,
clickhouse: web::Data<clickhouse::Client>,
@@ -482,7 +470,8 @@ pub async fn countries_downloads_get(
Ok(HttpResponse::Ok().json(hm))
}
/// Get country data for a set of projects or versions
/// Get country data for a set of projects or versions.
///
/// Data is returned as a hashmap of project/version ids to a hashmap of coutnry to views.
/// Unknown countries are labeled "".
/// This is usable to see significant performing countries per project
@@ -494,6 +483,8 @@ pub async fn countries_downloads_get(
///}
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
/// For this endpoint, provided dates are a range to aggregate over, not specific days to fetch
#[utoipa::path]
#[get("/countries/views")]
pub async fn countries_views_get(
req: HttpRequest,
clickhouse: web::Data<clickhouse::Client>,

View File

@@ -4,7 +4,6 @@ use actix_web::{HttpResponse, web};
use serde_json::json;
pub mod analytics_get;
pub mod analytics_get_old;
pub mod collections;
pub mod friends;
pub mod images;
@@ -33,8 +32,6 @@ pub fn config(cfg: &mut web::ServiceConfig) {
web::scope("v3")
.wrap(default_cors())
.configure(limits::config)
// .configure(analytics_get::config) // TODO: see `analytics_get`
.configure(analytics_get_old::config)
.configure(collections::config)
.configure(images::config)
.configure(notifications::config)
@@ -56,6 +53,15 @@ pub fn config(cfg: &mut web::ServiceConfig) {
);
}
pub fn utoipa_config(
cfg: &mut utoipa_actix_web::service_config::ServiceConfig,
) {
cfg.service(
utoipa_actix_web::scope("/v3/analytics")
.configure(analytics_get::config),
);
}
pub async fn hello_world() -> Result<HttpResponse, ApiError> {
Ok(HttpResponse::Ok().json(json!({
"hello": "world",

View File

@@ -6,6 +6,7 @@ use actix_web::{App, dev::ServiceResponse, test};
use async_trait::async_trait;
use labrinth::LabrinthConfig;
use std::rc::Rc;
use utoipa_actix_web::AppExt;
pub mod project;
pub mod request_data;
@@ -22,9 +23,15 @@ pub struct ApiV2 {
#[async_trait(?Send)]
impl ApiBuildable for ApiV2 {
async fn build(labrinth_config: LabrinthConfig) -> Self {
let app = App::new().configure(|cfg| {
labrinth::app_config(cfg, labrinth_config.clone())
});
let app = App::new()
.into_utoipa_app()
.configure(|cfg| {
labrinth::utoipa_app_config(cfg, labrinth_config.clone())
})
.into_app()
.configure(|cfg| {
labrinth::app_config(cfg, labrinth_config.clone())
});
let test_app: Rc<dyn LocalService> =
Rc::new(test::init_service(app).await);

View File

@@ -6,6 +6,7 @@ use actix_web::{App, dev::ServiceResponse, test};
use async_trait::async_trait;
use labrinth::LabrinthConfig;
use std::rc::Rc;
use utoipa_actix_web::AppExt;
pub mod collections;
pub mod limits;
@@ -27,9 +28,15 @@ pub struct ApiV3 {
#[async_trait(?Send)]
impl ApiBuildable for ApiV3 {
async fn build(labrinth_config: LabrinthConfig) -> Self {
let app = App::new().configure(|cfg| {
labrinth::app_config(cfg, labrinth_config.clone())
});
let app = App::new()
.into_utoipa_app()
.configure(|cfg| {
labrinth::utoipa_app_config(cfg, labrinth_config.clone())
})
.into_app()
.configure(|cfg| {
labrinth::app_config(cfg, labrinth_config.clone())
});
let test_app: Rc<dyn LocalService> =
Rc::new(test::init_service(app).await);

View File

@@ -12,6 +12,7 @@ serde_bytes = { workspace = true }
serde_cbor = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
utoipa = { workspace = true }
uuid = { workspace = true, features = ["fast-rng", "serde", "v4"] }
[lints]

View File

@@ -94,6 +94,7 @@ macro_rules! base62_id {
serde::Deserialize,
Debug,
Hash,
utoipa::ToSchema,
)]
#[serde(from = "ariadne::ids::Base62Id")]
#[serde(into = "ariadne::ids::Base62Id")]