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'm using the dotenv
package, BTW.
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");
}
But I failed to use it in main.rs
.
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?
1 Like
I tried mod config
in main.rs
.
I also think I shouldn't use pub mod config
in the config.rs
file, but without it, I have this error:
Aha, that's progress.
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
).
1 Like
I tried with static as well, let. It didn't work.
I tried to use the once_cell
library, but it didn't work as well. At least it compiled, but it wasn't getting the string, but an object instead.
use once_cell::sync::Lazy;
pub static API_URL: Lazy<String> = Lazy::new(|| std::env::var("API_URL").expect("API_URL must be set"));
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
).
1 Like
Hum. I didn't get the use of it. Assuming the part in the config.rs
file is right, how do I use it in this context:
use dotenv::dotenv;
mod config;
fn main() {
dotenv().ok();
let api_url_config = &config::API_URL;
println!("API_URL_BASE: {:?}", api_url_config);
}
The result of running it is:
API_URL_BASE: Lazy { cell: OnceCell(Uninit), init: ".." }
Trying without the debug ({:?}
) didn't compile.
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.
With the last bits of help from @Michael-F-Bryan , it now works. Thanks @Cerber-Ursi @Michael-F-Bryan .
For instance, what do I have now:
// config.rs
use once_cell::sync::Lazy;
pub static API_URL: Lazy<String> = Lazy::new(|| std::env::var("API_URL").expect("API_URL must be set"));
use dotenv::dotenv;
mod config;
fn main() {
dotenv().ok();
let api_url_config = &*config::API_URL;
println!("API_URL_BASE: {}", api_url_config);
println!("API_URL_BASE: {}", config::API_URL.as_str());
}
These work as intended.
1 Like
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.
For example,
use clap::Parser;
#[derive(Debug, Parser)]
struct Args {
#[clap(long, env)]
api_url: String,
#[clap(long, env)]
db_uri: String,
#[clap(long, env, default_value = "localhost")]
host: String,
#[clap(long, env)]
username: String,
#[clap(long, env)]
password: String,
}
fn main() {
dotenv::dotenv().ok();
let args = Args::parse();
...
}
(playground)
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
.
2 Likes