Skip to content

Commit b847513

Browse files
authored
Make version argument not require other fields and unify error messaging (#302)
## Usage and product changes Fix an incorrect requirement of `username` and `address` when executing `console --version`. Additionally, unify the format of error messages and add colors (e.g., the "error" header is printed in bold red and argument references are printed in yellow, similar to some of the existing parsing errors). ## Implementation ### --version Make all CLI arguments optional. This requires handling `username` and `address` manually, but it's the most scalable solution (in case we have more arguments like `--version` that differ from the standard Console flow). Considered alternatives: * use the standard Clap `--version` (not available for us because of the version overriding mechanism, same as in `typedb` core); * split CLI into subcommands to have a distinct difference between the standard execution flow and other commands (I don't like it). ### Error messages With this change, error messages became more random - some are colored, while others are not. And this problem extended for specifically parsing errors. A less important loss: if both `username` and `address` are missing, we will only notify about `username` first, and the `address` will be mentioned only after the initial issue is fixed. To compensate the stylistic mismatch, we introduce a better user-friendly error messaging with coloring. Instead of manual coloring with styling codes, we reuse [clap structs](https://github.com/clap-rs/clap/blob/4c6fd932cb1860fac450dc7992918a5bde0c6a2b/clap_builder/src/builder/styling.rs#L62) and formatting approach to make sure that it works consistently for all error messages, for all operating systems, for all terminal operations (e.g., redirecting output to a file).
1 parent 8372f8b commit b847513

File tree

3 files changed

+101
-29
lines changed

3 files changed

+101
-29
lines changed

src/cli.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
use clap::Parser;
88

9+
pub const ADDRESS_VALUE_NAME: &str = "host:port";
10+
pub const USERNAME_VALUE_NAME: &str = "username";
11+
912
#[derive(Parser, Debug)]
1013
#[command(author, about)]
1114
pub struct Args {
@@ -22,12 +25,12 @@ pub struct Args {
2225
pub script: Vec<String>,
2326

2427
/// TypeDB address to connect to. If using TLS encryption, this must start with "https://"
25-
#[arg(long, value_name = "host:port")]
26-
pub address: String,
28+
#[arg(long, value_name = ADDRESS_VALUE_NAME)]
29+
pub address: Option<String>,
2730

2831
/// Username for authentication
29-
#[arg(long, value_name = "username")]
30-
pub username: String,
32+
#[arg(long, value_name = USERNAME_VALUE_NAME)]
33+
pub username: Option<String>,
3134

3235
/// Password for authentication. Will be requested safely by default.
3336
#[arg(long, value_name = "password")]

src/main.rs

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use sentry::ClientOptions;
2525
use typedb_driver::{Credentials, DriverOptions, Transaction, TransactionType, TypeDBDriver};
2626

2727
use crate::{
28-
cli::Args,
28+
cli::{Args, ADDRESS_VALUE_NAME, USERNAME_VALUE_NAME},
2929
completions::{database_name_completer_fn, file_completer},
3030
operations::{
3131
database_create, database_delete, database_export, database_import, database_list, database_schema,
@@ -70,19 +70,19 @@ enum ExitCode {
7070
fn exit_with_error(err: &(dyn std::error::Error + 'static)) -> ! {
7171
use crate::repl::command::ReplError;
7272
if let Some(repl_err) = err.downcast_ref::<ReplError>() {
73-
eprintln!("Error: {}", repl_err);
73+
println_error!("Error: {}", repl_err);
7474
exit(ExitCode::UserInputError as i32);
7575
} else if let Some(io_err) = err.downcast_ref::<io::Error>() {
76-
eprintln!("I/O Error: {}", io_err);
76+
println_error!("I/O Error: {}", io_err);
7777
exit(ExitCode::GeneralError as i32);
7878
} else if let Some(driver_err) = err.downcast_ref::<typedb_driver::Error>() {
79-
eprintln!("TypeDB Error: {}", driver_err);
79+
println_error!("TypeDB Error: {}", driver_err);
8080
exit(ExitCode::QueryError as i32);
8181
} else if let Some(command_error) = err.downcast_ref::<CommandError>() {
82-
eprintln!("Command Error: {}", command_error);
82+
println_error!("Command Error: {}", command_error);
8383
exit(ExitCode::CommandError as i32);
8484
} else {
85-
eprintln!("Error: {}", err);
85+
println_error!("Error: {}", err);
8686
exit(ExitCode::GeneralError as i32);
8787
}
8888
}
@@ -126,34 +126,54 @@ fn main() {
126126
println!("{}", VERSION);
127127
exit(ExitCode::Success as i32);
128128
}
129+
let address = match args.address {
130+
Some(address) => address,
131+
None => {
132+
println_error!("missing server address ('{}').", format_argument!("--address <{ADDRESS_VALUE_NAME}>"));
133+
exit(ExitCode::UserInputError as i32);
134+
}
135+
};
136+
let username = match args.username {
137+
Some(username) => username,
138+
None => {
139+
println_error!(
140+
"username is required for connection authentication ('{}').",
141+
format_argument!("--username <{USERNAME_VALUE_NAME}>")
142+
);
143+
exit(ExitCode::UserInputError as i32);
144+
}
145+
};
129146
if args.password.is_none() {
130-
args.password = Some(LineReaderHidden::new().readline(&format!("password for '{}': ", args.username)));
147+
args.password = Some(LineReaderHidden::new().readline(&format!("password for '{username}': ")));
131148
}
132149
if !args.diagnostics_disable {
133150
init_diagnostics()
134151
}
135-
if !args.tls_disabled && !args.address.starts_with("https:") {
136-
eprintln!(
152+
if !args.tls_disabled && !address.starts_with("https:") {
153+
println_error!(
137154
"\
138-
TLS connections can only be enabled when connecting to HTTPS endpoints, for example using 'https://<ip>:port'. \
139-
Please modify the address, or disable TLS (--tls-disabled). WARNING: this will send passwords over plaintext!\
140-
"
155+
TLS connections can only be enabled when connecting to HTTPS endpoints. \
156+
For example, using 'https://<ip>:port'.\n\
157+
Please modify the address or disable TLS ('{}'). {}\
158+
",
159+
format_argument!("--tls-disabled"),
160+
format_warning!("WARNING: this will send passwords over plaintext!"),
141161
);
142162
exit(ExitCode::UserInputError as i32);
143163
}
144-
let runtime = BackgroundRuntime::new();
145164
let tls_root_ca_path = args.tls_root_ca.as_ref().map(|value| Path::new(value));
165+
166+
let runtime = BackgroundRuntime::new();
146167
let driver = match runtime.run(TypeDBDriver::new(
147-
args.address,
148-
Credentials::new(&args.username, args.password.as_ref().unwrap()),
168+
address,
169+
Credentials::new(&username, args.password.as_ref().unwrap()),
149170
DriverOptions::new(!args.tls_disabled, tls_root_ca_path).unwrap(),
150171
)) {
151172
Ok(driver) => Arc::new(driver),
152173
Err(err) => {
153-
eprintln!("Failed to create driver connection to server. {}", err);
154-
if !args.tls_disabled {
155-
eprintln!("Verify that the server is also configured with TLS encryption.");
156-
}
174+
let tls_error =
175+
if args.tls_disabled { "" } else { "\nVerify that the server is also configured with TLS encryption." };
176+
println_error!("Failed to create driver connection to server. {err}{tls_error}");
157177
exit(ExitCode::ConnectionError as i32);
158178
}
159179
};
@@ -170,7 +190,7 @@ fn main() {
170190
};
171191

172192
if !args.command.is_empty() && !args.script.is_empty() {
173-
eprintln!("Error: Cannot specify both commands and files");
193+
println_error!("cannot specify both commands and files");
174194
exit(ExitCode::UserInputError as i32);
175195
} else if !args.command.is_empty() {
176196
if let Err(err) = execute_command_list(&mut context, &args.command) {
@@ -227,7 +247,7 @@ fn execute_script(
227247
fn execute_command_list(context: &mut ConsoleContext, commands: &[String]) -> Result<(), Box<dyn Error>> {
228248
for command in commands {
229249
if let Err(err) = execute_commands(context, command, true, true) {
230-
eprintln!("### Stopped executing at command: {}", command);
250+
println_error!("### Stopped executing at command: {}", command);
231251
return Err(Box::new(err));
232252
}
233253
}
@@ -283,7 +303,7 @@ fn execute_commands(
283303
input = match current_repl.match_first_command(input, coerce_each_command_to_one_line) {
284304
Ok(None) => {
285305
let message = format!("Unrecognised command: {}", input);
286-
eprintln!("{}", message);
306+
println_error!("{}", message);
287307
return Err(CommandError { message });
288308
}
289309
Ok(Some((command, arguments, next_command_index))) => {
@@ -293,19 +313,19 @@ fn execute_commands(
293313
}
294314

295315
if must_log_command || multiple_commands.is_some_and(|b| b) {
296-
eprintln!("{} {}", "+".repeat(repl_index + 1), command_string.trim());
316+
println_error!("{} {}", "+".repeat(repl_index + 1), command_string.trim());
297317
}
298318
match command.execute(context, arguments) {
299319
Ok(_) => &input[next_command_index..],
300320
Err(err) => {
301321
let message = format!("Error executing command: '{}'\n{}", command_string.trim(), err);
302-
eprintln!("{}", message);
322+
println_error!("{}", message);
303323
return Err(CommandError { message });
304324
}
305325
}
306326
}
307327
Err(err) => {
308-
eprintln!("{}", err);
328+
println_error!("{}", err);
309329
return Err(CommandError { message: err.to_string() });
310330
}
311331
};

src/printer.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
55
*/
66

7+
use clap::builder::styling::{AnsiColor, Color, Style};
78
use typedb_driver::{
89
answer::{ConceptDocument, ConceptRow},
910
concept::{Concept, Value},
@@ -14,6 +15,54 @@ const TABLE_INDENT: &'static str = " ";
1415
const CONTENT_INDENT: &'static str = " ";
1516
const TABLE_DASHES: usize = 7;
1617

18+
pub const ERROR_STYLE: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Red))).bold();
19+
pub const WARNING_STYLE: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Yellow))).bold();
20+
pub const ARGUMENT_STYLE: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Yellow)));
21+
22+
#[macro_export]
23+
macro_rules! format_error {
24+
($($arg:tt)*) => {
25+
$crate::format_colored!($crate::printer::ERROR_STYLE, $($arg)*)
26+
};
27+
}
28+
29+
#[macro_export]
30+
macro_rules! format_warning {
31+
($($arg:tt)*) => {
32+
$crate::format_colored!($crate::printer::WARNING_STYLE, $($arg)*)
33+
};
34+
}
35+
36+
#[macro_export]
37+
macro_rules! format_argument {
38+
($($arg:tt)*) => {
39+
$crate::format_colored!($crate::printer::ARGUMENT_STYLE, $($arg)*)
40+
};
41+
}
42+
43+
#[macro_export]
44+
macro_rules! format_colored {
45+
($style:expr, $($arg:tt)*) => {
46+
format!(
47+
"{}{}{}",
48+
$style.render(),
49+
format!($($arg)*),
50+
$style.render_reset()
51+
)
52+
};
53+
}
54+
55+
#[macro_export]
56+
macro_rules! println_error {
57+
($($arg:tt)*) => {
58+
eprintln!(
59+
"{} {}",
60+
$crate::format_error!("error:"),
61+
format!($($arg)*)
62+
);
63+
}
64+
}
65+
1766
fn println(string: &str) {
1867
println!("{}", string)
1968
}

0 commit comments

Comments
 (0)