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 ListTests,
254 RunTest { name: String },
256 TestState,
259 RemoveTestResults {
262 #[serde(default)]
263 name: Option<String>,
264 },
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct BackupOutcome {
270 pub service: String,
271 pub paths: usize,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct RestoreOutcome {
278 pub service: String,
279 pub snapshot: String,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285#[serde(rename_all = "snake_case")]
286pub enum BackupBackendSpec {
287 Local { path: String },
289 S3 {
291 endpoint: String,
292 bucket: String,
293 access_key_id: String,
294 secret_access_key: String,
295 #[serde(default)]
296 prefix: Option<String>,
297 },
298 Managed,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct SnapshotView {
307 pub id: String,
309 pub time: String,
311 pub tags: Vec<String>,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct BackupStatusView {
319 pub configured: bool,
321 #[serde(skip_serializing_if = "Option::is_none")]
323 pub backend_label: Option<String>,
324 pub enrolled: Vec<String>,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct EnvKeyChangeView {
331 pub key: String,
332 pub from: Option<String>,
334 pub to: String,
335 pub secret: bool,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct ReconcilePlanView {
342 pub service: String,
343 pub changes: Vec<EnvKeyChangeView>,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct ReconcileOutcome {
349 pub plans: Vec<ReconcilePlanView>,
351 pub applied: usize,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct SearchHit {
358 pub name: String,
359 pub description: String,
360 pub installed: bool,
361 pub supports: Vec<String>,
363 #[serde(default)]
366 pub recommended_ram_mb: Option<u64>,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
371pub struct RegistryInfo {
372 pub name: String,
373 pub url: String,
374 pub service_count: usize,
375}
376
377#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
379#[serde(rename_all = "snake_case")]
380pub enum Severity {
381 Blocker,
383 Warning,
385 Info,
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct DoctorIssue {
392 pub code: String,
394 pub severity: Severity,
395 pub message: String,
398 #[serde(skip_serializing_if = "Option::is_none")]
400 pub service: Option<String>,
401}
402
403#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
405#[serde(rename_all = "snake_case")]
406pub enum DiffKind {
407 Unchanged,
408 Modified,
409 Drift,
411 Added,
412 Removed,
413}
414
415#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct DiffEntry {
418 pub path: String,
419 pub kind: DiffKind,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct EnvAddition {
425 pub key: String,
426 pub kind: String,
428 #[serde(skip_serializing_if = "Option::is_none")]
429 pub prompt: Option<String>,
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct DiffView {
435 pub service: String,
436 pub upgrade_available: bool,
438 pub blocked_by_drift: bool,
440 pub source_stale: bool,
442 pub entries: Vec<DiffEntry>,
444 pub env_additions: Vec<EnvAddition>,
446}
447
448#[derive(Debug, Clone, Serialize, Deserialize)]
450pub struct BackupSnapshotView {
451 pub timestamp: String,
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize)]
457pub struct RevertOutcome {
458 pub service: String,
459 pub timestamp: String,
461 pub files_restored: usize,
462 pub files_deleted: usize,
463}
464
465#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
467#[serde(rename_all = "snake_case")]
468pub enum ServiceState {
469 Running,
470 Stopped,
471 Installing,
475 Removed,
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct ServiceView {
483 pub name: String,
484 pub state: ServiceState,
485 #[serde(skip_serializing_if = "Option::is_none")]
487 pub url: Option<String>,
488 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
490 pub ports: BTreeMap<String, u16>,
491 #[serde(skip_serializing_if = "Option::is_none")]
493 pub registry: Option<String>,
494 #[serde(skip_serializing_if = "Option::is_none")]
496 pub version: Option<String>,
497 #[serde(default)]
499 pub upgrade_available: bool,
500}
501
502#[derive(Debug, Clone, Serialize, Deserialize)]
506pub struct ApplyOutcome {
507 pub service: ServiceView,
508 pub applied: usize,
509 #[serde(default)]
510 pub destructive: bool,
511}
512
513#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
518#[serde(rename_all = "snake_case")]
519pub enum EnvKindView {
520 Default,
521 Prompted,
522 Required,
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize)]
528pub struct EnvVarView {
529 pub name: String,
530 pub kind: EnvKindView,
531 #[serde(skip_serializing_if = "Option::is_none")]
532 pub prompt: Option<String>,
533 pub format: String,
535 pub generated: bool,
537 pub value_empty: bool,
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize)]
543pub struct EnvGroupView {
544 pub name: String,
545 pub prompt: String,
546 pub env: Vec<EnvVarView>,
547}
548
549#[derive(Debug, Clone, Serialize, Deserialize)]
551pub struct ChoiceOptionView {
552 pub name: String,
553 #[serde(skip_serializing_if = "Option::is_none")]
554 pub label: Option<String>,
555 pub env: Vec<EnvVarView>,
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize)]
560pub struct ChoiceView {
561 pub name: String,
562 pub prompt: String,
563 pub default: String,
564 pub options: Vec<ChoiceOptionView>,
565}
566
567#[derive(Debug, Clone, Serialize, Deserialize)]
569pub struct ServiceDefView {
570 pub name: String,
571 pub env: Vec<EnvVarView>,
572 pub env_groups: Vec<EnvGroupView>,
573 pub choices: Vec<ChoiceView>,
574}
575
576#[derive(Debug, Clone, Serialize, Deserialize)]
579pub struct ConfigureView {
580 pub name: String,
581 pub def: ServiceDefView,
582 pub selected_choices: BTreeMap<String, String>,
584 pub enabled_groups: Vec<String>,
586 pub current_env: BTreeMap<String, String>,
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize)]
592pub struct RegistryTestView {
593 pub name: String,
594 pub kind: String,
596 pub services: Vec<String>,
597 pub step_count: usize,
598 pub step_kinds: Vec<String>,
599 pub needs_browser: bool,
600 pub requires_sudo: bool,
601}
602
603#[derive(Debug, Clone, Serialize, Deserialize)]
605pub struct TestRunView {
606 pub name: String,
607 pub passed: bool,
608 pub duration_secs: f64,
609 pub outcome: String,
611 pub events: Vec<TestEventView>,
612}
613
614#[derive(Debug, Clone, Serialize, Deserialize)]
616pub struct TestEventView {
617 pub description: String,
618 pub kind: String,
620 pub passed: bool,
621 pub skipped: bool,
622 pub error: Option<String>,
623 pub duration_secs: f64,
624 pub stdout: String,
625 pub stderr: String,
626}
627
628#[derive(Debug, Clone, Serialize, Deserialize)]
630pub struct TestStateView {
631 pub sandbox_path: String,
632 pub tests: Vec<TestResultEntryView>,
633}
634
635#[derive(Debug, Clone, Serialize, Deserialize)]
637pub struct TestResultEntryView {
638 pub name: String,
639 pub status: String,
640 pub duration_ms: u64,
641 pub timestamp: u64,
642 pub has_playwright: bool,
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize)]
647#[serde(rename_all = "snake_case")]
648pub enum Response {
649 Applied(ApplyOutcome),
651 Service(ServiceView),
653 Services(Vec<ServiceView>),
655 Diff(DiffView),
657 Backups(Vec<BackupSnapshotView>),
659 Revert(RevertOutcome),
661 SearchResults(Vec<SearchHit>),
663 Registries(Vec<RegistryInfo>),
665 Doctor(Vec<DoctorIssue>),
667 Backup(BackupOutcome),
669 Restore(RestoreOutcome),
671 Snapshots(Vec<SnapshotView>),
673 BackupStatus(BackupStatusView),
675 ServiceDef(ServiceDefView),
677 ConfigureView(ConfigureView),
679 Reconcile(ReconcileOutcome),
681 Tests(Vec<RegistryTestView>),
683 TestRun(TestRunView),
685 TestState(TestStateView),
687 Done,
689}
690
691#[derive(Debug, Clone, Serialize, Deserialize)]
694#[serde(rename_all = "snake_case")]
695pub enum Reply {
696 Ok(Response),
697 Error(RpcError),
698}
699
700#[derive(Debug, Clone, Serialize, Deserialize)]
702pub struct RpcError {
703 pub code: ErrorCode,
704 pub message: String,
705}
706
707#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
709#[serde(rename_all = "snake_case")]
710pub enum ErrorCode {
711 BadRequest,
712 NotFound,
713 Conflict,
714 Internal,
715}
716
717impl RpcError {
718 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
719 RpcError {
720 code,
721 message: message.into(),
722 }
723 }
724}
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729
730 #[test]
731 fn request_maps_to_method_and_params() {
732 let req = Request::Add(AddRequest::new("forgejo"));
733 let v = serde_json::to_value(&req).unwrap();
734 assert_eq!(v["method"], "add");
735 assert_eq!(v["params"]["service"], "forgejo");
736 }
737
738 #[test]
739 fn unit_request_has_no_params() {
740 let v = serde_json::to_value(Request::List).unwrap();
741 assert_eq!(v["method"], "list");
742 assert!(v.get("params").is_none());
743 }
744
745 #[test]
746 fn service_view_round_trips_and_omits_empties() {
747 let view = ServiceView {
748 name: "forgejo".to_string(),
749 state: ServiceState::Running,
750 url: Some("https://forgejo.example.com".to_string()),
751 ports: BTreeMap::new(),
752 registry: None,
753 version: None,
754 upgrade_available: false,
755 };
756 let v = serde_json::to_value(&view).unwrap();
757 assert!(v.get("ports").is_none());
758 assert_eq!(v["state"], "running");
759 let back: ServiceView = serde_json::from_value(v).unwrap();
760 assert_eq!(back.name, "forgejo");
761 assert_eq!(back.state, ServiceState::Running);
762 }
763}