1use std::collections::{BTreeMap, BTreeSet};
16
17use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum ExposureRequest {
25 #[default]
26 Loopback,
27 Url(String),
29 Tailscale(String),
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum AuthKind {
37 Oidc,
38}
39
40#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum AuthRequested {
44 #[default]
45 No,
46 Yes,
48 Kind(AuthKind),
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct AddRequest {
55 pub service: String,
57 #[serde(default)]
58 pub exposure: ExposureRequest,
59 #[serde(default)]
60 pub auth: AuthRequested,
61 #[serde(default)]
64 pub smtp: Option<bool>,
65 #[serde(default)]
66 pub backup: bool,
67 #[serde(default)]
68 pub env: BTreeMap<String, String>,
69 #[serde(default)]
70 pub enable_groups: BTreeSet<String>,
71 #[serde(default)]
73 pub choose: BTreeMap<String, String>,
74}
75
76impl AddRequest {
77 pub fn new(service: impl Into<String>) -> Self {
79 AddRequest {
80 service: service.into(),
81 exposure: ExposureRequest::default(),
82 auth: AuthRequested::default(),
83 smtp: None,
84 backup: false,
85 env: BTreeMap::new(),
86 enable_groups: BTreeSet::new(),
87 choose: BTreeMap::new(),
88 }
89 }
90}
91
92#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(rename_all = "snake_case")]
95pub enum RemoveMode {
96 #[default]
98 Preserve,
99 Purge,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct RemoveRequest {
106 pub service: String,
107 #[serde(default)]
108 pub mode: RemoveMode,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "snake_case")]
114pub enum Lifecycle {
115 Start,
116 Stop,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct LifecycleRequest {
122 pub service: String,
123 pub action: Lifecycle,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct UpgradeRequest {
129 pub service: String,
130 #[serde(default)]
132 pub force: bool,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137#[serde(rename_all = "snake_case")]
138pub enum ExposureChange {
139 Url(String),
140 Tailscale(String),
141 Loopback,
142}
143
144#[derive(Debug, Clone, Default, Serialize, Deserialize)]
147#[serde(default)]
148pub struct Overrides {
149 pub exposure: Option<ExposureChange>,
150 pub smtp: Option<bool>,
151 pub backup: Option<bool>,
152 pub auth: Option<bool>,
153 pub enable_groups: BTreeSet<String>,
154 pub disable_groups: BTreeSet<String>,
155 pub choose: BTreeMap<String, String>,
156 pub env_overrides: BTreeMap<String, String>,
157 pub reassert_auth: bool,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct ConfigureRequest {
165 pub service: String,
166 pub changes: Overrides,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(tag = "method", content = "params", rename_all = "snake_case")]
174pub enum Request {
175 Add(AddRequest),
177 Remove(RemoveRequest),
179 Configure(ConfigureRequest),
181 Lifecycle(LifecycleRequest),
183 Upgrade(UpgradeRequest),
185 List,
187 Get { service: String },
189 Diff { service: String },
191 Backups { service: String },
193 Revert {
195 service: String,
196 #[serde(default)]
197 at: Option<String>,
198 },
199 Search {
201 #[serde(default)]
202 query: Option<String>,
203 #[serde(default)]
204 registry: Option<String>,
205 },
206 Registries,
208 AddRegistry { name: String, url: String },
210 RemoveRegistry { name: String },
212 Doctor,
214 Backup { service: String },
216 Restore { service: String, snapshot: String },
218 Snapshots { service: String },
220 BackupStatus,
223 ConfigureBackup {
227 backend: BackupBackendSpec,
228 #[serde(default)]
229 password: Option<String>,
230 },
231 SetBackupEnrolled { service: String, enabled: bool },
233 ServiceDef {
236 service: String,
237 #[serde(default)]
238 registry: Option<String>,
239 },
240 ConfigureView { service: String },
243 Reconcile {
247 #[serde(default)]
248 services: Vec<String>,
249 #[serde(default)]
250 dry_run: bool,
251 },
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct BackupOutcome {
257 pub service: String,
258 pub paths: usize,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct RestoreOutcome {
265 pub service: String,
266 pub snapshot: String,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272#[serde(rename_all = "snake_case")]
273pub enum BackupBackendSpec {
274 Local { path: String },
276 S3 {
278 endpoint: String,
279 bucket: String,
280 access_key_id: String,
281 secret_access_key: String,
282 #[serde(default)]
283 prefix: Option<String>,
284 },
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct SnapshotView {
290 pub id: String,
292 pub time: String,
294 pub tags: Vec<String>,
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct BackupStatusView {
302 pub configured: bool,
304 #[serde(skip_serializing_if = "Option::is_none")]
306 pub backend_label: Option<String>,
307 pub enrolled: Vec<String>,
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct EnvKeyChangeView {
314 pub key: String,
315 pub from: Option<String>,
317 pub to: String,
318 pub secret: bool,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct ReconcilePlanView {
325 pub service: String,
326 pub changes: Vec<EnvKeyChangeView>,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct ReconcileOutcome {
332 pub plans: Vec<ReconcilePlanView>,
334 pub applied: usize,
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct SearchHit {
341 pub name: String,
342 pub description: String,
343 pub installed: bool,
344 pub supports: Vec<String>,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct RegistryInfo {
351 pub name: String,
352 pub url: String,
353 pub service_count: usize,
354}
355
356#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
358#[serde(rename_all = "snake_case")]
359pub enum Severity {
360 Blocker,
362 Warning,
364 Info,
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct DoctorIssue {
371 pub code: String,
373 pub severity: Severity,
374 pub message: String,
377 #[serde(skip_serializing_if = "Option::is_none")]
379 pub service: Option<String>,
380}
381
382#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
384#[serde(rename_all = "snake_case")]
385pub enum DiffKind {
386 Unchanged,
387 Modified,
388 Drift,
390 Added,
391 Removed,
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct DiffEntry {
397 pub path: String,
398 pub kind: DiffKind,
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct EnvAddition {
404 pub key: String,
405 pub kind: String,
407 #[serde(skip_serializing_if = "Option::is_none")]
408 pub prompt: Option<String>,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct DiffView {
414 pub service: String,
415 pub upgrade_available: bool,
417 pub blocked_by_drift: bool,
419 pub source_stale: bool,
421 pub entries: Vec<DiffEntry>,
423 pub env_additions: Vec<EnvAddition>,
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct BackupSnapshotView {
430 pub timestamp: String,
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct RevertOutcome {
437 pub service: String,
438 pub timestamp: String,
440 pub files_restored: usize,
441 pub files_deleted: usize,
442}
443
444#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
446#[serde(rename_all = "snake_case")]
447pub enum ServiceState {
448 Running,
449 Stopped,
450 Removed,
452}
453
454#[derive(Debug, Clone, Serialize, Deserialize)]
457pub struct ServiceView {
458 pub name: String,
459 pub state: ServiceState,
460 #[serde(skip_serializing_if = "Option::is_none")]
462 pub url: Option<String>,
463 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
465 pub ports: BTreeMap<String, u16>,
466 #[serde(skip_serializing_if = "Option::is_none")]
468 pub registry: Option<String>,
469 #[serde(skip_serializing_if = "Option::is_none")]
471 pub version: Option<String>,
472 #[serde(default)]
474 pub upgrade_available: bool,
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct ApplyOutcome {
482 pub service: ServiceView,
483 pub applied: usize,
484 #[serde(default)]
485 pub destructive: bool,
486}
487
488#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
493#[serde(rename_all = "snake_case")]
494pub enum EnvKindView {
495 Default,
496 Prompted,
497 Required,
498}
499
500#[derive(Debug, Clone, Serialize, Deserialize)]
503pub struct EnvVarView {
504 pub name: String,
505 pub kind: EnvKindView,
506 #[serde(skip_serializing_if = "Option::is_none")]
507 pub prompt: Option<String>,
508 pub format: String,
510 pub generated: bool,
512 pub value_empty: bool,
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize)]
518pub struct EnvGroupView {
519 pub name: String,
520 pub prompt: String,
521 pub env: Vec<EnvVarView>,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct ChoiceOptionView {
527 pub name: String,
528 #[serde(skip_serializing_if = "Option::is_none")]
529 pub label: Option<String>,
530 pub env: Vec<EnvVarView>,
531}
532
533#[derive(Debug, Clone, Serialize, Deserialize)]
535pub struct ChoiceView {
536 pub name: String,
537 pub prompt: String,
538 pub default: String,
539 pub options: Vec<ChoiceOptionView>,
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct ServiceDefView {
545 pub name: String,
546 pub env: Vec<EnvVarView>,
547 pub env_groups: Vec<EnvGroupView>,
548 pub choices: Vec<ChoiceView>,
549}
550
551#[derive(Debug, Clone, Serialize, Deserialize)]
554pub struct ConfigureView {
555 pub name: String,
556 pub def: ServiceDefView,
557 pub selected_choices: BTreeMap<String, String>,
559 pub enabled_groups: Vec<String>,
561 pub current_env: BTreeMap<String, String>,
563}
564
565#[derive(Debug, Clone, Serialize, Deserialize)]
567#[serde(rename_all = "snake_case")]
568pub enum Response {
569 Applied(ApplyOutcome),
571 Service(ServiceView),
573 Services(Vec<ServiceView>),
575 Diff(DiffView),
577 Backups(Vec<BackupSnapshotView>),
579 Revert(RevertOutcome),
581 SearchResults(Vec<SearchHit>),
583 Registries(Vec<RegistryInfo>),
585 Doctor(Vec<DoctorIssue>),
587 Backup(BackupOutcome),
589 Restore(RestoreOutcome),
591 Snapshots(Vec<SnapshotView>),
593 BackupStatus(BackupStatusView),
595 ServiceDef(ServiceDefView),
597 ConfigureView(ConfigureView),
599 Reconcile(ReconcileOutcome),
601 Done,
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize)]
608#[serde(rename_all = "snake_case")]
609pub enum Reply {
610 Ok(Response),
611 Error(RpcError),
612}
613
614#[derive(Debug, Clone, Serialize, Deserialize)]
616pub struct RpcError {
617 pub code: ErrorCode,
618 pub message: String,
619}
620
621#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
623#[serde(rename_all = "snake_case")]
624pub enum ErrorCode {
625 BadRequest,
626 NotFound,
627 Conflict,
628 Internal,
629}
630
631impl RpcError {
632 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
633 RpcError {
634 code,
635 message: message.into(),
636 }
637 }
638}
639
640#[cfg(test)]
641mod tests {
642 use super::*;
643
644 #[test]
645 fn request_maps_to_method_and_params() {
646 let req = Request::Add(AddRequest::new("forgejo"));
647 let v = serde_json::to_value(&req).unwrap();
648 assert_eq!(v["method"], "add");
649 assert_eq!(v["params"]["service"], "forgejo");
650 }
651
652 #[test]
653 fn unit_request_has_no_params() {
654 let v = serde_json::to_value(Request::List).unwrap();
655 assert_eq!(v["method"], "list");
656 assert!(v.get("params").is_none());
657 }
658
659 #[test]
660 fn service_view_round_trips_and_omits_empties() {
661 let view = ServiceView {
662 name: "forgejo".to_string(),
663 state: ServiceState::Running,
664 url: Some("https://forgejo.example.com".to_string()),
665 ports: BTreeMap::new(),
666 registry: None,
667 version: None,
668 upgrade_available: false,
669 };
670 let v = serde_json::to_value(&view).unwrap();
671 assert!(v.get("ports").is_none());
672 assert_eq!(v["state"], "running");
673 let back: ServiceView = serde_json::from_value(v).unwrap();
674 assert_eq!(back.name, "forgejo");
675 assert_eq!(back.state, ServiceState::Running);
676 }
677}