From 7ae162bef486f4ec400f871add7487fd3ac570f0 Mon Sep 17 00:00:00 2001 From: Cain <75994858+cainthebest@users.noreply.github.com> Date: Mon, 15 Jan 2024 15:51:21 +0000 Subject: [PATCH] refactor: clean up and add more features --- crates/cli/Cargo.toml | 45 +++++++++++--- crates/cli/src/main.rs | 130 +++++++++++++++++++++++++++++++++-------- 2 files changed, 142 insertions(+), 33 deletions(-) diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index dd4ed97..7610004 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -11,15 +11,42 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["json", "packet_capture"] -json = ["dep:serde", "dep:serde_json", "gamedig/serde"] +default = ["packet_capture", "bson", "json", "xml", "browser"] + +# Tools packet_capture = ["gamedig/packet_capture"] -[dependencies] -clap = { version = "4.1.11", features = ["derive"] } -gamedig = { version = "*", path = "../lib", features = ["clap"] } -thiserror = "1.0.43" +# Output formats +bson = ["dep:serde", "dep:bson", "dep:hex", "gamedig/serde"] +json = ["dep:serde", "dep:serde_json", "gamedig/serde"] +xml = ["dep:serde", "dep:serde-xml-rs", "gamedig/serde"] -# JSON dependencies -serde = { version = "1", optional = true } -serde_json = { version = "1", optional = true } +# Misc +browser = ["dep:webbrowser"] + +[dependencies] +# Core Dependencies +thiserror = "1.0.43" +clap = { version = "4.1.11", default-features = false, features = ["derive"] } +gamedig = { version = "*", path = "../lib", default-features = false, features = [ + "clap", + "games", + "game_defs", +] } + +# Feature Dependencies +# Serialization / Deserialization +serde = { version = "1", optional = true, default-features = false } + +# BSON +bson = { version = "2.8.1", optional = true, default-features = false } +hex = { version = "0.4.3", optional = true, default-features = false } + +# JSON +serde_json = { version = "1", optional = true, default-features = false } + +# XML +serde-xml-rs = { version = "0.6.0", optional = true, default-features = false } + +# Browser +webbrowser = { version = "0.8.12", optional = true, default-features = false } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index b3eb937..84a4b1f 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -25,7 +25,7 @@ const GAMEDIG_HEADER: &str = r" |___/ A command line interface for querying game servers. - Copyright (C) 2022 - Present GameDig Organization & Contributors + Copyright (C) 2022 - 2023 GameDig Organization & Contributors Licensed under the MIT license "; @@ -56,10 +56,9 @@ enum Action { #[arg(short, long)] port: Option, - /// Flag indicating if the output should be in JSON format for programmatic use. - #[cfg(feature = "json")] - #[arg(short, long)] - json: bool, + /// Specifies the output format + #[arg(short, long, default_value = "debug", value_enum)] + format: OutputFormat, /// Which response variant to use when outputting #[arg(short, long, default_value = "generic")] @@ -95,6 +94,19 @@ enum OutputMode { ProtocolSpecific, } +#[derive(Clone, Debug, PartialEq, Eq, ValueEnum)] +enum OutputFormat { + Debug, + #[cfg(feature = "json")] + JsonPretty, + #[cfg(feature = "json")] + Json, + #[cfg(feature = "xml")] + Xml, + #[cfg(feature = "bson")] + Bson, +} + /// Attempt to find a game from the [library game definitions](GAMES) based on /// its unique identifier. /// @@ -122,13 +134,13 @@ fn find_game(game_id: &str) -> Result<&'static Game> { /// # Returns /// * `Result` - On sucess returns a resolved IP address; on failure /// returns an [Error::InvalidHostname] error. -fn resolve_ip_or_domain(host: &str, extra_options: &mut Option) -> Result { - if let Ok(parsed_ip) = host.parse() { +fn resolve_ip_or_domain>(host: T, extra_options: &mut Option) -> Result { + let host_str = host.as_ref(); + if let Ok(parsed_ip) = host_str.parse() { Ok(parsed_ip) } else { - set_hostname_if_missing(host, extra_options); - - resolve_domain(host) + set_hostname_if_missing(host_str, extra_options); + resolve_domain(host_str) } } @@ -173,15 +185,36 @@ fn set_hostname_if_missing(host: &str, extra_options: &mut Option(output_mode: OutputMode, format: OutputFormat, result: &T) { + match format { + OutputFormat::Debug => match output_mode { + OutputMode::Generic => output_result_debug(result.as_json()), + OutputMode::ProtocolSpecific => output_result_debug(result.as_original()), + }, #[cfg(feature = "json")] - OutputMode::Generic if json => output_result_json(result.as_json()), + OutputFormat::JsonPretty => match output_mode { + OutputMode::Generic => output_result_json_pretty(result.as_json()), + OutputMode::ProtocolSpecific => output_result_json_pretty(result.as_original()), + }, #[cfg(feature = "json")] - OutputMode::ProtocolSpecific if json => output_result_json(result.as_original()), - - OutputMode::Generic => output_result_debug(result.as_json()), - OutputMode::ProtocolSpecific => output_result_debug(result.as_original()), + OutputFormat::Json => match output_mode { + OutputMode::Generic => output_result_json(result.as_json()), + OutputMode::ProtocolSpecific => output_result_json(result.as_original()), + }, + #[cfg(feature = "xml")] + OutputFormat::Xml => match output_mode { + OutputMode::Generic => output_result_xml(result.as_json()), + //BUG: In this case we get a writer error with all serde write methods some reason with xml 0.6.0 + //BUG: gamedig-cli.exe query -g -i -p -f xml --output-mode protocol-specific + //BUG: Writer: emitter error: document start event has already been emitted + //BUG: With xml 0.5.1 we get unsupported operation: 'serialize_unit_variant' + OutputMode::ProtocolSpecific => panic!("XML format is not supported for protocol specific output"), + }, + #[cfg(feature = "bson")] + OutputFormat::Bson => match output_mode { + OutputMode::Generic => output_result_bson(result.as_json()), + OutputMode::ProtocolSpecific => output_result_bson(result.as_original()), + }, } } @@ -198,8 +231,33 @@ fn output_result_debug(result: R) { /// # Arguments /// * `result` - A serde serializable result. #[cfg(feature = "json")] -fn output_result_json(result: R) { - serde_json::to_writer_pretty(std::io::stdout(), &result).unwrap(); +fn output_result_json(result: T) { + println!("{}", serde_json::to_string(&result).unwrap()); +} + +#[cfg(feature = "json")] +fn output_result_json_pretty(result: T) { + println!("{}", serde_json::to_string_pretty(&result).unwrap()); +} + +/// Output the result as an XML object. +/// # Arguments +/// * `result` - A serde serializable result. +fn output_result_xml(result: T) { + println!("{}", serde_xml_rs::to_string(&result).unwrap()); +} + +#[cfg(feature = "bson")] +fn output_result_bson(result: T) { + let bson = bson::to_bson(&result).unwrap(); + + if let bson::Bson::Document(document) = bson { + let bytes = bson::to_vec(&document).unwrap(); + + println!("{}", hex::encode(bytes)); + } else { + panic!("Failed to convert result to BSON"); + } } fn main() -> Result<()> { @@ -210,7 +268,7 @@ fn main() -> Result<()> { game, ip, port, - json, + format, output_mode, capture, timeout_settings, @@ -225,14 +283,38 @@ fn main() -> Result<()> { gamedig::capture::setup_capture(capture); let result = query_with_timeout_and_extra_settings(game, &ip, port, timeout_settings, extra_options)?; - output_result(output_mode, json, result.as_ref()); + output_result(output_mode, format, result.as_ref()); } Action::Source => { println!("{}", GAMEDIG_HEADER); - // Print the source code location - println!("Source code: https://github.com/gamedig/rust-gamedig\n"); - println!("Be sure to leave a star if you like the project :)\n"); + #[cfg(feature = "browser")] + { + // Directly offering to open the URL + println!("\nWould you like to open the GitHub repository in your default browser? [Y/n]"); + + let mut choice = String::new(); + std::io::stdin().read_line(&mut choice).unwrap(); + if choice.trim().eq_ignore_ascii_case("Y") { + if webbrowser::open("https://github.com/gamedig/rust-gamedig").is_ok() { + println!("Opening GitHub repository in default browser..."); + } else { + println!("Failed to open GitHub repository in default browser."); + println!("Please use the following URL: https://github.com/gamedig/rust-gamedig"); + } + } else { + println!("Not to worry, you can always open the repository manually"); + println!("by visiting the following URL: https://github.com/gamedig/rust-gamedig"); + } + } + + #[cfg(not(feature = "browser"))] + { + println!("\nYou can find the source code for this project at the following URL:"); + println!("https://github.com/gamedig/rust-gamedig"); + } + + println!("\nBe sure to leave a star if you like the project :)"); } Action::License => { // Bake the license into the binary