tangled
alpha
login
or
join now
eldridge.cam
/
cartography
0
fork
atom
Trading card city builder game?
0
fork
atom
overview
issues
pulls
pipelines
filling in the server functionality that was lost
eldridge.cam
1 month ago
f716475b
fee7ac0c
verified
This commit was signed with the committer's
known signature
.
eldridge.cam
SSH Key Fingerprint:
SHA256:MAgO4sya2MgvdgUjSGKAO0lQ9X2HQp1Jb+x/Tpeeims=
+352
-70
16 changed files
expand all
collapse all
unified
split
.sqlx
query-4c464e0454d614dba55463a0332d09c67b56585eea7018d885a7bdc7a62400e5.json
query-d7a61570b34e911183eec8a3fd0b0c6ad64ef81a6b0581e5116aa3e0c1a52f2c.json
query-f4901c3731fabc8b0fc60129740902ce8fe74b73b088cbb632b34a3c7cf402ea.json
Cargo.lock
Cargo.toml
app
src
lib
appserver
socket
SocketV1.svelte.ts
SocketV1Protocol.ts
src
actor
field_state
mod.rs
mod.rs
player_socket
authenticate.rs
list_fields.rs
mod.rs
unsubscribe.rs
watch_field.rs
api
ws.rs
main.rs
+34
.sqlx/query-4c464e0454d614dba55463a0332d09c67b56585eea7018d885a7bdc7a62400e5.json
···
1
1
+
{
2
2
+
"db_name": "PostgreSQL",
3
3
+
"query": "\n SELECT tile_id AS \"id\", grid_x AS \"x\", grid_y AS \"y\"\n FROM field_tiles\n WHERE field_id = $1\n ",
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": "x",
14
14
+
"type_info": "Int4"
15
15
+
},
16
16
+
{
17
17
+
"ordinal": 2,
18
18
+
"name": "y",
19
19
+
"type_info": "Int4"
20
20
+
}
21
21
+
],
22
22
+
"parameters": {
23
23
+
"Left": [
24
24
+
"Int8"
25
25
+
]
26
26
+
},
27
27
+
"nullable": [
28
28
+
false,
29
29
+
false,
30
30
+
false
31
31
+
]
32
32
+
},
33
33
+
"hash": "4c464e0454d614dba55463a0332d09c67b56585eea7018d885a7bdc7a62400e5"
34
34
+
}
+22
.sqlx/query-d7a61570b34e911183eec8a3fd0b0c6ad64ef81a6b0581e5116aa3e0c1a52f2c.json
···
1
1
+
{
2
2
+
"db_name": "PostgreSQL",
3
3
+
"query": "SELECT name FROM fields WHERE id = $1",
4
4
+
"describe": {
5
5
+
"columns": [
6
6
+
{
7
7
+
"ordinal": 0,
8
8
+
"name": "name",
9
9
+
"type_info": "Text"
10
10
+
}
11
11
+
],
12
12
+
"parameters": {
13
13
+
"Left": [
14
14
+
"Int8"
15
15
+
]
16
16
+
},
17
17
+
"nullable": [
18
18
+
false
19
19
+
]
20
20
+
},
21
21
+
"hash": "d7a61570b34e911183eec8a3fd0b0c6ad64ef81a6b0581e5116aa3e0c1a52f2c"
22
22
+
}
+34
.sqlx/query-f4901c3731fabc8b0fc60129740902ce8fe74b73b088cbb632b34a3c7cf402ea.json
···
1
1
+
{
2
2
+
"db_name": "PostgreSQL",
3
3
+
"query": "\n SELECT citizen_id AS \"id\", grid_x AS \"x\", grid_y AS \"y\"\n FROM field_citizens\n WHERE field_id = $1\n ",
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": "x",
14
14
+
"type_info": "Int4"
15
15
+
},
16
16
+
{
17
17
+
"ordinal": 2,
18
18
+
"name": "y",
19
19
+
"type_info": "Int4"
20
20
+
}
21
21
+
],
22
22
+
"parameters": {
23
23
+
"Left": [
24
24
+
"Int8"
25
25
+
]
26
26
+
},
27
27
+
"nullable": [
28
28
+
false,
29
29
+
false,
30
30
+
false
31
31
+
]
32
32
+
},
33
33
+
"hash": "f4901c3731fabc8b0fc60129740902ce8fe74b73b088cbb632b34a3c7cf402ea"
34
34
+
}
+50
-6
Cargo.lock
···
147
147
"derive_more",
148
148
"futures",
149
149
"futures-rx",
150
150
+
"json-patch",
150
151
"kameo",
151
152
"rmp-serde",
152
153
"serde",
···
804
805
]
805
806
806
807
[[package]]
808
808
+
name = "json-patch"
809
809
+
version = "4.1.0"
810
810
+
source = "registry+https://github.com/rust-lang/crates.io-index"
811
811
+
checksum = "f300e415e2134745ef75f04562dd0145405c2f7fd92065db029ac4b16b57fe90"
812
812
+
dependencies = [
813
813
+
"jsonptr",
814
814
+
"serde",
815
815
+
"serde_json",
816
816
+
"thiserror 1.0.69",
817
817
+
"utoipa",
818
818
+
]
819
819
+
820
820
+
[[package]]
821
821
+
name = "jsonptr"
822
822
+
version = "0.7.1"
823
823
+
source = "registry+https://github.com/rust-lang/crates.io-index"
824
824
+
checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe"
825
825
+
dependencies = [
826
826
+
"serde",
827
827
+
"serde_json",
828
828
+
]
829
829
+
830
830
+
[[package]]
807
831
name = "kameo"
808
832
version = "0.19.2"
809
833
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1446
1470
"serde_json",
1447
1471
"sha2",
1448
1472
"smallvec",
1449
1449
-
"thiserror",
1473
1473
+
"thiserror 2.0.18",
1450
1474
"tokio",
1451
1475
"tokio-stream",
1452
1476
"tracing",
···
1528
1552
"smallvec",
1529
1553
"sqlx-core",
1530
1554
"stringprep",
1531
1531
-
"thiserror",
1555
1555
+
"thiserror 2.0.18",
1532
1556
"tracing",
1533
1557
"whoami",
1534
1558
]
···
1565
1589
"smallvec",
1566
1590
"sqlx-core",
1567
1591
"stringprep",
1568
1568
-
"thiserror",
1592
1592
+
"thiserror 2.0.18",
1569
1593
"tracing",
1570
1594
"whoami",
1571
1595
]
···
1589
1613
"serde",
1590
1614
"serde_urlencoded",
1591
1615
"sqlx-core",
1592
1592
-
"thiserror",
1616
1616
+
"thiserror 2.0.18",
1593
1617
"tracing",
1594
1618
"url",
1595
1619
]
···
1647
1671
1648
1672
[[package]]
1649
1673
name = "thiserror"
1674
1674
+
version = "1.0.69"
1675
1675
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1676
1676
+
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
1677
1677
+
dependencies = [
1678
1678
+
"thiserror-impl 1.0.69",
1679
1679
+
]
1680
1680
+
1681
1681
+
[[package]]
1682
1682
+
name = "thiserror"
1650
1683
version = "2.0.18"
1651
1684
source = "registry+https://github.com/rust-lang/crates.io-index"
1652
1685
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
1653
1686
dependencies = [
1654
1654
-
"thiserror-impl",
1687
1687
+
"thiserror-impl 2.0.18",
1688
1688
+
]
1689
1689
+
1690
1690
+
[[package]]
1691
1691
+
name = "thiserror-impl"
1692
1692
+
version = "1.0.69"
1693
1693
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1694
1694
+
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
1695
1695
+
dependencies = [
1696
1696
+
"proc-macro2",
1697
1697
+
"quote",
1698
1698
+
"syn",
1655
1699
]
1656
1700
1657
1701
[[package]]
···
1827
1871
"log",
1828
1872
"rand 0.9.2",
1829
1873
"sha1",
1830
1830
-
"thiserror",
1874
1874
+
"thiserror 2.0.18",
1831
1875
"utf-8",
1832
1876
]
1833
1877
+1
Cargo.toml
···
16
16
derive_more = { version = "2.1.1", features = ["error", "display"] }
17
17
futures = "0.3.31"
18
18
futures-rx = "0.2.1"
19
19
+
json-patch = { version = "4.1.0", features = ["utoipa"] }
19
20
kameo = "0.19.2"
20
21
rmp-serde = "1.3.1"
21
22
serde = { version = "1.0.228", features = ["derive"] }
+2
-2
app/src/lib/appserver/socket/SocketV1.svelte.ts
···
110
110
$watchField(data: { id: FieldId }, subscriber: (gameState: GameState | undefined) => void) {
111
111
let gameState: GameState | undefined = undefined;
112
112
this.#sendMessage({ type: "WatchField", data: data.id }).$subscribe((response) => {
113
113
-
if (response.type === "PutState") {
113
113
+
if (response.type === "PutFieldState") {
114
114
gameState = response.data;
115
115
-
} else if (response.type === "PatchState") {
115
115
+
} else if (response.type === "PatchFieldState") {
116
116
const patches = response.data;
117
117
gameState = jsonpatch.apply(gameState, patches);
118
118
}
+8
-7
app/src/lib/appserver/socket/SocketV1Protocol.ts
···
162
162
export const FieldList = Type.Object({ type: Type.Literal("FieldList"), data: Type.Array(Field) });
163
163
export type FieldList = StaticDecode<typeof FieldList>;
164
164
165
165
-
export const PutState = Type.Object({ type: Type.Literal("PutState"), data: GameState });
166
166
-
export type PutState = StaticDecode<typeof PutState>;
165
165
+
export const PutFieldState = Type.Object({ type: Type.Literal("PutFieldState"), data: GameState });
166
166
+
export type PutFieldState = StaticDecode<typeof PutFieldState>;
167
167
168
168
-
export const PatchState = Type.Object({
169
169
-
type: Type.Literal("PatchState"),
168
168
+
export const PatchFieldState = Type.Object({
169
169
+
type: Type.Literal("PatchFieldState"),
170
170
data: Type.Array(JsonPatch),
171
171
});
172
172
-
export type PatchState = StaticDecode<typeof PatchState>;
172
172
+
export type PatchFieldState = StaticDecode<typeof PatchFieldState>;
173
173
174
174
-
export const Response = Type.Union([Authenticated, FieldList, PutState, PatchState]);
174
174
+
export const Response = Type.Union([Authenticated, FieldList, PutFieldState, PatchFieldState]);
175
175
export type Response = StaticDecode<typeof Response>;
176
176
177
177
export type Once<T> = Branded<"Once", T>;
···
183
183
export interface SocketV1Protocol {
184
184
Authenticate: Once<Authenticated>;
185
185
ListFields: Once<FieldList>;
186
186
-
WatchField: Stream<PutState | PatchState>;
186
186
+
WatchField: Stream<PutFieldState | PatchFieldState>;
187
187
+
Unsubscribe: never;
187
188
}
188
189
189
190
function ProtocolV1Message<T extends TSchema>(data: T) {
+95
src/actor/field_state/mod.rs
···
1
1
+
use super::player_socket::Response;
2
2
+
use super::Unsubscribe;
3
3
+
use kameo::{prelude::Message, Actor};
4
4
+
use serde::{Deserialize, Serialize};
5
5
+
use sqlx::PgPool;
6
6
+
use tokio::sync::mpsc::UnboundedSender;
7
7
+
8
8
+
#[derive(Serialize, Deserialize, Clone, Debug)]
9
9
+
pub struct FieldTile {
10
10
+
pub id: i64,
11
11
+
pub x: i32,
12
12
+
pub y: i32,
13
13
+
}
14
14
+
15
15
+
#[derive(Serialize, Deserialize, Clone, Debug)]
16
16
+
pub struct FieldCitizen {
17
17
+
pub id: i64,
18
18
+
pub x: i32,
19
19
+
pub y: i32,
20
20
+
}
21
21
+
22
22
+
#[derive(Serialize, Deserialize, Clone, Debug)]
23
23
+
pub struct FieldState {
24
24
+
pub name: String,
25
25
+
pub tiles: Vec<FieldTile>,
26
26
+
pub citizens: Vec<FieldCitizen>,
27
27
+
}
28
28
+
29
29
+
#[derive(Actor)]
30
30
+
#[expect(dead_code)]
31
31
+
pub struct FieldWatcher {
32
32
+
state: FieldState,
33
33
+
field_id: i64,
34
34
+
db: PgPool,
35
35
+
tx: UnboundedSender<Response>,
36
36
+
}
37
37
+
38
38
+
impl FieldWatcher {
39
39
+
pub async fn build(
40
40
+
db: PgPool,
41
41
+
tx: UnboundedSender<Response>,
42
42
+
field_id: i64,
43
43
+
) -> anyhow::Result<Self> {
44
44
+
let mut conn = db.acquire().await?;
45
45
+
46
46
+
let field = sqlx::query!("SELECT name FROM fields WHERE id = $1", field_id)
47
47
+
.fetch_one(&mut *conn)
48
48
+
.await?;
49
49
+
let tiles = sqlx::query_as!(
50
50
+
FieldTile,
51
51
+
r#"
52
52
+
SELECT tile_id AS "id", grid_x AS "x", grid_y AS "y"
53
53
+
FROM field_tiles
54
54
+
WHERE field_id = $1
55
55
+
"#,
56
56
+
field_id
57
57
+
)
58
58
+
.fetch_all(&mut *conn)
59
59
+
.await?;
60
60
+
let citizens = sqlx::query_as!(
61
61
+
FieldCitizen,
62
62
+
r#"
63
63
+
SELECT citizen_id AS "id", grid_x AS "x", grid_y AS "y"
64
64
+
FROM field_citizens
65
65
+
WHERE field_id = $1
66
66
+
"#,
67
67
+
field_id
68
68
+
)
69
69
+
.fetch_all(&mut *conn)
70
70
+
.await?;
71
71
+
let state = FieldState {
72
72
+
name: field.name,
73
73
+
tiles,
74
74
+
citizens,
75
75
+
};
76
76
+
tx.send(Response::PutFieldState(state.clone()))?;
77
77
+
Ok(Self {
78
78
+
state,
79
79
+
db,
80
80
+
tx,
81
81
+
field_id,
82
82
+
})
83
83
+
}
84
84
+
}
85
85
+
86
86
+
impl Message<Unsubscribe> for FieldWatcher {
87
87
+
type Reply = ();
88
88
+
async fn handle(
89
89
+
&mut self,
90
90
+
_msg: Unsubscribe,
91
91
+
ctx: &mut kameo::prelude::Context<Self, Self::Reply>,
92
92
+
) -> Self::Reply {
93
93
+
ctx.stop();
94
94
+
}
95
95
+
}
+3
src/actor/mod.rs
···
1
1
+
pub mod field_state;
1
2
pub mod player_socket;
3
3
+
4
4
+
pub struct Unsubscribe;
+66
-51
src/actor/player_socket/mod.rs
···
1
1
+
use super::field_state::FieldState;
2
2
+
use crate::actor::Unsubscribe;
3
3
+
use crate::api::ws::ProtocolV1Message;
1
4
use crate::dto::{Account, Field};
5
5
+
use futures::Stream;
6
6
+
use json_patch::Patch;
7
7
+
use kameo::prelude::*;
2
8
use serde::{Deserialize, Serialize};
9
9
+
use sqlx::PgPool;
10
10
+
use std::collections::HashMap;
11
11
+
use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
12
12
+
use tokio_stream::wrappers::UnboundedReceiverStream;
13
13
+
use uuid::Uuid;
14
14
+
15
15
+
mod authenticate;
16
16
+
mod list_fields;
17
17
+
mod unsubscribe;
18
18
+
mod watch_field;
3
19
4
20
#[derive(Serialize, Deserialize, Clone, Debug)]
5
21
#[serde(tag = "type", content = "data")]
6
22
pub enum Request {
7
23
Authenticate(String),
8
24
ListFields,
25
25
+
WatchField(i64),
26
26
+
Unsubscribe,
9
27
}
10
28
11
29
#[derive(Serialize, Deserialize, Clone, Debug)]
···
13
31
pub enum Response {
14
32
Authenticated(Account),
15
33
FieldList(Vec<Field>),
34
34
+
PutFieldState(FieldState),
35
35
+
PatchFieldState(Vec<Patch>),
16
36
}
17
37
18
18
-
pub use server::PlayerSocket;
38
38
+
#[derive(Actor)]
39
39
+
pub struct PlayerSocket {
40
40
+
db: PgPool,
41
41
+
account_id: Option<String>,
42
42
+
subscriptions: HashMap<Uuid, Recipient<Unsubscribe>>,
43
43
+
}
19
44
20
20
-
mod server {
21
21
-
use super::{Request, Response};
22
22
-
use futures::Stream;
23
23
-
use kameo::prelude::*;
24
24
-
use sqlx::PgPool;
25
25
-
use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
26
26
-
use tokio_stream::wrappers::UnboundedReceiverStream;
45
45
+
impl PlayerSocket {
46
46
+
pub fn build(db: PgPool) -> Self {
47
47
+
Self {
48
48
+
db,
49
49
+
account_id: None,
50
50
+
subscriptions: HashMap::default(),
51
51
+
}
52
52
+
}
27
53
28
28
-
mod authenticate;
29
29
-
mod list_fields;
30
30
-
31
31
-
#[derive(Actor)]
32
32
-
pub struct PlayerSocket {
33
33
-
pub(super) db: PgPool,
34
34
-
pub(super) account_id: Option<String>,
54
54
+
pub async fn push(
55
55
+
actor: ActorRef<Self>,
56
56
+
request: ProtocolV1Message<Request>,
57
57
+
) -> Result<impl Stream<Item = Response>, SendError<PlayerSocketMessage>> {
58
58
+
let (tx, rx) = unbounded_channel();
59
59
+
actor.tell(PlayerSocketMessage { tx, request }).await?;
60
60
+
Ok(UnboundedReceiverStream::new(rx))
35
61
}
36
62
37
37
-
impl PlayerSocket {
38
38
-
pub fn build(db: PgPool) -> Self {
39
39
-
Self {
40
40
-
db,
41
41
-
account_id: None,
42
42
-
}
43
43
-
}
44
44
-
45
45
-
pub async fn push(
46
46
-
actor: ActorRef<Self>,
47
47
-
request: Request,
48
48
-
) -> Result<impl Stream<Item = Response>, SendError<PlayerSocketMessage>> {
49
49
-
let (tx, rx) = unbounded_channel();
50
50
-
actor.tell(PlayerSocketMessage { tx, request }).await?;
51
51
-
Ok(UnboundedReceiverStream::new(rx))
52
52
-
}
63
63
+
fn require_authentication(&self) -> anyhow::Result<&str> {
64
64
+
self.account_id
65
65
+
.as_deref()
66
66
+
.ok_or_else(|| anyhow::anyhow!("authentication required"))
53
67
}
68
68
+
}
54
69
55
55
-
pub struct PlayerSocketMessage {
56
56
-
tx: UnboundedSender<Response>,
57
57
-
request: Request,
58
58
-
}
70
70
+
pub struct PlayerSocketMessage {
71
71
+
tx: UnboundedSender<Response>,
72
72
+
request: ProtocolV1Message<Request>,
73
73
+
}
59
74
60
60
-
impl Message<PlayerSocketMessage> for PlayerSocket {
61
61
-
type Reply = ();
75
75
+
impl Message<PlayerSocketMessage> for PlayerSocket {
76
76
+
type Reply = ();
62
77
63
63
-
async fn handle(
64
64
-
&mut self,
65
65
-
PlayerSocketMessage { tx, request }: PlayerSocketMessage,
66
66
-
ctx: &mut Context<Self, Self::Reply>,
67
67
-
) -> Self::Reply {
68
68
-
let result = match request {
69
69
-
Request::Authenticate(account_id) => self.authenticate(tx, account_id).await,
70
70
-
Request::ListFields if self.account_id.is_some() => self.list_fields(tx).await,
71
71
-
_ => Err(anyhow::anyhow!("authentication required")),
72
72
-
};
73
73
-
if let Err(error) = result {
74
74
-
tracing::error!("error handling player socket message: {}", error);
75
75
-
ctx.stop()
76
76
-
}
78
78
+
async fn handle(
79
79
+
&mut self,
80
80
+
PlayerSocketMessage { tx, request }: PlayerSocketMessage,
81
81
+
ctx: &mut Context<Self, Self::Reply>,
82
82
+
) -> Self::Reply {
83
83
+
let result = match request.data {
84
84
+
Request::Authenticate(account_id) => self.authenticate(tx, account_id).await,
85
85
+
Request::ListFields => self.list_fields(tx).await,
86
86
+
Request::WatchField(field_id) => self.watch_field(tx, request.id, field_id).await,
87
87
+
Request::Unsubscribe => self.unsubscribe(request.id).await,
88
88
+
};
89
89
+
if let Err(error) = result {
90
90
+
tracing::error!("error handling player socket message: {}", error);
91
91
+
ctx.stop()
77
92
}
78
93
}
79
94
}
src/actor/player_socket/server/authenticate.rs
src/actor/player_socket/authenticate.rs
+1
-1
src/actor/player_socket/server/list_fields.rs
src/actor/player_socket/list_fields.rs
···
11
11
let field_list = sqlx::query_as!(
12
12
Field,
13
13
"SELECT id, name FROM fields WHERE account_id = $1",
14
14
-
self.account_id.as_ref().unwrap()
14
14
+
self.require_authentication()?,
15
15
)
16
16
.fetch_all(&mut *conn)
17
17
.await?;
+12
src/actor/player_socket/unsubscribe.rs
···
1
1
+
use super::super::Unsubscribe;
2
2
+
use super::PlayerSocket;
3
3
+
use uuid::Uuid;
4
4
+
5
5
+
impl PlayerSocket {
6
6
+
pub(super) async fn unsubscribe(&mut self, message_id: Uuid) -> anyhow::Result<()> {
7
7
+
if let Some(sub) = self.subscriptions.remove(&message_id) {
8
8
+
sub.tell(Unsubscribe).await?;
9
9
+
}
10
10
+
Ok(())
11
11
+
}
12
12
+
}
+20
src/actor/player_socket/watch_field.rs
···
1
1
+
use super::super::field_state::FieldWatcher;
2
2
+
use super::super::Unsubscribe;
3
3
+
use super::{PlayerSocket, Response};
4
4
+
use kameo::actor::Spawn;
5
5
+
use tokio::sync::mpsc::UnboundedSender;
6
6
+
use uuid::Uuid;
7
7
+
8
8
+
impl PlayerSocket {
9
9
+
pub(super) async fn watch_field(
10
10
+
&mut self,
11
11
+
tx: UnboundedSender<Response>,
12
12
+
message_id: Uuid,
13
13
+
field_id: i64,
14
14
+
) -> anyhow::Result<()> {
15
15
+
let actor = FieldWatcher::spawn(FieldWatcher::build(self.db.clone(), tx, field_id).await?);
16
16
+
let unsubscriber = actor.recipient::<Unsubscribe>();
17
17
+
self.subscriptions.insert(message_id, unsubscriber);
18
18
+
Ok(())
19
19
+
}
20
20
+
}
+3
-2
src/api/ws.rs
···
93
93
})
94
94
.filter_map({
95
95
let actor = actor.clone();
96
96
-
move |ProtocolV1Message { id, data }| {
96
96
+
move |message| {
97
97
let actor = actor.clone();
98
98
+
let id = message.id;
98
99
async move {
99
100
Some(
100
100
-
PlayerSocket::push(actor, data)
101
101
+
PlayerSocket::push(actor, message)
101
102
.await
102
103
.ok()?
103
104
.map(move |data| ProtocolV1Message { id, data }),
+1
-1
src/main.rs
···
5
5
6
6
use std::net::IpAddr;
7
7
8
8
-
use axum::{Extension, Json, response::Html};
8
8
+
use axum::{response::Html, Extension, Json};
9
9
use utoipa::OpenApi;
10
10
use utoipa_scalar::Scalar;
11
11