My first rust project (launcher for Python scripts)

Where is your mod pylaunch statement?

Hmm, I'm re-reading Packages and Crates - The Rust Programming Language. My project structure is

pylaunch
+-- Cargo.toml (contains package.name="pylaunch")
+-- src
    + lib.rs
    + pylaunch.rs
    + pylaunchw.rs

If I'm following what it's saying in the book, I have one package called "pylaunch". That contains a library crate (in lib.rs) with the name "pylaunch", a binary crate (in pylaunch.rs) with the name "pylaunch" and a binary crate (in pylaunchw.rs) called "pylaunchw".

If I'm right about that, there's no way to name "the function default_exe defined in the executable", because the executable crate's name is different for the two exes!

I don't have one. In the book I read "Modules let us organize code within a crate into groups for readability and easy reuse", and I have so little code, that I don't feel the need to split it up into modules.

I'm guessing from your comment that modules also help establish naming for objects, even if they aren't needed for splitting up the code, but that wasn't the impression I got - and I'm not entirely sure how I'd use them for that. Is it sufficient to have mod pylaunch at the top of each of the 3 files?

On a more general note, I'm concerned that this seems like the coupling between the parts of my code is wrong. I'm fine with defining Config in the library, and having a "read from file" method in there. It also seems OK that the caller should be able to say what the default values are for fields not specified in the file. But having the caller say that by providing globally visible functions with specific names, seems like a bad design. But I couldn't find a better way in serde to specify defaults for fields when deserialising. Am I missing a better approach here that will make the above irrelevant? (I'm still interested in the background about modules and naming, though!)

Which files are runnable with a main function?

Generally anything defined in an executable is not reachable from the library, because the library crate is a dependency of the executable crate, even though both crates are part of the same src/ folder and in the same package (whose name is defined in Cargo.toml).

Whether a file is part of the library or executable crate is determined by whether it is reachable with mod statements from the library crate's root (lib.rs) or the executable crate's root (typically main.rs but can be configured)

A file that is not reachable by following mod statements from either crate root is not compiled at all.

pylaunch.rs and pylaunchw.rs.

Yeah, that's the conclusion I'd more or less come to (at least to the extent that "it doesn't make sense to do that").

So I guess I need to look for an alternative defaulting mechanism for serde. But I feel like I'm not understanding something about why serde insists on named functions as the way of supplying defaults.

Here's an excerpt of my code:

#[derive(Deserialize)]
pub struct Config {
    pub exe_name: String,
    pub launcher_name: String,
    pub lib_location: String,
    pub env_locs: Vec<String>,
    pub script_locs: Vec<String>,
    pub extensions: Vec<String>,
}

impl Config {

    pub fn from_file<P: AsRef<Path>> (filename: P) -> Option<Config> {
        let file = filename.as_ref();
        if file.exists() {
            let contents = fs::read_to_string(filename)
                .expect("Something went wrong reading the file");
            Some(serde_json::from_str(&contents).unwrap())
        } else {
            None
        }
    }
}

I don't want to make my struct fields all Option types, so I want to do the defaulting as part of the deserialisation, which means I need to give serde the name of a function that returns the default. OK, but I want the caller of from_file to pass a default value. If I could deserialise using an anonymous function for the default, I'd have no issue. But it has to be static...

I feel like I'm thinking too much in Python here (where passing first-class functions around is natural). I'm not even sure I'm expressing my problem properly, as I feel like I don't know the terms to describe what's confusing me :slightly_frowning_face:

You can do this in Rust too, but the serde library does not support it for default values.

You could define one struct with Option and another without, then transform from one to the other by replacing None with the default value.

Yeah, maybe I'll have to take that approach. It seems a bit of a shame to have the duplicated structs, but it will do the job. If nothing else, I can do that and if I later find a better way, I can switch.

Thanks for the help.

I raised It doesn't seem to be possible to fill in struct defaults from a runtime value · Issue #1986 · serde-rs/serde · GitHub to see what the serde maintainers think about this question.

I've used the pattern of having a "cli" Config separate from "actual" Config. To me, it expresses the the logic of the program more clearly, so I don't see it as duplication.

I think it's even more obvious when dealing with several points of entry for configuration (cli, env vars, etc) that parsing the entry-point configs all separately, then folding them into the final config increases clarity.

Hmm, that's an interesting point. I'm starting simple here, so my design is fairly basic (by which I mean "probably wrong in the long run" :slightly_smiling_face:) but works for now. For example, some of my "config" values are actually static constants that depend only on which exe I'm building, but having them in a single config struct avoids me needing to pass too many values around. It's expedient, but not really "correct".

I'm trying to suppress my tendency to over-design things to the point where I never actually produce a working program :slight_smile: But maybe I'm taking that a bit too far here...

That worked quite well, thanks!

#[derive(Deserialize)]
struct UserConfig {
    pub lib_location: Option<String>,
    pub env_locs: Option<Vec<String>>,
    pub script_locs: Option<Vec<String>>,
}

#[derive(Debug)]
pub struct Config {
    pub exe_name: String,
    pub launcher_name: String,
    pub extensions: Vec<String>,
    pub lib_location: String,
    pub env_locs: Vec<String>,
    pub script_locs: Vec<String>,
}

impl Config {

    pub fn from_file<P: AsRef<Path>> (filename: P, default: Config) -> Config {
        let mut result = default;
        let file = filename.as_ref();
        if file.exists() {
            let contents = fs::read_to_string(filename)
                .expect("Something went wrong reading the file");
            let config: UserConfig = serde_json::from_str(&contents).unwrap();
            if let Some(lib_location) = config.lib_location { result.lib_location = lib_location }
            if let Some(env_locs) = config.env_locs { result.env_locs = env_locs }
            if let Some(script_locs) = config.script_locs { result.script_locs = script_locs }
        }
        result
    }
}

One thing that bothers me is the manual repetition of the 3 fields from the user config. I couldn't find any way to avoid that, though. Maybe when I understand derive or macros, there's a way using those. For now it's good enough, though :slight_smile:

One nice thing here (at least, I thought it was nice!) - by using a move to pass the default into the function, and returning the modified value, I can (safely!) pass in the default without copying, and get back the updated value, also with no copy. That feels a lot nicer to me than passing a mutable reference and modifying it in-place.