Trading card city builder game?

add endpoint for list packs

eldridge.cam ad8d57d8 fb290cd4

verified
Waiting for spindle ...
+359 -44
+4 -15
.sqlx/query-6f75a49a622c23453b9ba57afaecb7e26b6421ba6ae2c3b78fa194ebafd94033.json .sqlx/query-eb607dd49b5f6e9f1f70f2624fbc1e903ed1e8feaba2cbf00584b7d5994d6692.json
··· 1 1 { 2 2 "db_name": "PostgreSQL", 3 - "query": "\n WITH inserted_pack AS (\n INSERT INTO packs (account_id, pack_banner_id, seed, algorithm)\n VALUES ($1, $2, $3, 'xoshiro256++')\n RETURNING id, account_id, pack_banner_id, opened_at\n ),\n\n inserted_tile_cards AS (\n INSERT INTO cards (card_type_id)\n VALUES (UNNEST($4::text []))\n RETURNING *\n ),\n\n inserted_tiles AS (\n INSERT INTO tiles (id, tile_type_id, name)\n SELECT id, card_type_id, card_type_id\n FROM inserted_tile_cards\n RETURNING id\n ),\n\n inserted_citizen_cards AS (\n INSERT INTO cards (card_type_id)\n VALUES (UNNEST($5::text []))\n RETURNING *\n ),\n\n inserted_citizens AS (\n INSERT INTO citizens (id, species_id, name)\n SELECT id, card_type_id, card_type_id\n FROM inserted_citizen_cards\n RETURNING id\n ),\n\n inserted_cards AS (\n SELECT * FROM inserted_tile_cards\n UNION ALL\n SELECT * FROM inserted_citizen_cards\n ),\n\n inserted_pack_contents AS (\n INSERT INTO pack_contents (pack_id, \"position\", card_id)\n SELECT inserted_pack.id, ROW_NUMBER() OVER (), inserted_cards.id\n FROM inserted_pack, inserted_cards\n RETURNING *\n )\n\n SELECT\n inserted_pack.*,\n JSONB_AGG(\n JSONB_BUILD_OBJECT(\n 'id', inserted_cards.id,\n 'card_type_id', inserted_cards.card_type_id\n )\n ) AS \"pack_cards: sqlx::types::Json<Vec<Card>>\"\n FROM inserted_pack, inserted_cards\n GROUP BY\n inserted_pack.id,\n inserted_pack.account_id,\n inserted_pack.pack_banner_id,\n inserted_pack.opened_at\n ", 3 + "query": "\n WITH inserted_pack AS (\n INSERT INTO packs (account_id, pack_banner_id, seed, algorithm)\n VALUES ($1, $2, $3, 'xoshiro256++')\n RETURNING id, account_id, pack_banner_id, opened_at\n ),\n\n inserted_tile_cards AS (\n INSERT INTO cards (card_type_id)\n VALUES (UNNEST($4::text []))\n RETURNING *\n ),\n\n inserted_tiles AS (\n INSERT INTO tiles (id, tile_type_id, name)\n SELECT id, card_type_id, card_type_id\n FROM inserted_tile_cards\n RETURNING id\n ),\n\n inserted_citizen_cards AS (\n INSERT INTO cards (card_type_id)\n VALUES (UNNEST($5::text []))\n RETURNING *\n ),\n\n inserted_citizens AS (\n INSERT INTO citizens (id, species_id, name)\n SELECT id, card_type_id, card_type_id\n FROM inserted_citizen_cards\n RETURNING id\n ),\n\n inserted_cards AS (\n SELECT * FROM inserted_tile_cards\n UNION ALL\n SELECT * FROM inserted_citizen_cards\n ),\n\n inserted_pack_contents AS (\n INSERT INTO pack_contents (pack_id, \"position\", card_id)\n SELECT inserted_pack.id, ROW_NUMBER() OVER (), inserted_cards.id\n FROM inserted_pack, inserted_cards\n RETURNING *\n )\n\n SELECT\n inserted_pack.id,\n inserted_pack.pack_banner_id,\n inserted_pack.opened_at,\n JSONB_AGG(\n JSONB_BUILD_OBJECT(\n 'id', inserted_cards.id,\n 'card_type_id', inserted_cards.card_type_id\n )\n ) AS \"pack_cards: sqlx::types::Json<Vec<Card>>\"\n FROM inserted_pack, inserted_cards\n GROUP BY\n inserted_pack.id,\n inserted_pack.pack_banner_id,\n inserted_pack.opened_at\n ", 4 4 "describe": { 5 5 "columns": [ 6 6 { ··· 10 10 }, 11 11 { 12 12 "ordinal": 1, 13 - "name": "account_id", 14 - "type_info": { 15 - "Custom": { 16 - "name": "citext", 17 - "kind": "Simple" 18 - } 19 - } 20 - }, 21 - { 22 - "ordinal": 2, 23 13 "name": "pack_banner_id", 24 14 "type_info": "Text" 25 15 }, 26 16 { 27 - "ordinal": 3, 17 + "ordinal": 2, 28 18 "name": "opened_at", 29 19 "type_info": "Timestamptz" 30 20 }, 31 21 { 32 - "ordinal": 4, 22 + "ordinal": 3, 33 23 "name": "pack_cards: sqlx::types::Json<Vec<Card>>", 34 24 "type_info": "Jsonb" 35 25 } ··· 51 41 "nullable": [ 52 42 false, 53 43 false, 54 - false, 55 44 true, 56 45 null 57 46 ] 58 47 }, 59 - "hash": "6f75a49a622c23453b9ba57afaecb7e26b6421ba6ae2c3b78fa194ebafd94033" 48 + "hash": "eb607dd49b5f6e9f1f70f2624fbc1e903ed1e8feaba2cbf00584b7d5994d6692" 60 49 }
+41
.sqlx/query-e3975b60128580f1a0d7d91618761af2eb13c5bf95a32a4a0fd82cb1d7f23a82.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "\n SELECT packs.id, packs.pack_banner_id, packs.opened_at\n FROM packs\n WHERE\n account_id = $1\n AND (($2 AND opened_at IS NULL) OR ($3 AND opened_at IS NOT NULL))\n ", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "id", 9 + "type_info": "Int8" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "pack_banner_id", 14 + "type_info": "Text" 15 + }, 16 + { 17 + "ordinal": 2, 18 + "name": "opened_at", 19 + "type_info": "Timestamptz" 20 + } 21 + ], 22 + "parameters": { 23 + "Left": [ 24 + { 25 + "Custom": { 26 + "name": "citext", 27 + "kind": "Simple" 28 + } 29 + }, 30 + "Bool", 31 + "Bool" 32 + ] 33 + }, 34 + "nullable": [ 35 + false, 36 + false, 37 + true 38 + ] 39 + }, 40 + "hash": "e3975b60128580f1a0d7d91618761af2eb13c5bf95a32a4a0fd82cb1d7f23a82" 41 + }
+1 -1
app/orval.config.ts
··· 1 1 import { defineConfig } from "orval"; 2 2 3 3 export default defineConfig({ 4 - petstore: { 4 + cartography: { 5 5 output: { 6 6 mode: "single", 7 7 target: "src/lib/appserver/api.ts",
+57
packages/cartography/fixtures/packs.sql
··· 1 + WITH inserted_packs AS ( 2 + INSERT INTO packs (account_id, pack_banner_id, opened_at, seed, algorithm) 3 + VALUES 4 + ('foxfriends', 'base-standard', null, 1, 'constant'), 5 + ('foxfriends', 'base-standard', null, 2, 'constant'), 6 + ('foxfriends', 'base-standard', now(), 3, 'constant') 7 + RETURNING id, seed 8 + ), 9 + 10 + pack_one_cards AS ( 11 + INSERT INTO cards (card_type_id) VALUES ('bread-bakery'), 12 + ('salad-shop'), 13 + ('grain-farm'), 14 + ('cat-colony'), 15 + ('water-well') 16 + RETURNING *, 1 as seed 17 + ), 18 + 19 + pack_two_cards AS ( 20 + INSERT INTO cards (card_type_id) VALUES ('bread-bakery'), 21 + ('salad-shop'), 22 + ('grain-farm'), 23 + ('cat-colony'), 24 + ('water-well') 25 + RETURNING *, 2 as seed 26 + ), 27 + 28 + pack_three_cards AS ( 29 + INSERT INTO cards (card_type_id) VALUES ('bread-bakery'), 30 + ('salad-shop'), 31 + ('grain-farm'), 32 + ('cat-colony'), 33 + ('water-well') 34 + RETURNING *, 3 as seed 35 + ), 36 + 37 + all_cards AS ( 38 + SELECT * FROM pack_one_cards 39 + UNION ALL 40 + SELECT * FROM pack_two_cards 41 + UNION ALL 42 + SELECT * FROM pack_three_cards 43 + ), 44 + 45 + inserted_tiles AS ( -- noqa: ST03 46 + INSERT INTO tiles (id, tile_type_id, name) 47 + SELECT id, card_type_id, card_type_id AS name 48 + FROM all_cards 49 + ) 50 + 51 + INSERT INTO pack_contents (pack_id, card_id, "position") 52 + SELECT 53 + inserted_packs.id AS pack_id, 54 + all_cards.id AS card_id, 55 + row_number() OVER (PARTITION BY inserted_packs.id) AS "position" 56 + FROM inserted_packs 57 + INNER JOIN all_cards ON inserted_packs.seed = all_cards.seed
+2 -2
packages/cartography/src/api/operations/get_banner.rs
··· 81 81 use crate::api::errors::{ApiError, BannerNotFoundError, ErrorDetailResponse}; 82 82 use crate::test::prelude::*; 83 83 use axum::http::Request; 84 - use axum::{body::Body, http::StatusCode}; 84 + use axum::http::StatusCode; 85 85 use sqlx::PgPool; 86 86 use time::{Date, Month, OffsetDateTime, Time}; 87 87 ··· 95 95 let app = crate::app::Config::test(pool).into_router(); 96 96 97 97 let request = Request::get("/api/v1/banners/base-standard") 98 - .body(Body::empty()) 98 + .empty() 99 99 .unwrap(); 100 100 101 101 let Ok(response) = app.oneshot(request).await;
+17 -16
packages/cartography/src/api/operations/list_banners.rs
··· 8 8 banners: Vec<PackBanner>, 9 9 } 10 10 11 - fn default_active() -> Vec<Status> { 12 - vec![Status::Active] 11 + fn default_active() -> Vec<BannerStatus> { 12 + vec![BannerStatus::Active] 13 13 } 14 14 15 15 #[derive(serde::Deserialize, utoipa::ToSchema)] ··· 17 17 #[schema(default)] 18 18 pub struct ListBannersRequest { 19 19 #[serde(default = "default_active")] 20 - status: Vec<Status>, 20 + status: Vec<BannerStatus>, 21 21 } 22 22 23 23 impl Default for ListBannersRequest { 24 24 fn default() -> Self { 25 25 Self { 26 - status: vec![Status::Active], 26 + status: vec![BannerStatus::Active], 27 27 } 28 28 } 29 29 } ··· 31 31 #[derive( 32 32 PartialEq, Eq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, utoipa::ToSchema, 33 33 )] 34 - pub enum Status { 34 + pub enum BannerStatus { 35 35 Done, 36 36 Active, 37 37 Upcoming, ··· 67 67 OR ($3 AND start_date > NOW()) 68 68 ORDER BY start_date ASC 69 69 "#, 70 - request.status.contains(&Status::Done), 71 - request.status.contains(&Status::Active), 72 - request.status.contains(&Status::Upcoming), 70 + request.status.contains(&BannerStatus::Done), 71 + request.status.contains(&BannerStatus::Active), 72 + request.status.contains(&BannerStatus::Upcoming), 73 73 ) 74 74 .fetch_all(&mut *conn) 75 75 .await ··· 88 88 #[cfg(test)] 89 89 mod tests { 90 90 use crate::test::prelude::*; 91 - use axum::body::Body; 92 91 use axum::http::Request; 93 92 use sqlx::PgPool; 94 93 95 - use super::{ListBannersRequest, ListBannersResponse, Status}; 94 + use super::{BannerStatus, ListBannersRequest, ListBannersResponse}; 96 95 97 96 #[sqlx::test( 98 97 migrator = "MIGRATOR", ··· 101 100 async fn list_banners_default_active(pool: PgPool) { 102 101 let app = crate::app::Config::test(pool).into_router(); 103 102 104 - let request = Request::post("/api/v1/banners") 105 - .body(Body::empty()) 106 - .unwrap(); 103 + let request = Request::post("/api/v1/banners").empty().unwrap(); 107 104 108 105 let Ok(response) = app.oneshot(request).await; 109 106 assert_success!(response); ··· 122 119 123 120 let request = Request::post("/api/v1/banners") 124 121 .json(ListBannersRequest { 125 - status: vec![Status::Done, Status::Active, Status::Upcoming], 122 + status: vec![ 123 + BannerStatus::Done, 124 + BannerStatus::Active, 125 + BannerStatus::Upcoming, 126 + ], 126 127 }) 127 128 .unwrap(); 128 129 ··· 145 146 146 147 let request = Request::post("/api/v1/banners") 147 148 .json(ListBannersRequest { 148 - status: vec![Status::Done], 149 + status: vec![BannerStatus::Done], 149 150 }) 150 151 .unwrap(); 151 152 ··· 166 167 167 168 let request = Request::post("/api/v1/banners") 168 169 .json(ListBannersRequest { 169 - status: vec![Status::Upcoming], 170 + status: vec![BannerStatus::Upcoming], 170 171 }) 171 172 .unwrap(); 172 173
+1 -4
packages/cartography/src/api/operations/list_card_types.rs
··· 62 62 #[cfg(test)] 63 63 mod tests { 64 64 use crate::test::prelude::*; 65 - use axum::body::Body; 66 65 use axum::http::Request; 67 66 use sqlx::PgPool; 68 67 ··· 75 74 async fn get_banner_ok(pool: PgPool) { 76 75 let app = crate::app::Config::test(pool).into_router(); 77 76 78 - let request = Request::get("/api/v1/cardtypes") 79 - .body(Body::empty()) 80 - .unwrap(); 77 + let request = Request::get("/api/v1/cardtypes").empty().unwrap(); 81 78 82 79 let Ok(response) = app.oneshot(request).await; 83 80 assert_success!(response);
+1
packages/cartography/src/api/operations/list_fields.rs
··· 14 14 path = "/api/v1/players/{player_id}/fields", 15 15 description = "List player fields.", 16 16 tag = "Player", 17 + security(("trust" = [])), 17 18 responses( 18 19 (status = OK, description = "Successfully listed all fields.", body = ListFieldsResponse), 19 20 ),
+205
packages/cartography/src/api/operations/list_packs.rs
··· 1 + use crate::api::{errors::internal_server_error, middleware::authorization::Authorization}; 2 + use crate::dto::*; 3 + use axum::{Extension, Json, extract::Path}; 4 + 5 + #[derive(serde::Serialize, utoipa::ToSchema)] 6 + #[cfg_attr(test, derive(serde::Deserialize))] 7 + pub struct ListPacksResponse { 8 + packs: Vec<Pack>, 9 + } 10 + 11 + fn default_unopened() -> Vec<PackStatus> { 12 + vec![PackStatus::Unopened] 13 + } 14 + 15 + #[derive(serde::Deserialize, utoipa::ToSchema)] 16 + #[cfg_attr(test, derive(serde::Serialize))] 17 + #[schema(default)] 18 + pub struct ListPacksRequest { 19 + #[serde(default = "default_unopened")] 20 + status: Vec<PackStatus>, 21 + } 22 + 23 + impl Default for ListPacksRequest { 24 + fn default() -> Self { 25 + Self { 26 + status: vec![PackStatus::Unopened], 27 + } 28 + } 29 + } 30 + 31 + #[derive( 32 + PartialEq, Eq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, utoipa::ToSchema, 33 + )] 34 + pub enum PackStatus { 35 + Opened, 36 + Unopened, 37 + } 38 + 39 + #[utoipa::path( 40 + post, 41 + path = "/api/v1/players/{player_id}/packs", 42 + description = "List player packs.", 43 + tag = "Player", 44 + request_body = Option<ListPacksRequest>, 45 + security(("trust" = [])), 46 + responses( 47 + (status = OK, description = "Successfully listed all fields.", body = ListPacksResponse), 48 + ), 49 + params( 50 + ("player_id" = AccountIdOrMe, Path, description = "The ID of the player whose fields to list.") 51 + ) 52 + )] 53 + pub async fn list_packs( 54 + db: axum::Extension<sqlx::PgPool>, 55 + Extension(authorization): Extension<Authorization>, 56 + Path(account_id): Path<AccountIdOrMe>, 57 + request: Option<Json<ListPacksRequest>>, 58 + ) -> axum::response::Result<Json<ListPacksResponse>> { 59 + let request = request.unwrap_or_default(); 60 + let account_id = authorization.resolve_account_id(&account_id)?; 61 + authorization.require_authorization(account_id)?; 62 + 63 + let mut conn = db.acquire().await.map_err(internal_server_error)?; 64 + let packs = sqlx::query_as!( 65 + Pack, 66 + r#" 67 + SELECT packs.id, packs.pack_banner_id, packs.opened_at 68 + FROM packs 69 + WHERE 70 + account_id = $1 71 + AND (($2 AND opened_at IS NULL) OR ($3 AND opened_at IS NOT NULL)) 72 + "#, 73 + account_id, 74 + request.status.contains(&PackStatus::Unopened), 75 + request.status.contains(&PackStatus::Opened), 76 + ) 77 + .fetch_all(&mut *conn) 78 + .await 79 + .map_err(internal_server_error)?; 80 + Ok(Json(ListPacksResponse { packs })) 81 + } 82 + 83 + #[cfg(test)] 84 + mod tests { 85 + use crate::{api::operations::PackStatus, test::prelude::*}; 86 + use axum::http::{Request, StatusCode}; 87 + use sqlx::PgPool; 88 + 89 + use super::{ListPacksRequest, ListPacksResponse}; 90 + 91 + #[sqlx::test( 92 + migrator = "MIGRATOR", 93 + fixtures(path = "../../../fixtures", scripts("account")) 94 + )] 95 + pub fn list_packs_none(pool: PgPool) { 96 + let app = crate::app::Config::test(pool).into_router(); 97 + 98 + let request = Request::post("/api/v1/players/@me/packs") 99 + .header("Authorization", "Trust foxfriends") 100 + .empty() 101 + .unwrap(); 102 + 103 + let Ok(response) = app.oneshot(request).await; 104 + assert_success!(response); 105 + 106 + let response: ListPacksResponse = response.json().await.unwrap(); 107 + assert!(response.packs.is_empty()); 108 + } 109 + 110 + #[sqlx::test( 111 + migrator = "MIGRATOR", 112 + fixtures(path = "../../../fixtures", scripts("seed", "account", "packs")) 113 + )] 114 + pub fn list_packs_by_name(pool: PgPool) { 115 + let app = crate::app::Config::test(pool).into_router(); 116 + 117 + let request = Request::post("/api/v1/players/foxfriends/packs") 118 + .header("Authorization", "Trust foxfriends") 119 + .json(ListPacksRequest { 120 + status: vec![PackStatus::Unopened], 121 + }) 122 + .unwrap(); 123 + 124 + let Ok(response) = app.oneshot(request).await; 125 + assert_success!(response); 126 + 127 + let response: ListPacksResponse = response.json().await.unwrap(); 128 + assert_eq!(response.packs.len(), 2); 129 + } 130 + 131 + #[sqlx::test( 132 + migrator = "MIGRATOR", 133 + fixtures(path = "../../../fixtures", scripts("seed", "account", "packs")) 134 + )] 135 + pub fn list_packs_default_unopened(pool: PgPool) { 136 + let app = crate::app::Config::test(pool).into_router(); 137 + 138 + let request = Request::post("/api/v1/players/@me/packs") 139 + .header("Authorization", "Trust foxfriends") 140 + .empty() 141 + .unwrap(); 142 + 143 + let Ok(response) = app.oneshot(request).await; 144 + assert_success!(response); 145 + 146 + let response: ListPacksResponse = response.json().await.unwrap(); 147 + assert_eq!(response.packs.len(), 2); 148 + } 149 + 150 + #[sqlx::test( 151 + migrator = "MIGRATOR", 152 + fixtures(path = "../../../fixtures", scripts("seed", "account", "packs")) 153 + )] 154 + pub fn list_packs_opened(pool: PgPool) { 155 + let app = crate::app::Config::test(pool).into_router(); 156 + 157 + let request = Request::post("/api/v1/players/@me/packs") 158 + .header("Authorization", "Trust foxfriends") 159 + .json(ListPacksRequest { 160 + status: vec![PackStatus::Opened], 161 + }) 162 + .unwrap(); 163 + 164 + let Ok(response) = app.oneshot(request).await; 165 + assert_success!(response); 166 + 167 + let response: ListPacksResponse = response.json().await.unwrap(); 168 + assert_eq!(response.packs.len(), 1); 169 + } 170 + 171 + #[sqlx::test( 172 + migrator = "MIGRATOR", 173 + fixtures(path = "../../../fixtures", scripts("seed", "account", "packs")) 174 + )] 175 + pub fn list_packs_all(pool: PgPool) { 176 + let app = crate::app::Config::test(pool).into_router(); 177 + 178 + let request = Request::post("/api/v1/players/@me/packs") 179 + .header("Authorization", "Trust foxfriends") 180 + .json(ListPacksRequest { 181 + status: vec![PackStatus::Unopened, PackStatus::Opened], 182 + }) 183 + .unwrap(); 184 + 185 + let Ok(response) = app.oneshot(request).await; 186 + assert_success!(response); 187 + 188 + let response: ListPacksResponse = response.json().await.unwrap(); 189 + assert_eq!(response.packs.len(), 3); 190 + } 191 + 192 + #[sqlx::test(migrator = "MIGRATOR")] 193 + pub fn list_packs_requires_authentication(pool: PgPool) { 194 + let app = crate::app::Config::test(pool).into_router(); 195 + 196 + let request = Request::post("/api/v1/players/foxfriends/packs") 197 + .json(ListPacksRequest { 198 + status: vec![PackStatus::Unopened, PackStatus::Opened], 199 + }) 200 + .unwrap(); 201 + 202 + let Ok(response) = app.oneshot(request).await; 203 + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 204 + } 205 + }
+3
packages/cartography/src/api/operations/mod.rs
··· 10 10 11 11 mod list_fields; 12 12 pub use list_fields::*; 13 + 14 + mod list_packs; 15 + pub use list_packs::*;
+4 -4
packages/cartography/src/api/operations/pull_banner.rs
··· 22 22 path = "/api/v1/banners/{banner_id}/pull", 23 23 description = "Get full pack banner details.", 24 24 tag = "Game", 25 + security(("trust" = [])), 25 26 params( 26 27 ("banner_id" = String, Path, description = "Banner ID"), 27 28 ), ··· 143 144 ) 144 145 145 146 SELECT 146 - inserted_pack.*, 147 + inserted_pack.id, 148 + inserted_pack.pack_banner_id, 149 + inserted_pack.opened_at, 147 150 JSONB_AGG( 148 151 JSONB_BUILD_OBJECT( 149 152 'id', inserted_cards.id, ··· 153 156 FROM inserted_pack, inserted_cards 154 157 GROUP BY 155 158 inserted_pack.id, 156 - inserted_pack.account_id, 157 159 inserted_pack.pack_banner_id, 158 160 inserted_pack.opened_at 159 161 "#, ··· 172 174 Ok(Json(PullBannerResponse { 173 175 pack: Pack { 174 176 id: pack.id, 175 - account_id: pack.account_id, 176 177 pack_banner_id: pack.pack_banner_id, 177 178 opened_at: pack.opened_at, 178 179 }, ··· 208 209 209 210 let response: PullBannerResponse = response.json().await.unwrap(); 210 211 assert_eq!(response.pack.pack_banner_id, "base-standard"); 211 - assert_eq!(response.pack.account_id, "foxfriends"); 212 212 assert_eq!(response.pack.opened_at, None); 213 213 assert_eq!(response.pack_cards.len(), 5); 214 214 }
+21
packages/cartography/src/app.rs
··· 2 2 use crate::bus::Bus; 3 3 use axum::Router; 4 4 use kameo::actor::Spawn as _; 5 + use utoipa::Modify; 6 + use utoipa::openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}; 5 7 6 8 #[derive(utoipa::OpenApi)] 7 9 #[openapi( ··· 13 15 operations::list_card_types, 14 16 15 17 operations::list_fields, 18 + 19 + operations::list_packs, 16 20 ), 17 21 components( 18 22 schemas( ··· 24 28 (name = "Player", description = "Player specific data; typically requires authorization."), 25 29 (name = "Game", description = "Actions with effects on gameplay."), 26 30 ), 31 + modifiers(&SecurityAddon) 27 32 )] 28 33 pub struct ApiDoc; 34 + 35 + struct SecurityAddon; 36 + 37 + impl Modify for SecurityAddon { 38 + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { 39 + openapi.components.as_mut().unwrap() 40 + .add_security_scheme( 41 + "trust", 42 + SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::with_description("Authorization", r#"DEVELOPMENT ONLY custom "auth" scheme where a header of the format `Trust {account_id}` is accepted as authorization for the identified account."#))), 43 + ); 44 + } 45 + } 29 46 30 47 pub struct Config { 31 48 pool: sqlx::PgPool, ··· 71 88 .route( 72 89 "/api/v1/players/{player_id}/fields", 73 90 axum::routing::get(operations::list_fields), 91 + ) 92 + .route( 93 + "/api/v1/players/{player_id}/packs", 94 + axum::routing::post(operations::list_packs), 74 95 ) 75 96 .route("/play/ws", axum::routing::any(ws::v1)) 76 97 .layer(axum::middleware::from_fn(middleware::authorization::trust))
-1
packages/cartography/src/dto/mod.rs
··· 60 60 #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, ToSchema)] 61 61 pub struct Pack { 62 62 pub id: i64, 63 - pub account_id: String, 64 63 pub pack_banner_id: String, 65 64 pub opened_at: Option<OffsetDateTime>, 66 65 }
+2 -1
packages/cartography/src/test.rs
··· 41 41 ($response:expr) => { 42 42 assert!( 43 43 $response.status().is_success(), 44 - "{}", 44 + "{}: {}", 45 + $response.status(), 45 46 $response 46 47 .text() 47 48 .await