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, Copy, PartialEq, Eq)]
418pub enum SailboxSize {
419 Small,
422 Medium,
424 Large,
426}
427
428impl SailboxSize {
429 pub fn as_str(&self) -> &'static str {
431 match self {
432 SailboxSize::Small => "s",
433 SailboxSize::Medium => "m",
434 SailboxSize::Large => "l",
435 }
436 }
437
438 pub fn parse(s: &str) -> Option<SailboxSize> {
440 match s {
441 "s" => Some(SailboxSize::Small),
442 "m" => Some(SailboxSize::Medium),
443 "l" => Some(SailboxSize::Large),
444 _ => None,
445 }
446 }
447
448 pub fn allowed() -> &'static [&'static str] {
450 &["s", "m", "l"]
451 }
452}
453
454impl std::str::FromStr for SailboxSize {
455 type Err = SailError;
456
457 fn from_str(s: &str) -> Result<Self, Self::Err> {
458 SailboxSize::parse(s).ok_or_else(|| SailError::InvalidArgument {
459 message: format!("unknown size {s:?}; use one of s, m, l"),
460 })
461 }
462}
463
464impl std::fmt::Display for SailboxSize {
465 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
466 f.write_str(self.as_str())
467 }
468}
469
470impl Serialize for SailboxSize {
471 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
472 serializer.serialize_str(self.as_str())
473 }
474}
475
476#[derive(Debug, Clone, Default, Serialize)]
479#[non_exhaustive]
480pub struct SailboxHandle {
481 pub sailbox_id: String,
483 pub name: String,
486 pub status: SailboxStatus,
488 #[doc(hidden)]
490 pub worker_address: String,
491 #[doc(hidden)]
493 pub exec_endpoint: String,
494}
495
496#[derive(Debug, Clone, Serialize, Deserialize)]
498#[non_exhaustive]
499pub struct SailboxDeprecation {
500 pub deadline: String,
503 pub message: String,
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize)]
510#[non_exhaustive]
511pub struct SailboxInfo {
512 pub sailbox_id: String,
514 pub app_id: String,
516 pub app_name: String,
518 pub image_id: String,
520 pub name: String,
523 pub status: SailboxStatus,
525 pub memory_mib: i64,
527 pub vcpu_count: i64,
529 pub state_disk_size_gib: i64,
531 pub cpu_requested_vcpu: i64,
533 pub cpu_used_vcpu: f64,
535 pub memory_requested_bytes: i64,
537 pub memory_used_bytes: i64,
539 pub disk_requested_bytes: i64,
541 pub disk_used_bytes: i64,
543 #[serde(default)]
546 pub architecture: String,
547 #[serde(default)]
549 pub guest_schema_version: Option<i64>,
550 #[serde(default)]
552 pub deprecation: Option<SailboxDeprecation>,
553 #[serde(default)]
555 pub error_message: Option<String>,
556 pub checkpoint_generation: i64,
558 #[serde(with = "crate::rfc3339_micros::option", default)]
561 pub started_at: Option<OffsetDateTime>,
562 #[serde(with = "crate::rfc3339_micros::option", default)]
564 pub last_checkpointed_at: Option<OffsetDateTime>,
565 #[serde(with = "crate::rfc3339_micros")]
567 pub created_at: OffsetDateTime,
568 #[serde(with = "crate::rfc3339_micros")]
570 pub updated_at: OffsetDateTime,
571 #[serde(default)]
574 pub created_by_user_id: Option<String>,
575 #[serde(default)]
578 pub visibility: Option<String>,
579 #[serde(default)]
581 pub auto_sleep: AutoSleep,
582}
583
584#[doc(hidden)]
586#[derive(Debug, Clone, Deserialize)]
587#[non_exhaustive]
588pub struct WhoAmI {
589 pub org_id: String,
591 #[serde(default)]
593 pub user_id: Option<String>,
594}
595
596#[derive(Debug, Clone, Serialize)]
598#[non_exhaustive]
599pub struct SailboxPage {
600 pub items: Vec<SailboxInfo>,
602 pub limit: i64,
604 pub offset: i64,
606 pub total: i64,
608 pub has_more: bool,
610}
611
612#[derive(Debug, Clone, Default)]
614pub struct SailboxSpendQuery {
615 pub app_id: Option<String>,
617 pub sailbox_id: Option<String>,
619 pub from: Option<OffsetDateTime>,
622 pub to: Option<OffsetDateTime>,
625}
626
627#[derive(Debug, Clone, Serialize, Deserialize)]
629#[non_exhaustive]
630pub struct SailboxSpendResponse {
631 #[serde(with = "crate::rfc3339_micros")]
633 pub start_at: OffsetDateTime,
634 #[serde(with = "crate::rfc3339_micros")]
636 pub end_at: OffsetDateTime,
637 pub finalized_cost_usd_nanos: i64,
639 pub estimated_active_cost_usd_nanos: i64,
641 pub estimated_total_cost_usd_nanos: i64,
643 pub duration_seconds: i64,
645 pub vcpu_seconds: f64,
647 pub memory_gib_seconds: f64,
649 pub state_disk_gib_seconds: f64,
651 pub pricing_configured: bool,
653 pub rates: SailboxSpendRates,
655 pub sailboxes: Vec<SailboxSpendItem>,
657}
658
659#[derive(Debug, Clone, Default, Serialize, Deserialize)]
661#[non_exhaustive]
662pub struct SailboxSpendRates {
663 pub vcpu_second_usd_nanos: i64,
665 pub memory_gib_second_usd_nanos: i64,
667 pub state_disk_gib_second_usd_nanos: i64,
669 pub creation_usd_nanos: i64,
671 #[serde(default)]
673 pub amd64_state_disk_gib_second_usd_nanos: i64,
674}
675
676#[derive(Debug, Clone, Serialize, Deserialize)]
678#[non_exhaustive]
679pub struct SailboxSpendItem {
680 pub sailbox_id: String,
682 pub app_id: String,
684 pub finalized_cost_usd_nanos: i64,
686 pub estimated_active_cost_usd_nanos: i64,
688 pub estimated_total_cost_usd_nanos: i64,
690 pub duration_seconds: i64,
692 pub vcpu_seconds: f64,
694 pub memory_gib_seconds: f64,
696 pub state_disk_gib_seconds: f64,
698 pub active: bool,
700}
701
702#[derive(Debug, Clone)]
704pub struct SailboxMetricsQuery {
705 pub range: String,
707}
708
709impl Default for SailboxMetricsQuery {
710 fn default() -> SailboxMetricsQuery {
711 SailboxMetricsQuery {
712 range: "24h".to_string(),
713 }
714 }
715}
716
717#[derive(Debug, Clone, Serialize, Deserialize)]
719#[non_exhaustive]
720pub struct SailboxMetricsResponse {
721 pub range: String,
723 pub data: Vec<SailboxMetricPoint>,
725}
726
727#[derive(Debug, Clone, Serialize, Deserialize)]
729#[non_exhaustive]
730pub struct SailboxMetricPoint {
731 #[serde(with = "crate::rfc3339_micros")]
733 pub timestamp: OffsetDateTime,
734 pub cpu_used_vcpu: f64,
736 pub cpu_requested_vcpu: i64,
738 pub memory_used_bytes: i64,
740 pub memory_requested_bytes: i64,
742 pub disk_used_bytes: i64,
744 pub disk_requested_bytes: i64,
746}
747
748#[derive(Debug, Clone, Serialize)]
752#[non_exhaustive]
753pub struct SailboxCheckpoint {
754 pub checkpoint_id: String,
756 pub sailbox_id: String,
758 pub checkpoint_generation: i64,
760 #[serde(with = "crate::rfc3339_micros::option")]
763 pub expires_at: Option<OffsetDateTime>,
764 pub status: SailboxStatus,
766}
767
768#[derive(Debug, Clone)]
770pub struct ListSailboxesQuery {
771 pub app_id: Option<String>,
773 pub status: Option<SailboxStatusFilter>,
775 pub search: Option<String>,
777 #[doc(hidden)]
780 pub credential_policy_id: Option<String>,
781 pub order: SailboxListOrder,
783 pub limit: i64,
785 pub offset: i64,
787}
788
789#[doc(hidden)]
792pub const DEFAULT_LIST_LIMIT: i64 = 50;
793
794#[doc(hidden)]
797pub const MAX_LIST_LIMIT: i64 = 100;
798
799impl Default for ListSailboxesQuery {
800 fn default() -> ListSailboxesQuery {
801 ListSailboxesQuery {
802 app_id: None,
803 status: None,
804 search: None,
805 credential_policy_id: None,
806 order: SailboxListOrder::NewestActive,
807 limit: DEFAULT_LIST_LIMIT,
808 offset: 0,
809 }
810 }
811}
812
813#[derive(Debug, Clone, Serialize)]
815pub struct IngressPort {
816 pub guest_port: u32,
818 pub protocol: IngressProtocol,
820 pub allowlist: Vec<String>,
826}
827
828#[derive(Debug, Clone, Serialize)]
830pub struct VolumeMount {
831 pub volume_id: String,
833 pub mount_path: String,
835}
836
837#[doc(hidden)]
839#[derive(Debug, Clone, Serialize, Deserialize)]
840#[non_exhaustive]
841pub struct CustomDomainInfo {
842 pub domain: String,
844 pub sailbox_id: String,
846 pub guest_port: u32,
848 pub url: String,
850 pub cname_target: String,
852 #[serde(with = "crate::rfc3339_micros")]
854 pub created_at: OffsetDateTime,
855}
856
857#[derive(Debug, Clone, Serialize, Deserialize)]
859#[non_exhaustive]
860pub struct VolumeInfo {
861 pub volume_id: String,
863 pub name: String,
865 pub backend: String,
867 pub status: String,
869 #[serde(with = "crate::rfc3339_micros::option", default)]
871 pub created_at: Option<OffsetDateTime>,
872 #[serde(with = "crate::rfc3339_micros::option", default)]
874 pub updated_at: Option<OffsetDateTime>,
875}
876
877#[derive(Debug, Clone, Deserialize)]
882pub(crate) struct AddListenerWire {
883 pub(crate) guest_port: u32,
884 pub(crate) protocol: ListenerProtocol,
885 #[serde(default)]
886 pub(crate) public_url: String,
887 #[serde(default)]
888 pub(crate) public_host: String,
889 #[serde(default)]
890 pub(crate) public_port: u32,
891}
892
893impl From<AddListenerWire> for crate::worker::Listener {
894 fn from(wire: AddListenerWire) -> crate::worker::Listener {
895 crate::worker::Listener {
896 guest_port: wire.guest_port,
897 protocol: wire.protocol,
898 route_status: ListenerRouteStatus::Unknown,
901 public_url: wire.public_url,
902 public_host: wire.public_host,
903 public_port: wire.public_port,
904 }
905 }
906}
907
908#[derive(Debug, Clone)]
911#[non_exhaustive]
912pub struct IssuedUserCert {
913 pub certificate: String,
915 pub key_id: String,
917}
918
919#[derive(Debug, Clone, PartialEq, Eq)]
922pub enum ListenerEndpoint {
923 Http {
925 url: String,
927 },
928 Tcp {
930 host: String,
932 port: u32,
934 },
935}
936
937impl std::fmt::Display for ListenerEndpoint {
938 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
939 match self {
940 ListenerEndpoint::Http { url } => f.write_str(url),
941 ListenerEndpoint::Tcp { host, port } => write!(f, "{host}:{port}"),
942 }
943 }
944}
945
946#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
961pub enum AutoSleep {
962 #[default]
964 Automatic,
965 NotBefore(Duration),
970 Never,
972}
973
974#[derive(Debug, Clone)]
981pub struct CreateSailboxRequest {
982 pub app_id: String,
984 pub name: String,
986 pub ingress_ports: Vec<IngressPort>,
988 pub volume_mounts: Vec<VolumeMount>,
990 pub image: crate::image::ImageSpec,
993 pub size: Option<SailboxSize>,
995 pub memory_limit_gib: Option<u32>,
998 pub disk_limit_gib: Option<u32>,
1001 pub ssh: bool,
1005 pub private: bool,
1013 pub image_build_timeout: Option<Duration>,
1016 pub auto_sleep: AutoSleep,
1018}
1019
1020impl Default for CreateSailboxRequest {
1021 fn default() -> CreateSailboxRequest {
1024 CreateSailboxRequest {
1025 app_id: String::new(),
1026 name: String::new(),
1027 ingress_ports: Vec::new(),
1028 volume_mounts: Vec::new(),
1029 image: crate::image::ImageSpec {
1030 base: Some(crate::image::BaseImage::Debian),
1031 ..crate::image::ImageSpec::default()
1032 },
1033 size: None,
1034 memory_limit_gib: None,
1035 disk_limit_gib: None,
1036 ssh: false,
1037 private: false,
1038 image_build_timeout: None,
1039 auto_sleep: AutoSleep::Automatic,
1040 }
1041 }
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046 use super::*;
1047
1048 #[test]
1052 fn a_part_second_auto_sleep_floor_rounds_up() {
1053 let seconds = |wait: Duration| {
1054 AutoSleep::NotBefore(wait).to_json()["min_seconds_before_sleep"].clone()
1055 };
1056 assert_eq!(seconds(Duration::from_millis(1500)), serde_json::json!(2));
1057 assert_eq!(seconds(Duration::from_millis(1)), serde_json::json!(1));
1058 assert_eq!(seconds(Duration::from_mins(5)), serde_json::json!(300));
1059 assert_eq!(
1062 AutoSleep::NotBefore(Duration::ZERO).to_json(),
1063 AutoSleep::Automatic.to_json()
1064 );
1065 assert_eq!(
1068 seconds(crate::sailbox::api::MAX_AUTO_SLEEP_WAIT),
1069 serde_json::json!(3600)
1070 );
1071 }
1072
1073 #[test]
1074 fn closed_enums_round_trip_through_fromstr_and_display() {
1075 for (text, value) in [
1076 ("newest_active", SailboxListOrder::NewestActive),
1077 ("newest_created", SailboxListOrder::NewestCreated),
1078 ] {
1079 assert_eq!(text.parse::<SailboxListOrder>().unwrap(), value);
1080 assert_eq!(value.to_string(), text);
1081 }
1082 for (text, value) in [
1083 ("running", SailboxStatusFilter::Running),
1084 ("terminated", SailboxStatusFilter::Terminated),
1085 ] {
1086 assert_eq!(text.parse::<SailboxStatusFilter>().unwrap(), value);
1087 assert_eq!(value.to_string(), text);
1088 }
1089 for (text, value) in [
1090 ("tcp", IngressProtocol::Tcp),
1091 ("http", IngressProtocol::Http),
1092 ] {
1093 assert_eq!(text.parse::<IngressProtocol>().unwrap(), value);
1094 assert_eq!(value.to_string(), text);
1095 }
1096 for (text, value) in [
1097 ("s", SailboxSize::Small),
1098 ("m", SailboxSize::Medium),
1099 ("l", SailboxSize::Large),
1100 ] {
1101 assert_eq!(text.parse::<SailboxSize>().unwrap(), value);
1102 assert_eq!(value.to_string(), text);
1103 }
1104 let err = "udp".parse::<IngressProtocol>().unwrap_err();
1105 assert!(matches!(err, SailError::InvalidArgument { .. }));
1106 assert!(err.to_string().contains("udp"));
1107 }
1108
1109 #[test]
1110 fn list_query_default_uses_a_valid_nonzero_limit() {
1111 let query = ListSailboxesQuery::default();
1114 assert_eq!(query.limit, DEFAULT_LIST_LIMIT);
1115 assert!(query.limit > 0);
1116 assert_eq!(query.offset, 0);
1117 }
1118
1119 #[test]
1120 fn route_status_round_trips_through_serde() {
1121 for status in [
1125 ListenerRouteStatus::Unknown,
1126 ListenerRouteStatus::Pending,
1127 ListenerRouteStatus::Active,
1128 ListenerRouteStatus::Restoring,
1129 ListenerRouteStatus::Unavailable,
1130 ListenerRouteStatus::Other("degraded".to_string()),
1131 ] {
1132 let json = serde_json::to_string(&status).unwrap();
1133 let back: ListenerRouteStatus = serde_json::from_str(&json).unwrap();
1134 assert_eq!(
1135 status, back,
1136 "round-trip failed for {status:?} (json {json})"
1137 );
1138 }
1139 assert_eq!(
1141 ListenerRouteStatus::from("LISTENER_ROUTE_STATUS_ACTIVE"),
1142 ListenerRouteStatus::Active,
1143 );
1144 }
1145
1146 #[test]
1147 fn info_deserializes_a_deprecation_notice() {
1148 let value = serde_json::json!({
1149 "sailbox_id": "sb-1", "app_id": "app-1", "app_name": "a", "name": "n",
1150 "image_id": "img-1",
1151 "status": "running", "memory_mib": 2048, "vcpu_count": 4,
1152 "state_disk_size_gib": 10,
1153 "cpu_requested_vcpu": 2, "cpu_used_vcpu": 1.5,
1154 "memory_requested_bytes": 1024, "memory_used_bytes": 512,
1155 "disk_requested_bytes": 4096, "disk_used_bytes": 2048,
1156 "architecture": "amd64", "checkpoint_generation": 7,
1157 "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-02T00:00:00Z",
1158 "deprecation": {
1159 "deadline": "2026-08-01",
1160 "message": "Upgrade this sailbox before 2026-08-01."
1161 }
1162 });
1163 let info: SailboxInfo = serde_json::from_value(value).unwrap();
1164 let notice = info.deprecation.unwrap();
1165 assert_eq!(notice.deadline, "2026-08-01");
1166 assert!(notice.message.starts_with("Upgrade this sailbox"));
1167 }
1168
1169 #[test]
1170 fn add_listener_reads_the_tcp_dial_target() {
1171 for body in [
1172 r#"{"guest_port":2222,"protocol":"tcp","public_host":"tcp.example.com","public_port":22003}"#,
1173 r#"{"guest_port":2222,"protocol":"tcp","public_host":"tcp.example.com","public_port":22003,"tcp_public_host":"old.example.com","tcp_public_port":1}"#,
1176 ] {
1177 let wire: AddListenerWire = serde_json::from_str(body).unwrap();
1178 let listener = crate::worker::Listener::from(wire);
1179 assert_eq!(listener.public_host, "tcp.example.com");
1180 assert_eq!(listener.public_port, 22003);
1181 }
1182 }
1183}
1184
1185#[derive(Deserialize)]
1189struct AutoSleepWire {
1190 #[serde(default = "crate::sailbox::types::auto_sleep_default_automatic")]
1191 automatic: bool,
1192 #[serde(default)]
1193 min_seconds_before_sleep: Option<u64>,
1194}
1195
1196fn auto_sleep_default_automatic() -> bool {
1197 true
1198}
1199
1200impl Serialize for AutoSleep {
1201 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1202 (*self).to_json().serialize(serializer)
1203 }
1204}
1205
1206impl<'de> Deserialize<'de> for AutoSleep {
1207 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<AutoSleep, D::Error> {
1208 let wire = AutoSleepWire::deserialize(deserializer)?;
1209 if !wire.automatic {
1210 return Ok(AutoSleep::Never);
1211 }
1212 Ok(match wire.min_seconds_before_sleep {
1213 Some(seconds) if seconds > 0 => AutoSleep::NotBefore(Duration::from_secs(seconds)),
1214 _ => AutoSleep::Automatic,
1215 })
1216 }
1217}
1218
1219impl AutoSleep {
1220 pub(crate) fn to_json(self) -> serde_json::Value {
1224 match self {
1225 AutoSleep::Automatic => serde_json::json!({"automatic": true}),
1228 AutoSleep::NotBefore(wait) if wait.is_zero() => {
1229 serde_json::json!({"automatic": true})
1230 }
1231 AutoSleep::NotBefore(wait) => serde_json::json!({
1236 "automatic": true,
1237 "min_seconds_before_sleep": wait.as_secs()
1238 + u64::from(wait.subsec_nanos() > 0),
1239 }),
1240 AutoSleep::Never => serde_json::json!({"automatic": false}),
1241 }
1242 }
1243}