1use std::time::Duration;
10
11use serde::{Deserialize, Serialize};
12use time::OffsetDateTime;
13
14use crate::error::SailError;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum SailboxStatus {
22 Running,
24 Paused,
26 Sleeping,
28 Failed,
30 Terminated,
32 Other(String),
34}
35
36impl Default for SailboxStatus {
37 fn default() -> Self {
38 SailboxStatus::Other(String::new())
39 }
40}
41
42impl SailboxStatus {
43 pub fn as_str(&self) -> &str {
45 match self {
46 SailboxStatus::Running => "running",
47 SailboxStatus::Paused => "paused",
48 SailboxStatus::Sleeping => "sleeping",
49 SailboxStatus::Failed => "failed",
50 SailboxStatus::Terminated => "terminated",
51 SailboxStatus::Other(s) => s,
52 }
53 }
54}
55
56impl From<&str> for SailboxStatus {
57 fn from(s: &str) -> SailboxStatus {
58 match s {
59 "running" => SailboxStatus::Running,
60 "paused" => SailboxStatus::Paused,
61 "sleeping" => SailboxStatus::Sleeping,
62 "failed" => SailboxStatus::Failed,
63 "terminated" => SailboxStatus::Terminated,
64 other => SailboxStatus::Other(other.to_string()),
65 }
66 }
67}
68
69impl std::fmt::Display for SailboxStatus {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.write_str(self.as_str())
72 }
73}
74
75impl Serialize for SailboxStatus {
76 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
77 serializer.serialize_str(self.as_str())
78 }
79}
80
81impl<'de> Deserialize<'de> for SailboxStatus {
82 fn deserialize<D: serde::Deserializer<'de>>(
83 deserializer: D,
84 ) -> Result<SailboxStatus, D::Error> {
85 Ok(SailboxStatus::from(
86 String::deserialize(deserializer)?.as_str(),
87 ))
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum SailboxStatusFilter {
96 Running,
98 Paused,
100 Sleeping,
102 Failed,
104 Terminated,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
110pub enum SailboxListOrder {
111 #[default]
113 NewestActive,
114 NewestCreated,
116}
117
118impl SailboxListOrder {
119 pub fn as_str(&self) -> &'static str {
121 match self {
122 SailboxListOrder::NewestActive => "newest_active",
123 SailboxListOrder::NewestCreated => "newest_created",
124 }
125 }
126
127 pub fn parse(raw: &str) -> Option<SailboxListOrder> {
129 match raw.trim() {
130 "newest_active" => Some(SailboxListOrder::NewestActive),
131 "newest_created" => Some(SailboxListOrder::NewestCreated),
132 _ => None,
133 }
134 }
135}
136
137impl std::str::FromStr for SailboxListOrder {
138 type Err = SailError;
139
140 fn from_str(s: &str) -> Result<Self, Self::Err> {
141 SailboxListOrder::parse(s).ok_or_else(|| SailError::InvalidArgument {
142 message: format!(
143 "unknown list order {s:?}; use \"newest_active\" or \"newest_created\""
144 ),
145 })
146 }
147}
148
149impl std::fmt::Display for SailboxListOrder {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 f.write_str(self.as_str())
152 }
153}
154
155impl SailboxStatusFilter {
156 pub fn as_str(&self) -> &'static str {
158 match self {
159 SailboxStatusFilter::Running => "running",
160 SailboxStatusFilter::Paused => "paused",
161 SailboxStatusFilter::Sleeping => "sleeping",
162 SailboxStatusFilter::Failed => "failed",
163 SailboxStatusFilter::Terminated => "terminated",
164 }
165 }
166
167 pub fn parse(s: &str) -> Option<SailboxStatusFilter> {
169 match s {
170 "running" => Some(SailboxStatusFilter::Running),
171 "paused" => Some(SailboxStatusFilter::Paused),
172 "sleeping" => Some(SailboxStatusFilter::Sleeping),
173 "failed" => Some(SailboxStatusFilter::Failed),
174 "terminated" => Some(SailboxStatusFilter::Terminated),
175 _ => None,
176 }
177 }
178}
179
180impl std::str::FromStr for SailboxStatusFilter {
181 type Err = SailError;
182
183 fn from_str(s: &str) -> Result<Self, Self::Err> {
184 SailboxStatusFilter::parse(s).ok_or_else(|| SailError::InvalidArgument {
185 message: format!(
186 "unknown status filter {s:?}; use \"running\", \"paused\", \"sleeping\", \
187 \"failed\", or \"terminated\""
188 ),
189 })
190 }
191}
192
193impl std::fmt::Display for SailboxStatusFilter {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 f.write_str(self.as_str())
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
202pub enum ListenerProtocol {
203 Tcp,
205 Http,
207 Other(String),
209}
210
211impl ListenerProtocol {
212 pub fn as_str(&self) -> &str {
214 match self {
215 ListenerProtocol::Tcp => "tcp",
216 ListenerProtocol::Http => "http",
217 ListenerProtocol::Other(s) => s,
218 }
219 }
220}
221
222impl From<&str> for ListenerProtocol {
223 fn from(s: &str) -> ListenerProtocol {
224 match s {
225 "tcp" => ListenerProtocol::Tcp,
226 "http" => ListenerProtocol::Http,
227 other => ListenerProtocol::Other(other.to_string()),
228 }
229 }
230}
231
232impl std::fmt::Display for ListenerProtocol {
233 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234 f.write_str(self.as_str())
235 }
236}
237
238impl Serialize for ListenerProtocol {
239 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
240 serializer.serialize_str(self.as_str())
241 }
242}
243
244impl<'de> Deserialize<'de> for ListenerProtocol {
245 fn deserialize<D: serde::Deserializer<'de>>(
246 deserializer: D,
247 ) -> Result<ListenerProtocol, D::Error> {
248 Ok(ListenerProtocol::from(
249 String::deserialize(deserializer)?.as_str(),
250 ))
251 }
252}
253
254#[derive(Debug, Clone, PartialEq, Eq)]
257pub enum ListenerRouteStatus {
258 Unknown,
260 Pending,
262 Active,
264 Restoring,
266 Unavailable,
268 Other(String),
270}
271
272impl ListenerRouteStatus {
273 pub fn as_str(&self) -> &str {
277 match self {
278 ListenerRouteStatus::Unknown => "unknown",
279 ListenerRouteStatus::Pending => "pending",
280 ListenerRouteStatus::Active => "active",
281 ListenerRouteStatus::Restoring => "restoring",
282 ListenerRouteStatus::Unavailable => "unavailable",
283 ListenerRouteStatus::Other(s) => s,
284 }
285 }
286}
287
288impl From<&str> for ListenerRouteStatus {
289 fn from(s: &str) -> ListenerRouteStatus {
295 match s {
296 "LISTENER_ROUTE_STATUS_UNSPECIFIED" | "unknown" => ListenerRouteStatus::Unknown,
297 "LISTENER_ROUTE_STATUS_PENDING" | "pending" => ListenerRouteStatus::Pending,
298 "LISTENER_ROUTE_STATUS_ACTIVE" | "active" => ListenerRouteStatus::Active,
299 "LISTENER_ROUTE_STATUS_RESTORING" | "restoring" => ListenerRouteStatus::Restoring,
300 "LISTENER_ROUTE_STATUS_UNAVAILABLE" | "unavailable" => ListenerRouteStatus::Unavailable,
301 other => ListenerRouteStatus::Other(
302 other
303 .strip_prefix("LISTENER_ROUTE_STATUS_")
304 .map_or_else(|| other.to_string(), str::to_ascii_lowercase),
305 ),
306 }
307 }
308}
309
310impl std::fmt::Display for ListenerRouteStatus {
311 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312 f.write_str(self.as_str())
313 }
314}
315
316impl Serialize for ListenerRouteStatus {
317 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
318 serializer.serialize_str(self.as_str())
319 }
320}
321
322impl<'de> Deserialize<'de> for ListenerRouteStatus {
323 fn deserialize<D: serde::Deserializer<'de>>(
324 deserializer: D,
325 ) -> Result<ListenerRouteStatus, D::Error> {
326 Ok(ListenerRouteStatus::from(
327 String::deserialize(deserializer)?.as_str(),
328 ))
329 }
330}
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq)]
336pub enum IngressProtocol {
337 Tcp,
339 Http,
341}
342
343impl IngressProtocol {
344 pub fn as_str(&self) -> &'static str {
346 match self {
347 IngressProtocol::Tcp => "tcp",
348 IngressProtocol::Http => "http",
349 }
350 }
351
352 pub fn parse(s: &str) -> Option<IngressProtocol> {
355 match s {
356 "tcp" => Some(IngressProtocol::Tcp),
357 "http" => Some(IngressProtocol::Http),
358 _ => None,
359 }
360 }
361}
362
363impl std::str::FromStr for IngressProtocol {
364 type Err = SailError;
365
366 fn from_str(s: &str) -> Result<Self, Self::Err> {
367 IngressProtocol::parse(s).ok_or_else(|| SailError::InvalidArgument {
368 message: format!("unknown ingress protocol {s:?}; use \"tcp\" or \"http\""),
369 })
370 }
371}
372
373impl std::fmt::Display for IngressProtocol {
374 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
375 f.write_str(self.as_str())
376 }
377}
378
379impl Serialize for IngressProtocol {
380 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
381 serializer.serialize_str(self.as_str())
382 }
383}
384
385#[derive(Debug, Clone, Default)]
387pub struct CheckpointOptions {
388 pub name: Option<String>,
390 pub ttl: Option<std::time::Duration>,
392}
393
394#[derive(Debug, Clone)]
398pub struct WaitForListenerOptions {
399 pub timeout: std::time::Duration,
402}
403
404impl Default for WaitForListenerOptions {
405 fn default() -> WaitForListenerOptions {
406 WaitForListenerOptions {
407 timeout: std::time::Duration::from_mins(1),
408 }
409 }
410}
411
412#[derive(Debug, Clone, Default)]
414pub struct ForkOptions {
415 pub name: Option<String>,
417 pub timeout: Option<std::time::Duration>,
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq)]
428pub enum SailboxSize {
429 Small,
432 Medium,
434 Large,
436}
437
438impl SailboxSize {
439 pub fn as_str(&self) -> &'static str {
441 match self {
442 SailboxSize::Small => "s",
443 SailboxSize::Medium => "m",
444 SailboxSize::Large => "l",
445 }
446 }
447
448 pub fn parse(s: &str) -> Option<SailboxSize> {
450 match s {
451 "s" => Some(SailboxSize::Small),
452 "m" => Some(SailboxSize::Medium),
453 "l" => Some(SailboxSize::Large),
454 _ => None,
455 }
456 }
457
458 pub fn allowed() -> &'static [&'static str] {
460 &["s", "m", "l"]
461 }
462}
463
464impl std::str::FromStr for SailboxSize {
465 type Err = SailError;
466
467 fn from_str(s: &str) -> Result<Self, Self::Err> {
468 SailboxSize::parse(s).ok_or_else(|| SailError::InvalidArgument {
469 message: format!("unknown size {s:?}; use one of s, m, l"),
470 })
471 }
472}
473
474impl std::fmt::Display for SailboxSize {
475 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476 f.write_str(self.as_str())
477 }
478}
479
480impl Serialize for SailboxSize {
481 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
482 serializer.serialize_str(self.as_str())
483 }
484}
485
486#[derive(Debug, Clone, Default, Serialize)]
489#[non_exhaustive]
490pub struct SailboxHandle {
491 pub sailbox_id: String,
493 pub name: String,
495 pub status: SailboxStatus,
497 #[doc(hidden)]
499 pub worker_address: String,
500 #[doc(hidden)]
502 pub exec_endpoint: String,
503}
504
505#[derive(Debug, Clone, Serialize, Deserialize)]
507#[non_exhaustive]
508pub struct SailboxDeprecation {
509 pub deadline: String,
512 pub message: String,
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize)]
519#[non_exhaustive]
520pub struct SailboxInfo {
521 pub sailbox_id: String,
523 pub app_id: String,
525 pub app_name: String,
527 pub image_id: String,
529 pub name: String,
531 pub status: SailboxStatus,
533 pub memory_mib: i64,
535 pub vcpu_count: i64,
537 pub state_disk_size_gib: i64,
539 pub cpu_requested_vcpu: i64,
541 pub cpu_used_vcpu: f64,
543 pub memory_requested_bytes: i64,
545 pub memory_used_bytes: i64,
547 pub disk_requested_bytes: i64,
549 pub disk_used_bytes: i64,
551 #[serde(default)]
554 pub architecture: String,
555 #[serde(default)]
557 pub guest_schema_version: Option<i64>,
558 #[serde(default)]
560 pub deprecation: Option<SailboxDeprecation>,
561 #[serde(default)]
563 pub error_message: Option<String>,
564 pub checkpoint_generation: i64,
566 #[serde(with = "crate::rfc3339_micros::option", default)]
568 pub started_at: Option<OffsetDateTime>,
569 #[serde(with = "crate::rfc3339_micros::option", default)]
571 pub last_checkpointed_at: Option<OffsetDateTime>,
572 #[serde(with = "crate::rfc3339_micros")]
574 pub created_at: OffsetDateTime,
575 #[serde(with = "crate::rfc3339_micros")]
577 pub updated_at: OffsetDateTime,
578 #[serde(default)]
581 pub created_by_user_id: Option<String>,
582 #[serde(default)]
585 pub visibility: Option<String>,
586}
587
588#[doc(hidden)]
590#[derive(Debug, Clone, Deserialize)]
591#[non_exhaustive]
592pub struct WhoAmI {
593 pub org_id: String,
595 #[serde(default)]
597 pub user_id: Option<String>,
598}
599
600#[derive(Debug, Clone, Serialize)]
602#[non_exhaustive]
603pub struct SailboxPage {
604 pub items: Vec<SailboxInfo>,
606 pub limit: i64,
608 pub offset: i64,
610 pub total: i64,
612 pub has_more: bool,
614}
615
616#[derive(Debug, Clone, Default)]
618pub struct SailboxSpendQuery {
619 pub app_id: Option<String>,
621 pub sailbox_id: Option<String>,
623 pub from: Option<OffsetDateTime>,
626 pub to: Option<OffsetDateTime>,
629}
630
631#[derive(Debug, Clone, Serialize, Deserialize)]
633#[non_exhaustive]
634pub struct SailboxSpendResponse {
635 #[serde(with = "crate::rfc3339_micros")]
637 pub start_at: OffsetDateTime,
638 #[serde(with = "crate::rfc3339_micros")]
640 pub end_at: OffsetDateTime,
641 pub finalized_cost_usd_nanos: i64,
643 pub estimated_active_cost_usd_nanos: i64,
645 pub estimated_total_cost_usd_nanos: i64,
647 pub duration_seconds: i64,
649 pub vcpu_seconds: f64,
651 pub memory_gib_seconds: f64,
653 pub state_disk_gib_seconds: f64,
655 pub pricing_configured: bool,
657 pub rates: SailboxSpendRates,
659 pub sailboxes: Vec<SailboxSpendItem>,
661}
662
663#[derive(Debug, Clone, Default, Serialize, Deserialize)]
665#[non_exhaustive]
666pub struct SailboxSpendRates {
667 pub vcpu_second_usd_nanos: i64,
669 pub memory_gib_second_usd_nanos: i64,
671 pub state_disk_gib_second_usd_nanos: i64,
673 pub creation_usd_nanos: i64,
675 #[serde(default)]
677 pub amd64_state_disk_gib_second_usd_nanos: i64,
678}
679
680#[derive(Debug, Clone, Serialize, Deserialize)]
682#[non_exhaustive]
683pub struct SailboxSpendItem {
684 pub sailbox_id: String,
686 pub app_id: String,
688 pub finalized_cost_usd_nanos: i64,
690 pub estimated_active_cost_usd_nanos: i64,
692 pub estimated_total_cost_usd_nanos: i64,
694 pub duration_seconds: i64,
696 pub vcpu_seconds: f64,
698 pub memory_gib_seconds: f64,
700 pub state_disk_gib_seconds: f64,
702 pub active: bool,
704}
705
706#[derive(Debug, Clone)]
708pub struct SailboxMetricsQuery {
709 pub range: String,
711}
712
713impl Default for SailboxMetricsQuery {
714 fn default() -> SailboxMetricsQuery {
715 SailboxMetricsQuery {
716 range: "24h".to_string(),
717 }
718 }
719}
720
721#[derive(Debug, Clone, Serialize, Deserialize)]
723#[non_exhaustive]
724pub struct SailboxMetricsResponse {
725 pub range: String,
727 pub data: Vec<SailboxMetricPoint>,
729}
730
731#[derive(Debug, Clone, Serialize, Deserialize)]
733#[non_exhaustive]
734pub struct SailboxMetricPoint {
735 #[serde(with = "crate::rfc3339_micros")]
737 pub timestamp: OffsetDateTime,
738 pub cpu_used_vcpu: f64,
740 pub cpu_requested_vcpu: i64,
742 pub memory_used_bytes: i64,
744 pub memory_requested_bytes: i64,
746 pub disk_used_bytes: i64,
748 pub disk_requested_bytes: i64,
750}
751
752#[derive(Debug, Clone, Serialize)]
756#[non_exhaustive]
757pub struct SailboxCheckpoint {
758 pub checkpoint_id: String,
760 pub sailbox_id: String,
762 pub checkpoint_generation: i64,
764 #[serde(with = "crate::rfc3339_micros::option")]
768 pub expires_at: Option<OffsetDateTime>,
769 pub status: SailboxStatus,
771}
772
773#[derive(Debug, Clone)]
775pub struct ListSailboxesQuery {
776 pub app_id: Option<String>,
778 pub status: Option<SailboxStatusFilter>,
780 pub search: Option<String>,
782 #[doc(hidden)]
785 pub credential_policy_id: Option<String>,
786 pub order: SailboxListOrder,
788 pub limit: i64,
790 pub offset: i64,
792}
793
794#[doc(hidden)]
797pub const DEFAULT_LIST_LIMIT: i64 = 50;
798
799#[doc(hidden)]
802pub const MAX_LIST_LIMIT: i64 = 100;
803
804impl Default for ListSailboxesQuery {
805 fn default() -> ListSailboxesQuery {
806 ListSailboxesQuery {
807 app_id: None,
808 status: None,
809 search: None,
810 credential_policy_id: None,
811 order: SailboxListOrder::NewestActive,
812 limit: DEFAULT_LIST_LIMIT,
813 offset: 0,
814 }
815 }
816}
817
818#[derive(Debug, Clone, Serialize)]
820pub struct IngressPort {
821 pub guest_port: u32,
823 pub protocol: IngressProtocol,
825 pub allowlist: Vec<String>,
829}
830
831#[derive(Debug, Clone, Serialize)]
833pub struct VolumeMount {
834 pub volume_id: String,
836 pub mount_path: String,
838}
839
840#[derive(Debug, Clone, Serialize, Deserialize)]
843#[non_exhaustive]
844pub struct VolumeInfo {
845 pub volume_id: String,
847 pub name: String,
849 pub backend: String,
851 pub status: String,
853 #[serde(with = "crate::rfc3339_micros::option", default)]
855 pub created_at: Option<OffsetDateTime>,
856 #[serde(with = "crate::rfc3339_micros::option", default)]
858 pub updated_at: Option<OffsetDateTime>,
859}
860
861#[derive(Debug, Clone, Deserialize)]
866pub(crate) struct AddListenerWire {
867 pub(crate) guest_port: u32,
868 pub(crate) protocol: ListenerProtocol,
869 #[serde(default)]
870 pub(crate) public_url: String,
871 #[serde(default, rename = "tcp_public_host")]
872 pub(crate) public_host: String,
873 #[serde(default, rename = "tcp_public_port")]
874 pub(crate) public_port: u32,
875}
876
877impl From<AddListenerWire> for crate::worker::Listener {
878 fn from(wire: AddListenerWire) -> crate::worker::Listener {
879 crate::worker::Listener {
880 guest_port: wire.guest_port,
881 protocol: wire.protocol,
882 route_status: ListenerRouteStatus::Unknown,
885 public_url: wire.public_url,
886 public_host: wire.public_host,
887 public_port: wire.public_port,
888 }
889 }
890}
891
892#[derive(Debug, Clone)]
895#[non_exhaustive]
896pub struct IssuedUserCert {
897 pub certificate: String,
899 pub key_id: String,
901}
902
903#[derive(Debug, Clone, PartialEq, Eq)]
906pub enum ListenerEndpoint {
907 Http {
909 url: String,
911 },
912 Tcp {
914 host: String,
916 port: u32,
918 },
919}
920
921impl std::fmt::Display for ListenerEndpoint {
922 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
923 match self {
924 ListenerEndpoint::Http { url } => f.write_str(url),
925 ListenerEndpoint::Tcp { host, port } => write!(f, "{host}:{port}"),
926 }
927 }
928}
929
930#[derive(Debug, Clone)]
936pub struct CreateSailboxRequest {
937 pub app_id: String,
939 pub name: String,
941 pub ingress_ports: Vec<IngressPort>,
943 pub volume_mounts: Vec<VolumeMount>,
945 pub image: crate::image::ImageSpec,
948 pub size: Option<SailboxSize>,
950 pub memory_gib: Option<u32>,
953 pub disk_gib: Option<u32>,
956 pub ssh: bool,
960 pub private: bool,
967 pub image_build_timeout: Option<Duration>,
970}
971
972impl Default for CreateSailboxRequest {
973 fn default() -> CreateSailboxRequest {
976 CreateSailboxRequest {
977 app_id: String::new(),
978 name: String::new(),
979 ingress_ports: Vec::new(),
980 volume_mounts: Vec::new(),
981 image: crate::image::ImageSpec {
982 base: Some(crate::image::BaseImage::Debian),
983 ..crate::image::ImageSpec::default()
984 },
985 size: None,
986 memory_gib: None,
987 disk_gib: None,
988 ssh: false,
989 private: false,
990 image_build_timeout: None,
991 }
992 }
993}
994
995#[cfg(test)]
996mod tests {
997 use super::*;
998
999 #[test]
1000 fn closed_enums_round_trip_through_fromstr_and_display() {
1001 for (text, value) in [
1002 ("newest_active", SailboxListOrder::NewestActive),
1003 ("newest_created", SailboxListOrder::NewestCreated),
1004 ] {
1005 assert_eq!(text.parse::<SailboxListOrder>().unwrap(), value);
1006 assert_eq!(value.to_string(), text);
1007 }
1008 for (text, value) in [
1009 ("running", SailboxStatusFilter::Running),
1010 ("terminated", SailboxStatusFilter::Terminated),
1011 ] {
1012 assert_eq!(text.parse::<SailboxStatusFilter>().unwrap(), value);
1013 assert_eq!(value.to_string(), text);
1014 }
1015 for (text, value) in [
1016 ("tcp", IngressProtocol::Tcp),
1017 ("http", IngressProtocol::Http),
1018 ] {
1019 assert_eq!(text.parse::<IngressProtocol>().unwrap(), value);
1020 assert_eq!(value.to_string(), text);
1021 }
1022 for (text, value) in [
1023 ("s", SailboxSize::Small),
1024 ("m", SailboxSize::Medium),
1025 ("l", SailboxSize::Large),
1026 ] {
1027 assert_eq!(text.parse::<SailboxSize>().unwrap(), value);
1028 assert_eq!(value.to_string(), text);
1029 }
1030 let err = "udp".parse::<IngressProtocol>().unwrap_err();
1031 assert!(matches!(err, SailError::InvalidArgument { .. }));
1032 assert!(err.to_string().contains("udp"));
1033 }
1034
1035 #[test]
1036 fn list_query_default_uses_a_valid_nonzero_limit() {
1037 let query = ListSailboxesQuery::default();
1040 assert_eq!(query.limit, DEFAULT_LIST_LIMIT);
1041 assert!(query.limit > 0);
1042 assert_eq!(query.offset, 0);
1043 }
1044
1045 #[test]
1046 fn route_status_round_trips_through_serde() {
1047 for status in [
1051 ListenerRouteStatus::Unknown,
1052 ListenerRouteStatus::Pending,
1053 ListenerRouteStatus::Active,
1054 ListenerRouteStatus::Restoring,
1055 ListenerRouteStatus::Unavailable,
1056 ListenerRouteStatus::Other("degraded".to_string()),
1057 ] {
1058 let json = serde_json::to_string(&status).unwrap();
1059 let back: ListenerRouteStatus = serde_json::from_str(&json).unwrap();
1060 assert_eq!(
1061 status, back,
1062 "round-trip failed for {status:?} (json {json})"
1063 );
1064 }
1065 assert_eq!(
1067 ListenerRouteStatus::from("LISTENER_ROUTE_STATUS_ACTIVE"),
1068 ListenerRouteStatus::Active,
1069 );
1070 }
1071
1072 #[test]
1073 fn info_deserializes_a_deprecation_notice() {
1074 let value = serde_json::json!({
1075 "sailbox_id": "sb-1", "app_id": "app-1", "app_name": "a", "name": "n",
1076 "image_id": "img-1",
1077 "status": "running", "memory_mib": 2048, "vcpu_count": 4,
1078 "state_disk_size_gib": 10,
1079 "cpu_requested_vcpu": 2, "cpu_used_vcpu": 1.5,
1080 "memory_requested_bytes": 1024, "memory_used_bytes": 512,
1081 "disk_requested_bytes": 4096, "disk_used_bytes": 2048,
1082 "architecture": "amd64", "checkpoint_generation": 7,
1083 "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-02T00:00:00Z",
1084 "deprecation": {
1085 "deadline": "2026-08-01",
1086 "message": "Upgrade this sailbox before 2026-08-01."
1087 }
1088 });
1089 let info: SailboxInfo = serde_json::from_value(value).unwrap();
1090 let notice = info.deprecation.unwrap();
1091 assert_eq!(notice.deadline, "2026-08-01");
1092 assert!(notice.message.starts_with("Upgrade this sailbox"));
1093 }
1094}