seaplane_cli/cli/cmds/restrict/
common.rs

1use clap::{builder::PossibleValue, value_parser, Arg, ArgMatches};
2use seaplane::api::shared::v1::{Provider as ProviderModel, Region as RegionModel};
3
4use crate::OutputFormat;
5
6const LONG_DECODE: &str = "Decode the directories before printing them
7
8Binary values will be written directly to standard output (which may do strange
9things to your terminal)";
10
11// TODO: Factor region and provider stuff as usual
12static LONG_REGION: &str = "A region where the data placement is allowed (See REGION SPEC below)
13
14Multiple items can be passed as a comma separated list, or by using the argument
15multiple times.";
16
17static LONG_PROVIDER: &str = "A provider where the data placement is allowed
18
19Multiple items can be passed as a comma separated list, or by using the argument
20multiple times.";
21
22static LONG_EXCLUDE_PROVIDER: &str = "A provider where the data placement is *NOT* allowed
23
24This will override any values given to --provider
25
26Multiple items can be passed as a comma separated list, or by using the argument
27multiple times.";
28
29static LONG_EXCLUDE_REGION: &str =
30    "A region  where the data placement is *NOT* allowed (See REGION SPEC below)
31
32This will override any values given to --region
33
34Multiple items can be passed as a comma separated list, or by using the argument
35multiple times.";
36
37// NOTE: we can't use `derive(clap::ValueEnum)` because it of how it derives the to_possible_value
38// which appears to unconditionally use shish-ka-bob case which we don't want.
39/// We provide a shim between the Seaplane Provider so we can do some additional UX work like 'all'
40#[derive(Debug, Copy, Clone, PartialEq, Eq, strum::EnumString)]
41#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
42pub enum Provider {
43    Aws,
44    Azure,
45    DigitalOcean,
46    Equinix,
47    Gcp,
48    All,
49}
50
51impl clap::ValueEnum for Provider {
52    fn value_variants<'a>() -> &'a [Self] {
53        use Provider::*;
54        &[Aws, Azure, DigitalOcean, Equinix, Gcp, All]
55    }
56
57    fn to_possible_value<'a>(&self) -> Option<PossibleValue> {
58        use Provider::*;
59        match self {
60            Aws => Some(PossibleValue::new("aws")),
61            Azure => Some(PossibleValue::new("azure")),
62            DigitalOcean => Some(PossibleValue::new("digitalocean")),
63            Equinix => Some(PossibleValue::new("equinix")),
64            Gcp => Some(PossibleValue::new("gcp")),
65            All => Some(PossibleValue::new("all")),
66        }
67    }
68
69    fn from_str(input: &str, _ignore_case: bool) -> Result<Self, String> {
70        // Because we use strum(ascii_ignore_case) for our FromStr impl we unconditionally ignore
71        // case and can ignore clap's hint
72        input.parse().map_err(|e| format!("{e}"))
73    }
74}
75
76impl Provider {
77    pub fn into_model(&self) -> Option<ProviderModel> {
78        if self == &Provider::All {
79            None
80        } else {
81            Some(self.into())
82        }
83    }
84}
85
86#[allow(clippy::from_over_into)]
87impl<'a> Into<ProviderModel> for &'a Provider {
88    fn into(self) -> ProviderModel {
89        use Provider::*;
90        match self {
91            Aws => ProviderModel::AWS,
92            Azure => ProviderModel::Azure,
93            DigitalOcean => ProviderModel::DigitalOcean,
94            Equinix => ProviderModel::Equinix,
95            Gcp => ProviderModel::GCP,
96            All => {
97                panic!("Provider::All cannot be converted into seaplane::api::shared::v1::Provider")
98            }
99        }
100    }
101}
102
103// TODO: @needs-decision @pre-1.0 we need to come back and address how many aliases there are and
104// their sort order for the CLI's "possible values" message. They're currently "grouped" in region
105// aliases, but for one it's too many values, also it makes the sort look wild in the help message.
106// The Compute API only uses the XA, XC, XE values, but those are the least user friendly.
107/// We provide a shim between the Seaplane Region so we can do some additional UX work
108#[allow(clippy::upper_case_acronyms)]
109#[derive(Debug, Copy, Clone, PartialEq, Eq, strum::EnumString)]
110#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
111pub enum Region {
112    XA,
113    Asia,
114    XC,
115    PRC,
116    PeoplesRepublicOfChina,
117    XE,
118    Europe,
119    EU,
120    XF,
121    Africa,
122    XN,
123    NorthAmerica,
124    NAmerica,
125    XO,
126    Oceania,
127    XQ,
128    Antarctica,
129    XS,
130    SAmerica,
131    SouthAmerica,
132    XU,
133    UK,
134    UnitedKingdom,
135    All,
136}
137
138impl clap::ValueEnum for Region {
139    fn value_variants<'a>() -> &'a [Self] {
140        use Region::*;
141        &[
142            XA, // Asia,
143            XC, // PRC, PeoplesRepublicOfChina,
144            XE, // Europe, EU,
145            XF, // Africa,
146            XN, // NorthAmerica, NAmerica,
147            XO, // Oceania,
148            XQ, // Antarctica,
149            XS, // SAmerica, SouthAmerica,
150            XU, // UK, UnitedKingdom,
151            All,
152        ]
153    }
154
155    fn to_possible_value<'a>(&self) -> Option<PossibleValue> {
156        use Region::*;
157        match self {
158            XA => Some(PossibleValue::new("xa").alias("asia")),
159            Asia => None,
160            XC => Some(PossibleValue::new("xc").aliases([
161                "prc",
162                "china",
163                "peoples-republic-of-china",
164                "peoples_republic_of_china",
165                "peoplesrepublicofchina",
166            ])),
167            PRC => None,
168            PeoplesRepublicOfChina => None,
169            XE => Some(PossibleValue::new("xe").aliases(["europe", "eu"])),
170            Europe => None,
171            EU => None,
172            XF => Some(PossibleValue::new("xf").alias("africa")),
173            Africa => None,
174            XN => Some(PossibleValue::new("xn").aliases([
175                "namerica",
176                "northamerica",
177                "n-america",
178                "north-america",
179                "n_america",
180                "north_america",
181            ])),
182            NorthAmerica => None,
183            NAmerica => None,
184            XO => Some(PossibleValue::new("xo").alias("oceania")),
185            Oceania => None,
186            XQ => Some(PossibleValue::new("xq").alias("antarctica")),
187            Antarctica => None,
188            XS => Some(PossibleValue::new("xs").aliases([
189                "samerica",
190                "southamerica",
191                "s-america",
192                "south-america",
193                "s_america",
194                "south_america",
195            ])),
196            SAmerica => None,
197            SouthAmerica => None,
198            XU => Some(PossibleValue::new("xu").aliases([
199                "uk",
200                "unitedkingdom",
201                "united-kingdom",
202                "united_kingdom",
203            ])),
204            UK => None,
205            UnitedKingdom => None,
206            All => Some(PossibleValue::new("all")),
207        }
208    }
209
210    fn from_str(input: &str, _ignore_case: bool) -> Result<Self, String> {
211        // Because we use strum(ascii_ignore_case) for our FromStr impl we unconditionally ignore
212        // case and can ignore clap's hint
213        input.parse().map_err(|e| format!("{e}"))
214    }
215}
216
217impl Region {
218    pub fn into_model(&self) -> Option<RegionModel> {
219        if self == &Region::All {
220            None
221        } else {
222            Some(self.into())
223        }
224    }
225}
226
227#[allow(clippy::from_over_into)]
228impl<'a> Into<RegionModel> for &'a Region {
229    fn into(self) -> RegionModel {
230        use Region::*;
231        match self {
232            XA | Asia => RegionModel::XA,
233            XC | PRC | PeoplesRepublicOfChina => RegionModel::XC,
234            XE | Europe | EU => RegionModel::XE,
235            XF | Africa => RegionModel::XF,
236            XN | NorthAmerica | NAmerica => RegionModel::XN,
237            XO | Oceania => RegionModel::XO,
238            XQ | Antarctica => RegionModel::XQ,
239            XS | SAmerica | SouthAmerica => RegionModel::XS,
240            XU | UK | UnitedKingdom => RegionModel::XU,
241            All => panic!("Region::All cannot be converted into seaplane::api::shared::v1::Region"),
242        }
243    }
244}
245
246/// A newtype wrapper to enforce where the ArgMatches came from which reduces
247/// errors in checking if values of arguments were used or not. i.e. `seaplane
248/// formation create` may not have the same arguments as `seaplane account
249/// token` even though both produce an `ArgMatches`.
250#[allow(missing_debug_implementations)]
251#[derive(Debug)]
252pub struct SeaplaneRestrictCommonArgMatches<'a>(pub &'a ArgMatches);
253
254pub fn display_args() -> Vec<Arg> {
255    vec![
256        arg!(--format =["FORMAT"=>"table"] global)
257            .help("Change the output format")
258            .value_parser(value_parser!(OutputFormat)),
259        arg!(--decode - ('D'))
260            .help("Decode the directories before printing them")
261            .long_help(LONG_DECODE)
262            .overrides_with("no-decode"),
263        arg!(--("no-decode"))
264            .help("Print directories without decoding them")
265            .overrides_with("decode"),
266        arg!(--("no-header") | ("no-heading") | ("no-headers"))
267            .help("Omit the header when printing with `--format=table`"),
268    ]
269}
270
271pub fn restriction_details() -> Vec<Arg> {
272    vec![
273    arg!(--provider|providers =["PROVIDER"=>"all"]... ignore_case)
274    .display_order(1)
275    .next_line_help(true)
276    .help("A provider where the data placement is allowed (supports comma separated list, or multiple uses)")
277    .long_help(LONG_PROVIDER)
278    .value_parser(value_parser!(Provider)),
279    arg!(--("exclude-provider")|("exclude-providers") =["PROVIDER"]... ignore_case)
280    .display_order(2)
281    .next_line_help(true)
282    .help("A provider where the data placement is *NOT* allowed (supports comma separated list, or multiple uses)")
283    .long_help(LONG_EXCLUDE_PROVIDER)
284    .value_parser(value_parser!(Provider)),
285    arg!(--region|regions =["REGION"=>"all"]... ignore_case)
286    .display_order(1)
287    .next_line_help(true)
288    .help("A region where the data placement is allowed (supports comma separated list, or multiple uses) (See REGION SPEC below)")
289    .long_help(LONG_REGION)
290    .value_parser(value_parser!(Region)),
291    arg!(--("exclude-region")|("exclude-regions") =["REGION"]... ignore_case)
292    .display_order(2)
293    .next_line_help(true)
294    .help("A region where the data placement is *NOT* allowed (supports comma separated list, or multiple uses) (See REGION SPEC below)")
295    .long_help(LONG_EXCLUDE_REGION)
296    .value_parser(value_parser!(Region)),
297    ]
298}
299
300pub fn base64() -> Arg {
301    arg!(--base64 - ('B')).help("The directory is already encoded in URL safe Base64")
302}
303
304pub fn api() -> Arg { arg!(api =["API"] required ).help("The API of the restricted directory") }
305
306pub fn directory() -> Arg {
307    arg!(directory =["DIRECTORY"] required ).help("The restricted directory")
308}