1use serde::{Deserialize, Serialize};
2
3use super::deserialize_bool_or_null;
4use crate::output::Tabular;
5
6#[derive(Debug, Deserialize, Serialize)]
7#[serde(rename_all = "camelCase")]
8pub struct Machine {
9 pub id: u64,
10 pub name: String,
11 pub os: String,
12 #[serde(default)]
13 pub points: u32,
14 #[serde(default)]
15 pub rating: Option<f64>,
16 #[serde(default)]
17 pub difficulty: Option<u32>,
18 #[serde(default)]
19 pub difficulty_text: Option<String>,
20 #[serde(default)]
21 pub user_owns_count: u32,
22 #[serde(default)]
23 pub root_owns_count: u32,
24 #[serde(default)]
25 pub auth_user_in_user_owns: Option<bool>,
26 #[serde(default)]
27 pub auth_user_in_root_owns: Option<bool>,
28 #[serde(default, deserialize_with = "deserialize_bool_or_null")]
29 pub todo: bool,
30 #[serde(default)]
31 pub state: Option<String>,
32 #[serde(default)]
33 pub ip: Option<String>,
34 #[serde(default, deserialize_with = "deserialize_bool_or_null")]
35 pub free: bool,
36 #[serde(default)]
37 pub release_date: Option<String>,
38 #[serde(default)]
39 pub play_info: Option<MachinePlayInfo>,
40 #[serde(default)]
41 pub first_creator: Option<Creator>,
42 #[serde(default)]
43 pub labels: Vec<Label>,
44}
45
46#[derive(Debug, Deserialize, Serialize)]
47#[serde(rename_all = "camelCase")]
48pub struct MachinePlayInfo {
49 #[serde(default)]
50 pub is_spawned: Option<bool>,
51 #[serde(default)]
52 pub is_spawning: Option<bool>,
53 #[serde(default, deserialize_with = "deserialize_bool_or_null")]
54 pub is_active: bool,
55 #[serde(default)]
56 pub active_player_count: Option<u32>,
57 #[serde(default)]
58 pub expires_at: Option<String>,
59}
60
61#[derive(Debug, Deserialize, Serialize)]
62#[serde(rename_all = "camelCase")]
63pub struct Creator {
64 pub id: u64,
65 pub name: String,
66 #[serde(default)]
67 pub avatar: Option<String>,
68}
69
70#[derive(Debug, Deserialize, Serialize)]
71pub struct Label {
72 pub color: String,
73 pub name: String,
74}
75
76#[derive(Debug, Deserialize)]
77pub struct ActiveVmResponse {
78 pub info: Option<ActiveVmInfo>,
79}
80
81#[derive(Debug, Deserialize, Serialize)]
82#[serde(rename_all = "camelCase")]
83pub struct ActiveVmInfo {
84 pub id: u64,
85 pub name: String,
86 #[serde(default)]
87 pub avatar: Option<String>,
88 #[serde(default)]
89 pub expires_at: Option<String>,
90 #[serde(default, rename = "type")]
91 pub vm_type: Option<String>,
92 #[serde(default)]
93 pub lab_server: Option<String>,
94}
95
96#[derive(Debug, Deserialize)]
97pub struct MachineProfileResponse {
98 pub info: Machine,
99}
100
101#[derive(Debug, Deserialize)]
102pub struct TodoListResponse {
103 pub info: Vec<Machine>,
104}
105
106impl Tabular for Machine {
107 fn headers() -> Vec<&'static str> {
108 vec![
109 "ID",
110 "Name",
111 "OS",
112 "Difficulty",
113 "Rating",
114 "Points",
115 "State",
116 "User",
117 "Root",
118 ]
119 }
120
121 fn row(&self) -> Vec<String> {
122 let user_own = match self.auth_user_in_user_owns {
123 Some(true) => "✓",
124 _ => "",
125 };
126 let root_own = match self.auth_user_in_root_owns {
127 Some(true) => "✓",
128 _ => "",
129 };
130 vec![
131 self.id.to_string(),
132 self.name.clone(),
133 self.os.clone(),
134 self.difficulty_text.clone().unwrap_or_default(),
135 self.rating.map(|r| format!("{r:.1}")).unwrap_or_default(),
136 self.points.to_string(),
137 self.state.clone().unwrap_or_default(),
138 user_own.to_string(),
139 root_own.to_string(),
140 ]
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use crate::models::Paginated;
148
149 #[test]
150 fn deserialize_machines_list() {
151 let json = include_str!("../../tests/fixtures/v5-machines-list.json");
152 let result: Paginated<Machine> = serde_json::from_str(json).unwrap();
153 assert_eq!(result.data.len(), 15);
154 let cap = &result.data[0];
155 assert_eq!(cap.name, "Cap");
156 assert_eq!(cap.os, "Linux");
157 assert_eq!(cap.difficulty_text.as_deref(), Some("Easy"));
158 assert_eq!(cap.auth_user_in_user_owns, Some(true));
159 }
160
161 #[test]
162 fn deserialize_machines_list_live() {
163 let json = include_str!("../../tests/fixtures/v5-machines-list-live.json");
164 let result: Paginated<Machine> = serde_json::from_str(json).unwrap();
165 assert_eq!(result.data.len(), 3);
166 assert_eq!(result.data[0].name, "TestMachine-1");
167 assert_eq!(result.data[0].os, "Linux");
168 assert_eq!(result.data[0].difficulty, Some(28));
169 assert!(result.data[0].free);
170 assert_eq!(result.data[0].state.as_deref(), Some("retired_free"));
171 assert!(result.data[0].labels.iter().any(|l| l.name == "STAFF PICK"));
172 assert!(result.data[0].first_creator.is_some());
173 }
174
175 #[test]
176 fn deserialize_active_vm_null() {
177 let json = include_str!("../../tests/fixtures/v5-virtual-machine-active.json");
178 let result: ActiveVmResponse = serde_json::from_str(json).unwrap();
179 assert!(result.info.is_none());
180 }
181}