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 = "time::serde::rfc3339::option", default)]
568 pub started_at: Option<OffsetDateTime>,
569 #[serde(with = "time::serde::rfc3339::option", default)]
571 pub last_checkpointed_at: Option<OffsetDateTime>,
572 #[serde(with = "time::serde::rfc3339")]
574 pub created_at: OffsetDateTime,
575 #[serde(with = "time::serde::rfc3339")]
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 = "time::serde::rfc3339")]
637 pub start_at: OffsetDateTime,
638 #[serde(with = "time::serde::rfc3339")]
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 = "time::serde::rfc3339")]
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 = "time::serde::rfc3339::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 pub order: SailboxListOrder,
784 pub limit: i64,
786 pub offset: i64,
788}
789
790#[doc(hidden)]
793pub const DEFAULT_LIST_LIMIT: i64 = 50;
794
795impl Default for ListSailboxesQuery {
796 fn default() -> ListSailboxesQuery {
797 ListSailboxesQuery {
798 app_id: None,
799 status: None,
800 search: None,
801 order: SailboxListOrder::NewestActive,
802 limit: DEFAULT_LIST_LIMIT,
803 offset: 0,
804 }
805 }
806}
807
808#[derive(Debug, Clone, Serialize)]
810pub struct IngressPort {
811 pub guest_port: u32,
813 pub protocol: IngressProtocol,
815 pub allowlist: Vec<String>,
819}
820
821#[derive(Debug, Clone, Serialize)]
823pub struct VolumeMount {
824 pub volume_id: String,
826 pub mount_path: String,
828}
829
830#[derive(Debug, Clone, Serialize, Deserialize)]
833#[non_exhaustive]
834pub struct VolumeInfo {
835 pub volume_id: String,
837 pub name: String,
839 pub backend: String,
841 pub status: String,
843 #[serde(with = "time::serde::rfc3339::option", default)]
845 pub created_at: Option<OffsetDateTime>,
846 #[serde(with = "time::serde::rfc3339::option", default)]
848 pub updated_at: Option<OffsetDateTime>,
849}
850
851#[derive(Debug, Clone, Deserialize)]
856pub(crate) struct AddListenerWire {
857 pub(crate) guest_port: u32,
858 pub(crate) protocol: ListenerProtocol,
859 #[serde(default)]
860 pub(crate) public_url: String,
861 #[serde(default, rename = "tcp_public_host")]
862 pub(crate) public_host: String,
863 #[serde(default, rename = "tcp_public_port")]
864 pub(crate) public_port: u32,
865}
866
867impl From<AddListenerWire> for crate::worker::Listener {
868 fn from(wire: AddListenerWire) -> crate::worker::Listener {
869 crate::worker::Listener {
870 guest_port: wire.guest_port,
871 protocol: wire.protocol,
872 route_status: ListenerRouteStatus::Unknown,
875 public_url: wire.public_url,
876 public_host: wire.public_host,
877 public_port: wire.public_port,
878 }
879 }
880}
881
882#[derive(Debug, Clone)]
885#[non_exhaustive]
886pub struct IssuedUserCert {
887 pub certificate: String,
889 pub key_id: String,
891}
892
893#[derive(Debug, Clone, PartialEq, Eq)]
896pub enum ListenerEndpoint {
897 Http {
899 url: String,
901 },
902 Tcp {
904 host: String,
906 port: u32,
908 },
909}
910
911impl std::fmt::Display for ListenerEndpoint {
912 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
913 match self {
914 ListenerEndpoint::Http { url } => f.write_str(url),
915 ListenerEndpoint::Tcp { host, port } => write!(f, "{host}:{port}"),
916 }
917 }
918}
919
920#[derive(Debug, Clone)]
926pub struct CreateSailboxRequest {
927 pub app_id: String,
929 pub name: String,
931 pub ingress_ports: Vec<IngressPort>,
933 pub volume_mounts: Vec<VolumeMount>,
935 pub image: crate::image::ImageSpec,
938 pub size: Option<SailboxSize>,
940 pub memory_gib: Option<u32>,
943 pub disk_gib: Option<u32>,
946 pub ssh: bool,
950 pub private: bool,
957 pub image_build_timeout: Option<Duration>,
960}
961
962impl Default for CreateSailboxRequest {
963 fn default() -> CreateSailboxRequest {
966 CreateSailboxRequest {
967 app_id: String::new(),
968 name: String::new(),
969 ingress_ports: Vec::new(),
970 volume_mounts: Vec::new(),
971 image: crate::image::ImageSpec {
972 base: Some(crate::image::BaseImage::Debian),
973 ..crate::image::ImageSpec::default()
974 },
975 size: None,
976 memory_gib: None,
977 disk_gib: None,
978 ssh: false,
979 private: false,
980 image_build_timeout: None,
981 }
982 }
983}
984
985#[cfg(test)]
986mod tests {
987 use super::*;
988
989 #[test]
990 fn closed_enums_round_trip_through_fromstr_and_display() {
991 for (text, value) in [
992 ("newest_active", SailboxListOrder::NewestActive),
993 ("newest_created", SailboxListOrder::NewestCreated),
994 ] {
995 assert_eq!(text.parse::<SailboxListOrder>().unwrap(), value);
996 assert_eq!(value.to_string(), text);
997 }
998 for (text, value) in [
999 ("running", SailboxStatusFilter::Running),
1000 ("terminated", SailboxStatusFilter::Terminated),
1001 ] {
1002 assert_eq!(text.parse::<SailboxStatusFilter>().unwrap(), value);
1003 assert_eq!(value.to_string(), text);
1004 }
1005 for (text, value) in [
1006 ("tcp", IngressProtocol::Tcp),
1007 ("http", IngressProtocol::Http),
1008 ] {
1009 assert_eq!(text.parse::<IngressProtocol>().unwrap(), value);
1010 assert_eq!(value.to_string(), text);
1011 }
1012 for (text, value) in [
1013 ("s", SailboxSize::Small),
1014 ("m", SailboxSize::Medium),
1015 ("l", SailboxSize::Large),
1016 ] {
1017 assert_eq!(text.parse::<SailboxSize>().unwrap(), value);
1018 assert_eq!(value.to_string(), text);
1019 }
1020 let err = "udp".parse::<IngressProtocol>().unwrap_err();
1021 assert!(matches!(err, SailError::InvalidArgument { .. }));
1022 assert!(err.to_string().contains("udp"));
1023 }
1024
1025 #[test]
1026 fn list_query_default_uses_a_valid_nonzero_limit() {
1027 let query = ListSailboxesQuery::default();
1030 assert_eq!(query.limit, DEFAULT_LIST_LIMIT);
1031 assert!(query.limit > 0);
1032 assert_eq!(query.offset, 0);
1033 }
1034
1035 #[test]
1036 fn route_status_round_trips_through_serde() {
1037 for status in [
1041 ListenerRouteStatus::Unknown,
1042 ListenerRouteStatus::Pending,
1043 ListenerRouteStatus::Active,
1044 ListenerRouteStatus::Restoring,
1045 ListenerRouteStatus::Unavailable,
1046 ListenerRouteStatus::Other("degraded".to_string()),
1047 ] {
1048 let json = serde_json::to_string(&status).unwrap();
1049 let back: ListenerRouteStatus = serde_json::from_str(&json).unwrap();
1050 assert_eq!(
1051 status, back,
1052 "round-trip failed for {status:?} (json {json})"
1053 );
1054 }
1055 assert_eq!(
1057 ListenerRouteStatus::from("LISTENER_ROUTE_STATUS_ACTIVE"),
1058 ListenerRouteStatus::Active,
1059 );
1060 }
1061
1062 #[test]
1063 fn info_deserializes_a_deprecation_notice() {
1064 let value = serde_json::json!({
1065 "sailbox_id": "sb-1", "app_id": "app-1", "app_name": "a", "name": "n",
1066 "image_id": "img-1",
1067 "status": "running", "memory_mib": 2048, "vcpu_count": 4,
1068 "state_disk_size_gib": 10,
1069 "cpu_requested_vcpu": 2, "cpu_used_vcpu": 1.5,
1070 "memory_requested_bytes": 1024, "memory_used_bytes": 512,
1071 "disk_requested_bytes": 4096, "disk_used_bytes": 2048,
1072 "architecture": "amd64", "checkpoint_generation": 7,
1073 "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-02T00:00:00Z",
1074 "deprecation": {
1075 "deadline": "2026-08-01",
1076 "message": "Upgrade this sailbox before 2026-08-01."
1077 }
1078 });
1079 let info: SailboxInfo = serde_json::from_value(value).unwrap();
1080 let notice = info.deprecation.unwrap();
1081 assert_eq!(notice.deadline, "2026-08-01");
1082 assert!(notice.message.starts_with("Upgrade this sailbox"));
1083 }
1084}