Hello all! I've been having a blast learning Rust, it's actually a very intuitive language once you get past the learning curve. I'm coming from higher level languages like JavaScript and Python, Rust is the first Systems Programming language I have learned (Except Arduino C++).
I'm looking for some code review, some suggestions on how I can improve and get more Rusty. I'm sure I'm bringing some bad habits, but I can't catch them. The program is at the moment only a desktop application using egui and reqwest. It gets it's data from the NASA API, it's meant to be educational software.
I'm looking for feedback like:
is my project following the "Rust way"
how is the speed/performance
how can I improve?
Anything else you think I should know in my Rust journey?
** NOTE **: You don't have to make an account at the NASA API, you can go to settings->set api key and type in DEMO_KEY and it will work with the demo key, just limited amount of requests and slower speeds.
Run cargo clippy to get a sense of whether the code is idiomatic. The lints sometimes have false positives, and some people don't like the defaults. But it's definitely a good starting point to help clean up things that look weird.
It seems ok to me. Nothing stands out as egregiously power hungry.
These are not related to the code, but there are a few things that are jarring while using the app:
Running the exe on Windows opens a terminal in the background.
Most of the menu items are unimplemented, only printing a message to stdout.
In the code specifically:
Three of the unit tests are todo!()
Tests in the urls module have a race condition in the file system:
test_build_url_neows() depends on secret.json, but it does not create it.
test_get_secret_sauce() creates secret.json, but it runs in parallel with test_build_url_neows().
test_build_url_neows() fails on the first cargo test invocation and succeeds on subsequent invocations. Remove secret.json to see the initial failure condition.
Writing JSON as text is not very robust (and you already have json as a dependency. Should probably use it).
In fact, this whole test has way too much code...
match foo { Err(e) => panic!("{e}") } is also spelled foo.unwrap():
#[test]
fn test_get_secret_sauce() {
// Create a test secret.json
let mut f = File::create("secret.json").unwrap();
f.write_all(json::object!{"key": "TEST_KEY"}.dump().as_bytes())
.unwrap();
assert_eq!(Urls::get_secret_sauce().unwrap(), "TEST_KEY");
}
Using thiserror can clean up a lot of the boilerplate on your error types.
It can also generate From impls so you don't need to use expect(): instead, convert the error with the ? operator.
let naive_start_date = NaiveDate::parse_from_str(&dates.0, "%Y-%m-%d")?;
Urls::make_secret_sauce() does a lot of unnecessary work by constructing and destructing its own type. It calls the constructor Urls::urls() (which is usually named new) and immediately replaces the heap-allocated strings that it contains. I would:
Move the strings to module-level constants.
Remove the Urls::urls() constructor and refer to the string constants where appropraite.
After these changes, Urls::make_secret_sauce() becomes:
The Result here is weird. It looks like it was planned to read a file at some point. This will never return an error. Consider removing it.
SpacePixUi has two separate caches for "APOD", one is unused.
Apod should probably have an inner struct type instead of the tuples. Naming fields will make the code easier to follow (what's a data.4? I have no idea; I have to look it up in some unrelated code! Oh, it's a title. 4 is a weird name for a title.)
If the wrong API key is entered after the app first starts, it will show the error placeholder until the app is restarted, no matter how many times you try to set the correct API key. It's as easy as adding self.apod.cache = None; to App::set_api_key().
if open::that("https://api.nasa.gov/").is_err() {
ui.label("Failed to open web browser.");
}
But clippy will suggest collapsing this with its parent if. Which is sort of ugly:
if ui.link("Don't have a NASA API Key?").clicked()
&& open::that("https://api.nasa.gov/").is_err()
{
ui.label("Failed to open web browser.");
}
This is egui-specific, but egui_extras::install_image_loaders() is ideally called once in your App constructor, not in the update cycle (which runs every frame -- it's safe to do this, but wasteful.)