tangled
alpha
login
or
join now
eldridge.cam
/
cartography
0
fork
atom
Trading card city builder game?
0
fork
atom
overview
issues
pulls
pipelines
add api for creating new field
eldridge.cam
4 weeks ago
1643bd1e
7fede6f6
verified
This commit was signed with the committer's
known signature
.
eldridge.cam
SSH Key Fingerprint:
SHA256:MAgO4sya2MgvdgUjSGKAO0lQ9X2HQp1Jb+x/Tpeeims=
0/0
Waiting for spindle ...
+272
-2
9 changed files
expand all
collapse all
unified
split
.sqlx
query-4cdc9fb57670878f0024a6b490079ac90dcdc9631e3444399bc82d72e6d24fa2.json
query-54b77f4805439e169ab88acd84b999629ff47128c346969b463a786f6afb4dd5.json
query-a826246de180b0e3be96a50c355cb86389f8fb706366de54e5011afaa2d1c0f1.json
Cargo.lock
packages
cartography
Cargo.toml
src
api
operations
create_field.rs
mod.rs
app.rs
test.rs
+12
.sqlx/query-4cdc9fb57670878f0024a6b490079ac90dcdc9631e3444399bc82d72e6d24fa2.json
reviewed
···
1
1
+
{
2
2
+
"db_name": "PostgreSQL",
3
3
+
"query": "INSERT INTO fields (account_id, name) VALUES ('foxfriends', 'Field 1')",
4
4
+
"describe": {
5
5
+
"columns": [],
6
6
+
"parameters": {
7
7
+
"Left": []
8
8
+
},
9
9
+
"nullable": []
10
10
+
},
11
11
+
"hash": "4cdc9fb57670878f0024a6b490079ac90dcdc9631e3444399bc82d72e6d24fa2"
12
12
+
}
+34
.sqlx/query-54b77f4805439e169ab88acd84b999629ff47128c346969b463a786f6afb4dd5.json
reviewed
···
1
1
+
{
2
2
+
"db_name": "PostgreSQL",
3
3
+
"query": "INSERT INTO fields (name, account_id) VALUES ($1, $2) RETURNING id, name",
4
4
+
"describe": {
5
5
+
"columns": [
6
6
+
{
7
7
+
"ordinal": 0,
8
8
+
"name": "id",
9
9
+
"type_info": "Int8"
10
10
+
},
11
11
+
{
12
12
+
"ordinal": 1,
13
13
+
"name": "name",
14
14
+
"type_info": "Text"
15
15
+
}
16
16
+
],
17
17
+
"parameters": {
18
18
+
"Left": [
19
19
+
"Text",
20
20
+
{
21
21
+
"Custom": {
22
22
+
"name": "citext",
23
23
+
"kind": "Simple"
24
24
+
}
25
25
+
}
26
26
+
]
27
27
+
},
28
28
+
"nullable": [
29
29
+
false,
30
30
+
false
31
31
+
]
32
32
+
},
33
33
+
"hash": "54b77f4805439e169ab88acd84b999629ff47128c346969b463a786f6afb4dd5"
34
34
+
}
+20
.sqlx/query-a826246de180b0e3be96a50c355cb86389f8fb706366de54e5011afaa2d1c0f1.json
reviewed
···
1
1
+
{
2
2
+
"db_name": "PostgreSQL",
3
3
+
"query": "INSERT INTO fields (account_id, name) VALUES ('foxfriends', 'Field 1') RETURNING id",
4
4
+
"describe": {
5
5
+
"columns": [
6
6
+
{
7
7
+
"ordinal": 0,
8
8
+
"name": "id",
9
9
+
"type_info": "Int8"
10
10
+
}
11
11
+
],
12
12
+
"parameters": {
13
13
+
"Left": []
14
14
+
},
15
15
+
"nullable": [
16
16
+
false
17
17
+
]
18
18
+
},
19
19
+
"hash": "a826246de180b0e3be96a50c355cb86389f8fb706366de54e5011afaa2d1c0f1"
20
20
+
}
+12
Cargo.lock
reviewed
···
146
146
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
147
147
dependencies = [
148
148
"axum-core 0.5.6",
149
149
+
"axum-macros",
149
150
"base64",
150
151
"bytes",
151
152
"form_urlencoded",
···
258
259
"tower-layer",
259
260
"tower-service",
260
261
"tracing",
262
262
+
]
263
263
+
264
264
+
[[package]]
265
265
+
name = "axum-macros"
266
266
+
version = "0.5.0"
267
267
+
source = "registry+https://github.com/rust-lang/crates.io-index"
268
268
+
checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
269
269
+
dependencies = [
270
270
+
"proc-macro2",
271
271
+
"quote",
272
272
+
"syn",
261
273
]
262
274
263
275
[[package]]
+1
-1
packages/cartography/Cargo.toml
reviewed
···
12
12
13
13
[dependencies]
14
14
anyhow = "1.0.101"
15
15
-
axum = { version = "0.8.8", features = ["ws"] }
15
15
+
axum = { version = "0.8.8", features = ["ws", "macros"] }
16
16
axum-extra = { version = "0.12.5", features = ["typed-header"] }
17
17
clap = { version = "4.5.58", features = ["derive"] }
18
18
derive_more = { version = "2.1.1", features = ["error", "display"] }
+185
packages/cartography/src/api/operations/create_field.rs
reviewed
···
1
1
+
use crate::api::errors::internal_server_error;
2
2
+
use crate::api::middleware::authorization::Authorization;
3
3
+
use crate::dto::*;
4
4
+
use axum::extract::Path;
5
5
+
use axum::{Extension, Json};
6
6
+
7
7
+
#[derive(serde::Serialize, utoipa::ToSchema)]
8
8
+
#[cfg_attr(test, derive(serde::Deserialize))]
9
9
+
pub struct CreateFieldResponse {
10
10
+
field: Field,
11
11
+
}
12
12
+
13
13
+
#[derive(serde::Deserialize, utoipa::ToSchema)]
14
14
+
#[cfg_attr(test, derive(serde::Serialize))]
15
15
+
pub struct CreateFieldRequest {
16
16
+
name: String,
17
17
+
}
18
18
+
19
19
+
#[utoipa::path(
20
20
+
post,
21
21
+
path = "/api/v1/players/{player_id}/fields",
22
22
+
description = "Create field.",
23
23
+
tag = "Player",
24
24
+
security(("trust" = [])),
25
25
+
request_body = CreateFieldRequest,
26
26
+
responses(
27
27
+
(status = OK, description = "Field created", body = CreateFieldResponse),
28
28
+
),
29
29
+
params(
30
30
+
("player_id" = AccountIdOrMe, Path, description = "The ID of the player to create this field for.")
31
31
+
)
32
32
+
)]
33
33
+
pub async fn create_field(
34
34
+
db: Extension<sqlx::PgPool>,
35
35
+
authorization: Extension<Authorization>,
36
36
+
Path(account_id): Path<AccountIdOrMe>,
37
37
+
request: Json<CreateFieldRequest>,
38
38
+
) -> axum::response::Result<Json<CreateFieldResponse>> {
39
39
+
let account_id = authorization.resolve_account_id(&account_id)?;
40
40
+
authorization.require_authorization(account_id)?;
41
41
+
42
42
+
let mut conn = db.begin().await.map_err(internal_server_error)?;
43
43
+
let field = sqlx::query_as!(
44
44
+
Field,
45
45
+
"INSERT INTO fields (name, account_id) VALUES ($1, $2) RETURNING id, name",
46
46
+
request.name,
47
47
+
account_id,
48
48
+
)
49
49
+
.fetch_one(&mut *conn)
50
50
+
.await
51
51
+
.map_err(internal_server_error)?;
52
52
+
53
53
+
Ok(Json(CreateFieldResponse { field }))
54
54
+
}
55
55
+
56
56
+
#[cfg(test)]
57
57
+
mod tests {
58
58
+
use crate::test::prelude::*;
59
59
+
use axum::http::{Request, StatusCode};
60
60
+
use sqlx::PgPool;
61
61
+
62
62
+
use super::*;
63
63
+
64
64
+
#[sqlx::test(
65
65
+
migrator = "MIGRATOR",
66
66
+
fixtures(path = "../../../fixtures", scripts("account"))
67
67
+
)]
68
68
+
pub fn create_field(pool: PgPool) {
69
69
+
let app = crate::app::Config::test(pool).into_router();
70
70
+
71
71
+
let request = Request::post("/api/v1/players/@me/fields")
72
72
+
.header("Authorization", "Trust foxfriends")
73
73
+
.json(CreateFieldRequest {
74
74
+
name: "New Field".to_owned(),
75
75
+
})
76
76
+
.unwrap();
77
77
+
78
78
+
let Ok(response) = app.oneshot(request).await;
79
79
+
assert_success!(response);
80
80
+
81
81
+
let response: CreateFieldResponse = response.json().await.unwrap();
82
82
+
assert_eq!(response.field.name, "New Field");
83
83
+
}
84
84
+
85
85
+
#[sqlx::test(
86
86
+
migrator = "MIGRATOR",
87
87
+
fixtures(path = "../../../fixtures", scripts("account"))
88
88
+
)]
89
89
+
pub fn create_multiple_fields(pool: PgPool) {
90
90
+
sqlx::query!("INSERT INTO fields (account_id, name) VALUES ('foxfriends', 'Field 1')")
91
91
+
.execute(&pool)
92
92
+
.await
93
93
+
.unwrap();
94
94
+
let app = crate::app::Config::test(pool).into_router();
95
95
+
96
96
+
let request = Request::post("/api/v1/players/@me/fields")
97
97
+
.header("Authorization", "Trust foxfriends")
98
98
+
.json(CreateFieldRequest {
99
99
+
name: "Field 2".to_owned(),
100
100
+
})
101
101
+
.unwrap();
102
102
+
103
103
+
let Ok(response) = app.oneshot(request).await;
104
104
+
assert_success!(response);
105
105
+
106
106
+
let response: CreateFieldResponse = response.json().await.unwrap();
107
107
+
assert_eq!(response.field.name, "Field 2");
108
108
+
}
109
109
+
110
110
+
#[sqlx::test(
111
111
+
migrator = "MIGRATOR",
112
112
+
fixtures(path = "../../../fixtures", scripts("account"))
113
113
+
)]
114
114
+
pub fn create_field_same_name(pool: PgPool) {
115
115
+
let field = sqlx::query!(
116
116
+
"INSERT INTO fields (account_id, name) VALUES ('foxfriends', 'Field 1') RETURNING id"
117
117
+
)
118
118
+
.fetch_one(&pool)
119
119
+
.await
120
120
+
.unwrap();
121
121
+
let app = crate::app::Config::test(pool).into_router();
122
122
+
123
123
+
let request = Request::post("/api/v1/players/@me/fields")
124
124
+
.header("Authorization", "Trust foxfriends")
125
125
+
.json(CreateFieldRequest {
126
126
+
name: "Field 1".to_owned(),
127
127
+
})
128
128
+
.unwrap();
129
129
+
130
130
+
let Ok(response) = app.oneshot(request).await;
131
131
+
assert_success!(response);
132
132
+
133
133
+
let response: CreateFieldResponse = response.json().await.unwrap();
134
134
+
assert_ne!(response.field.id, field.id);
135
135
+
assert_eq!(response.field.name, "Field 1");
136
136
+
}
137
137
+
138
138
+
#[sqlx::test(
139
139
+
migrator = "MIGRATOR",
140
140
+
fixtures(path = "../../../fixtures", scripts("account"))
141
141
+
)]
142
142
+
pub fn create_field_body_required(pool: PgPool) {
143
143
+
let app = crate::app::Config::test(pool).into_router();
144
144
+
145
145
+
let request = Request::post("/api/v1/players/foxfriends/fields")
146
146
+
.header("Authorization", "Trust not-foxfriends")
147
147
+
.json(serde_json::json!({}))
148
148
+
.unwrap();
149
149
+
150
150
+
let Ok(response) = app.oneshot(request).await;
151
151
+
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
152
152
+
}
153
153
+
154
154
+
#[sqlx::test(migrator = "MIGRATOR")]
155
155
+
pub fn create_field_unauthorized(pool: PgPool) {
156
156
+
let app = crate::app::Config::test(pool).into_router();
157
157
+
158
158
+
let request = Request::post("/api/v1/players/@me/fields")
159
159
+
.json(CreateFieldRequest {
160
160
+
name: "Field".to_owned(),
161
161
+
})
162
162
+
.unwrap();
163
163
+
164
164
+
let Ok(response) = app.oneshot(request).await;
165
165
+
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
166
166
+
}
167
167
+
168
168
+
#[sqlx::test(
169
169
+
migrator = "MIGRATOR",
170
170
+
fixtures(path = "../../../fixtures", scripts("account"))
171
171
+
)]
172
172
+
pub fn create_field_wrong_user_forbidden(pool: PgPool) {
173
173
+
let app = crate::app::Config::test(pool).into_router();
174
174
+
175
175
+
let request = Request::post("/api/v1/players/foxfriends/fields")
176
176
+
.header("Authorization", "Trust not-foxfriends")
177
177
+
.json(CreateFieldRequest {
178
178
+
name: "Field".to_owned(),
179
179
+
})
180
180
+
.unwrap();
181
181
+
182
182
+
let Ok(response) = app.oneshot(request).await;
183
183
+
assert_eq!(response.status(), StatusCode::FORBIDDEN);
184
184
+
}
185
185
+
}
+2
packages/cartography/src/api/operations/mod.rs
reviewed
···
8
8
mod list_card_types;
9
9
pub use list_card_types::*;
10
10
11
11
+
mod create_field;
11
12
mod list_fields;
13
13
+
pub use create_field::*;
12
14
pub use list_fields::*;
13
15
14
16
mod get_pack;
+5
packages/cartography/src/app.rs
reviewed
···
15
15
operations::list_card_types,
16
16
17
17
operations::list_fields,
18
18
+
operations::create_field,
18
19
19
20
operations::list_packs,
20
21
operations::get_pack,
···
100
101
.route(
101
102
"/api/v1/players/{player_id}/fields",
102
103
axum::routing::get(operations::list_fields),
104
104
+
)
105
105
+
.route(
106
106
+
"/api/v1/players/{player_id}/fields",
107
107
+
axum::routing::post(operations::create_field),
103
108
)
104
109
.route(
105
110
"/api/v1/players/{player_id}/packs",
+1
-1
packages/cartography/src/test.rs
reviewed
···
86
86
async fn handle(
87
87
&mut self,
88
88
_msg: TakeCollection,
89
89
-
ctx: &mut kameo::prelude::Context<Self, Self::Reply>,
89
89
+
_ctx: &mut kameo::prelude::Context<Self, Self::Reply>,
90
90
) -> Self::Reply {
91
91
std::mem::take(&mut self.0)
92
92
}