linkrs-wire 0.1.0

Wire contract DTOs shared between linkrs-api and linkrs-cli
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
//! Meta contract DTOs: auth, session, transaction, config, statistics and
//! cold-snapshot endpoints.
//!
//! The CLI-side mirrors (`cli/client/{config_types,stats,snapshot,
//! transaction}.rs` and parts of `request_types.rs`) are consolidated here.

use serde::{Deserialize, Serialize};

// ── Auth ──────────────────────────────────────────────────────────────────

/// Login request
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct LoginRequest {
    pub username: String,
    pub password: String,
}

/// Login response
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct LoginResponse {
    pub session_id: i64,
    pub username: String,
    #[serde(default)]
    pub expires_at: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub role: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub display_role: Option<String>,
    #[serde(default)]
    pub roles: Vec<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub must_change_password: Option<bool>,
}

/// Current user response for role-aware clients.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct AuthMeResponse {
    pub username: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub role: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub display_role: Option<String>,
    #[serde(default)]
    pub roles: Vec<String>,
}

/// User list entry for the management view.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct UserListItem {
    pub username: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub role: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_active: Option<String>,
}

/// User list response for the management view.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct UserListResponse {
    #[serde(default)]
    pub users: Vec<UserListItem>,
}

/// Logout request
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct LogoutRequest {
    pub session_id: i64,
}

/// Create user request for the admin channel.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct CreateUserRequest {
    pub username: String,
    pub password: String,
}

/// Reset password request for the admin channel.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ResetPasswordRequest {
    pub password: String,
}

/// Grant role request for the admin channel.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct GrantRoleRequest {
    pub role: String,
    pub space: String,
}

/// Revoke role request for the admin channel.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct RevokeRoleRequest {
    pub space: String,
}

// ── Session ───────────────────────────────────────────────────────────────

/// Create session request
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct CreateSessionRequest {
    pub username: String,
    pub client_ip: String,
    /// Optional password. Required when authentication is enabled;
    /// omitted or left empty when the server has disabled auth.
    #[serde(default)]
    pub password: Option<String>,
}

/// Session response
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct SessionResponse {
    pub session_id: i64,
    pub username: String,
    #[serde(default)]
    pub created_at: u64,
}

// ── Transaction ───────────────────────────────────────────────────────────

/// Begin transaction request
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct BeginTransactionRequest {
    #[serde(default)]
    pub read_only: bool,
    #[serde(default)]
    pub timeout_seconds: Option<u64>,
    #[serde(default)]
    pub query_timeout_seconds: Option<u64>,
    #[serde(default)]
    pub statement_timeout_seconds: Option<u64>,
    #[serde(default)]
    pub idle_timeout_seconds: Option<u64>,
    /// `repeatable_read` or `read_committed`.
    #[serde(default)]
    pub isolation_level: Option<String>,
}

/// Transaction response
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct TransactionResponse {
    pub transaction_id: u64,
    pub status: String,
}

/// Transaction action request (commit/rollback body).
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct TransactionActionRequest {
    pub session_id: i64,
}

// ── Config ────────────────────────────────────────────────────────────────

/// Update config request
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct UpdateConfigRequest {
    pub section: String,
    pub key: String,
    pub value: serde_json::Value,
}

/// Server configuration
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ServerConfig {
    pub version: String,
    #[serde(default)]
    pub sections: Vec<ConfigSection>,
}

/// Configuration section
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ConfigSection {
    pub name: String,
    #[serde(default)]
    pub description: Option<String>,
    #[serde(default)]
    pub items: Vec<ConfigItem>,
}

/// Configuration item
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ConfigItem {
    pub key: String,
    pub value: serde_json::Value,
    #[serde(default)]
    pub default_value: Option<serde_json::Value>,
    #[serde(default)]
    pub description: Option<String>,
    #[serde(default)]
    pub mutable: bool,
}

// ── Statistics ────────────────────────────────────────────────────────────

