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,
496 pub status: SailboxStatus,
498 #[doc(hidden)]
500 pub worker_address: String,
501 #[doc(hidden)]
503 pub exec_endpoint: String,
504}
505
506#[derive(Debug, Clone, Serialize, Deserialize)]
508#[non_exhaustive]
509pub struct SailboxDeprecation {
510 pub deadline: String,
513 pub message: String,
515}
516
517#[derive(Debug, Clone, Serialize, Deserialize)]
520#[non_exhaustive]
521pub struct SailboxInfo {
522 pub sailbox_id: String,
524 pub app_id: String,
526 pub app_name: String,
528 pub image_id: String,
530 pub name: String,
533 pub status: SailboxStatus,
535 pub memory_mib: i64,
537 pub vcpu_count: i64,
539 pub state_disk_size_gib: i64,
541 pub cpu_requested_vcpu: i64,
543 pub cpu_used_vcpu: f64,
545 pub memory_requested_bytes: i64,
547 pub memory_used_bytes: i64,
549 pub disk_requested_bytes: i64,
551 pub disk_used_bytes: i64,
553 #[serde(default)]
556 pub architecture: String,
557 #[serde(default)]
559 pub guest_schema_version: Option<i64>,
560 #[serde(default)]
562 pub deprecation: Option<SailboxDeprecation>,
563 #[serde(default)]
565 pub error_message: Option<String>,
566 pub checkpoint_generation: i64,
568 #[serde(with = "crate::rfc3339_micros::option", default)]
571 pub started_at: Option<OffsetDateTime>,
572 #[serde(with = "crate::rfc3339_micros::option", default)]
574 pub last_checkpointed_at: Option<OffsetDateTime>,
575 #[serde(with = "crate::rfc3339_micros")]
577 pub created_at: OffsetDateTime,
578 #[serde(with = "crate::rfc3339_micros")]
580 pub updated_at: OffsetDateTime,
581 #[serde(default)]
584 pub created_by_user_id: Option<String>,
585 #[serde(default)]
588 pub visibility: Option<String>,
589}
590
591#[doc(hidden)]
593#[derive(Debug, Clone, Deserialize)]
594#[non_exhaustive]
595pub struct WhoAmI {
596 pub org_id: String,
598 #[serde(default)]
600 pub user_id: Option<String>,
601}
602
603#[derive(Debug, Clone, Serialize)]
605#[non_exhaustive]
606pub struct SailboxPage {
607 pub items: Vec<SailboxInfo>,
609 pub limit: i64,
611 pub offset: i64,
613 pub total: i64,
615 pub has_more: bool,
617}
618
619#[derive(Debug, Clone, Default)]
621pub struct SailboxSpendQuery {
622 pub app_id: Option<String>,
624 pub sailbox_id: Option<String>,
626 pub from: Option<OffsetDateTime>,
629 pub to: Option<OffsetDateTime>,
632}
633
634#[derive(Debug, Clone, Serialize, Deserialize)]
636#[non_exhaustive]
637pub struct SailboxSpendResponse {
638 #[serde(with = "crate::rfc3339_micros")]
640 pub start_at: OffsetDateTime,
641 #[serde(with = "crate::rfc3339_micros")]
643 pub end_at: OffsetDateTime,
644 pub finalized_cost_usd_nanos: i64,
646 pub estimated_active_cost_usd_nanos: i64,
648 pub estimated_total_cost_usd_nanos: i64,
650 pub duration_seconds: i64,
652 pub vcpu_seconds: f64,
654 pub memory_gib_seconds: f64,
656 pub state_disk_gib_seconds: f64,
658 pub pricing_configured: bool,
660 pub rates: SailboxSpendRates,
662 pub sailboxes: Vec<SailboxSpendItem>,
664}
665
666#[derive(Debug, Clone, Default, Serialize, Deserialize)]
668#[non_exhaustive]
669pub struct SailboxSpendRates {
670 pub vcpu_second_usd_nanos: i64,
672 pub memory_gib_second_usd_nanos: i64,
674 pub state_disk_gib_second_usd_nanos: i64,
676 pub creation_usd_nanos: i64,
678 #[serde(default)]
680 pub amd64_state_disk_gib_second_usd_nanos: i64,
681}
682
683#[derive(Debug, Clone, Serialize, Deserialize)]
685#[non_exhaustive]
686pub struct SailboxSpendItem {
687 pub sailbox_id: String,
689 pub app_id: String,
691 pub finalized_cost_usd_nanos: i64,
693 pub estimated_active_cost_usd_nanos: i64,
695 pub estimated_total_cost_usd_nanos: i64,
697 pub duration_seconds: i64,
699 pub vcpu_seconds: f64,
701 pub memory_gib_seconds: f64,
703 pub state_disk_gib_seconds: f64,
705 pub active: bool,
707}
708
709#[derive(Debug, Clone)]
711pub struct SailboxMetricsQuery {
712 pub range: String,
714}
715
716impl Default for SailboxMetricsQuery {
717 fn default() -> SailboxMetricsQuery {
718 SailboxMetricsQuery {
719 range: "24h".to_string(),
720 }
721 }
722}
723
724#[derive(Debug, Clone, Serialize, Deserialize)]
726#[non_exhaustive]
727pub struct SailboxMetricsResponse {
728 pub range: String,
730 pub data: Vec<SailboxMetricPoint>,
732}
733
734#[derive(Debug, Clone, Serialize, Deserialize)]
736#[non_exhaustive]
737pub struct SailboxMetricPoint {
738 #[serde(with = "crate::rfc3339_micros")]
740 pub timestamp: OffsetDateTime,
741 pub cpu_used_vcpu: f64,
743 pub cpu_requested_vcpu: i64,
745 pub memory_used_bytes: i64,
747 pub memory_requested_bytes: i64,
749 pub disk_used_bytes: i64,
751 pub disk_requested_bytes: i64,
753}
754
755#[derive(Debug, Clone, Serialize)]
759#[non_exhaustive]
760pub struct SailboxCheckpoint {
761 pub checkpoint_id: String,
763 pub sailbox_id: String,
765 pub checkpoint_generation: i64,
767 #[serde(with = "crate::rfc3339_micros::option")]
770 pub expires_at: Option<OffsetDateTime>,
771 pub status: SailboxStatus,
773}
774
775#[derive(Debug, Clone)]
777pub struct ListSailboxesQuery {
778 pub app_id: Option<String>,
780 pub status: Option<SailboxStatusFilter>,
782 pub search: Option<String>,
784 #[doc(hidden)]
787 pub credential_policy_id: Option<String>,
788 pub order: SailboxListOrder,
790 pub limit: i64,
792 pub offset: i64,
794}
795
796#[doc(hidden)]
799pub const DEFAULT_LIST_LIMIT: i64 = 50;
800
801#[doc(hidden)]
804pub const MAX_LIST_LIMIT: i64 = 100;
805
806impl Default for ListSailboxesQuery {
807 fn default() -> ListSailboxesQuery {
808 ListSailboxesQuery {
809 app_id: None,
810 status: None,
811 search: None,
812 credential_policy_id: None,
813 order: SailboxListOrder::NewestActive,
814 limit: DEFAULT_LIST_LIMIT,
815 offset: 0,
816 }
817 }
818}
819
820#[derive(Debug, Clone, Serialize)]
822pub struct IngressPort {
823 pub guest_port: u32,
825 pub protocol: IngressProtocol,
827 pub allowlist: Vec<String>,
833}
834
835#[derive(Debug, Clone, Serialize)]
837pub struct VolumeMount {
838 pub volume_id: String,
840 pub mount_path: String,
842}
843
844#[doc(hidden)]
846#[derive(Debug, Clone, Serialize, Deserialize)]
847#[non_exhaustive]
848pub struct CustomDomainInfo {
849 pub domain: String,
851 pub sailbox_id: String,
853 pub guest_port: u32,
855 pub url: String,
857 pub cname_target: String,
859 #[serde(with = "crate::rfc3339_micros")]
861 pub created_at: OffsetDateTime,
862}
863
864#[derive(Debug, Clone, Serialize, Deserialize)]
866#[non_exhaustive]
867pub struct VolumeInfo {
868 pub volume_id: String,
870 pub name: String,
872 pub backend: String,
874 pub status: String,
876 #[serde(with = "crate::rfc3339_micros::option", default)]
878 pub created_at: Option<OffsetDateTime>,
879 #[serde(with = "crate::rfc3339_micros::option", default)]
881 pub updated_at: Option<OffsetDateTime>,
882}
883
884#[derive(Debug, Clone, Deserialize)]
889pub(crate) struct AddListenerWire {
890 pub(crate) guest_port: u32,
891 pub(crate) protocol: ListenerProtocol,
892 #[serde(default)]
893 pub(crate) public_url: String,
894 #[serde(default)]
895 pub(crate) public_host: String,
896 #[serde(default)]
897 pub(crate) public_port: u32,
898}
899
900impl From<AddListenerWire> for crate::worker::Listener {
901 fn from(wire: AddListenerWire) -> crate::worker::Listener {
902 crate::worker::Listener {
903 guest_port: wire.guest_port,
904 protocol: wire.protocol,
905 route_status: ListenerRouteStatus::Unknown,
908 public_url: wire.public_url,
909 public_host: wire.public_host,
910 public_port: wire.public_port,
911 }
912 }
913}
914
915#[derive(Debug, Clone)]
918#[non_exhaustive]
919pub struct IssuedUserCert {
920 pub certificate: String,
922 pub key_id: String,
924}
925
926#[derive(Debug, Clone, PartialEq, Eq)]
929pub enum ListenerEndpoint {
930 Http {
932 url: String,
934 },
935 Tcp {
937 host: String,
939 port: u32,
941 },
942}
943
944impl std::fmt::Display for ListenerEndpoint {
945 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
946 match self {
947 ListenerEndpoint::Http { url } => f.write_str(url),
948 ListenerEndpoint::Tcp { host, port } => write!(f, "{host}:{port}"),
949 }
950 }
951}
952
953#[derive(Debug, Clone)]
959pub struct CreateSailboxRequest {
960 pub app_id: String,
962 pub name: String,
964 pub ingress_ports: Vec<IngressPort>,
966 pub volume_mounts: Vec<VolumeMount>,
968 pub image: crate::image::ImageSpec,
971 pub size: Option<SailboxSize>,
973 pub memory_gib: Option<u32>,
976 pub disk_gib: Option<u32>,
979 pub ssh: bool,
983 pub private: bool,
991 pub image_build_timeout: Option<Duration>,
994}
995
996impl Default for CreateSailboxRequest {
997 fn default() -> CreateSailboxRequest {
1000 CreateSailboxRequest {
1001 app_id: String::new(),
1002 name: String::new(),
1003 ingress_ports: Vec::new(),
1004 volume_mounts: Vec::new(),
1005 image: crate::image::ImageSpec {
1006 base: Some(crate::image::BaseImage::Debian),
1007 ..crate::image::ImageSpec::default()
1008 },
1009 size: None,
1010 memory_gib: None,
1011 disk_gib: None,
1012 ssh: false,
1013 private: false,
1014 image_build_timeout: None,
1015 }
1016 }
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021 use super::*;
1022
1023 #[test]
1024 fn closed_enums_round_trip_through_fromstr_and_display() {
1025 for (text, value) in [
1026 ("newest_active", SailboxListOrder::NewestActive),
1027 ("newest_created", SailboxListOrder::NewestCreated),
1028 ] {
1029 assert_eq!(text.parse::<SailboxListOrder>().unwrap(), value);
1030 assert_eq!(value.to_string(), text);
1031 }
1032 for (text, value) in [
1033 ("running", SailboxStatusFilter::Running),
1034 ("terminated", SailboxStatusFilter::Terminated),
1035 ] {
1036 assert_eq!(text.parse::<SailboxStatusFilter>().unwrap(), value);
1037 assert_eq!(value.to_string(), text);
1038 }
1039 for (text, value) in [
1040 ("tcp", IngressProtocol::Tcp),
1041 ("http", IngressProtocol::Http),
1042 ] {
1043 assert_eq!(text.parse::<IngressProtocol>().unwrap(), value);
1044 assert_eq!(value.to_string(), text);
1045 }
1046 for (text, value) in [
1047 ("s", SailboxSize::Small),
1048 ("m", SailboxSize::Medium),
1049 ("l", SailboxSize::Large),
1050 ] {
1051 assert_eq!(text.parse::<SailboxSize>().unwrap(), value);
1052 assert_eq!(value.to_string(), text);
1053 }
1054 let err = "udp".parse::<IngressProtocol>().unwrap_err();
1055 assert!(matches!(err, SailError::InvalidArgument { .. }));
1056 assert!(err.to_string().contains("udp"));
1057 }
1058
1059 #[test]
1060 fn list_query_default_uses_a_valid_nonzero_limit() {
1061 let query = ListSailboxesQuery::default();
1064 assert_eq!(query.limit, DEFAULT_LIST_LIMIT);
1065 assert!(query.limit > 0);
1066 assert_eq!(query.offset, 0);
1067 }
1068
1069 #[test]
1070 fn route_status_round_trips_through_serde() {
1071 for status in [
1075 ListenerRouteStatus::Unknown,
1076 ListenerRouteStatus::Pending,
1077 ListenerRouteStatus::Active,
1078 ListenerRouteStatus::Restoring,
1079 ListenerRouteStatus::Unavailable,
1080 ListenerRouteStatus::Other("degraded".to_string()),
1081 ] {
1082 let json = serde_json::to_string(&status).unwrap();
1083 let back: ListenerRouteStatus = serde_json::from_str(&json).unwrap();
1084 assert_eq!(
1085 status, back,
1086 "round-trip failed for {status:?} (json {json})"
1087 );
1088 }
1089 assert_eq!(
1091 ListenerRouteStatus::from("LISTENER_ROUTE_STATUS_ACTIVE"),
1092 ListenerRouteStatus::Active,
1093 );
1094 }
1095
1096 #[test]
1097 fn info_deserializes_a_deprecation_notice() {
1098 let value = serde_json::json!({
1099 "sailbox_id": "sb-1", "app_id": "app-1", "app_name": "a", "name": "n",
1100 "image_id": "img-1",
1101 "status": "running", "memory_mib": 2048, "vcpu_count": 4,
1102 "state_disk_size_gib": 10,
1103 "cpu_requested_vcpu": 2, "cpu_used_vcpu": 1.5,
1104 "memory_requested_bytes": 1024, "memory_used_bytes": 512,
1105 "disk_requested_bytes": 4096, "disk_used_bytes": 2048,
1106 "architecture": "amd64", "checkpoint_generation": 7,
1107 "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-02T00:00:00Z",
1108 "deprecation": {
1109 "deadline": "2026-08-01",
1110 "message": "Upgrade this sailbox before 2026-08-01."
1111 }
1112 });
1113 let info: SailboxInfo = serde_json::from_value(value).unwrap();
1114 let notice = info.deprecation.unwrap();
1115 assert_eq!(notice.deadline, "2026-08-01");
1116 assert!(notice.message.starts_with("Upgrade this sailbox"));
1117 }
1118
1119 #[test]
1120 fn add_listener_reads_the_tcp_dial_target() {
1121 for body in [
1122 r#"{"guest_port":2222,"protocol":"tcp","public_host":"tcp.example.com","public_port":22003}"#,
1123 r#"{"guest_port":2222,"protocol":"tcp","public_host":"tcp.example.com","public_port":22003,"tcp_public_host":"old.example.com","tcp_public_port":1}"#,
1126 ] {
1127 let wire: AddListenerWire = serde_json::from_str(body).unwrap();
1128 let listener = crate::worker::Listener::from(wire);
1129 assert_eq!(listener.public_host, "tcp.example.com");
1130 assert_eq!(listener.public_port, 22003);
1131 }
1132 }
1133}