seaplane_cli/cli/cmds/formation/
fetch.rs

1use std::collections::HashSet;
2
3use clap::{ArgMatches, Command};
4
5use crate::{api::FormationsReq, cli::CliCommand, error::Result, printer::Pb, Ctx};
6
7#[derive(Copy, Clone, Debug)]
8pub struct SeaplaneFormationFetch;
9
10impl SeaplaneFormationFetch {
11    pub fn command() -> Command {
12        // TODO: add a --no-overwrite or similar
13        Command::new("fetch-remote")
14            .visible_aliases(["fetch", "sync", "synchronize"])
15            .about("Fetch remote Formation Instances and create/synchronize local Plan definitions")
16            .override_usage("
17    seaplane formation fetch-remote
18    seaplane formation fetch-remote [NAME|ID]",
19            )
20            .arg(
21                arg!(formation = ["NAME|ID"])
22                    .help("The NAME or ID of the remote Formation Instance to fetch, omit to fetch all Formation Instances"),
23            )
24    }
25}
26
27impl CliCommand for SeaplaneFormationFetch {
28    // TODO: async
29    fn run(&self, ctx: &mut Ctx) -> Result<()> {
30        let pb = Pb::new(ctx);
31        pb.set_message("Gathering Formation Names...");
32
33        let mut req = FormationsReq::new_delay_token(ctx)?;
34        if let Some(name) = &ctx.args.name_id {
35            req.set_name(name)?;
36        }
37        let names = req.get_formation_names()?;
38
39        pb.set_message("Syncing Formations...");
40        // This gets us everything the API knows about, but with totally new local IDs. So we need
41        // to ignore those except how they relate to eachother (i.e. they won't match anything in
42        // our local DB, but they will match within these instances returned by the API).
43        //
44        // We need to map them to our OWN local IDs and update the DB.
45        let mut remote_instances = req.get_all_formations(&names, &pb)?;
46
47        // Keep track of what new items we've downloaded that our local DB didn't know about
48        let mut flights_added = HashSet::new();
49        let mut formations_added = HashSet::new();
50        let mut configs_updated = HashSet::new();
51
52        // Start going through the instances by formation
53        for formation in remote_instances.formations.iter_mut() {
54            // Loop through all the Formation Configurations defined in this Formation
55            for cfg in formation.configs().iter().filter_map(|id| {
56                // get the index of the Config where the ID matches
57                if let Some(i) = remote_instances
58                    .configurations
59                    .iter()
60                    .enumerate()
61                    .find_map(|(i, cfg)| if &cfg.id == id { Some(i) } else { None })
62                {
63                    // Map a config ID to an actual Config. We have to use these long chained calls
64                    // so Rust can tell that `formations` itself isn't being
65                    // borrowed, just it's fields.
66                    Some(remote_instances.configurations.swap_remove(i))
67                } else {
68                    None
69                }
70            }) {
71                // Add or update all flights this configuration references
72                for flight in cfg.model.flights() {
73                    // If the name AND image match something in our local DB we update, otherwise
74                    // we assume it's new and add it to our local DB, new ID and all
75                    let names_ids = ctx.db.flights.update_or_create_flight(flight);
76                    flights_added.extend(names_ids);
77                }
78
79                // Keep track of the old ID incase we need to replace it
80                let old_id = cfg.id;
81                // if we only updated, and didn't create, we need to replace the random local ID
82                // that was assigned when we downloaded all the configs, with the *real* local ID
83                if let Some(real_id) = ctx.db.formations.update_or_create_configuration(cfg) {
84                    formation.replace_id(&old_id, real_id);
85                    configs_updated.insert((formation.name.clone().unwrap(), real_id));
86                }
87            }
88            // Add or update the formation itself (which is really just a list of configuration
89            // local IDs)
90            if let Some(id) = ctx
91                .db
92                .formations
93                .update_or_create_formation(formation.clone())
94            {
95                formations_added.insert((formation.name.clone().unwrap(), id));
96            }
97        }
98
99        pb.finish_and_clear();
100
101        if !ctx.internal_run {
102            let mut count = 0;
103            for (name, id) in formations_added {
104                count += 1;
105                cli_print!("Successfully synchronized Formation Instance '");
106                cli_print!(@Green, "{name}");
107                cli_print!("' with local Formation ID '");
108                cli_print!(@Green, "{}", &id.to_string()[..8]);
109                cli_println!("'");
110            }
111            for (name, id) in configs_updated {
112                count += 1;
113                cli_print!("Successfully synchronized Formation Configuration in Formation '");
114                cli_print!(@Green, "{name}");
115                cli_print!("' with local Formation Configuration ID '");
116                cli_print!(@Green, "{}", &id.to_string()[..8]);
117                cli_println!("'");
118            }
119            for (name, id) in flights_added {
120                count += 1;
121                cli_print!("Successfully synchronized Flight Plan '");
122                cli_print!(@Green, "{name}");
123                cli_print!("' with local Flight Plan ID '");
124                cli_print!(@Green, "{}", &id.to_string()[..8]);
125                cli_println!("'!");
126            }
127            if names.is_empty() {
128                cli_println!("No remote Formation Instances found");
129            } else if count > 0 {
130                cli_println!("");
131                cli_println!("Successfully fetched {count} items");
132            } else {
133                cli_println!("All local definitions are up to date!");
134            }
135        }
136
137        ctx.persist_flights()?;
138        ctx.persist_formations()?;
139
140        Ok(())
141    }
142
143    fn update_ctx(&self, matches: &ArgMatches, ctx: &mut Ctx) -> Result<()> {
144        ctx.args.name_id = matches
145            .get_one::<String>("formation")
146            .map(ToOwned::to_owned);
147        Ok(())
148    }
149}