/// Statistics for a session
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct SessionStatistics {
    pub total_queries: u64,
    pub total_changes: u64,
    pub avg_execution_time_ms: f64,
}

/// Query type statistics
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct QueryTypeStatistics {
    pub match_queries: u64,
    pub create_queries: u64,
    pub update_queries: u64,
    pub delete_queries: u64,
    pub insert_queries: u64,
    pub go_queries: u64,
    pub fetch_queries: u64,
    pub lookup_queries: u64,
    pub show_queries: u64,
}

/// Query statistics
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct QueryStatistics {
    pub total_queries: u64,
    #[serde(default)]
    pub slow_queries: Vec<SlowQueryInfo>,
    pub query_types: QueryTypeStatistics,
}

/// Information about a slow query
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct SlowQueryInfo {
    pub trace_id: String,
    pub session_id: i64,
    pub query: String,
    pub duration_ms: f64,
    pub status: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub started_at_secs: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub stages: Option<crate::query::QueryStageTimings>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub plan_node_count: Option<usize>,
}

/// Database statistics
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct DatabaseStatistics {
    pub space_count: i64,
    pub total_vertices: i64,
    pub total_edges: i64,
    pub total_queries: u64,
    pub active_queries: u64,
    pub queries_per_second: f64,
    pub avg_latency_ms: f64,
}

/// Memory usage block shared by system responses.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct MemoryUsage {
    pub used_bytes: u64,
    pub total_bytes: u64,
}

/// Connection counters shared by system responses.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ConnectionStats {
    pub active: usize,
    pub total: usize,
    pub max: usize,
}

/// Latency percentiles in microseconds.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct LatencyPercentilesUs {
    pub avg: u64,
    pub p50: u64,
    pub p95: u64,
    pub p99: u64,
}

/// System resource response with stable keys plus process and disk detail.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct SystemResourceResponse {
    pub cpu_usage_percent: f64,
    pub memory_usage: MemoryUsage,
    pub connections: ConnectionStats,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub process_memory_bytes: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub uptime_secs: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub data_dir_size_bytes: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub wal_dir_size_bytes: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_file_descriptors: Option<u64>,
}

/// Checkpoint summary embedded in database storage sections.
#[derive(Debug, Clone, Serialize, Deserialize, Default, utoipa::ToSchema)]
pub struct CheckpointSummary {
    #[serde(default)]
    pub success_count: u64,
    #[serde(default)]
    pub failure_count: u64,
    #[serde(default)]
    pub trigger_count: u64,
    #[serde(default)]
    pub avg_duration_us: f64,
    #[serde(default)]
    pub triggered_by_wal_size: u64,
    #[serde(default)]
    pub triggered_by_interval: u64,
    #[serde(default)]
    pub triggered_explicit: u64,
}

