Hello Justin Turpin! Sorry to hear your struggles with rust. It's always going to be a bit more verbose using rust than Python due to type information, but I think there are some things we could do to simplify your code. Would you be comfortable posting the 20 line code for us to review? I didn't see a link in your post.
Anyway, so some things that could make your script easier:
* for simple scripts I tend to use the `.expect` method if I plan on killing the program if there is an error. It's just like unwrap, but it will print out a custom error message. So you could write something like this to get a file:
let mut file = File::open("conf.json")
.expect("could not open file");
(Aside: I never liked the method name `expect` for this, but is too late to do anything about that now).
* next, you don't have to create a struct for serde if you don't want to. serde_derive is definitely cool and magical, but it can be too magical for one off scripts. Instead you could use serde_jaon::Value [0], which is roughly equivalent to when python's json parser would produce.
* next, serde_json has a function called from from_reader [1], which you can use to parse directly from a `Read` type. So combined with Value you would get:
let config: Value = serde::from_reader(file)
.expect("config has invalid json");
* Next you could get the config values out with some methods on Value:
let jenkins_server = config.get("jenkins_server")
.expect("jenkins_server key not in config")
.as_str()
.expect("jenkins_server key is not a string");
There might be some other things we could simplify. Just let us know how to help.
In my Rust crates, I use a custom trait[1], with implementations for Option and Result, that provides an expected() method which, if it panics, prefixes the message with "expected: ". So you can write
foo().pop().expected("foo length >= 1");
and if it fails, the error is something like "panicked at 'expected foo length >= 1'".
A failed expectation for one user isn't always an error for other users; calling out on_error may confuse people because it indicates an error may occur and must be handled.
If I recall correctly we just couldn't come up with a great name for it. To me "expect" is a positive action, but the argument is about it failing to meet the expectation. Semantically I like `thing.unwrap_or(|| panic!("failure message"))`. It feels more like what I would want to say, but it just is so wordy.
Ultimately I'm happy we just picked something and moved on, but still mildly annoys me whenever I write it. If only we found that perfect method name way back when...
I think that was one of the proposed variations, but we ended up picking the shorter .expect to cut down on repetition. We expected (ha) that this function would be used in these one-offs, so we wanted something more efficient.
I actually initially had it as sort of a callback:
.otherwise(panic(msg))
(although I assume a rust panic! isn't really a function call).
But isn't the way to get the default, simply to use unwrap()?
In a simple script, failing to open a configuration file for reading is likely a show stopper, and you probably want to log/print an error (no such file, wrong permission, etc).
But in, say, a paint program, you'd normally not want to panic and crash if the image file a user selected to open in a file dialog is invalid or went away between the click-to-select and the click-to-open. In such a program you'd want to handle most file errors much more defensively.
Not a Rust guy, but it looks like a way to say "this option value is required and if it is not present, crash with the following error". So "expect" is a fair name, since it means a value is expected and the absence of a value is unexpected.
I could see "required" or "require" as a better name. Or even just break the "positive names" rule and go with "notOptional".
Looking at it that way, I think it's possible to make "expect" more comfortable to read by how you phrase your error messages.
Instead of
let mut file = File::open("conf.json")
.expect("could not open file");
and
let config: Value = serde::from_reader(file)
.expect("config has invalid json");
and
let jenkins_server = config.get("jenkins_server")
.expect("jenkins_server key not in config")
.as_str()
.expect("jenkins_server key is not a string");
One could write
let mut file = File::open("conf.json")
.expect("Need to be able to open file `conf.json'.");
and
let config: Value = serde::from_reader(file)
.expect("The file `conf.json' must contain valid JSON.");
and
let jenkins_server = config.get("jenkins_server")
.expect("The config must have a key named `jenkins_server'.")
.as_str()
.expect("The config value of `jenkins_server' must be a string.");
4 chained function calls from config -> read a key from it is quite an ask. Though I realize there's reason for it. There are many cases I'd be happy to have panics occur for invariants.
config.strictString('foo') or something of that nature seems like it could be a more ergonomic choice in cases like thise.
What he is showing is how someone who knows Rust well, would potentially approach this problem.
What you're point out is that it's a large bar to ask a new comer to the language to do this because it requires a deeper understanding of the language to use.
Is it not appropriate to show that you can reduce the complexity of a program by using other features of the language?
Each function call is a transformation on the previous argument. Whether or not you assign the results to a variable first before calling the second doesn't change the behavior of the code.
But yes, it does appear that the config library needs an extra wrapper that loads from files and returns configs (in a context) that does the common work for you.
You may have confused the method "expect" as being "except", as in "exception". That's not what it means - it means to "panic" with the given string as the error message. Panics in Rust have some similarities to exceptions in C++, but are not the same thing.
Anyway, so some things that could make your script easier:
* for simple scripts I tend to use the `.expect` method if I plan on killing the program if there is an error. It's just like unwrap, but it will print out a custom error message. So you could write something like this to get a file:
(Aside: I never liked the method name `expect` for this, but is too late to do anything about that now).* next, you don't have to create a struct for serde if you don't want to. serde_derive is definitely cool and magical, but it can be too magical for one off scripts. Instead you could use serde_jaon::Value [0], which is roughly equivalent to when python's json parser would produce. * next, serde_json has a function called from from_reader [1], which you can use to parse directly from a `Read` type. So combined with Value you would get:
* Next you could get the config values out with some methods on Value: There might be some other things we could simplify. Just let us know how to help.[0]: https://docs.serde.rs/serde_json/enum.Value.html
[1] https://docs.serde.rs/serde_json/de/fn.from_reader.html