diff --git a/Cargo.lock b/Cargo.lock index d5fcf4e..fbc863c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,6 +15,7 @@ dependencies = [ name = "ambi_mock_client" version = "0.1.0" dependencies = [ + "clap", "rand", "regex", "reqwest", @@ -22,6 +23,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.0.1" @@ -64,6 +76,36 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "3.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c47df61d9e16dc010b55dba1952a57d8c215dbb533fd13cdd13369aac73b1c" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3aab4734e083b809aaf5794e14e756d1c798d2c69c7f7de7a09a2f5214993c1" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "core-foundation" version = "0.9.2" @@ -213,6 +255,12 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -487,6 +535,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -517,6 +574,30 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.36" @@ -744,6 +825,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.85" @@ -769,6 +856,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + [[package]] name = "tinyvec" version = "1.5.1" @@ -894,6 +996,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "want" version = "0.3.0" @@ -1002,6 +1110,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/src/lib.rs b/src/lib.rs index 7788d92..6ba18f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,39 +3,49 @@ //! //! This file provides for a separation of concerns from main.rs for application //! logic, per the standard Rust pattern. -//! +//! //! See `main.rs` for more details about what this application does. //! //! See the `LICENSE` file for Copyright and license details. -//! +//! +use clap::Parser; use rand::{thread_rng, Rng}; -use reqwest::blocking::Client; +use reqwest::blocking::{Client, Response}; use reqwest::header::CONTENT_TYPE; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; use std::fmt; -use clap::{Parser}; +use std::io::Write; +use std::thread::{spawn, JoinHandle, ThreadId}; /// Defines the Ambi Mock Client command line interface as a struct #[derive(Parser, Debug)] #[clap(name = "Ambi Mock Client")] #[clap(author = "Rust Never Sleeps community (https://github.com/Jim-Hodapp-Coaching/)")] #[clap(version = "0.1.0")] -#[clap(about = "Provides a mock Ambi client that emulates real sensor hardware such as an Edge client.")] -#[clap(long_about = "This application emulates a real set of hardware sensors that can report on environmental conditions such as temperature, pressure, humidity, etc.")] +#[clap( + about = "Provides a mock Ambi client that emulates real sensor hardware such as an Edge client." +)] +#[clap( + long_about = "This application emulates a real set of hardware sensors that can report on environmental conditions such as temperature, pressure, humidity, etc." +)] pub struct Cli { /// Turns verbose console debug output on #[clap(short, long)] pub debug: bool, + + /// Make number of concurrent requests + #[clap(short, long, default_value_t = 1)] + pub int: u16, } #[derive(Serialize, Deserialize)] struct Reading { - temperature: String, - humidity: String, - pressure: String, - dust_concentration: String, - air_purity: String + temperature: String, + humidity: String, + pressure: String, + dust_concentration: String, + air_purity: String, } impl Reading { @@ -44,14 +54,88 @@ impl Reading { humidity: String, pressure: String, dust_concentration: String, - air_purity: String + air_purity: String, ) -> Reading { Reading { temperature, humidity, pressure, dust_concentration, - air_purity + air_purity, + } + } +} + +#[derive(Debug, PartialEq)] +struct ResponseLog<'a, W: Write> { + debug: bool, + writer: &'a mut W, +} + +impl<'a, W: Write> ResponseLog<'a, W> { + pub fn new(debug: bool, writer: &'a mut W) -> Self { + ResponseLog { + debug: debug, + writer: writer, + } + } + + pub fn print(&mut self, result: R) { + let result_string = if self.debug { + format!("{:?}", result) + } else { + format!("{}", result) + }; + self.writer.write_all(result_string.as_bytes()).unwrap(); + } +} + +#[derive(Debug)] +struct RequestResult { + description: String, + error: Option, + data: Option, + thread_id: ThreadId, +} + +impl RequestResult { + pub fn new( + description: String, + error: Option, + data: Option, + thread_id: ThreadId, + ) -> RequestResult { + RequestResult { + description, + error, + data, + thread_id, + } + } + + pub fn is_error(&self) -> bool { + self.error.is_some() + } +} + +impl fmt::Display for RequestResult { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.is_error() { + let error = self.error.as_ref().unwrap(); + let status = error.status(); + write!( + f, + "{} Status: {:?}, Thread ID: {:?}", + self.description, status, self.thread_id + ) + } else { + let response = self.data.as_ref().unwrap(); + let status = response.status().as_u16(); + write!( + f, + "{} Status: {}, Thread ID: {:?}", + self.description, status, self.thread_id + ) } } } @@ -61,7 +145,7 @@ enum AirPurity { Dangerous, High, Low, - FreshAir + FreshAir, } impl AirPurity { @@ -83,7 +167,7 @@ impl fmt::Display for AirPurity { AirPurity::Low => write!(f, "Fresh Air"), AirPurity::High => write!(f, "Low Pollution"), AirPurity::Dangerous => write!(f, "High Pollution"), - AirPurity::FreshAir => write!(f, "Dangerous Pollution") + AirPurity::FreshAir => write!(f, "Dangerous Pollution"), } } } @@ -110,9 +194,7 @@ fn random_gen_dust_concentration() -> String { rng.gen_range(0..=1000).to_string() } -pub fn run(cli: &Cli) { - println!("\r\ncli: {:?}\r\n", cli); - +fn send_request(url: &str, client: Client) -> reqwest::Result { let dust_concentration = random_gen_dust_concentration(); let air_purity = AirPurity::from_value(dust_concentration.parse::().unwrap()).to_string(); let reading = Reading::new( @@ -120,43 +202,51 @@ pub fn run(cli: &Cli) { random_gen_humidity(), random_gen_pressure(), dust_concentration, - air_purity + air_purity, ); let json = serde_json::to_string(&reading).unwrap(); - const URL: &str = "http://localhost:4000/api/readings/add"; - - println!("Sending POST request to {} as JSON: {}", URL, json); - - let client = Client::new(); - let res = client - .post(URL) + println!("Sending POST request to {} as JSON: {}", url, json); + client + .post(url) .header(CONTENT_TYPE, "application/json") .body(json) - .send(); - match res { - Ok(response) => { - match cli.debug { - true => println!("Response from Ambi backend: {:#?}", response), - false => println!("Response from Ambi backend: {:?}", response.status().as_str()) + .send() +} + +pub fn run(cli: &Cli) { + println!("\r\ncli: {:?}\r\n", cli); + + const URL: &str = "http://localhost:4000/api/readings/add"; + let mut handlers: Vec> = vec![]; + let debug: bool = cli.debug; + for _ in 0..cli.int { + let handler = spawn(move || match send_request(URL, Client::new()) { + Ok(response) => { + let result = RequestResult::new( + String::from("Response from Ambi backend."), + None, + Some(response), + std::thread::current().id(), + ); + ResponseLog::new(debug, &mut std::io::stdout()).print(result) } - } - Err(e) => { - match cli.debug { - // Print out the entire reqwest::Error for verbose debugging - true => eprintln!("Response error from Ambi backend: {:?}", e), - // Keep the error reports more succinct - false => { - if e.is_request() { - eprintln!("Response error from Ambi backend: request error"); - } else if e.is_timeout() { - eprintln!("Response error from Ambi backend: request timed out"); - } else { - eprintln!("Response error from Ambi backend: specific error type unknown"); - } - } + Err(error) => { + let result = RequestResult::new( + String::from("Response error from Ambi backend."), + Some(error), + None, + std::thread::current().id(), + ); + ResponseLog::new(debug, &mut std::io::stderr()).print(result) } - } + }); + + handlers.push(handler); + } + + for handler in handlers { + handler.join().unwrap(); } } @@ -167,18 +257,18 @@ mod tests { #[test] fn random_gen_humidity_returns_correctly_formatted_humidity_data() { - let result = random_gen_humidity(); - let regex = Regex::new(r"\d{1,2}.\d{1}").unwrap(); + let result = random_gen_humidity(); + let regex = Regex::new(r"\d{1,2}.\d{1}").unwrap(); - assert!(regex.is_match(&result)); + assert!(regex.is_match(&result)); } - + #[test] fn random_gen_temperature_returns_correctly_formatted_humidity_data() { - let result = random_gen_temperature(); - let regex = Regex::new(r"\d{1,2}.\d{1}").unwrap(); + let result = random_gen_temperature(); + let regex = Regex::new(r"\d{1,2}.\d{1}").unwrap(); - assert!(regex.is_match(&result)); + assert!(regex.is_match(&result)); } #[test] @@ -208,4 +298,29 @@ mod tests { assert_eq!(AirPurity::from_value(high), AirPurity::High); assert_eq!(AirPurity::from_value(dangerous), AirPurity::Dangerous); } + + #[test] + fn response_log_new_returns_response_log() { + let mut writer1 = vec![0, 0, 0]; + let mut writer2 = writer1.clone(); + assert_eq!( + ResponseLog::new(true, &mut writer1), + ResponseLog { + debug: true, + writer: &mut writer2 + } + ) + } + + #[test] + fn response_log_prints() { + let mut writer = Vec::new(); + let result = "123"; + let mut response_log = ResponseLog::new(false, &mut writer); + let expected = result.as_bytes().to_vec(); + + response_log.print(result); + + assert_eq!(writer, expected) + } } diff --git a/src/main.rs b/src/main.rs index 99efe11..9e45ef2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,16 @@ -//! # Provides a mock Ambi client that emulates real sensor hardware such as +//! # Provides a mock Ambi client that emulates real sensor hardware such as //! an Edge client. //! //! This application emulates a real set of hardware sensors that can report on //! environmental conditions such as temperature, pressure, humidity, etc. -//! +//! //! Please see the `ambi` repository for the web backend that this client connects to //! and the `edge-rs` repository for what this client is emulating. //! //! See the `LICENSE` file for Copyright and license details. -//! +//! -use clap::{Parser}; +use clap::Parser; // Internal library namespace for separation of app logic use ambi_mock_client;