/// Database overview response mirroring the handler JSON keys.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct DatabaseOverviewResponse {
    pub spaces: DatabaseSpaces,
    pub storage: DatabaseStorage,
    pub performance: DatabasePerformance,
    pub search: DatabaseSearchSummary,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct DatabaseSpaces {
    pub count: usize,
    pub total_vertices: usize,
    pub total_edges: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct DatabaseStorage {
    pub total_size_bytes: u64,
    pub index_size_bytes: u64,
    pub data_size_bytes: u64,
    #[serde(default)]
    pub fragmentation_permille: u64,
    #[serde(default)]
    pub wasted_bytes: u64,
    #[serde(default)]
    pub tombstone_count: u64,
    #[serde(default)]
    pub tombstone_memory_bytes: u64,
    #[serde(default)]
    pub dirty_pages: u64,
    #[serde(default)]
    pub dirty_pages_total: u64,
    #[serde(default)]
    pub checkpoint: CheckpointSummary,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct DatabasePerformance {
    pub total_queries: u64,
    pub active_queries: u64,
    pub query_cache_size: usize,
    pub queries_per_second: f64,
    pub avg_latency_ms: f64,
    pub cache_hit_rate: f64,
    #[serde(default)]
    pub cache_hit_rate_source: String,
    #[serde(default)]
    pub error_total: u64,
    #[serde(default)]
    pub latency_percentiles_us: Option<LatencyPercentilesUs>,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct DatabaseSearchSummary {
    pub total_queries: u64,
    pub total_errors: u64,
    pub avg_latency_ms: f64,
    pub total_index_operations: u64,
    pub total_delete_operations: u64,
    pub cache_hit_count: u64,
    pub cache_miss_count: u64,
    pub cache_hit_rate: f64,
}

/// Aggregated query pattern entry.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct QueryPatternEntry {
    pub normalized_query: String,
    pub query_type: String,
    #[serde(default)]
    pub labels: Vec<String>,
    pub execution_count: u64,
    pub avg_duration_ms: f64,
    pub p95_duration_ms: f64,
    pub p99_duration_ms: f64,
    pub error_rate: f64,
    #[serde(default)]
    pub error_count: u64,
}

/// Executor rollup entry.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ExecutorSummaryEntry {
    pub executor_type: String,
    pub count: u64,
    pub total_time_ms: u64,
    pub total_rows: u64,
    pub avg_time_ms: f64,
}

/// Query statistics response with time-window filtering and error breakdowns.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct QueryStatsResponse {
    pub total_queries: u64,
    pub slow_queries: Vec<SlowQueryInfo>,
    pub query_types: QueryTypeStatistics,
    #[serde(default)]
    pub errors_by_type: std::collections::HashMap<String, u64>,
    #[serde(default)]
    pub errors_by_phase: std::collections::HashMap<String, u64>,
    #[serde(default)]
    pub error_total: u64,
    #[serde(default)]
    pub top_patterns: Vec<QueryPatternEntry>,
    #[serde(default)]
    pub executor_summary: Vec<ExecutorSummaryEntry>,
    #[serde(default)]
    pub latency_percentiles_us: Option<LatencyPercentilesUs>,
    #[serde(default)]
    pub from: Option<String>,
    #[serde(default)]
    pub to: Option<String>,
}

/// Per-index search breakdown entry.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct SearchIndexEntry {
    pub index: String,
    pub search_queries: u64,
    pub search_errors: u64,
    pub search_latency_ms: u64,
    pub avg_search_latency_ms: f64,
    pub index_operations: u64,
    pub index_errors: u64,
    pub index_latency_ms: u64,
}

