How to handle file imports in a sane way

I have a project where I store my domain logic in various sub folders. The main.rs holds all the imports and starts a manager which starts the various threads that make up the program.

My main.rs now looks like this... is there a more "sane" way to do this? There are about 10 more models and persistence structs being developed so this list will grow. I have the gut feeling that I am not doing it the right way. (even though it works perfectly)

use std::env;

use dotenv::dotenv;
use tokio::runtime::Runtime;

use crate::domain::managers::manager::Manager;

mod lib {
    pub mod utils {
        pub mod amqp;
        pub mod helpers;
    }
}

pub mod domain {
    pub mod constants;
    pub mod env_vars;

    pub mod consumers {
        pub mod consume_broadcast_queue;
        pub mod consume_private_queue;

        pub mod core {
            pub mod models {
                pub mod contract;
                pub mod order;
                pub mod user;
                pub mod exe_rprt;
            }

            pub mod persistence {
                pub mod persist_trait;
                pub mod persist_client;
                pub mod persist_contract;
                pub mod persist_err_resp;
                pub mod persist_order;
                pub mod persist_user;
                pub mod persist_exe_rprt;
            }
        }
    }

    pub mod managers {
        pub mod manager;
        pub mod monitor;
        pub mod core {
            pub mod structs;
        }
    }

    pub mod publisher {
        pub mod publisher;

        mod core {
            pub mod enums;
            pub mod requests;
            pub mod structs;
        }
    }
}

const LOG_LEVEL: &str = "info";

fn main() {
    set_log_level();

    let manager: Manager = Manager::default();
    let rt = Runtime::new().unwrap();

    rt.block_on(async {
        manager.start().await;
    });
}

fn set_log_level() {
    dotenv().ok();
    if env::var("RUST_LOG").is_err() {
        env::set_var("RUST_LOG", LOG_LEVEL);
    }
    tracing_subscriber::fmt::init();
}


Imports in Manager look like this

use std::sync::{Arc, Mutex};
use std::thread;
use std::thread::JoinHandle;

use crate::domain::consumers::consume_broadcast_queue::ConsumeBroadcastQueue;
use crate::domain::consumers::consume_private_queue::ConsumePrivateQueue;
use crate::domain::managers::core::structs::State;
use crate::domain::managers::monitor::Monitor;
use crate::domain::publisher::publisher::Publisher;
use crate::lib::utils::amqp::create_connection;

#[derive(Default)]
pub struct Manager {
    state: Arc<Mutex<State>>,
}

...

What I would expect is the top of main.rs to look like this:

// uses here

mod lib;
pub mod domain;

// rest of the module

With the following directory structure:

  • main.rs
  • lib/
    • mod.rs
    • utils/
      • mod.rs
      • amqp.rs
      • helpers.rs
  • domain/
    • mod.rs
    • constants.rs
    • env_vars.rs
    • consumers/
      • mod.rs
      • consume_broadcast_queue.rs

etc.

Each module then contains just the mod X; items for its sub-modules. So lib/mod.rs would contain pub mod utils;. Then, domain/mod.rs would contain: pub mod constants; pub mod env_vars; pub mod consumers; etc.

Aside: you can use either src/lib/ + src/lib/mod.rs or src/lib/ + src/lib.rs.

3 Likes

You could do:

mod prelude {
    pub use crate::nested::stuff;
    pub use crate::etc;
}

and then:

use crate::prelude::*;

everywhere else. That's a standard pattern.

"Flattening" the API by pub use in the root of the library (src/lib.rs) is also a good way to simplify things.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.