1use std::{
2 fs,
3 io::{self, Read, Write},
4 path::{Path, PathBuf},
5};
6
7use seaplane::api::compute::v1::Flight as FlightModel;
8use serde::{Deserialize, Serialize};
9use tabwriter::TabWriter;
10
11use crate::{
12 context::{Ctx, FlightCtx},
13 error::{CliError, CliErrorKind, Context, Result},
14 fs::{FromDisk, ToDisk},
15 ops::Id,
16 printer::{Color, Output},
17};
18
19#[derive(Deserialize, Serialize, Clone, Debug)]
21pub struct Flight {
22 pub id: Id,
23 #[serde(flatten)]
24 pub model: FlightModel,
25}
26
27impl Flight {
28 pub fn new(model: FlightModel) -> Self { Self { id: Id::new(), model } }
29
30 pub fn from_json(s: &str) -> Result<Flight> { serde_json::from_str(s).map_err(CliError::from) }
31
32 pub fn starts_with(&self, s: &str) -> bool {
33 self.id.to_string().starts_with(s) || self.model.name().starts_with(s)
34 }
35
36 pub fn update_from(&mut self, ctx: &FlightCtx, keep_src_name: bool) -> Result<()> {
38 let mut dest_builder = FlightModel::builder();
39
40 if keep_src_name {
42 dest_builder = dest_builder.name(self.model.name());
43 } else {
44 dest_builder = dest_builder.name(&ctx.name_id);
45 }
46
47 if let Some(image) = ctx.image.clone() {
48 dest_builder = dest_builder.image_reference(image);
49 } else {
50 dest_builder = dest_builder.image_reference(self.model.image().clone());
51 }
52
53 if ctx.minimum != 1 {
54 dest_builder = dest_builder.minimum(ctx.minimum);
55 } else {
56 dest_builder = dest_builder.minimum(self.model.minimum());
57 }
58
59 if let Some(max) = ctx.maximum {
60 dest_builder = dest_builder.maximum(max);
61 } else if ctx.reset_maximum {
62 dest_builder.clear_maximum();
63 } else if let Some(max) = self.model.maximum() {
64 dest_builder = dest_builder.maximum(max);
65 }
66
67 for arch in ctx.architecture.iter().chain(self.model.architecture()) {
69 dest_builder = dest_builder.add_architecture(*arch);
70 }
71
72 #[cfg(feature = "unstable")]
74 {
75 let orig_api_perms = self.model.api_permission();
76 let cli_api_perms = ctx.api_permission;
77 match (orig_api_perms, cli_api_perms) {
78 (true, false) => dest_builder = dest_builder.api_permission(false),
79 (false, true) => dest_builder = dest_builder.api_permission(true),
80 _ => (),
81 }
82 }
83
84 self.model = dest_builder.build().expect("Failed to build Flight");
85 Ok(())
86 }
87
88 fn from_at_str(flight: &str) -> Result<Self> {
90 if flight == "@-" {
92 let mut buf = String::new();
93 let stdin = io::stdin();
94 let mut stdin_lock = stdin.lock();
95 stdin_lock.read_to_string(&mut buf)?;
96
97 let new_flight = Flight::from_json(&buf)?;
99 return Ok(new_flight);
100 } else if let Some(path) = flight.strip_prefix('@') {
102 let new_flight = Flight::from_json(
103 &fs::read_to_string(path)
104 .map_err(CliError::from)
105 .context("\n\tpath: ")
106 .with_color_context(|| (Color::Yellow, path))?,
107 )?;
108 return Ok(new_flight);
109 }
110
111 Err(CliErrorKind::InvalidCliValue(None, flight.into()).into_err())
112 }
113}
114
115#[derive(Debug, Deserialize, Serialize, Default, Clone)]
116#[serde(transparent)]
117pub struct Flights {
118 #[serde(skip)]
119 loaded_from: Option<PathBuf>,
120 inner: Vec<Flight>,
121}
122
123impl FromDisk for Flights {
124 fn set_loaded_from<P: AsRef<Path>>(&mut self, p: P) {
125 self.loaded_from = Some(p.as_ref().into());
126 }
127
128 fn loaded_from(&self) -> Option<&Path> { self.loaded_from.as_deref() }
129}
130
131impl ToDisk for Flights {}
132
133impl Flights {
134 pub fn add_from_at_strs<S>(&mut self, flights: Vec<S>) -> Result<Vec<String>>
139 where
140 S: AsRef<str>,
141 {
142 if flights.iter().filter(|f| f.as_ref() == "@-").count() > 1 {
143 return Err(CliErrorKind::MultipleAtStdin.into_err());
144 }
145 let mut ret = Vec::new();
146
147 for flight in flights {
148 let new_flight = Flight::from_at_str(flight.as_ref())?;
149 ret.push(new_flight.model.name().to_owned());
150 self.inner.push(new_flight);
151 }
152
153 Ok(ret)
154 }
155
156 pub fn remove_indices(&mut self, indices: &[usize]) -> Vec<Flight> {
159 indices
163 .iter()
164 .enumerate()
165 .map(|(i, idx)| self.inner.remove(idx - i))
166 .collect()
167 }
168
169 pub fn indices_of_left_matches(&self, needle: &str) -> Vec<usize> {
171 self.inner
173 .iter()
174 .enumerate()
175 .filter(|(_idx, flight)| flight.starts_with(needle))
176 .map(|(idx, _flight)| idx)
177 .collect()
178 }
179
180 pub fn indices_of_matches(&self, needle: &str) -> Vec<usize> {
182 self.inner
184 .iter()
185 .enumerate()
186 .filter(|(_idx, flight)| {
187 flight.id.to_string() == needle || flight.model.name() == needle
188 })
189 .map(|(idx, _flight)| idx)
190 .collect()
191 }
192
193 pub fn iter(&self) -> impl Iterator<Item = &Flight> { self.inner.iter() }
194
195 pub fn clone_flight(&mut self, src: &str, exact: bool) -> Result<Flight> {
196 let src_flight = self.remove_flight(src, exact)?;
197 let model = src_flight.model.clone();
198
199 self.inner.push(src_flight);
201
202 Ok(Flight::new(model))
203 }
204
205 pub fn update_or_create_flight(&mut self, model: &FlightModel) -> Vec<(String, Id)> {
208 let mut found = false;
209 let mut ret = Vec::new();
210 for flight in self
211 .inner
212 .iter_mut()
213 .filter(|f| f.model.name() == model.name() && f.model.image_str() == model.image_str())
214 {
215 found = true;
216 flight.model.set_minimum(model.minimum());
217 flight.model.set_maximum(model.maximum());
218
219 for arch in model.architecture() {
220 flight.model.add_architecture(*arch);
221 }
222
223 #[cfg(feature = "unstable")]
224 {
225 flight.model.set_api_permission(model.api_permission());
226 }
227 }
228
229 if !found {
230 let f = Flight::new(model.clone());
231 ret.push((f.model.name().to_owned(), f.id));
232 self.inner.push(f);
233 }
234
235 ret
236 }
237
238 pub fn update_flight(&mut self, src: &str, exact: bool, ctx: &FlightCtx) -> Result<()> {
239 let mut src_flight = self.remove_flight(src, exact)?;
240 src_flight.update_from(ctx, ctx.generated_name)?;
241
242 self.inner.push(src_flight);
244
245 Ok(())
246 }
247
248 pub fn add_flight(&mut self, flight: Flight) { self.inner.push(flight); }
249
250 pub fn remove_flight(&mut self, src: &str, exact: bool) -> Result<Flight> {
251 let indices =
254 if exact { self.indices_of_matches(src) } else { self.indices_of_left_matches(src) };
255 match indices.len() {
256 0 => return Err(CliErrorKind::NoMatchingItem(src.into()).into_err()),
257 1 => (),
258 _ => return Err(CliErrorKind::AmbiguousItem(src.into()).into_err()),
259 }
260
261 Ok(self.remove_indices(&indices).pop().unwrap())
262 }
263
264 pub fn find_name(&self, name: &str) -> Option<&Flight> {
265 self.inner.iter().find(|f| f.model.name() == name)
266 }
267
268 pub fn find_name_or_partial_id(&self, needle: &str) -> Option<&Flight> {
269 self.inner
270 .iter()
271 .find(|f| f.model.name() == needle || f.id.to_string().starts_with(needle))
272 }
273}
274
275impl Output for Flights {
276 fn print_json(&self, _ctx: &Ctx) -> Result<()> {
277 cli_println!("{}", serde_json::to_string(self)?);
278
279 Ok(())
280 }
281
282 fn print_table(&self, ctx: &Ctx) -> Result<()> {
283 let buf = Vec::new();
284 let mut tw = TabWriter::new(buf);
285 writeln!(tw, "LOCAL ID\tNAME\tIMAGE\tMIN\tMAX\tARCH\tAPI PERMS")?;
287 for flight in self.iter() {
288 let arch = flight
289 .model
290 .architecture()
291 .map(ToString::to_string)
292 .collect::<Vec<_>>()
293 .join(",");
294
295 #[cfg_attr(not(feature = "unstable"), allow(unused_mut))]
296 let mut api_perms = false;
297 let _ = api_perms;
299 #[cfg(feature = "unstable")]
300 {
301 api_perms = flight.model.api_permission();
302 }
303 writeln!(
304 tw,
305 "{}\t{}\t{}\t{}\t{}\t{}\t{}",
306 &flight.id.to_string()[..8], flight.model.name(),
308 flight.model.image_str().trim_start_matches(&ctx.registry),
309 flight.model.minimum(),
310 flight
311 .model
312 .maximum()
313 .map(|n| format!("{n}"))
314 .unwrap_or_else(|| "INF".into()),
315 if arch.is_empty() { "auto" } else { &*arch },
316 api_perms,
317 )?;
318 }
319 tw.flush()?;
320
321 cli_println!(
322 "{}",
323 String::from_utf8_lossy(
324 &tw.into_inner()
325 .map_err(|_| CliError::bail("IO flush error"))?
326 )
327 );
328
329 Ok(())
330 }
331}