Newbie question: how do you create a module with some "const definitions" and import them from other modules?
In Python, for example, I would do:
# config.py
# Some code to initialize and load env files here
HARDCODED_CONFIGURATION = 10
DB_URI = env.get("DB_URI")
# Maybe validating some values here
assert DB_URI, "Please see the DB_URI env variable"
So, in other modules, I can use like:
import config
db = create_db(config.DB_URI)
How do you do that in rust? I created a config.rs file in the top-level src directory, but trying to export it to have the same effect on the usage seems impossible because of so many compiling errors I can't understand.
I would like to have something like this in main.rs:
use dotenv::dotenv;
fn main() {
dotenv().ok();
println!("THE SETTING FROM ENV: {}", config::SETTING_FROM_ENV);
}
My last attempt was this:
// config.rs
pub mod config {
pub const HOST: String = std::env::var("HOST").expect("HOST must be set");
pub const API_URL: String = std::env::var("API_URL").expect("API_URL must be set");
pub const USERNAME: String = std::env::var("USERNAME").expect("USERNAME must be set");
pub const PASSWORD: String = std::env::var("PASSWORD").expect("PASSWORD must be set");
}
You probably don't want to add mod foo inside foo.rs, since you'll have to refer to it as foo::foo::item. Anyway, have you added mod config; to main.rs, so that config.rs is seen by the compiler?
const in Rust have some very specific meaning - it essentially means "compiler, please calculate this value and substitute it everywhere I use this name". So it must be computable at compile-time, not depending on any runtime conditions - and std::env::var obviously does depend on runtime.
If you want these values to be calculated as early as possible and then being available for reading for the whole program, you probably want something like OnceLock (or the unstable LazyLock), which can be put in static (not const).
static doesn't help by itself, yes, since its content must also be set at compile-time.
once_cell::sync::Lazy implements Deref, so you can use &Lazy<T> wherever you need &T (and in case of String, due to its own Deref implementation - wherever you need &str).
The Lazy type works by dereferencing to a String. When you pass &config::API_URL to a function it'll be dereferenced automatically, but there isn't any implicit dereferencing when you pass a value to a macro like println!().
To print the string here, you'll need dereference manually.
let api_url_config: &String = &*config::API_URL;
println!("API_URL_BASE: {:?}", api_url_config);
Alternatively, you could call String's as_str() method because calling methods is one of those places that will implicitly dereference.
let api_url_config: &str = config::API_URL.as_str();
println!("API_URL_BASE: {:?}", api_url_config);
I've added type annotations to both examples just so you know what type of value you get. They're completely optional.
For what it's worth, I've found the best approach to this sort of thing isn't to use a module of constants, but instead to use a command-line parser which supports loading from environment variables.
The benefit to this approach is that you don't get unexpected panics from deep in your code because the config::API_URL variable just happens to have not been accessed until that point.
It also lets you validate your configuration on startup because Args::from_args() will print an error and exit if it doesn't get all the arguments it needs, plus you have the ability to override things manually (e.g. during testing) by passing --username=admin --password=password.