Skip to content
Open
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 116 additions & 35 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
//!

use rand::{thread_rng, Rng};
use reqwest::blocking::Client;
use reqwest::blocking::{Client, Response};
use reqwest::header::CONTENT_TYPE;
use serde::{Serialize, Deserialize};
use std::fmt;
use clap::{Parser};
use std::thread::{spawn, JoinHandle, ThreadId};

/// Defines the Ambi Mock Client command line interface as a struct
#[derive(Parser, Debug)]
Expand All @@ -27,6 +28,10 @@ pub struct Cli {
/// Turns verbose console debug output on
#[clap(short, long)]
pub debug: bool,

// Make int number of concurrent requests
#[clap(short, long, default_value_t = 1)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an upper limit to how many concurrent requests we want to allow for? Perhaps adding CLI interface data check on that would be in order. For example, do we really want to allow for 65k simultaneous threads, or at least without having tested it well? :)

pub int: u16
}

#[derive(Serialize, Deserialize)]
Expand Down Expand Up @@ -56,6 +61,74 @@ impl Reading {
}
}

#[derive(Debug)]
struct Output {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One idea for you - what if we called this Log, or Logger or even more specific ResponseLog \ ResponseLogger instead of Output? There may even be naming convention that's common in other Rust programs here (and perhaps that is Output and I just aren't aware of this).

description: String,
error: Option<reqwest::Error>,
data: Option<Response>,
thread_id: ThreadId,
debug: bool
}

impl Output {
pub fn new(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could have one static instance of Output instead of having to call new() each time. That way it'd be a very lightweight operation both in execution time and in memory allocation. So it wouldn't be on the heap then, it'd be in a special section of memory called the Block Starting Symbol (BSS). These become global instances and are meant to be shared and used in different portions of an application.

Happy to discuss this more, just wanted to plant the idea.

description: String,
error: Option<reqwest::Error>,
data: Option<Response>,
thread_id: ThreadId,
debug: bool
) -> Output {
Output {
description,
error,
data,
thread_id,
debug
}
}

pub fn is_error(&self) -> bool {
self.error.is_some()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I absolutely love the use of Rust's Option concept here, fits perfectly in line with success vs error cases.

}

pub fn print(&self) {
if self.is_error() {
self.print_to_stderr()
} else {
self.print_to_stdout()
}
}

fn print_to_stderr(&self) {
if self.debug {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how we accomplish debug output vs non-debug

eprintln!("{:#?}", self)
} else {
eprintln!("{}", self)
}
}

fn print_to_stdout(&self) {
if self.debug {
println!("{:#?}", self)
} else {
println!("{}", self)
}
}
}

impl fmt::Display for Output {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This trait is how we accomplish the customization for non-debug output

fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.is_error() {
let error = self.error.as_ref().unwrap();
write!(f, "Response error from Ambi. Status: {:?}, Thread ID: {:?}", error.status(), self.thread_id)
} else {
let response = self.data.as_ref().unwrap();
let status = response.status().as_u16();
write!(f, "Response from Ambi. Status: {}, Thread ID: {:?}", status, self.thread_id)
}
}
}

#[derive(Debug, PartialEq)]
enum AirPurity {
Dangerous,
Expand Down Expand Up @@ -110,53 +183,61 @@ 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<Response> {
let dust_concentration = random_gen_dust_concentration();
let air_purity = AirPurity::from_value(dust_concentration.parse::<u16>().unwrap()).to_string();
let reading = Reading::new(
random_gen_temperature(),
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())
}
}
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");
}
}
.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<JoinHandle<()>> = vec![];
let debug: bool = cli.debug;
for _ in 0..cli.int {
let handler = spawn(move ||
match send_request(URL, Client::new()) {
Ok(response) => {
Output::new(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In reference to my above suggestion for a different name and a static version, this then might look something like:

ResponseLog.print(
    None,
    Some(response),
    std::thread::current().id()
);

The String could become implicit because it's a specific enough kind of logger type, and you could initialize the static instance of it with debug once instead of needing to pass it along every time.

One other thought: could the None and Some become a literal Option that you pass to print() in which it could figure out whether it's a success message vs an error message?

Just an idea, curious what you think.

String::from("Response from Ambi backend"),
None,
Some(response),
std::thread::current().id(),
debug
).print();
}
Err(error) => {
Output::new(
String::from("Response error from Ambi backend"),
Some(error),
None,
std::thread::current().id(),
debug
).print();
}
}
}
);

handlers.push(handler);
}

for handler in handlers {
handler.join().unwrap();
}
}

Expand Down