/// Search statistics response with per-index detail.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct SearchStatsResponse {
    pub search: SearchSection,
    pub index: IndexSection,
    pub delete: DeleteSection,
    pub cache: CacheSection,
    #[serde(default)]
    pub by_index: Vec<SearchIndexEntry>,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct SearchSection {
    pub total_queries: u64,
    pub total_errors: u64,
    pub total_latency_ms: u64,
    pub avg_latency_ms: f64,
    pub total_results: u64,
    pub latency_percentiles_us: LatencyPercentilesUs,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct IndexSection {
    pub total_operations: u64,
    pub total_errors: u64,
    pub total_latency_ms: u64,
    pub avg_latency_ms: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct DeleteSection {
    pub total_operations: u64,
    pub total_errors: u64,
    pub total_latency_ms: u64,
    pub avg_latency_ms: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct CacheSection {
    pub hit_count: u64,
    pub miss_count: u64,
    pub hit_rate: f64,
}

/// Cross-subsystem overview for the monitoring first screen.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct OverviewResponse {
    pub system: SystemResourceResponse,
    pub database: DatabaseOverviewResponse,
    pub query_latency_us: LatencyPercentilesUs,
    pub errors: OverviewErrors,
    pub storage: OverviewStorage,
    pub transaction: OverviewTransaction,
    pub sync: OverviewSync,
    pub timeseries: Vec<OverviewTimeseriesPoint>,
}

/// Sync summary embedded in the overview; mirrors the sync status shape.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct OverviewSync {
    pub is_running: bool,
    pub outbox_pending: usize,
    pub outbox_retries: u64,
    pub outbox_dead_lettered: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct OverviewErrors {
    pub total: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct OverviewStorage {
    pub read_ops: u64,
    pub write_ops: u64,
    pub fragmentation_permille: u64,
    pub tombstone_count: u64,
    pub checkpoint_success: u64,
    pub checkpoint_failure: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct OverviewTransaction {
    pub begun: u64,
    pub committed: u64,
    pub rolled_back: u64,
    pub active: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct OverviewTimeseriesPoint {
    pub second: u64,
    pub queries: u64,
    pub avg_latency_ms: f64,
    pub errors: u64,
}

/// Single query portrait detail for console jumps.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct QueryProfileDetailResponse {
    pub trace_id: String,
    pub session_id: i64,
    pub query: String,
    pub duration_ms: f64,
    pub status: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub stages: Option<crate::query::QueryStageTimings>,
    #[serde(default)]
    pub executors: Vec<QueryProfileExecutor>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub result_count: Option<usize>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub plan_node_count: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct QueryProfileExecutor {
    pub executor_type: String,
    pub duration_ms: f64,
    pub rows: usize,
    pub memory_bytes: usize,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn login_roundtrip() {
        let request = LoginRequest {
            username: "root".to_string(),
            password: "secret".to_string(),
        };
        let json = serde_json::to_string(&request).unwrap();
        let back: LoginRequest = serde_json::from_str(&json).unwrap();
        assert_eq!(back.username, "root");
    }

    #[test]
    fn transaction_response_roundtrip() {
        let response = TransactionResponse {
            transaction_id: 42,
            status: "Active".to_string(),
        };
        let json = serde_json::to_string(&response).unwrap();
        let back: TransactionResponse = serde_json::from_str(&json).unwrap();
        assert_eq!(back.transaction_id, 42);
        assert_eq!(back.status, "Active");
    }

    #[test]
    fn begin_transaction_request_defaults() {
        let back: BeginTransactionRequest = serde_json::from_str(r#"{"read_only": true}"#).unwrap();
        assert!(back.read_only);
        assert!(back.timeout_seconds.is_none());
        assert!(back.isolation_level.is_none());
    }

    #[test]
    fn server_config_roundtrip() {
        let config = ServerConfig {
            version: "0.1.0".to_string(),
            sections: vec![ConfigSection {
                name: "database".to_string(),
                description: None,
                items: vec![ConfigItem {
                    key: "host".to_string(),
                    value: serde_json::json!("127.0.0.1"),
                    default_value: None,
                    description: None,
                    mutable: true,
                }],
            }],
        };
        let json = serde_json::to_string(&config).unwrap();
        let back: ServerConfig = serde_json::from_str(&json).unwrap();
        assert_eq!(back.version, "0.1.0");
        assert_eq!(back.sections[0].items[0].key, "host");
    }

    #[test]
    fn query_statistics_roundtrip() {
        let stats = QueryStatistics {
            total_queries: 10,
            slow_queries: Vec::new(),
            query_types: QueryTypeStatistics {
                match_queries: 4,
                create_queries: 0,
                update_queries: 0,
                delete_queries: 0,
                insert_queries: 6,
                go_queries: 0,
                fetch_queries: 0,
                lookup_queries: 0,
                show_queries: 0,
            },
        };
        let json = serde_json::to_string(&stats).unwrap();
        let back: QueryStatistics = serde_json::from_str(&json).unwrap();
        assert_eq!(back.total_queries, 10);
        assert_eq!(back.query_types.match_queries, 4);
    }

    #[test]
    fn update_config_request_roundtrip() {
        let request = UpdateConfigRequest {
            section: "database".to_string(),
            key: "port".to_string(),
            value: serde_json::json!(9090),
        };
        let json = serde_json::to_string(&request).unwrap();
        let back: UpdateConfigRequest = serde_json::from_str(&json).unwrap();
        assert_eq!(back.section, "database");
        assert_eq!(back.key, "port");
        assert_eq!(back.value, serde_json::json!(9090));
    }
}