Skip to content

Commit 4406d58

Browse files
authored
Make prompt_multi_option more typey (#1028)
1 parent 1ba7db7 commit 4406d58

File tree

8 files changed

+114
-82
lines changed

8 files changed

+114
-82
lines changed

toolkit/partner-chains-cli/src/config.rs

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ impl<'a, T> ConfigFieldDefinition<'a, T> {
7373
T: DeserializeOwned + std::fmt::Display + FromStr + serde::Serialize,
7474
{
7575
let loaded_value = self.load_from_file(context).map(|v| v.to_string());
76-
let default_value = loaded_value.as_deref().or(self.default);
76+
let default_value = loaded_value.as_deref().or_else(|| self.default);
7777
let value = context.prompt(&format!("Enter the {}", self.name), default_value);
7878
let parsed_value: T = value.parse()?;
7979
self.save_to_file(&parsed_value, context);
@@ -86,15 +86,27 @@ impl<'a, T> ConfigFieldDefinition<'a, T> {
8686
context: &C,
8787
) -> Result<T, <T as FromStr>::Err>
8888
where
89-
T: DeserializeOwned + std::fmt::Display + FromStr + serde::Serialize + SelectOptions,
89+
T: DeserializeOwned
90+
+ std::fmt::Display
91+
+ FromStr
92+
+ serde::Serialize
93+
+ SelectOptions<Opt = T>
94+
+ Clone
95+
+ std::fmt::Debug
96+
+ std::panic::UnwindSafe
97+
+ std::panic::RefUnwindSafe,
9098
{
91-
let loaded_value = self.load_from_file(context).map(|v| v.to_string());
92-
let default_value_opt = loaded_value.as_deref().or(self.default);
93-
let options = T::select_options_with_default(default_value_opt);
94-
let value = context.prompt_multi_option(prompt, options);
95-
let parsed_value: T = value.parse()?;
96-
self.save_to_file(&parsed_value, context);
97-
Ok(parsed_value)
99+
let loaded_value = self.load_from_file(context);
100+
let default_value_opt: Option<T> =
101+
loaded_value.or_else(|| self.default.and_then(|s| s.parse().ok()));
102+
let options: Vec<T> = default_value_opt.map_or_else(
103+
|| T::select_options(),
104+
|default_value| T::select_options_with_default(&default_value),
105+
);
106+
let value_ix = context.prompt_multi_option(prompt, options.clone());
107+
let value = options[value_ix].clone();
108+
self.save_to_file(&value, context);
109+
Ok(value)
98110
}
99111

100112
/// Loads and parses the config field value.
@@ -205,13 +217,11 @@ impl ServiceConfig {
205217
}
206218

207219
pub(crate) trait SelectOptions {
208-
fn select_options() -> Vec<String>;
209-
fn select_options_with_default(default_value_opt: Option<&str>) -> Vec<String> {
220+
type Opt: PartialEq;
221+
fn select_options() -> Vec<Self::Opt>;
222+
fn select_options_with_default(default_value: &Self::Opt) -> Vec<Self::Opt> {
210223
let mut options = Self::select_options();
211-
212-
if let Some(default_value) = default_value_opt {
213-
options.sort_by_key(|option| if *option != default_value { 1 } else { 0 });
214-
}
224+
options.sort_by_key(|option| if option != default_value { 1 } else { 0 });
215225
options
216226
}
217227
}
@@ -250,8 +260,9 @@ impl FromStr for NetworkProtocol {
250260
}
251261

252262
impl SelectOptions for NetworkProtocol {
253-
fn select_options() -> Vec<String> {
254-
vec![NetworkProtocol::Http.to_string(), NetworkProtocol::Https.to_string()]
263+
type Opt = NetworkProtocol;
264+
fn select_options() -> Vec<NetworkProtocol> {
265+
vec![NetworkProtocol::Http, NetworkProtocol::Https]
255266
}
256267
}
257268

@@ -335,7 +346,7 @@ pub(crate) struct NativeTokenConfig {
335346
pub(crate) illiquid_circulation_supply_validator_address: String,
336347
}
337348

338-
#[derive(Deserialize, Serialize, Clone, Debug)]
349+
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
339350
pub(crate) struct GovernanceAuthoritiesKeyHashes(pub(crate) Vec<MainchainKeyHash>);
340351

341352
impl FromStr for GovernanceAuthoritiesKeyHashes {

toolkit/partner-chains-cli/src/io.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ use crate::cmd_traits::*;
22
use crate::config::{ConfigFile, ServiceConfig};
33
use crate::ogmios::{OgmiosRequest, OgmiosResponse, ogmios_request};
44
use anyhow::{Context, anyhow};
5+
use core::fmt::Display;
56
use inquire::InquireError;
67
use inquire::error::InquireResult;
78
use ogmios_client::jsonrpsee::{OgmiosClients, client_for_url};
89
use sp_core::offchain::Timestamp;
10+
use std::fmt::Debug;
11+
use std::panic::{RefUnwindSafe, UnwindSafe};
912
use std::{
1013
fs,
1114
io::{BufRead, BufReader, Read},
@@ -33,8 +36,11 @@ pub trait IOContext {
3336
fn enewline(&self);
3437
fn prompt(&self, prompt: &str, default: Option<&str>) -> String;
3538
fn prompt_yes_no(&self, prompt: &str, default: bool) -> bool;
36-
// TODO: fn prompt_multi_option<T: ToString>(&self, msg: &str, options: Vec<T>) -> T;
37-
fn prompt_multi_option(&self, msg: &str, options: Vec<String>) -> String;
39+
fn prompt_multi_option<T: Debug + Display + UnwindSafe + RefUnwindSafe>(
40+
&self,
41+
msg: &str,
42+
options: Vec<T>,
43+
) -> usize;
3844
fn write_file(&self, path: &str, content: &str);
3945

4046
fn new_tmp_dir(&self) -> PathBuf;
@@ -136,8 +142,12 @@ impl IOContext for DefaultCmdRunContext {
136142
handle_inquire_result(inquire::Confirm::new(prompt).with_default(default).prompt())
137143
}
138144

139-
fn prompt_multi_option(&self, msg: &str, options: Vec<String>) -> String {
140-
handle_inquire_result(inquire::Select::new(msg, options).prompt()).to_string()
145+
fn prompt_multi_option<T: Debug + Display + UnwindSafe + RefUnwindSafe>(
146+
&self,
147+
msg: &str,
148+
options: Vec<T>,
149+
) -> usize {
150+
handle_inquire_result(inquire::Select::new(msg, options).raw_prompt()).index
141151
}
142152

143153
fn write_file(&self, path: &str, content: &str) {

toolkit/partner-chains-cli/src/ogmios/config.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ pub(crate) mod tests {
8484
MockIO::Group(vec![
8585
prompt_multi_option_with_default(
8686
OGMIOS_PROTOCOL,
87-
Some(&default_config.protocol.to_string()),
88-
&config_to_set.protocol.to_string(),
87+
&default_config.protocol,
88+
&config_to_set.protocol,
8989
),
9090
prompt_with_default(
9191
OGMIOS_HOSTNAME,

toolkit/partner-chains-cli/src/prepare_configuration/mod.rs

Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::CmdRun;
22
use crate::cmd_traits::{GetScriptsData, InitGovernance};
3+
use crate::config::SelectOptions;
34
use crate::config::config_fields::{BOOTNODES, SUBSTRATE_NODE_DATA_BASE_PATH};
45
use crate::generate_keys::network_key_path;
56
use crate::io::IOContext;
@@ -16,6 +17,7 @@ use partner_chains_cardano_offchain::csl::NetworkTypeExt;
1617
use partner_chains_cardano_offchain::governance::MultiSigParameters;
1718
use partner_chains_cardano_offchain::scripts_data::{ScriptsData, get_scripts_data};
1819
use sidechain_domain::{McTxHash, UtxoId};
20+
use std::fmt::Display;
1921
use std::net::Ipv4Addr;
2022
use std::str::FromStr;
2123
use std::vec;
@@ -44,7 +46,7 @@ impl CmdRun for PrepareConfigurationCmd {
4446
context,
4547
)? {
4648
prepare_main_chain_config(context, &ogmios_config, genesis_utxo)?;
47-
context.eprint("🚀 Chain configuration wizards completed successufully!");
49+
context.eprint("🚀 Chain configuration wizards completed successfully!");
4850
Ok(())
4951
} else {
5052
context
@@ -126,13 +128,11 @@ fn read_bootnode_defaults(context: &impl IOContext) -> (Protocol, String, u16) {
126128
}
127129

128130
fn choose_protocol(context: &impl IOContext, default_protocol: Protocol) -> Protocol {
129-
let mut protocols: Vec<String> = vec![Protocol::Dns.into(), Protocol::Ipv4.into()];
130-
// default protocol should be the first in the list
131-
protocols
132-
.sort_by_key(|protocol| if *protocol != String::from(default_protocol) { 1 } else { 0 });
131+
let protocols = Protocol::select_options_with_default(&default_protocol);
133132

134-
Protocol::from_str(&context.prompt_multi_option(CHOOSE_PROTOCOL_PROMPT, protocols))
135-
.expect("Invalid protocol cannot be chosen from valid options only")
133+
let protocol_selection_ix =
134+
context.prompt_multi_option(CHOOSE_PROTOCOL_PROMPT, protocols.clone());
135+
protocols[protocol_selection_ix]
136136
}
137137

138138
fn deconstruct_bootnode(bootnode_opt: Option<String>) -> Option<(Protocol, String, u16)> {
@@ -160,7 +160,7 @@ fn peer_id_from_config(context: &impl IOContext) -> anyhow::Result<Option<String
160160
}
161161

162162
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
163-
enum Protocol {
163+
pub(crate) enum Protocol {
164164
Dns,
165165
Ipv4,
166166
}
@@ -174,11 +174,11 @@ impl Protocol {
174174
}
175175
}
176176

177-
impl From<Protocol> for String {
178-
fn from(value: Protocol) -> Self {
179-
match value {
180-
Protocol::Dns => "hostname".to_string(),
181-
Protocol::Ipv4 => "IP address".to_string(),
177+
impl Display for Protocol {
178+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179+
match self {
180+
Protocol::Dns => write!(f, "hostname"),
181+
Protocol::Ipv4 => write!(f, "IP address"),
182182
}
183183
}
184184
}
@@ -195,6 +195,14 @@ impl FromStr for Protocol {
195195
}
196196
}
197197

198+
impl SelectOptions for Protocol {
199+
type Opt = Protocol;
200+
201+
fn select_options() -> Vec<Self::Opt> {
202+
vec![Protocol::Dns, Protocol::Ipv4]
203+
}
204+
}
205+
198206
const INTRO: &str =
199207
"This 🧙 wizard will:
200208
* establish single bootnode configuration (to be later included in chain-spec file)
@@ -315,29 +323,25 @@ pub mod tests {
315323
}
316324

317325
pub fn pick_ip_protocol_with_defaults() -> MockIO {
318-
pick_ip_protocol(
319-
vec![Dns.into(), Ipv4.into()],
320-
DEFAULT_PORT,
321-
Ipv4.default_address().to_string(),
322-
)
326+
pick_ip_protocol(vec![Dns, Ipv4], DEFAULT_PORT, Ipv4.default_address().to_string())
323327
}
324328

325329
pub fn pick_ip_protocol(
326-
options: Vec<String>,
330+
options: Vec<Protocol>,
327331
default_port: u16,
328332
default_ip_address: String,
329333
) -> MockIO {
330334
pick_chosen_ip_protocol(options, default_port, &default_ip_address, "10.2.2.4")
331335
}
332336

333337
pub fn pick_chosen_ip_protocol(
334-
options: Vec<String>,
338+
options: Vec<Protocol>,
335339
default_port: u16,
336340
default_ip_address: &str,
337341
input: &str,
338342
) -> MockIO {
339343
MockIO::Group(vec![
340-
MockIO::prompt_multi_option(CHOOSE_PROTOCOL_PROMPT, options, &String::from(Ipv4)),
344+
MockIO::prompt_multi_option(CHOOSE_PROTOCOL_PROMPT, options, &Ipv4),
341345
MockIO::prompt(
342346
CHOOSE_PORT_PROMPT,
343347
Some(&default_port.to_string()),
@@ -355,20 +359,16 @@ pub mod tests {
355359
}
356360

357361
pub fn pick_dns_protocol_with_defaults() -> MockIO {
358-
pick_dns_protocol(
359-
vec![Dns.into(), Ipv4.into()],
360-
DEFAULT_PORT,
361-
Dns.default_address().to_string(),
362-
)
362+
pick_dns_protocol(vec![Dns, Ipv4], DEFAULT_PORT, Dns.default_address().to_string())
363363
}
364364

365365
pub fn pick_dns_protocol(
366-
options: Vec<String>,
366+
options: Vec<Protocol>,
367367
default_port: u16,
368368
default_hostname: String,
369369
) -> MockIO {
370370
MockIO::Group(vec![
371-
MockIO::prompt_multi_option(CHOOSE_PROTOCOL_PROMPT, options, &String::from(Dns)),
371+
MockIO::prompt_multi_option(CHOOSE_PROTOCOL_PROMPT, options, &Dns),
372372
MockIO::prompt(
373373
CHOOSE_PORT_PROMPT,
374374
Some(&default_port.to_string()),
@@ -460,7 +460,7 @@ pub mod tests {
460460
scenarios::read_config(),
461461
scenarios::choose_to_configure_bootnode(true),
462462
scenarios::pick_dns_protocol(
463-
vec![Ipv4.into(), Dns.into()],
463+
vec![Ipv4, Dns],
464464
3034,
465465
Dns.default_address().to_string(),
466466
),
@@ -486,7 +486,7 @@ pub mod tests {
486486
scenarios::read_config(),
487487
scenarios::choose_to_configure_bootnode(true),
488488
scenarios::pick_ip_protocol(
489-
vec![Ipv4.into(), Dns.into()],
489+
vec![Ipv4, Dns],
490490
3034,
491491
"ip_address".to_string(),
492492
),
@@ -550,7 +550,7 @@ pub mod tests {
550550
scenarios::read_config(),
551551
scenarios::choose_to_configure_bootnode(true),
552552
scenarios::pick_chosen_ip_protocol(
553-
vec![Dns.into(), Ipv4.into()],
553+
vec![Dns, Ipv4],
554554
DEFAULT_PORT,
555555
Ipv4.default_address(),
556556
"100",
@@ -581,14 +581,16 @@ pub mod tests {
581581
MockIO::prompt(&format!("Enter the {}", field_definition.name), default, value)
582582
}
583583

584-
pub fn prompt_multi_option_with_default<T: SelectOptions>(
584+
pub fn prompt_multi_option_with_default<
585+
T: Clone + PartialEq + ToString + SelectOptions<Opt = T>,
586+
>(
585587
field_definition: ConfigFieldDefinition<'_, T>,
586-
default: Option<&str>,
587-
value: &str,
588+
default: &T,
589+
value: &T,
588590
) -> MockIO {
589591
MockIO::prompt_multi_option(
590592
&format!("Select {}", field_definition.name),
591-
T::select_options_with_default(default),
593+
T::select_options_with_default(&default),
592594
value,
593595
)
594596
}

toolkit/partner-chains-cli/src/prepare_configuration/select_genesis_utxo.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub fn select_genesis_utxo<C: IOContext>(
2626
return Err(anyhow::anyhow!("No UTXOs found"));
2727
};
2828
let genesis_utxo =
29-
select_from_utxos(context, "Select an UTXO to use as the genesis UTXO", utxo_query_result)?;
29+
select_from_utxos(context, "Select an UTXO to use as the genesis UTXO", utxo_query_result);
3030

3131
GENESIS_UTXO.save_to_file(&genesis_utxo, context);
3232
Ok((genesis_utxo, private_key, ogmios_configuration))
@@ -91,7 +91,7 @@ mod tests {
9191
MockIO::prompt_multi_option(
9292
"Select an UTXO to use as the genesis UTXO",
9393
mock_7_valid_utxos_rows(),
94-
"4704a903b01514645067d851382efd4a6ed5d2ff07cf30a538acc78fed7c4c02#93 (1100000 lovelace)",
94+
&"4704a903b01514645067d851382efd4a6ed5d2ff07cf30a538acc78fed7c4c02#93 (1100000 lovelace)".to_owned(),
9595
),
9696
]);
9797

toolkit/partner-chains-cli/src/register/register1.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ impl CmdRun for Register1Cmd {
4949
};
5050

5151
let registration_utxo: UtxoId =
52-
select_from_utxos(context, "Select UTXO to use for registration", utxo_query_result)?;
52+
select_from_utxos(context, "Select UTXO to use for registration", utxo_query_result);
5353

5454
context.print(
5555
"Please do not spend this UTXO, it needs to be consumed by the registration transaction.",
@@ -520,7 +520,7 @@ mod tests {
520520
MockIO::prompt_multi_option(
521521
"Select UTXO to use for registration",
522522
mock_7_valid_utxos_rows(),
523-
"4704a903b01514645067d851382efd4a6ed5d2ff07cf30a538acc78fed7c4c02#93 (1100000 lovelace)",
523+
&"4704a903b01514645067d851382efd4a6ed5d2ff07cf30a538acc78fed7c4c02#93 (1100000 lovelace)".to_owned(),
524524
),
525525
MockIO::print(
526526
"Please do not spend this UTXO, it needs to be consumed by the registration transaction.",

toolkit/partner-chains-cli/src/select_utxo.rs

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use crate::{
66
use anyhow::anyhow;
77
use ogmios_client::types::OgmiosUtxo;
88
use sidechain_domain::{McTxHash, UtxoId};
9-
use std::str::FromStr;
109

1110
fn utxo_id(utxo: &OgmiosUtxo) -> UtxoId {
1211
UtxoId {
@@ -41,19 +40,11 @@ pub(crate) fn select_from_utxos<C: IOContext>(
4140
context: &C,
4241
prompt: &str,
4342
utxos: Vec<OgmiosUtxo>,
44-
) -> Result<UtxoId, anyhow::Error> {
43+
) -> UtxoId {
4544
let utxo_display_options: Vec<String> =
4645
utxos.iter().map(|utxo| to_display_string(utxo)).collect();
47-
let selected_utxo_display_string = context.prompt_multi_option(prompt, utxo_display_options);
48-
let selected_utxo = utxos
49-
.iter()
50-
.find(|utxo| to_display_string(utxo) == selected_utxo_display_string)
51-
.map(|utxo| utxo_id(utxo).to_string())
52-
.ok_or_else(|| anyhow!("⚠️ Failed to find selected UTXO"))?;
53-
UtxoId::from_str(&selected_utxo).map_err(|e| {
54-
context.eprint(&format!("⚠️ Failed to parse selected UTXO: {e}"));
55-
anyhow!(e)
56-
})
46+
let selected_utxo_ix = context.prompt_multi_option(prompt, utxo_display_options);
47+
utxo_id(&utxos[selected_utxo_ix])
5748
}
5849

5950
#[cfg(test)]

0 commit comments

Comments
 (0)