1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::convert::TryFrom;
4
5use sysknife_proto::sysknife::v1 as proto;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct DistroHint {
27 pub family: &'static str,
32 pub version: Option<String>,
35}
36
37pub const DISTRO_FAMILY_FEDORA: &str = "fedora";
39
40pub const DISTRO_FAMILY_DEBIAN: &str = "debian";
42
43pub const DISTRO_FAMILY_OTHER: &str = "other";
45
46pub const KNOWN_ACTION_NAMES: &[&str] = &[
71 "GetSystemState",
72 "CollectDiagnostics",
73 "GetDeploymentHistory",
74 "ListDeployments",
75 "UpdateSystem",
76 "CleanupDeployments",
77 "RebootSystem",
78 "RollbackDeployment",
79 "GetKernelArguments",
80 "PinDeployment",
81 "UnpinDeployment",
82 "RebaseSystem",
83 "SetKernelArguments",
84 "InstallFlatpak",
85 "RemoveFlatpak",
86 "UpdateFlatpak",
87 "SearchFlatpakApps",
88 "ListFlatpakRemotes",
89 "ListInstalledFlatpaks",
90 "AddFlatpakRemote",
91 "RemoveFlatpakRemote",
92 "GetFlatpakAppInfo",
93 "ListToolboxes",
94 "CreateToolbox",
95 "RemoveToolbox",
96 "GetLayeredPackages",
97 "ResetLayeredPackageOverride",
98 "GetPendingUpdates",
99 "InstallPackages",
100 "RemovePackages",
101 "AddLayeredPackage",
102 "RemoveLayeredPackage",
103 "ReplaceLayeredPackage",
104 "RemoveBasePackage",
105 "ListServices",
106 "ListTimers",
107 "ReloadDaemon",
108 "CreateScheduledJob",
109 "StartService",
110 "StopService",
111 "RestartService",
112 "ReloadService",
113 "SetServiceEnabled",
114 "MaskService",
115 "UnmaskService",
116 "GetServiceLogs",
117 "GetServiceStatus",
118 "GetServiceResourceLimits",
119 "SetServiceResourceLimits",
120 "GetFirewallState",
121 "GetNetworkStatus",
122 "GetListeningPorts",
123 "ConfigureWifi",
124 "SetDnsServers",
125 "ConfigureFirewall",
126 "GetDiskUsage",
127 "ListProcesses",
128 "SignalProcess",
129 "GetMemoryInfo",
130 "GetJournalLog",
132 "VacuumJournal",
133 "GetLvmReport",
135 "ExtendLogicalVolume",
136 "CreateLogicalVolume",
137 "CreateLvSnapshot",
138 "GetSysctl",
140 "SetSysctl",
141 "GetMounts",
143 "AddMount",
144 "RemoveMount",
145 "AddSwap",
146 "RemoveSwap",
147 "GetSudoGrants",
149 "GrantSudoAccess",
150 "RevokeSudoAccess",
151 "GetLogrotateStatus",
153 "ConfigureLogRotation",
154 "RemoveLogRotation",
155 "ConfigureRemoteSyslog",
156 "RemoveRemoteSyslog",
157 "GetPasswordAging",
159 "SetPasswordAging",
160 "SetPasswordPolicy",
161 "SetAccountLockout",
162 "GetAuditRules",
164 "AddAuditRule",
165 "RemoveAuditRule",
166 "GetCertificates",
168 "ObtainCertificate",
169 "RenewCertificates",
170 "GetDateTime",
171 "SetHostname",
172 "SetTimezone",
173 "SetLocale",
174 "SetNtp",
175 "ListPackageRepositories",
176 "AddPackageRepository",
177 "RemovePackageRepository",
178 "EnablePackageRepository",
179 "DisablePackageRepository",
180 "ListContainers",
181 "CreateContainer",
182 "StartContainer",
183 "StopContainer",
184 "RemoveContainer",
185 "GetContainerInfo",
186 "ListUsers",
187 "ListGroups",
188 "CreateUser",
189 "DeleteUser",
190 "AddUserToGroup",
191 "RemoveUserFromGroup",
192 "CreateGroup",
193 "DeleteGroup",
194 "LockUserAccount",
195 "UnlockUserAccount",
196 "GetAuthorizedKeys",
197 "AddAuthorizedKey",
198 "RemoveAuthorizedKey",
199 "SetSshdOption",
200 "ListJobHistory",
201 "AptUpdate",
203 "AptUpgrade",
204 "AptInstall",
205 "AptRemove",
206 "AptPurge",
207 "AptAutoremove",
208 "AptHold",
209 "AptUnhold",
210 "AptSearch",
211 "AptListInstalled",
212 "AptShow",
213 "AptListUpgradable",
214 "AptHistoryList",
215 "ConfigureUnattendedUpgrades",
216 "GetAptPins",
217 "SetAptPin",
218 "RemoveAptPin",
219 "AddPpa",
221 "RemovePpa",
222 "SnapInstall",
224 "SnapRemove",
225 "SnapRefresh",
226 "SnapHold",
227 "SnapUnhold",
228 "SnapList",
229 "SnapInfo",
230 "SnapRevert",
231 "SnapClassicInstall",
232 "UfwEnable",
234 "UfwDisable",
235 "UfwAllow",
236 "UfwDeny",
237 "UfwReset",
238 "UfwStatus",
239 "DistroboxList",
241 "DistroboxCreate",
242 "DistroboxRemove",
243 "NetplanGetConfig",
245 "NetplanApply",
246 "NetplanSet",
247 "NetplanGenerate",
248 "GrubGetKargs",
250 "GrubSetKargs",
251 "CheckPendingReboot",
253 "ResolvectlStatus",
255 "ResolvectlSetDns",
256 "AppArmorStatus",
258 "AppArmorEnforce",
259 "AppArmorComplain",
260 "CloudInitStatus",
262 "UbuntuInstallFlatpak",
264 "UbuntuRemoveFlatpak",
265 "UbuntuUpdateFlatpak",
266 "UbuntuListFlatpaks",
267 "Fail2banStatus",
269 "Fail2banBanIp",
270 "Fail2banUnbanIp",
271 "ConfigureFail2banJail",
272 "UbuntuReleaseUpgrade",
274 "ProStatus",
276 "ProAttach",
277 "ProDetach",
278 "EnableProService",
279 "DisableProService",
280 "LivepatchStatus",
282 "MultipassList",
284 "UfwDeleteRule",
286 "UfwLimit",
287];
288
289#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
296#[serde(transparent)]
297pub struct ActionName(String);
298
299impl ActionName {
300 pub fn parse(name: impl Into<String>) -> Result<Self, UnknownActionName> {
302 let name = name.into();
303 if KNOWN_ACTION_NAMES.contains(&name.as_str()) {
304 Ok(Self(name))
305 } else {
306 Err(UnknownActionName(name))
307 }
308 }
309
310 pub fn as_str(&self) -> &str {
312 &self.0
313 }
314}
315
316impl std::fmt::Display for ActionName {
317 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318 f.write_str(&self.0)
319 }
320}
321
322impl AsRef<str> for ActionName {
323 fn as_ref(&self) -> &str {
324 &self.0
325 }
326}
327
328#[derive(Clone, Debug, PartialEq, Eq)]
330pub struct UnknownActionName(pub String);
331
332impl std::fmt::Display for UnknownActionName {
333 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334 write!(f, "unknown action name: '{}'", self.0)
335 }
336}
337
338impl std::error::Error for UnknownActionName {}
339
340#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
349#[serde(transparent)]
350pub struct RequestHash(String);
351
352impl RequestHash {
353 pub fn new(s: impl Into<String>) -> Self {
354 Self(s.into())
355 }
356 pub fn as_str(&self) -> &str {
357 &self.0
358 }
359 pub fn into_inner(self) -> String {
360 self.0
361 }
362}
363
364impl std::fmt::Display for RequestHash {
365 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
366 f.write_str(&self.0)
367 }
368}
369
370impl AsRef<str> for RequestHash {
371 fn as_ref(&self) -> &str {
372 &self.0
373 }
374}
375
376impl From<String> for RequestHash {
377 fn from(s: String) -> Self {
378 Self(s)
379 }
380}
381
382#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
395#[serde(transparent)]
396pub struct TransactionId(String);
397
398#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
405#[serde(transparent)]
406pub struct ApprovalReceipt(String);
407
408impl std::fmt::Debug for ApprovalReceipt {
409 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410 f.write_str("ApprovalReceipt(<redacted>)")
411 }
412}
413
414macro_rules! opaque_string_newtype {
415 ($name:ident) => {
416 impl $name {
417 pub fn new(s: impl Into<String>) -> Self {
418 Self(s.into())
419 }
420 pub fn as_str(&self) -> &str {
421 &self.0
422 }
423 pub fn into_inner(self) -> String {
424 self.0
425 }
426 }
427
428 impl AsRef<str> for $name {
429 fn as_ref(&self) -> &str {
430 &self.0
431 }
432 }
433
434 impl From<String> for $name {
435 fn from(s: String) -> Self {
436 Self(s)
437 }
438 }
439 };
440}
441
442opaque_string_newtype!(TransactionId);
443opaque_string_newtype!(ApprovalReceipt);
444
445impl std::fmt::Display for TransactionId {
450 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451 f.write_str(&self.0)
452 }
453}
454
455#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
466#[serde(rename_all = "snake_case")]
467pub enum CallerRole {
468 Observer,
469 Dev,
470 Admin,
471 Boot,
472}
473
474impl CallerRole {
475 pub fn as_str(&self) -> &'static str {
483 match self {
484 Self::Observer => "observer",
485 Self::Dev => "dev",
486 Self::Admin => "admin",
487 Self::Boot => "boot",
488 }
489 }
490}
491
492#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
493#[serde(rename_all = "snake_case")]
494pub enum RiskLevel {
495 Low,
496 Medium,
497 High,
498}
499
500#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
501#[serde(rename_all = "snake_case")]
502pub enum JobState {
503 Queued,
504 Running,
505 Succeeded,
506 Failed,
507 Canceled,
508 RolledBack,
509 NeedsReboot,
510}
511
512#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
519#[serde(rename_all = "snake_case")]
520pub enum FailureCategory {
521 ValidationFailure,
522 AuthorizationFailure,
523 PolicyDenied,
524 StaleApproval,
525 ExecutionFailure,
526 TransientInfrastructureFailure,
527 Cancellation,
528 StuckExecution,
529 RebootRequired,
530 RollbackFailure,
531}
532
533#[derive(Debug, thiserror::Error)]
534pub enum BridgeError {
535 #[error("invalid json payload for {0}")]
536 InvalidJson(&'static str, #[source] serde_json::Error),
537
538 #[error("invalid enum value {value} for {field}")]
539 InvalidEnum { field: &'static str, value: i32 },
540}
541
542#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
543pub struct RequestEnvelope {
544 pub action_name: String,
545 pub request_id: String,
546 pub params: Value,
547 pub caller_role: CallerRole,
548 pub request_hash: RequestHash,
549}
550
551#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
552pub struct PreviewEnvelope {
553 pub summary: String,
554 pub risk_level: RiskLevel,
555 pub current_state: Value,
556 pub proposed_change: Value,
557 pub expected_side_effects: Vec<String>,
558 pub reboot_required: bool,
559 pub rollback_available: bool,
560 pub warnings: Vec<String>,
561 pub request_hash: RequestHash,
562}
563
564#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
565pub struct ResultEnvelope {
566 pub status: JobState,
567 pub summary: String,
568 pub warnings: Vec<String>,
569 pub job_id: Option<String>,
570 pub needs_reboot: bool,
571 pub rollback_ref: Option<String>,
572 pub transaction_id: String,
573}
574
575#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
576pub struct TransactionRecord {
577 pub transaction_id: String,
578 pub request_id: String,
579 pub request_hash: String,
580 pub action_name: String,
581 pub risk_level: RiskLevel,
582 pub status: JobState,
583 pub approval_id: Option<String>,
584 pub summary: String,
585 pub warnings: Vec<String>,
586}
587
588fn caller_role_code(value: CallerRole) -> i32 {
589 match value {
590 CallerRole::Observer => 1,
591 CallerRole::Dev => 2,
592 CallerRole::Admin => 3,
593 CallerRole::Boot => 4,
594 }
595}
596
597fn risk_level_code(value: RiskLevel) -> i32 {
598 match value {
599 RiskLevel::Low => 1,
600 RiskLevel::Medium => 2,
601 RiskLevel::High => 3,
602 }
603}
604
605fn job_state_code(value: JobState) -> i32 {
606 match value {
607 JobState::Queued => 1,
608 JobState::Running => 2,
609 JobState::Succeeded => 3,
610 JobState::Failed => 4,
611 JobState::Canceled => 5,
612 JobState::RolledBack => 6,
613 JobState::NeedsReboot => 7,
614 }
615}
616
617impl From<CallerRole> for proto::CallerRole {
618 fn from(value: CallerRole) -> Self {
623 match value {
624 CallerRole::Observer => proto::CallerRole::Observer,
625 CallerRole::Dev => proto::CallerRole::Dev,
626 CallerRole::Admin => proto::CallerRole::Admin,
627 CallerRole::Boot => proto::CallerRole::Boot,
628 }
629 }
630}
631
632impl TryFrom<proto::CallerRole> for CallerRole {
633 type Error = BridgeError;
634
635 fn try_from(value: proto::CallerRole) -> Result<Self, Self::Error> {
636 match i32::from(value) {
637 1 => Ok(CallerRole::Observer),
638 2 => Ok(CallerRole::Dev),
639 3 => Ok(CallerRole::Admin),
640 4 => Ok(CallerRole::Boot),
641 code => Err(BridgeError::InvalidEnum {
642 field: "caller_role",
643 value: code,
644 }),
645 }
646 }
647}
648
649impl From<RiskLevel> for proto::RiskLevel {
650 fn from(value: RiskLevel) -> Self {
651 match value {
652 RiskLevel::Low => proto::RiskLevel::Low,
653 RiskLevel::Medium => proto::RiskLevel::Medium,
654 RiskLevel::High => proto::RiskLevel::High,
655 }
656 }
657}
658
659impl TryFrom<proto::RiskLevel> for RiskLevel {
660 type Error = BridgeError;
661
662 fn try_from(value: proto::RiskLevel) -> Result<Self, Self::Error> {
663 match i32::from(value) {
664 1 => Ok(RiskLevel::Low),
665 2 => Ok(RiskLevel::Medium),
666 3 => Ok(RiskLevel::High),
667 code => Err(BridgeError::InvalidEnum {
668 field: "risk_level",
669 value: code,
670 }),
671 }
672 }
673}
674
675impl From<JobState> for proto::JobState {
676 fn from(value: JobState) -> Self {
677 match value {
678 JobState::Queued => proto::JobState::Queued,
679 JobState::Running => proto::JobState::Running,
680 JobState::Succeeded => proto::JobState::Succeeded,
681 JobState::Failed => proto::JobState::Failed,
682 JobState::Canceled => proto::JobState::Canceled,
683 JobState::RolledBack => proto::JobState::RolledBack,
684 JobState::NeedsReboot => proto::JobState::NeedsReboot,
685 }
686 }
687}
688
689impl TryFrom<proto::JobState> for JobState {
690 type Error = BridgeError;
691
692 fn try_from(value: proto::JobState) -> Result<Self, Self::Error> {
693 match i32::from(value) {
694 1 => Ok(JobState::Queued),
695 2 => Ok(JobState::Running),
696 3 => Ok(JobState::Succeeded),
697 4 => Ok(JobState::Failed),
698 5 => Ok(JobState::Canceled),
699 6 => Ok(JobState::RolledBack),
700 7 => Ok(JobState::NeedsReboot),
701 code => Err(BridgeError::InvalidEnum {
702 field: "job_state",
703 value: code,
704 }),
705 }
706 }
707}
708
709impl From<FailureCategory> for proto::FailureCategory {
710 fn from(value: FailureCategory) -> Self {
711 match value {
712 FailureCategory::ValidationFailure => proto::FailureCategory::ValidationFailure,
713 FailureCategory::AuthorizationFailure => proto::FailureCategory::AuthorizationFailure,
714 FailureCategory::PolicyDenied => proto::FailureCategory::PolicyDenied,
715 FailureCategory::StaleApproval => proto::FailureCategory::StaleApproval,
716 FailureCategory::ExecutionFailure => proto::FailureCategory::ExecutionFailure,
717 FailureCategory::TransientInfrastructureFailure => {
718 proto::FailureCategory::TransientInfrastructureFailure
719 }
720 FailureCategory::Cancellation => proto::FailureCategory::Cancellation,
721 FailureCategory::StuckExecution => proto::FailureCategory::StuckExecution,
722 FailureCategory::RebootRequired => proto::FailureCategory::RebootRequired,
723 FailureCategory::RollbackFailure => proto::FailureCategory::RollbackFailure,
724 }
725 }
726}
727
728impl TryFrom<proto::FailureCategory> for FailureCategory {
729 type Error = BridgeError;
730
731 fn try_from(value: proto::FailureCategory) -> Result<Self, Self::Error> {
732 match i32::from(value) {
733 1 => Ok(FailureCategory::ValidationFailure),
734 2 => Ok(FailureCategory::AuthorizationFailure),
735 3 => Ok(FailureCategory::PolicyDenied),
736 4 => Ok(FailureCategory::StaleApproval),
737 5 => Ok(FailureCategory::ExecutionFailure),
738 6 => Ok(FailureCategory::TransientInfrastructureFailure),
739 7 => Ok(FailureCategory::Cancellation),
740 8 => Ok(FailureCategory::StuckExecution),
741 9 => Ok(FailureCategory::RebootRequired),
742 10 => Ok(FailureCategory::RollbackFailure),
743 code => Err(BridgeError::InvalidEnum {
744 field: "failure_category",
745 value: code,
746 }),
747 }
748 }
749}
750
751impl From<RequestEnvelope> for proto::RequestEnvelope {
752 fn from(value: RequestEnvelope) -> Self {
753 Self {
754 action_name: value.action_name,
755 request_id: value.request_id,
756 params_json: serde_json::to_string(&value.params).expect("json serialization"),
757 caller_role: caller_role_code(value.caller_role),
758 request_hash: value.request_hash.into_inner(),
759 }
760 }
761}
762
763impl TryFrom<proto::RequestEnvelope> for RequestEnvelope {
764 type Error = BridgeError;
765
766 fn try_from(value: proto::RequestEnvelope) -> Result<Self, Self::Error> {
767 let params = serde_json::from_str(&value.params_json)
768 .map_err(|error| BridgeError::InvalidJson("params_json", error))?;
769
770 Ok(Self {
771 action_name: value.action_name,
772 request_id: value.request_id,
773 params,
774 caller_role: CallerRole::try_from(
775 proto::CallerRole::try_from(value.caller_role).map_err(|_| {
776 BridgeError::InvalidEnum {
777 field: "caller_role",
778 value: value.caller_role,
779 }
780 })?,
781 )?,
782 request_hash: RequestHash::from(value.request_hash),
783 })
784 }
785}
786
787impl From<PreviewEnvelope> for proto::PreviewEnvelope {
788 fn from(value: PreviewEnvelope) -> Self {
789 Self {
790 summary: value.summary,
791 risk_level: risk_level_code(value.risk_level),
792 current_state_json: serde_json::to_string(&value.current_state)
793 .expect("json serialization"),
794 proposed_change_json: serde_json::to_string(&value.proposed_change)
795 .expect("json serialization"),
796 expected_side_effects: value.expected_side_effects,
797 reboot_required: value.reboot_required,
798 rollback_available: value.rollback_available,
799 warnings: value.warnings,
800 request_hash: value.request_hash.into_inner(),
801 }
802 }
803}
804
805impl TryFrom<proto::PreviewEnvelope> for PreviewEnvelope {
806 type Error = BridgeError;
807
808 fn try_from(value: proto::PreviewEnvelope) -> Result<Self, Self::Error> {
809 let current_state = serde_json::from_str(&value.current_state_json)
810 .map_err(|error| BridgeError::InvalidJson("current_state_json", error))?;
811 let proposed_change = serde_json::from_str(&value.proposed_change_json)
812 .map_err(|error| BridgeError::InvalidJson("proposed_change_json", error))?;
813
814 Ok(Self {
815 summary: value.summary,
816 risk_level: RiskLevel::try_from(
817 proto::RiskLevel::try_from(value.risk_level).map_err(|_| {
818 BridgeError::InvalidEnum {
819 field: "risk_level",
820 value: value.risk_level,
821 }
822 })?,
823 )?,
824 current_state,
825 proposed_change,
826 expected_side_effects: value.expected_side_effects,
827 reboot_required: value.reboot_required,
828 rollback_available: value.rollback_available,
829 warnings: value.warnings,
830 request_hash: RequestHash::from(value.request_hash),
831 })
832 }
833}
834
835impl From<ResultEnvelope> for proto::ResultEnvelope {
836 fn from(value: ResultEnvelope) -> Self {
837 Self {
838 status: job_state_code(value.status),
839 summary: value.summary,
840 warnings: value.warnings,
841 job_id: value.job_id.unwrap_or_default(),
842 needs_reboot: value.needs_reboot,
843 rollback_ref: value.rollback_ref.unwrap_or_default(),
844 transaction_id: value.transaction_id,
845 }
846 }
847}
848
849impl TryFrom<proto::ResultEnvelope> for ResultEnvelope {
850 type Error = BridgeError;
851
852 fn try_from(value: proto::ResultEnvelope) -> Result<Self, Self::Error> {
853 Ok(Self {
854 status: JobState::try_from(proto::JobState::try_from(value.status).map_err(|_| {
855 BridgeError::InvalidEnum {
856 field: "job_state",
857 value: value.status,
858 }
859 })?)?,
860 summary: value.summary,
861 warnings: value.warnings,
862 job_id: if value.job_id.is_empty() {
863 None
864 } else {
865 Some(value.job_id)
866 },
867 needs_reboot: value.needs_reboot,
868 rollback_ref: if value.rollback_ref.is_empty() {
869 None
870 } else {
871 Some(value.rollback_ref)
872 },
873 transaction_id: value.transaction_id,
874 })
875 }
876}
877
878impl From<TransactionRecord> for proto::TransactionRecord {
879 fn from(value: TransactionRecord) -> Self {
880 Self {
881 transaction_id: value.transaction_id,
882 request_id: value.request_id,
883 request_hash: value.request_hash,
884 action_name: value.action_name,
885 risk_level: risk_level_code(value.risk_level),
886 status: job_state_code(value.status),
887 approval_id: value.approval_id.unwrap_or_default(),
888 summary: value.summary,
889 warnings: value.warnings,
890 }
891 }
892}
893
894impl TryFrom<proto::TransactionRecord> for TransactionRecord {
895 type Error = BridgeError;
896
897 fn try_from(value: proto::TransactionRecord) -> Result<Self, Self::Error> {
898 Ok(Self {
899 transaction_id: value.transaction_id,
900 request_id: value.request_id,
901 request_hash: value.request_hash,
902 action_name: value.action_name,
903 risk_level: RiskLevel::try_from(
904 proto::RiskLevel::try_from(value.risk_level).map_err(|_| {
905 BridgeError::InvalidEnum {
906 field: "risk_level",
907 value: value.risk_level,
908 }
909 })?,
910 )?,
911 status: JobState::try_from(proto::JobState::try_from(value.status).map_err(|_| {
912 BridgeError::InvalidEnum {
913 field: "job_state",
914 value: value.status,
915 }
916 })?)?,
917 approval_id: if value.approval_id.is_empty() {
918 None
919 } else {
920 Some(value.approval_id)
921 },
922 summary: value.summary,
923 warnings: value.warnings,
924 })
925 }
926}
927
928#[cfg(test)]
929mod request_hash_tests {
930 use super::*;
931
932 #[test]
933 fn serde_round_trips_via_transparent_string() {
934 let r = RequestHash::new("abc123".to_string());
937 let json = serde_json::to_string(&r).unwrap();
938 assert_eq!(json, "\"abc123\"");
939 let back: RequestHash = serde_json::from_str("\"abc123\"").unwrap();
940 assert_eq!(back, r);
941 }
942}
943
944#[cfg(test)]
945mod caller_role_ordering_tests {
946 use super::CallerRole;
947
948 #[test]
949 fn caller_role_orders_least_to_most_privileged() {
950 assert!(CallerRole::Observer < CallerRole::Dev);
953 assert!(CallerRole::Dev < CallerRole::Admin);
954 assert!(CallerRole::Admin < CallerRole::Boot);
955
956 assert!(CallerRole::Admin >= CallerRole::Dev);
958 assert!(!(CallerRole::Observer >= CallerRole::Dev));
959 }
960
961 #[test]
962 fn max_selects_the_higher_privilege() {
963 assert_eq!(
966 CallerRole::Observer.max(CallerRole::Admin),
967 CallerRole::Admin
968 );
969 assert_eq!(CallerRole::Boot.max(CallerRole::Dev), CallerRole::Boot);
970 }
971}