1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
//! Context describes the normalized and processed "settings" that a command can use at runtime.
//! This differs from the "config" or the "CLI Arguments" as the Context is built and updated from
//! those sources. This means the context is responsible for de-conflicting mutually exclusive
//! options, or overriding values.
//!
//! The Context is source of truth for all runtime decisions.
//!
//! The order of evaluation is as follows (note lower layers override layers above):
//!
//! 1. System configuration files are loaded (if any...currently none are defined)
//! 2. User configuration files are loaded (if any are found)
//! 3. Environment Variables (currently none are defined)
//! 4. Command Line Arguments
//!   4a. Because we use subcommands and global arguments each subcommand acts as it's own set of
//!   Command Line Arguments, and can thus affect the Context at each level in the command
//! hierarchy.   4b. Before the Context is handed off mutably to the next nested level, all updates
//! from the   parent should be finalized.
//!
//! After these steps the final Context is what is used to make runtime decisions.
//!
//! The context struct itself contains "global" values or those that apply in many scenarios or to
//! many commands. It also contains specialized contexts that contain values only relevant to those
//! commands or processes that need them. These specialized contexts should be lazily derived.

pub mod flight;
pub use flight::FlightCtx;
pub mod formation;
pub use formation::{FormationCfgCtx, FormationCtx};
pub mod metadata;
pub use metadata::MetadataCtx;
pub mod locks;
pub use locks::LocksCtx;
pub mod restrict;
use std::path::{Path, PathBuf};

use clap_complete::Shell;
use once_cell::unsync::OnceCell;
use reqwest::Url;
pub use restrict::RestrictCtx;

use crate::{
    config::RawConfig,
    error::{CliErrorKind, Context, Result},
    fs::{self, FromDisk, ToDisk},
    ops::{flight::Flights, formation::Formations},
    printer::{ColorChoice, OutputFormat},
};

const FLIGHTS_FILE: &str = "flights.json";
const FORMATIONS_FILE: &str = "formations.json";

#[derive(Debug, Default, Clone)]
pub struct Args {
    // @TODO we may need to get more granular than binary yes/no. For example, there may be times
    /// when the answer is "yes...but only if the stream is a TTY." In these cases an enum of
    /// Never, Auto, Always would be more appropriate
    ///
    /// Should be display ANSI color codes in output?
    pub color: ColorChoice,

    /// The name or local ID of an item
    pub name_id: Option<String>,

    /// What to overwrite
    pub overwrite: Vec<String>,

    /// Do items need to be exact to match
    pub exact: bool,

    /// Match all items
    pub all: bool,

    /// Display third party licenses
    pub third_party: bool,

    /// The shell to generate completions for
    pub shell: Option<Shell>,

    /// How to display output
    pub out_format: OutputFormat,

    /// Try to force the operation to happen
    pub force: bool,

    /// Do not use local state files
    pub stateless: bool,

    /// The API Key associated with an account provided by the CLI, env, or Config used to request
    /// access tokens
    pub api_key: Option<String>,

    /// Should we fetch remote refs?
    pub fetch: bool,
}

impl Args {
    pub fn api_key(&self) -> Result<&str> {
        self.api_key
            .as_deref()
            .ok_or_else(|| CliErrorKind::MissingApiKey.into_err())
    }
}

/// The source of truth "Context" that is passed to all runtime processes to make decisions based
/// on user configuration
// TODO: we may not want to derive this we implement circular references
#[derive(Debug)]
pub struct Ctx {
    /// The platform specific path to a data location
    data_dir: PathBuf,

    /// Context relate to exclusively to Flight operations and commands
    pub flight_ctx: LateInit<FlightCtx>,

    /// Context relate to exclusively to Formation operations and commands
    pub formation_ctx: LateInit<FormationCtx>,

    /// Context relate to exclusively to key-value operations and commands
    pub md_ctx: LateInit<MetadataCtx>,

    /// Context relate to exclusively to Locks operations and commands
    pub locks_ctx: LateInit<LocksCtx>,

    /// Context relate to exclusively to Restrict operations and commands
    pub restrict_ctx: LateInit<RestrictCtx>,

    /// Where the configuration files were loaded from
    pub conf_files: Vec<PathBuf>,

    /// Common CLI arguments
    pub args: Args,

    /// The in memory databases
    pub db: Db,

    /// Allows tracking if we're running a command internally and skippy certain checks or output
    pub internal_run: bool,

    /// Did we run initialization automatically or not on startup?
    pub did_init: bool,

    /// Disable progress bar indicators
    pub disable_pb: bool,

    /// Set the base URL for the request
    pub compute_url: Option<Url>,
    pub identity_url: Option<Url>,
    pub metadata_url: Option<Url>,
    pub locks_url: Option<Url>,
    pub insecure_urls: bool,
    pub invalid_certs: bool,
}

