Replies: 8 comments 19 replies
-
In all of the solutions the naming could be changed, for example |
Beta Was this translation helpful? Give feedback.
-
Personally, I prefer the 4th solution - espeically with a module. A further example: /// If testing or not compiled with `--release`: no `.env` file causes panic.
fn init() {
if cfg!(any(test, debug_assertions)) {
dotenvy::dotenv().expect("can't load .env file");
} else {
dotenvy::silent::dotenv();
}
}
fn main() {
init();
} |
Beta Was this translation helpful? Give feedback.
-
Thank you for the writeup. I've had similar thoughts on confusion caused by I think what you brought up is caused by shortcomings in the documentation. I want to clarify that the use of
Another source of confusion is in the naming of the Combine these two sources together and we have a perfect storm of confusion. Like you said, what does it mean to ok a dotenv? This is how I would replace dotenvy::load().expect("Failed to load .env file"); It should be clear that // A distinction between the behaviour in dev and prod modes is an interesting idea. Although the general sense is that dotenv is for dev environments, there are many who do use .env files in production without issue. I would be hesitant to associate debug builds with dev and release builds with prod. Dotenvy is a small library with the capability to load environment variables from a text file. My feeling is that ignoring errors, and error handling in general, should be left to the user. I think this can be assisted by including more examples. This one incorporates the suggestions in your conclusion: use std::str::FromStr;
enum AppEnvironment {
Development,
Production,
}
impl FromStr for AppEnvironment {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"development" => Ok(AppEnvironment::Development),
"production" => Ok(AppEnvironment::Production),
_ => Err(format!("Unknown app environment: {}", s)),
}
}
}
fn main() {
// Loads the dotenv file
let dotenv_path = dotenvy::dotenv().ok();
// Loads the environment variable. It doesn't matter how how it was set.
let app_env = dotenvy::var("APP_ENV")
.expect("APP_ENV not found")
.parse()
.expect("Failed to parse APP_ENV");
match (app_env, dotenv_path) {
(AppEnvironment::Development, Some(path)) => {
println!("Loaded dotenv file: {}", path.as_path().display())
}
(AppEnvironment::Development, None) => panic!("No .env file found"),
(AppEnvironment::Production, Some(_)) => panic!(".env file found in production"),
(AppEnvironment::Production, None) => (),
}
// proceed...
} Thanks again for starting this discussion. |
Beta Was this translation helpful? Give feedback.
-
As I (kind of) mention in #44 , I think a So what I'm suggesting there is to change the return type of dotenv()?; // or .unvrap() if not returning a result while users who require a dotenv()?.ok_or(MyError::MissingDotenv)?; We could of course also add an extension trait to dotenv().required()?; |
Beta Was this translation helpful? Give feedback.
-
Hi. I've started using this library for a few personal projects. I too agree that we should make .env being optional easy. To that end I forked the project and added a new loader: See here. If the aim of this project is to be a drop-in replacement for the original That said, a root and branch redesign of the API would be welcome. LMK if you'd like me to prepare a pull request for the changes above. I am happy to make adjustments, e.g. to naming. |
Beta Was this translation helpful? Give feedback.
-
Below, I refer to let _: PathBuf = load()?; // errors if file is missing or invalid
let _: Option<PathBuf> = load_opt()?; // ignores non-existent files, errors if file is invalid How other implementations do itgodotenv errors when the file is not found. This is like // https://github.com/joho/godotenv/blob/3fc4292b58a67b78e1dbb6e47b4879a6cc602ec4/godotenv.go#L40-L61
func Load(filenames ...string) (err error) {
filenames = filenamesOrDefault(filenames)
for _, filename := range filenames {
err = loadFile(filename, false)
if err != nil {
return // return early on a spazout
}
}
return
} Ruby has an ignore flag, which is defaults to true. The default is like # https://github.com/bkeepers/dotenv/blob/c9ecfa4e5598593dbadcf6c610cf5d8a9ed0bec1/lib/dotenv.rb#L17-L24
def load(*filenames, overwrite: false, ignore: true)
parse(*filenames, overwrite: overwrite, ignore: ignore) do |env|
instrument(:load, env: env) do |payload|
update(env, overwrite: overwrite)
end
end
end My thoughts
I am in favour, as long as we caution against calling dotenv loading functions in prod environments. This can be covered with documentation. |
Beta Was this translation helpful? Give feedback.
-
:-)
It's not a panic, just an error, and is the behaviour today. Handling this error when you want to allow the env file to be missing is tricky as you need to interpret the content of the
Yes, this is part of the proposal above. Creating a builder would enable a root-and-branch update to the API to enable more flexibility. Check out a proposal here: #39 (reply in thread)
Yes. We could go one further and attach file options to a file being added to the builder. e.g. dotenvy::builder()
.add(filename(".env.staging").allow_missing())
.add(default())
.first()
.load()?; This would allow a staging env to be loaded in preference to a default
The idea is to ensure that an error is not returned for a missing file, instead we use Option to signal if a file was found and loaded. We can still get errors, e.g. if the file contents are malformed. (The library does not panic, instead it returns errors which the caller may handle by panicing if wanted.) |
Beta Was this translation helpful? Give feedback.
-
There is a new API. Please check out the v0.16 branch. let loader = EnvLoader::from_path(path).sequence(EnvSequence::FilesThenEnv);
let env_map: HashMap<String, String> = loader.load()?; // does not modify environment
let env_map: HashMap<String, String> = unsafe { loader.load_and_modify() }?; // modifies environment using `std::env::set_var` There is also an attribute macro that is thread-safe. // this loads from the file into the environment before the async runtime is started
#[dotenvy::load]
#[tokio::main]
async fn main() {
println!("inside async runtime");
} There are examples for optional loading, multiple files, dev/prod, and many more. The marking of |
Beta Was this translation helpful? Give feedback.
-
This issue(#37) and its PR(#38), highlight a pretty big problem with the API and its documentation.
The Problem
Most of the public functions return a
Result
with a potential error. The primary reason for an error is if a.env
file is not found. Some users would like to know if their file isn't loaded, whereas some would prefer the error to be silent.Dotenv files were conceived as a development tool to simulate a production environment. Once an app is a deployed, it would get its environment variables from its environment, not a
.env
file. Therefore, it is appropriate to ignore any errors.A user is expected to ignore the error themselves with
.ok()
. This transforms theResult
into anOption
which doesn't produce any compiler warnings or errors. However, silencing errors is not the intended use-case for.ok()
, which leads to potential confusion over its use for this library. Furthermore, its naming doesn't help - what does it mean to "ok" a "dotenv".Solutions
Unwrap or
?
This has already been done in a lot of the rustdoc. The error could be unwrapped, which causes a panic when the
.env
file is not found. Even when using the more idiomatic?
operator, the issue remains - what if the user considers their dotenv file optional?New Trait
We could add a trait which takes a
Result
as input and simply ignores it. The resulting API usage would look something like:This improves the naming issue because the reason for the
.silent()
call is explicit. Also, those who want to handle the error, need not use the trait. However, it could be considered rather clunky - with an additional import and a bloated trait onResult
.Change
Result
toOption
The
.ok()
method does this already. We could cut out this step and just return an option instead. That way the user could still handle the event of no.env
file, and use the return value (the file path) if they wish. Example:Obviously this is a BREAKING CHANGE and is potentially confusing. A user who MUST have a dotenv file would probably consider "file not found" to be an error. Getting
None
is less informative. Also this blocks other errors in parsing the file bubbling up to the user.New Functions
By introducing a new "silent" version of the
dotenv
function, we could avoid breaking changes and additional imports. The silent version could return anOption
, like the solution above, or it could return nothing - as the user could still use the originaldotenv
for error handing. API example:Similar to the current solution (
.ok()
), a user who wants to handle the error in development, but not in production, would still have to change their code or have two versions based ondebug_assertions
or Cargo features.This also fractures the code base. Other public functions would need an associated
silent
version too. Perhaps moving them into a separate module would improve the "feel" of the API. Moreover, the module could be hidden behind a Cargo feature flag. Module example:Documentation
We could just improve the documentation to show how to handle AND how to ignore the errors.
Conclusion
The error handling story is tricky to deal with as there are several use cases for the library:
It would be good if we could figure out how to document the library without alienating any of these users. The current
.unwrap()
solution seems like it caters to only one kind of user.Beta Was this translation helpful? Give feedback.
All reactions