Trading card city builder game?

adding get single banner API

eldridge.cam 147f674f 5c45f506

verified
+611 -216
+36
.sqlx/query-6f92726299b0901eab22e5285176d099667e82915cd7f945016c61bfe6f13ab3.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "\n SELECT\n pack_banners.id,\n start_date,\n end_date\n FROM pack_banners\n WHERE\n ($1 AND end_date <= NOW())\n OR ($2 AND start_date <= NOW() AND (end_date IS NULL OR end_date > NOW()))\n OR ($3 AND start_date > NOW())\n ORDER BY start_date ASC\n ", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "id", 9 + "type_info": "Text" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "start_date", 14 + "type_info": "Timestamptz" 15 + }, 16 + { 17 + "ordinal": 2, 18 + "name": "end_date", 19 + "type_info": "Timestamptz" 20 + } 21 + ], 22 + "parameters": { 23 + "Left": [ 24 + "Bool", 25 + "Bool", 26 + "Bool" 27 + ] 28 + }, 29 + "nullable": [ 30 + false, 31 + false, 32 + true 33 + ] 34 + }, 35 + "hash": "6f92726299b0901eab22e5285176d099667e82915cd7f945016c61bfe6f13ab3" 36 + }
+3 -5
.sqlx/query-b712a93c4e656c40718c476491586567bc708a600c33181785cbb8d87a60a780.json .sqlx/query-faf3fe47ef5f40a2353b632b0368b690a0cf36b7b6e77e40056695c95c96789b.json
··· 1 1 { 2 2 "db_name": "PostgreSQL", 3 - "query": "\n SELECT\n pack_banners.id,\n start_date,\n end_date,\n COALESCE(\n JSONB_AGG(\n JSONB_BUILD_OBJECT(\n 'card_type_id',\n card_type_id,\n 'frequency',\n frequency\n )\n )\n FILTER\n (WHERE pack_banner_cards IS NOT NULL),\n '[]'::jsonb\n ) AS \"distribution: sqlx::types::Json<Vec<PackBannerCard>>\"\n FROM pack_banners\n LEFT JOIN\n pack_banner_cards\n ON pack_banner_cards.pack_banner_id = pack_banners.id\n WHERE\n ($1 AND end_date <= NOW())\n OR ($2 AND start_date <= NOW() AND (end_date IS NULL OR end_date > NOW()))\n OR ($3 AND start_date > NOW())\n GROUP BY pack_banners.id, start_date, end_date\n ORDER BY start_date ASC\n ", 3 + "query": "\n SELECT\n pack_banners.id,\n start_date,\n end_date,\n COALESCE(\n JSONB_AGG(\n JSONB_BUILD_OBJECT(\n 'card_type_id',\n card_type_id,\n 'frequency',\n frequency\n )\n )\n FILTER\n (WHERE pack_banner_cards IS NOT NULL),\n '[]'::jsonb\n ) AS \"distribution: sqlx::types::Json<Vec<PackBannerCard>>\"\n FROM pack_banners\n LEFT JOIN\n pack_banner_cards\n ON pack_banner_cards.pack_banner_id = pack_banners.id\n WHERE id = $1\n GROUP BY id\n ", 4 4 "describe": { 5 5 "columns": [ 6 6 { ··· 26 26 ], 27 27 "parameters": { 28 28 "Left": [ 29 - "Bool", 30 - "Bool", 31 - "Bool" 29 + "Text" 32 30 ] 33 31 }, 34 32 "nullable": [ ··· 38 36 null 39 37 ] 40 38 }, 41 - "hash": "b712a93c4e656c40718c476491586567bc708a600c33181785cbb8d87a60a780" 39 + "hash": "faf3fe47ef5f40a2353b632b0368b690a0cf36b7b6e77e40056695c95c96789b" 42 40 }
+228 -15
Cargo.lock
··· 74 74 checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" 75 75 76 76 [[package]] 77 + name = "async-trait" 78 + version = "0.1.89" 79 + source = "registry+https://github.com/rust-lang/crates.io-index" 80 + checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" 81 + dependencies = [ 82 + "proc-macro2", 83 + "quote", 84 + "syn", 85 + ] 86 + 87 + [[package]] 77 88 name = "atoi" 78 89 version = "2.0.0" 79 90 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 96 107 97 108 [[package]] 98 109 name = "axum" 110 + version = "0.7.9" 111 + source = "registry+https://github.com/rust-lang/crates.io-index" 112 + checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" 113 + dependencies = [ 114 + "async-trait", 115 + "axum-core 0.4.5", 116 + "bytes", 117 + "futures-util", 118 + "http", 119 + "http-body", 120 + "http-body-util", 121 + "hyper", 122 + "hyper-util", 123 + "itoa", 124 + "matchit 0.7.3", 125 + "memchr", 126 + "mime", 127 + "percent-encoding", 128 + "pin-project-lite", 129 + "rustversion", 130 + "serde", 131 + "serde_json", 132 + "serde_path_to_error", 133 + "serde_urlencoded", 134 + "sync_wrapper", 135 + "tokio", 136 + "tower", 137 + "tower-layer", 138 + "tower-service", 139 + "tracing", 140 + ] 141 + 142 + [[package]] 143 + name = "axum" 99 144 version = "0.8.8" 100 145 source = "registry+https://github.com/rust-lang/crates.io-index" 101 146 checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" 102 147 dependencies = [ 103 - "axum-core", 148 + "axum-core 0.5.6", 104 149 "base64", 105 150 "bytes", 106 151 "form_urlencoded", ··· 111 156 "hyper", 112 157 "hyper-util", 113 158 "itoa", 114 - "matchit", 159 + "matchit 0.8.4", 115 160 "memchr", 116 161 "mime", 117 162 "percent-encoding", ··· 132 177 133 178 [[package]] 134 179 name = "axum-core" 180 + version = "0.4.5" 181 + source = "registry+https://github.com/rust-lang/crates.io-index" 182 + checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" 183 + dependencies = [ 184 + "async-trait", 185 + "bytes", 186 + "futures-util", 187 + "http", 188 + "http-body", 189 + "http-body-util", 190 + "mime", 191 + "pin-project-lite", 192 + "rustversion", 193 + "sync_wrapper", 194 + "tower-layer", 195 + "tower-service", 196 + "tracing", 197 + ] 198 + 199 + [[package]] 200 + name = "axum-core" 135 201 version = "0.5.6" 136 202 source = "registry+https://github.com/rust-lang/crates.io-index" 137 203 checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" ··· 150 216 ] 151 217 152 218 [[package]] 219 + name = "axum-extra" 220 + version = "0.9.6" 221 + source = "registry+https://github.com/rust-lang/crates.io-index" 222 + checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04" 223 + dependencies = [ 224 + "axum 0.7.9", 225 + "axum-core 0.4.5", 226 + "bytes", 227 + "fastrand", 228 + "futures-util", 229 + "http", 230 + "http-body", 231 + "http-body-util", 232 + "mime", 233 + "multer", 234 + "pin-project-lite", 235 + "serde", 236 + "tower", 237 + "tower-layer", 238 + "tower-service", 239 + ] 240 + 241 + [[package]] 153 242 name = "base64" 154 243 version = "0.22.1" 155 244 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 202 291 version = "0.1.0" 203 292 dependencies = [ 204 293 "anyhow", 205 - "axum", 294 + "axum 0.8.8", 206 295 "clap", 207 296 "derive_more", 208 297 "futures", ··· 210 299 "json-patch", 211 300 "kameo", 212 301 "rmp-serde", 302 + "scalar_api_reference", 213 303 "serde", 214 304 "serde_json", 215 305 "sqlx", ··· 220 310 "tracing", 221 311 "tracing-subscriber", 222 312 "utoipa", 223 - "utoipa-scalar", 224 313 "uuid", 225 314 ] 226 315 ··· 450 539 ] 451 540 452 541 [[package]] 542 + name = "encoding_rs" 543 + version = "0.8.35" 544 + source = "registry+https://github.com/rust-lang/crates.io-index" 545 + checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 546 + dependencies = [ 547 + "cfg-if", 548 + ] 549 + 550 + [[package]] 453 551 name = "equivalent" 454 552 version = "1.0.2" 455 553 source = "registry+https://github.com/rust-lang/crates.io-index" 456 554 checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 457 555 458 556 [[package]] 557 + name = "errno" 558 + version = "0.3.14" 559 + source = "registry+https://github.com/rust-lang/crates.io-index" 560 + checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 561 + dependencies = [ 562 + "libc", 563 + "windows-sys 0.61.2", 564 + ] 565 + 566 + [[package]] 459 567 name = "etcetera" 460 568 version = "0.8.0" 461 569 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 476 584 "parking", 477 585 "pin-project-lite", 478 586 ] 587 + 588 + [[package]] 589 + name = "fastrand" 590 + version = "2.3.0" 591 + source = "registry+https://github.com/rust-lang/crates.io-index" 592 + checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 479 593 480 594 [[package]] 481 595 name = "flume" ··· 1053 1167 1054 1168 [[package]] 1055 1169 name = "matchit" 1170 + version = "0.7.3" 1171 + source = "registry+https://github.com/rust-lang/crates.io-index" 1172 + checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" 1173 + 1174 + [[package]] 1175 + name = "matchit" 1056 1176 version = "0.8.4" 1057 1177 source = "registry+https://github.com/rust-lang/crates.io-index" 1058 1178 checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" ··· 1088 1208 "libc", 1089 1209 "wasi", 1090 1210 "windows-sys 0.61.2", 1211 + ] 1212 + 1213 + [[package]] 1214 + name = "multer" 1215 + version = "3.1.0" 1216 + source = "registry+https://github.com/rust-lang/crates.io-index" 1217 + checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" 1218 + dependencies = [ 1219 + "bytes", 1220 + "encoding_rs", 1221 + "futures-util", 1222 + "http", 1223 + "httparse", 1224 + "memchr", 1225 + "mime", 1226 + "spin", 1227 + "version_check", 1091 1228 ] 1092 1229 1093 1230 [[package]] ··· 1434 1571 ] 1435 1572 1436 1573 [[package]] 1574 + name = "rust-embed" 1575 + version = "8.11.0" 1576 + source = "registry+https://github.com/rust-lang/crates.io-index" 1577 + checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" 1578 + dependencies = [ 1579 + "rust-embed-impl", 1580 + "rust-embed-utils", 1581 + "walkdir", 1582 + ] 1583 + 1584 + [[package]] 1585 + name = "rust-embed-impl" 1586 + version = "8.11.0" 1587 + source = "registry+https://github.com/rust-lang/crates.io-index" 1588 + checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" 1589 + dependencies = [ 1590 + "proc-macro2", 1591 + "quote", 1592 + "rust-embed-utils", 1593 + "syn", 1594 + "walkdir", 1595 + ] 1596 + 1597 + [[package]] 1598 + name = "rust-embed-utils" 1599 + version = "8.11.0" 1600 + source = "registry+https://github.com/rust-lang/crates.io-index" 1601 + checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" 1602 + dependencies = [ 1603 + "sha2", 1604 + "walkdir", 1605 + ] 1606 + 1607 + [[package]] 1437 1608 name = "rustc_version" 1438 1609 version = "0.4.1" 1439 1610 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1455 1626 checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" 1456 1627 1457 1628 [[package]] 1629 + name = "same-file" 1630 + version = "1.0.6" 1631 + source = "registry+https://github.com/rust-lang/crates.io-index" 1632 + checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1633 + dependencies = [ 1634 + "winapi-util", 1635 + ] 1636 + 1637 + [[package]] 1638 + name = "scalar_api_reference" 1639 + version = "0.1.0" 1640 + source = "registry+https://github.com/rust-lang/crates.io-index" 1641 + checksum = "7b3efdac145ec337b9db75e6d1d8732980551c07490ef9db72947e2af592f051" 1642 + dependencies = [ 1643 + "axum 0.7.9", 1644 + "axum-extra", 1645 + "rust-embed", 1646 + "serde_json", 1647 + "tokio", 1648 + ] 1649 + 1650 + [[package]] 1458 1651 name = "scopeguard" 1459 1652 version = "1.2.0" 1460 1653 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1561 1754 checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 1562 1755 dependencies = [ 1563 1756 "lazy_static", 1757 + ] 1758 + 1759 + [[package]] 1760 + name = "signal-hook-registry" 1761 + version = "1.4.8" 1762 + source = "registry+https://github.com/rust-lang/crates.io-index" 1763 + checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" 1764 + dependencies = [ 1765 + "errno", 1766 + "libc", 1564 1767 ] 1565 1768 1566 1769 [[package]] ··· 1980 2183 "bytes", 1981 2184 "libc", 1982 2185 "mio", 2186 + "parking_lot", 1983 2187 "pin-project-lite", 2188 + "signal-hook-registry", 1984 2189 "socket2", 1985 2190 "tokio-macros", 1986 2191 "tracing", ··· 2270 2475 ] 2271 2476 2272 2477 [[package]] 2273 - name = "utoipa-scalar" 2274 - version = "0.3.0" 2275 - source = "registry+https://github.com/rust-lang/crates.io-index" 2276 - checksum = "59559e1509172f6b26c1cdbc7247c4ddd1ac6560fe94b584f81ee489b141f719" 2277 - dependencies = [ 2278 - "serde", 2279 - "serde_json", 2280 - "utoipa", 2281 - ] 2282 - 2283 - [[package]] 2284 2478 name = "uuid" 2285 2479 version = "1.20.0" 2286 2480 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2309 2503 version = "0.9.5" 2310 2504 source = "registry+https://github.com/rust-lang/crates.io-index" 2311 2505 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2506 + 2507 + [[package]] 2508 + name = "walkdir" 2509 + version = "2.5.0" 2510 + source = "registry+https://github.com/rust-lang/crates.io-index" 2511 + checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 2512 + dependencies = [ 2513 + "same-file", 2514 + "winapi-util", 2515 + ] 2312 2516 2313 2517 [[package]] 2314 2518 name = "wasi" ··· 2384 2588 dependencies = [ 2385 2589 "libredox", 2386 2590 "wasite", 2591 + ] 2592 + 2593 + [[package]] 2594 + name = "winapi-util" 2595 + version = "0.1.11" 2596 + source = "registry+https://github.com/rust-lang/crates.io-index" 2597 + checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" 2598 + dependencies = [ 2599 + "windows-sys 0.61.2", 2387 2600 ] 2388 2601 2389 2602 [[package]]
+1 -1
Cargo.toml
··· 20 20 json-patch = { version = "4.1.0", features = ["utoipa"] } 21 21 kameo = "0.19.2" 22 22 rmp-serde = "1.3.1" 23 + scalar_api_reference = { version = "0.1.0", features = ["axum"] } 23 24 serde = { version = "1.0.228", features = ["derive"] } 24 25 serde_json = "1.0.149" 25 26 sqlx = { version = "0.8.6", features = ["runtime-tokio", "postgres", "time"] } ··· 30 31 tracing = "0.1.44" 31 32 tracing-subscriber = { version = "0.3.22", features = ["env-filter", "json"] } 32 33 utoipa = { version = "5.4.0", features = ["time"] } 33 - utoipa-scalar = { version = "0.3.0" } 34 34 uuid = { version = "1.20.0", features = ["serde", "v7"] }
+83
src/api/error.rs
··· 1 + use axum::body::Body; 2 + use axum::http::{Response, StatusCode}; 3 + use axum::response::{IntoResponse, Json}; 4 + use serde_json::Value; 5 + use std::error::Error; 6 + 7 + pub trait ApiError: Error { 8 + const STATUS: StatusCode; 9 + const CODE: &str; 10 + type Detail: serde::Serialize; 11 + 12 + fn detail(&self) -> Self::Detail; 13 + } 14 + 15 + #[derive(Debug, derive_more::Display, derive_more::Error)] 16 + #[display("{_0}")] 17 + pub struct InternalServerError(anyhow::Error); 18 + 19 + macro_rules! respond_internal_server_error { 20 + ($($fmt:tt)+) => { 21 + return Err(internal_server_error(anyhow::anyhow!($($fmt)+)).into()) 22 + }; 23 + () => { 24 + return Err(internal_server_error(anyhow::anyhow!("Unexpected server error")).into()) 25 + }; 26 + } 27 + 28 + pub(crate) use respond_internal_server_error; 29 + 30 + pub fn internal_server_error<T>(error: T) -> JsonError<InternalServerError> 31 + where 32 + anyhow::Error: From<T>, 33 + { 34 + JsonError(InternalServerError(error.into())) 35 + } 36 + 37 + impl ApiError for InternalServerError { 38 + const STATUS: StatusCode = StatusCode::INTERNAL_SERVER_ERROR; 39 + const CODE: &str = "Internal server error"; 40 + type Detail = (); 41 + 42 + fn detail(&self) -> Self::Detail {} 43 + } 44 + 45 + pub struct JsonError<T>(pub T); 46 + 47 + impl<T: ApiError> From<T> for JsonError<T> { 48 + fn from(value: T) -> Self { 49 + Self(value) 50 + } 51 + } 52 + 53 + #[derive(utoipa::ToSchema)] 54 + #[expect( 55 + dead_code, 56 + reason = "this is a stub type used for OpenAPI schema generation only" 57 + )] 58 + pub struct ErrorDetailResponse { 59 + code: &'static str, 60 + message: String, 61 + detail: Value, 62 + } 63 + 64 + impl<T: ApiError> IntoResponse for JsonError<T> { 65 + fn into_response(self) -> Response<Body> { 66 + #[derive(serde::Serialize)] 67 + struct ErrorData<T: ApiError> { 68 + code: &'static str, 69 + message: String, 70 + detail: T::Detail, 71 + } 72 + 73 + ( 74 + T::STATUS, 75 + Json(ErrorData::<T> { 76 + code: T::CODE, 77 + message: self.0.to_string(), 78 + detail: self.0.detail(), 79 + }), 80 + ) 81 + .into_response() 82 + } 83 + }
-107
src/api/list_banners.rs
··· 1 - use axum::http::StatusCode; 2 - use axum::response::{IntoResponse, Response}; 3 - use axum::Json; 4 - use serde::{Deserialize, Serialize}; 5 - use utoipa::ToSchema; 6 - 7 - use crate::dto::*; 8 - 9 - #[derive(Serialize, Deserialize, ToSchema)] 10 - pub struct ListBannersResponse { 11 - banners: Vec<PackBanner>, 12 - } 13 - 14 - fn default_active() -> Vec<Status> { 15 - vec![Status::Active] 16 - } 17 - 18 - #[derive(Serialize, Deserialize, ToSchema)] 19 - #[schema(default)] 20 - pub struct ListBannersRequest { 21 - #[serde(default = "default_active")] 22 - status: Vec<Status>, 23 - } 24 - 25 - impl Default for ListBannersRequest { 26 - fn default() -> Self { 27 - Self { 28 - status: vec![Status::Active], 29 - } 30 - } 31 - } 32 - 33 - #[derive(Serialize, Deserialize, PartialEq, Eq, Copy, Clone, Debug, ToSchema)] 34 - pub enum Status { 35 - Done, 36 - Active, 37 - Upcoming, 38 - } 39 - 40 - #[utoipa::path( 41 - post, 42 - path = "/api/v1/banners", 43 - description = "List pack banners.", 44 - tag = "Global", 45 - request_body = Option<ListBannersRequest>, 46 - responses( 47 - (status = OK, description = "Successfully listed all banners.", body = ListBannersResponse), 48 - ), 49 - )] 50 - pub async fn list_banners( 51 - db: axum::Extension<sqlx::PgPool>, 52 - request: Option<Json<ListBannersRequest>>, 53 - ) -> Result<Json<ListBannersResponse>, Response> { 54 - let request = request.map(|json| json.0).unwrap_or_default(); 55 - let mut conn = db 56 - .acquire() 57 - .await 58 - .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())?; 59 - 60 - let banners = sqlx::query!( 61 - r#" 62 - SELECT 63 - pack_banners.id, 64 - start_date, 65 - end_date, 66 - COALESCE( 67 - JSONB_AGG( 68 - JSONB_BUILD_OBJECT( 69 - 'card_type_id', 70 - card_type_id, 71 - 'frequency', 72 - frequency 73 - ) 74 - ) 75 - FILTER 76 - (WHERE pack_banner_cards IS NOT NULL), 77 - '[]'::jsonb 78 - ) AS "distribution: sqlx::types::Json<Vec<PackBannerCard>>" 79 - FROM pack_banners 80 - LEFT JOIN 81 - pack_banner_cards 82 - ON pack_banner_cards.pack_banner_id = pack_banners.id 83 - WHERE 84 - ($1 AND end_date <= NOW()) 85 - OR ($2 AND start_date <= NOW() AND (end_date IS NULL OR end_date > NOW())) 86 - OR ($3 AND start_date > NOW()) 87 - GROUP BY pack_banners.id, start_date, end_date 88 - ORDER BY start_date ASC 89 - "#, 90 - request.status.contains(&Status::Done), 91 - request.status.contains(&Status::Active), 92 - request.status.contains(&Status::Upcoming), 93 - ) 94 - .fetch_all(&mut *conn) 95 - .await 96 - .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())? 97 - .into_iter() 98 - .map(|record| PackBanner { 99 - id: record.id, 100 - start_date: record.start_date, 101 - end_date: record.end_date, 102 - distribution: record.distribution.unwrap().0, 103 - }) 104 - .collect(); 105 - 106 - Ok(Json(ListBannersResponse { banners })) 107 - }
+7 -14
src/api/list_card_types.rs src/api/operations/list_card_types.rs
··· 1 - use axum::http::StatusCode; 2 - use axum::response::{IntoResponse, Response}; 3 - use axum::Json; 4 - use serde::{Deserialize, Serialize}; 5 - use utoipa::ToSchema; 6 - 1 + use crate::api::error::internal_server_error; 7 2 use crate::dto::*; 3 + use axum::Json; 8 4 9 - #[derive(Serialize, Deserialize, ToSchema)] 5 + #[derive(serde::Serialize, utoipa::ToSchema)] 10 6 pub struct ListCardTypesResponse { 11 7 card_types: Vec<CardType>, 12 8 } ··· 22 18 )] 23 19 pub async fn list_card_types( 24 20 db: axum::Extension<sqlx::PgPool>, 25 - ) -> Result<Json<ListCardTypesResponse>, Response> { 26 - let mut conn = db 27 - .acquire() 28 - .await 29 - .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())?; 21 + ) -> axum::response::Result<Json<ListCardTypesResponse>> { 22 + let mut conn = db.acquire().await.map_err(internal_server_error)?; 30 23 31 24 let tile_types = sqlx::query_as!( 32 25 TileType, ··· 43 36 ) 44 37 .fetch_all(&mut *conn) 45 38 .await 46 - .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())?; 39 + .map_err(internal_server_error)?; 47 40 48 41 let species_types = sqlx::query_as!( 49 42 Species, ··· 55 48 ) 56 49 .fetch_all(&mut *conn) 57 50 .await 58 - .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())?; 51 + .map_err(internal_server_error)?; 59 52 60 53 let card_types = tile_types 61 54 .into_iter()
-51
src/api/list_fields.rs
··· 1 - use axum::extract::Path; 2 - use axum::http::StatusCode; 3 - use axum::response::{IntoResponse, Response}; 4 - use axum::Json; 5 - use serde::{Deserialize, Serialize}; 6 - use utoipa::ToSchema; 7 - 8 - use crate::dto::*; 9 - 10 - #[derive(Serialize, Deserialize, ToSchema)] 11 - pub struct ListFieldsResponse { 12 - fields: Vec<Field>, 13 - } 14 - 15 - #[utoipa::path( 16 - get, 17 - path = "/api/v1/players/{player_id}/fields", 18 - description = "List player fields.", 19 - tag = "Player", 20 - responses( 21 - (status = OK, description = "Successfully listed all fields.", body = ListFieldsResponse), 22 - ), 23 - params( 24 - ("player_id" = AccountIdOrMe, Path, description = "The ID of the player whose fields to list.") 25 - ) 26 - )] 27 - pub async fn list_fields( 28 - db: axum::Extension<sqlx::PgPool>, 29 - Path(player_id): Path<AccountIdOrMe>, 30 - ) -> Result<Json<ListFieldsResponse>, Response> { 31 - let AccountIdOrMe::AccountId(account_id) = player_id else { 32 - return Err(( 33 - StatusCode::INTERNAL_SERVER_ERROR, 34 - "unimplemented".to_owned(), 35 - ) 36 - .into_response()); 37 - }; 38 - let mut conn = db 39 - .acquire() 40 - .await 41 - .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())?; 42 - let fields = sqlx::query_as!( 43 - Field, 44 - "SELECT id, name FROM fields WHERE account_id = $1", 45 - account_id, 46 - ) 47 - .fetch_all(&mut *conn) 48 - .await 49 - .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response())?; 50 - Ok(Json(ListFieldsResponse { fields })) 51 - }
+3 -3
src/api/mod.rs
··· 1 - pub mod list_banners; 2 - pub mod list_card_types; 3 - pub mod list_fields; 1 + mod error; 2 + 3 + pub mod operations; 4 4 pub mod ws;
+89
src/api/operations/get_banner.rs
··· 1 + use crate::api::error::{internal_server_error, ApiError, ErrorDetailResponse, JsonError}; 2 + use crate::dto::*; 3 + use axum::extract::Path; 4 + use axum::http::StatusCode; 5 + use axum::Json; 6 + 7 + #[derive(serde::Serialize, utoipa::ToSchema)] 8 + pub struct GetBannerResponse { 9 + banner: PackBanner, 10 + banner_cards: Vec<PackBannerCard>, 11 + } 12 + 13 + #[derive(Debug, derive_more::Display, derive_more::Error)] 14 + #[display("a banner with id {banner_id} was not found")] 15 + pub struct BannerNotFoundError { 16 + banner_id: String, 17 + } 18 + 19 + impl ApiError for BannerNotFoundError { 20 + const STATUS: StatusCode = StatusCode::NOT_FOUND; 21 + const CODE: &str = "BannerNotFound"; 22 + type Detail = (); 23 + 24 + fn detail(&self) -> Self::Detail {} 25 + } 26 + 27 + #[utoipa::path( 28 + get, 29 + path = "/api/v1/banners/{banner_id}", 30 + description = "Get full pack banner details.", 31 + tag = "Global", 32 + params( 33 + ("banner_id" = String, Path, description = "Banner ID"), 34 + ), 35 + responses( 36 + (status = OK, description = "Successfully retrieved banner", body = GetBannerResponse), 37 + (status = NOT_FOUND, description = "Banner was not found", body = ErrorDetailResponse), 38 + ), 39 + )] 40 + pub async fn get_banner( 41 + db: axum::Extension<sqlx::PgPool>, 42 + Path(banner_id): Path<String>, 43 + ) -> axum::response::Result<Json<GetBannerResponse>> { 44 + let mut conn = db.acquire().await.map_err(internal_server_error)?; 45 + 46 + let row = sqlx::query!( 47 + r#" 48 + SELECT 49 + pack_banners.id, 50 + start_date, 51 + end_date, 52 + COALESCE( 53 + JSONB_AGG( 54 + JSONB_BUILD_OBJECT( 55 + 'card_type_id', 56 + card_type_id, 57 + 'frequency', 58 + frequency 59 + ) 60 + ) 61 + FILTER 62 + (WHERE pack_banner_cards IS NOT NULL), 63 + '[]'::jsonb 64 + ) AS "distribution: sqlx::types::Json<Vec<PackBannerCard>>" 65 + FROM pack_banners 66 + LEFT JOIN 67 + pack_banner_cards 68 + ON pack_banner_cards.pack_banner_id = pack_banners.id 69 + WHERE id = $1 70 + GROUP BY id 71 + "#, 72 + banner_id 73 + ) 74 + .fetch_optional(&mut *conn) 75 + .await 76 + .map_err(internal_server_error)? 77 + .ok_or(JsonError(BannerNotFoundError { banner_id }))?; 78 + 79 + let banner = PackBanner { 80 + id: row.id, 81 + start_date: row.start_date, 82 + end_date: row.end_date, 83 + }; 84 + 85 + Ok(Json(GetBannerResponse { 86 + banner, 87 + banner_cards: row.distribution.unwrap().0, 88 + })) 89 + }
+84
src/api/operations/list_banners.rs
··· 1 + use crate::api::error::internal_server_error; 2 + use crate::dto::*; 3 + use axum::Json; 4 + 5 + #[derive(serde::Serialize, utoipa::ToSchema)] 6 + pub struct ListBannersResponse { 7 + banners: Vec<PackBanner>, 8 + } 9 + 10 + fn default_active() -> Vec<Status> { 11 + vec![Status::Active] 12 + } 13 + 14 + #[derive(serde::Deserialize, utoipa::ToSchema)] 15 + #[schema(default)] 16 + pub struct ListBannersRequest { 17 + #[serde(default = "default_active")] 18 + status: Vec<Status>, 19 + } 20 + 21 + impl Default for ListBannersRequest { 22 + fn default() -> Self { 23 + Self { 24 + status: vec![Status::Active], 25 + } 26 + } 27 + } 28 + 29 + #[derive( 30 + PartialEq, Eq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, utoipa::ToSchema, 31 + )] 32 + pub enum Status { 33 + Done, 34 + Active, 35 + Upcoming, 36 + } 37 + 38 + #[utoipa::path( 39 + post, 40 + path = "/api/v1/banners", 41 + description = "List pack banners.", 42 + tag = "Global", 43 + request_body = Option<ListBannersRequest>, 44 + responses( 45 + (status = OK, description = "Successfully listed all banners.", body = ListBannersResponse), 46 + ), 47 + )] 48 + pub async fn list_banners( 49 + db: axum::Extension<sqlx::PgPool>, 50 + request: Option<Json<ListBannersRequest>>, 51 + ) -> axum::response::Result<Json<ListBannersResponse>> { 52 + let request = request.map(|json| json.0).unwrap_or_default(); 53 + let mut conn = db.acquire().await.map_err(internal_server_error)?; 54 + 55 + let banners = sqlx::query!( 56 + r#" 57 + SELECT 58 + pack_banners.id, 59 + start_date, 60 + end_date 61 + FROM pack_banners 62 + WHERE 63 + ($1 AND end_date <= NOW()) 64 + OR ($2 AND start_date <= NOW() AND (end_date IS NULL OR end_date > NOW())) 65 + OR ($3 AND start_date > NOW()) 66 + ORDER BY start_date ASC 67 + "#, 68 + request.status.contains(&Status::Done), 69 + request.status.contains(&Status::Active), 70 + request.status.contains(&Status::Upcoming), 71 + ) 72 + .fetch_all(&mut *conn) 73 + .await 74 + .map_err(internal_server_error)? 75 + .into_iter() 76 + .map(|record| PackBanner { 77 + id: record.id, 78 + start_date: record.start_date, 79 + end_date: record.end_date, 80 + }) 81 + .collect(); 82 + 83 + Ok(Json(ListBannersResponse { banners })) 84 + }
+41
src/api/operations/list_fields.rs
··· 1 + use axum::extract::Path; 2 + use axum::Json; 3 + 4 + use crate::api::error::{internal_server_error, respond_internal_server_error}; 5 + use crate::dto::*; 6 + 7 + #[derive(serde::Serialize, utoipa::ToSchema)] 8 + pub struct ListFieldsResponse { 9 + fields: Vec<Field>, 10 + } 11 + 12 + #[utoipa::path( 13 + get, 14 + path = "/api/v1/players/{player_id}/fields", 15 + description = "List player fields.", 16 + tag = "Player", 17 + responses( 18 + (status = OK, description = "Successfully listed all fields.", body = ListFieldsResponse), 19 + ), 20 + params( 21 + ("player_id" = AccountIdOrMe, Path, description = "The ID of the player whose fields to list.") 22 + ) 23 + )] 24 + pub async fn list_fields( 25 + db: axum::Extension<sqlx::PgPool>, 26 + Path(player_id): Path<AccountIdOrMe>, 27 + ) -> axum::response::Result<Json<ListFieldsResponse>> { 28 + let AccountIdOrMe::AccountId(account_id) = player_id else { 29 + respond_internal_server_error!("unimplemented"); 30 + }; 31 + let mut conn = db.acquire().await.map_err(internal_server_error)?; 32 + let fields = sqlx::query_as!( 33 + Field, 34 + "SELECT id, name FROM fields WHERE account_id = $1", 35 + account_id, 36 + ) 37 + .fetch_all(&mut *conn) 38 + .await 39 + .map_err(internal_server_error)?; 40 + Ok(Json(ListFieldsResponse { fields })) 41 + }
+10
src/api/operations/mod.rs
··· 1 + mod get_banner; 2 + mod list_banners; 3 + pub use get_banner::*; 4 + pub use list_banners::*; 5 + 6 + mod list_card_types; 7 + pub use list_card_types::*; 8 + 9 + mod list_fields; 10 + pub use list_fields::*;
+7 -8
src/dto/mod.rs
··· 11 11 AccountId(String), 12 12 } 13 13 14 - #[derive(Serialize, Deserialize, PartialEq, Clone, Debug, ToSchema)] 14 + #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, ToSchema)] 15 15 pub struct Account { 16 16 pub id: String, 17 17 } 18 18 19 - #[derive(Serialize, Deserialize, PartialEq, Clone, Debug, ToSchema)] 19 + #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, ToSchema)] 20 20 pub struct Field { 21 21 pub id: i64, 22 22 pub name: String, 23 23 } 24 24 25 - #[derive(Serialize, Deserialize, PartialEq, Clone, Debug, ToSchema)] 25 + #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, ToSchema)] 26 26 #[serde(tag = "class")] 27 27 pub enum CardType { 28 28 Tile(TileType), 29 29 Citizen(Species), 30 30 } 31 31 32 - #[derive(Serialize, Deserialize, PartialEq, Clone, Debug, ToSchema)] 32 + #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, ToSchema)] 33 33 pub struct TileType { 34 34 pub id: String, 35 35 pub card_set_id: String, ··· 38 38 pub employs: i32, 39 39 } 40 40 41 - #[derive(Serialize, Deserialize, PartialEq, Clone, Debug, ToSchema)] 41 + #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, ToSchema)] 42 42 pub struct Species { 43 43 pub id: String, 44 44 pub card_set_id: String, 45 45 } 46 46 47 - #[derive(Serialize, Deserialize, PartialEq, Clone, Debug, ToSchema)] 47 + #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, ToSchema)] 48 48 pub struct PackBanner { 49 49 pub id: String, 50 50 pub start_date: OffsetDateTime, 51 51 pub end_date: Option<OffsetDateTime>, 52 - pub distribution: Vec<PackBannerCard>, 53 52 } 54 53 55 - #[derive(Serialize, Deserialize, PartialEq, Clone, Debug, ToSchema)] 54 + #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, ToSchema)] 56 55 pub struct PackBannerCard { 57 56 pub card_type_id: String, 58 57 pub frequency: u32,
+19 -12
src/main.rs
··· 12 12 #[derive(utoipa::OpenApi)] 13 13 #[openapi( 14 14 paths( 15 - api::list_card_types::list_card_types, 16 - api::list_banners::list_banners, 17 - api::list_fields::list_fields, 15 + api::operations::get_banner, 16 + api::operations::list_banners, 17 + 18 + api::operations::list_card_types, 19 + 20 + api::operations::list_fields, 18 21 ), 19 22 tags( 20 23 (name = "Global", description = "Publicly available global data about the Cartography game."), ··· 59 62 .connect(&db_url) 60 63 .await?; 61 64 65 + // let scalar_config = serde_json::json!({ 66 + // "url": "/api/openapi.json", 67 + // "agent": scalar_api_reference::config::AgentOptions::disabled() 68 + // }); 69 + 62 70 let bus = bus::Bus::spawn(()); 63 71 let app = axum::Router::new() 64 72 .route( 65 73 "/api/v1/cardtypes", 66 - axum::routing::get(api::list_card_types::list_card_types), 74 + axum::routing::get(api::operations::list_card_types), 67 75 ) 68 76 .route( 69 77 "/api/v1/banners", 70 - axum::routing::post(api::list_banners::list_banners), 78 + axum::routing::post(api::operations::list_banners), 79 + ) 80 + .route( 81 + "/api/v1/banners/{banner_id}", 82 + axum::routing::get(api::operations::get_banner), 71 83 ) 72 84 .route( 73 85 "/api/v1/players/{player_id}/fields", 74 - axum::routing::get(api::list_fields::list_fields), 86 + axum::routing::get(api::operations::list_fields), 75 87 ) 76 88 .route("/play/ws", axum::routing::any(api::ws::v1)) 77 89 .route( 78 90 "/api/openapi.json", 79 91 axum::routing::get(axum::response::Json(ApiDoc::openapi())), 80 92 ) 81 - .route( 82 - "/api", 83 - axum::routing::get(axum::response::Html( 84 - utoipa_scalar::Scalar::new(ApiDoc::openapi()).to_html(), 85 - )), 86 - ) 93 + // .merge(scalar_api_reference::axum::router("/docs", &scalar_config)) // TODO: waiting on scalar_api_reference to re-publish 87 94 .layer(axum::Extension(bus)) 88 95 .layer(axum::Extension(pool)) 89 96 .layer(tower_http::trace::TraceLayer::new_for_http());