impl Clone for Ctx {
    fn clone(&self) -> Self {
        Self {
            data_dir: self.data_dir.clone(),
            flight_ctx: if self.flight_ctx.get().is_some() {
                let li = LateInit::default();
                li.init(self.flight_ctx.get().cloned().unwrap());
                li
            } else {
                LateInit::default()
            },
            formation_ctx: if self.formation_ctx.get().is_some() {
                let li = LateInit::default();
                li.init(self.formation_ctx.get().cloned().unwrap());
                li
            } else {
                LateInit::default()
            },
            md_ctx: if self.md_ctx.get().is_some() {
                let li = LateInit::default();
                li.init(self.md_ctx.get().cloned().unwrap());
                li
            } else {
                LateInit::default()
            },
            locks_ctx: if self.locks_ctx.get().is_some() {
                let li = LateInit::default();
                li.init(self.locks_ctx.get().cloned().unwrap());
                li
            } else {
                LateInit::default()
            },
            restrict_ctx: if self.restrict_ctx.get().is_some() {
                let li = LateInit::default();
                li.init(self.restrict_ctx.get().cloned().unwrap());
                li
            } else {
                LateInit::default()
            },
            conf_files: self.conf_files.clone(),
            args: self.args.clone(),
            db: self.db.clone(),
            internal_run: self.internal_run,
            did_init: self.did_init,
            disable_pb: self.disable_pb,
            compute_url: self.compute_url.clone(),
            identity_url: self.identity_url.clone(),
            metadata_url: self.metadata_url.clone(),
            locks_url: self.locks_url.clone(),
            insecure_urls: self.insecure_urls,
            invalid_certs: self.invalid_certs,
        }
    }
}

impl Default for Ctx {
    fn default() -> Self {
        Self {
            data_dir: fs::data_dir(),
            flight_ctx: LateInit::default(),
            formation_ctx: LateInit::default(),
            md_ctx: LateInit::default(),
            locks_ctx: LateInit::default(),
            restrict_ctx: LateInit::default(),
            conf_files: Vec::new(),
            args: Args::default(),
            db: Db::default(),
            internal_run: false,
            did_init: false,
            disable_pb: false,
            compute_url: None,
            identity_url: None,
            metadata_url: None,
            locks_url: None,
            insecure_urls: false,
            invalid_certs: false,
        }
    }
}

impl From<RawConfig> for Ctx {
    fn from(cfg: RawConfig) -> Self {
        Self {
            data_dir: fs::data_dir(),
            conf_files: cfg.loaded_from.clone(),
            args: Args {
                // We default to using color. Later when the context is updated from the CLI args,
                // this may change.
                color: cfg.seaplane.color.unwrap_or_default(),
                api_key: cfg.account.api_key,
                ..Default::default()
            },
            compute_url: cfg.api.compute_url,
            identity_url: cfg.api.identity_url,
            metadata_url: cfg.api.metadata_url,
            locks_url: cfg.api.locks_url,
            did_init: cfg.did_init,
            #[cfg(feature = "allow_insecure_urls")]
            insecure_urls: cfg.danger_zone.allow_insecure_urls,
            #[cfg(feature = "allow_invalid_certs")]
            invalid_certs: cfg.danger_zone.allow_invalid_certs,
            ..Self::default()
        }
    }
}

impl Ctx {
    pub fn update_from_env(&mut self) -> Result<()> {
        // TODO: this just gets it compiling. Using `todo!` blocks progress since loading the
        // context happens at program startup, so we cannot panic on unimplemented
        Ok(())
    }

    #[inline]
    pub fn data_dir(&self) -> &Path { &self.data_dir }

    pub fn conf_files(&self) -> &[PathBuf] { &self.conf_files }

    pub fn flights_file(&self) -> PathBuf { self.data_dir.join(FLIGHTS_FILE) }

    pub fn formations_file(&self) -> PathBuf { self.data_dir.join(FORMATIONS_FILE) }

    /// Write out an entirely new JSON file if `--stateless` wasn't used
    pub fn persist_formations(&self) -> Result<()> {
        self.db
            .formations
            .persist_if(!self.args.stateless)
            .with_context(|| format!("Path: {:?}\n", self.formations_file()))
    }

    /// Write out an entirely new JSON file if `--stateless` wasn't used
    pub fn persist_flights(&self) -> Result<()> {
        self.db
            .flights
            .persist_if(!self.args.stateless)
            .with_context(|| format!("Path: {:?}\n", self.flights_file()))
    }
}

/// The in memory "Databases"
#[derive(Debug, Default, Clone)]
pub struct Db {
    /// The in memory Flights database
    pub flights: Flights,

    /// The in memory Formations database
    pub formations: Formations,

    /// A *hint* that we should persist at some point. Not gospel
    pub needs_persist: bool,
}

impl Db {
    pub fn load<P: AsRef<Path>>(flights: P, formations: P) -> Result<Self> {
        Self::load_if(flights, formations, true)
    }

    pub fn load_if<P: AsRef<Path>>(flights: P, formations: P, yes: bool) -> Result<Self> {
        Ok(Self {
            flights: FromDisk::load_if(flights, yes).unwrap_or_else(|| Ok(Flights::default()))?,
            formations: FromDisk::load_if(formations, yes)
                .unwrap_or_else(|| Ok(Formations::default()))?,
            needs_persist: false,
        })
    }
}

// TODO: we may not want to derive this we implement circular references
#[derive(Debug)]
pub struct LateInit<T> {
    inner: OnceCell<T>,
}

impl<T> Default for LateInit<T> {
    fn default() -> Self { Self { inner: OnceCell::default() } }
}

impl<T> LateInit<T> {
    pub fn init(&self, val: T) { assert!(self.inner.set(val).is_ok()) }
    pub fn get(&self) -> Option<&T> { self.inner.get() }
    pub fn get_mut(&mut self) -> Option<&mut T> { self.inner.get_mut() }
}

impl<T: Default> LateInit<T> {
    pub fn get_or_init(&self) -> &T { self.inner.get_or_init(|| T::default()) }
    pub fn get_mut_or_init(&mut self) -> &mut T {
        self.inner.get_or_init(|| T::default());
        self.inner.get_mut().unwrap()
    }
}