diff --git a/src/env_file.rs b/src/env_file.rs index b8d44ba60fdd2ca50ffb6ecca43eb04781d0beef..01b81e257ef2f08559707574a29531ad1eb2aa44 100644 --- a/src/env_file.rs +++ b/src/env_file.rs @@ -105,9 +105,26 @@ impl EnvironmentFile { } } +/// Varies the behaviour of applying an environment file. +/// +/// ## Defaults +/// +/// When not provided, `envish` will use the default values for this type: +/// - `prefix` is set to `None` (Keys in env files will be used as-written) +/// - `overwrite` is set to `false` (If a variable is already set, the value in the file will not be used) #[derive(Clone, Debug, Default)] pub struct ApplyOptions { + /// If present, this exact string will be prepended to the names of all environment variables + /// when applying. + /// + /// ### Example + /// + /// With the prefix "APP_", a variable with the key "DATABASE_URL" will be added to the environment as + /// "APP_DATABASE_URL". pub prefix: Option<String>, + /// By default, `envish` will not update an existing environment variable, ignoring the value + /// in the environment file. When the `overwrite` option is set to `true`, `envish` will apply + /// any values from the file regardless of whether they are already set. pub overwrite: bool, } diff --git a/src/filesystem.rs b/src/filesystem.rs index 55aded5090b42f02fae579cca51db2c01f6fdcc4..d44692045481e48676e7edf0ba5fc5a5e7792dcf 100644 --- a/src/filesystem.rs +++ b/src/filesystem.rs @@ -8,22 +8,36 @@ use std::path::Path; #[derive(Debug, thiserror::Error)] #[allow(clippy::enum_variant_names)] pub enum EnvFsError { + /// Error opening the file, or reading it into a buffer. #[error(transparent)] IoError(#[from] std::io::Error), + /// Error parsing the file from a string into an EnvironmentFile. Denotes syntax errors in the + /// file. #[error(transparent)] ParseError(#[from] crate::EnvironmentFileError), + /// An error thrown specifically when the given environment variable exists, but is cannot be + /// represented in Rust (e.g. not Unicode). This error does **not** occur when the environment + /// variable exists as valid Unicode. #[error("The target environment variable exists, but is not Unicode")] EnvironmentError, } +/// Reads the file `.env` into memory, but does not apply it to the environment. pub fn env_file() -> Result<EnvironmentFile, EnvFsError> { env_file_from_path(".env") } +/// Reads the file `.env.<environment>` into memory, but does not apply it to the environment. +/// +/// ### Example +/// +/// Calling `env_file_suffix("production")` will attempt to read the file `.env.production` in the +/// directory the server was launched from. pub fn env_file_suffix(environment: impl Display) -> Result<EnvironmentFile, EnvFsError> { env_file_from_path(format!(".env.{}", environment)) } +/// Reads the file at the specified path into memory, but does not apply it to the environment. pub fn env_file_from_path(path: impl AsRef<Path>) -> Result<EnvironmentFile, EnvFsError> { let mut file = File::open(path)?; let mut buffer = String::new(); @@ -31,20 +45,97 @@ pub fn env_file_from_path(path: impl AsRef<Path>) -> Result<EnvironmentFile, Env Ok(buffer.parse()?) } +/// Look up a `.env` file in the working directory and apply its contents to the current environment. +/// +/// ### Errors +/// +/// This method returns an error under the following circumstances: +/// +/// - The target file does not exist +/// - There was an error when reading the target file +/// - The target was not a correctly formatted `.env` file +/// +/// ### Example +/// +/// This example will attempt to read the file `.env` in the directory the server was launched from. +/// ```rust +/// use envish::dotenv; +/// let _ = dotenv(); +/// ``` pub fn dotenv() -> Result<(), EnvFsError> { env_file()?.apply(Default::default()); Ok(()) } + +/// Look up a `.env` file in the working directory and apply its contents to the current environment, +/// with the provided options, allowing for prefixing and overwriting existing values. +/// +/// ### Errors +/// +/// This method returns an error under the following circumstances: +/// +/// - The target file does not exist +/// - There was an error when reading the target file +/// - The target was not a correctly formatted `.env` file +/// +/// ### Example +/// +/// This example will attempt to read the file `.env`, and will prepend the given prefix to all the +/// contained variables. For example, a variable in the file named "DATABASE_URL" will be added to +/// the environment as "APP_DATABASE_URL". +/// +/// ```rust +/// use envish::{dotenv_opts, ApplyOptions}; +/// let _ = dotenv_opts(ApplyOptions::with_prefix("APP_")); +/// ``` pub fn dotenv_opts(options: ApplyOptions) -> Result<(), EnvFsError> { env_file()?.apply(options); Ok(()) } +/// Look up a `.env` file with the provided suffix in the working directory and apply its contents +/// to the current environment. +/// +/// ### Errors +/// +/// This method returns an error under the following circumstances: +/// +/// - The target file does not exist +/// - There was an error when reading the target file +/// - The target was not a correctly formatted `.env` file +/// +/// ### Example +/// +/// This example will attempt to read the file `.env.development` +/// +/// ```rust +/// use envish::dotenv_suffix; +/// let _ = dotenv_suffix("development"); +/// ``` pub fn dotenv_suffix(environment: impl Display) -> Result<(), EnvFsError> { env_file_suffix(environment)?.apply(Default::default()); Ok(()) } +/// Look up an environment file at the given path and apply its contents to the current environment. +/// +/// ### Errors +/// +/// This method returns an error under the following circumstances: +/// +/// - The target file does not exist +/// - There was an error when reading the target file +/// - The target was not a correctly formatted `.env` file +/// +/// ### Example +/// +/// This example will attempt to read the file `my_dotenv_file` at the specified path. The file must +/// be correctly formatted as any other .env file, but does not need to have a specific name. +/// +/// ```rust +/// use envish::dotenv_from; +/// let _ = dotenv_from("/some/other/path/to/my_dotenv_file"); +/// ``` pub fn dotenv_from(path: impl AsRef<Path>) -> Result<(), EnvFsError> { env_file_from_path(path)?.apply(Default::default()); Ok(()) diff --git a/src/lib.rs b/src/lib.rs index fa36368a1f0af35a0324ac9d691c856bdcbeaa78..9493597383911c59a092a8e924b5c7a06945cb93 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,17 @@ #![allow(unused_labels)] +//! This crate provides a simple interface for reading, parsing, and using `.env` style environment +//! files. The goal is to provide comprehensive compatibility with the variety of .env formats +//! and options found in popular tools across languages, both for reading and writing. +//! +//! ## Embedding +//! +//! This library is designed to be easy to use with both Rust and non-Rust projects. The core +//! functionality covers parsing and manipulating environment files. By default, `envish` also +//! includes filesystem support for reading and writing `.env` files, but this can be disabled by +//! turning off default features. +//! +//! It is recommended to disable filesystem support when embedding `envish`, and instead +//! using the platform-native filesystem operations in tandem with this crate's parsing module. mod env_file; #[cfg(feature = "fs")] diff --git a/src/parser.rs b/src/parser.rs index 2d41e13ea98e5d13b6a28c6197faa41a8462e2c6..64618ecf0581199c2841bcd5c6157a959ddc800b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -13,10 +13,17 @@ use nom::{ }; use std::fmt::Display; +/// Holds one part of the value of an environment variable. Static parts will be used verbatim, +/// while variables will be looked up at render time when an environment file is applied. +/// Comments are always ignored by the renderer, but are preserved for serialization. #[derive(Clone, Debug, PartialEq)] pub enum ValuePart { + /// A static value that will be rendered verbatim. Static(String), + /// The exact name of an environment variable that will be looked up at render time. + /// Case-sensitivity is dependent on the operating system. Variable(String), + /// Arbitrary text that will not be added to the environment. Comment(String), } @@ -31,12 +38,15 @@ impl Display for ValuePart { } impl ValuePart { + /// Create a static value that will be used verbatim. pub fn new(value: impl ToString) -> Self { Self::Static(value.to_string()) } + /// Create a reference to another environment variable that will be interpolated into the value. pub fn variable(value: impl ToString) -> Self { Self::Variable(value.to_string()) } + /// Create a comment that will be ignored by the value renderer. pub fn comment(value: impl ToString) -> Self { Self::Comment(value.to_string()) } @@ -59,10 +69,18 @@ impl ValuePart { } } +/// A whole line of an environment file. May contain comments, static strings, or interpolation +/// variables. #[derive(Clone, Debug, PartialEq)] pub enum FileLine { + /// A blank lin in a file, containing only whitespace. Ignored for all purposes except 1-1 + /// serialisation. Empty, + /// A full line comment, starting with a '#' and containing arbitrary text for the rest of the + /// line. Comment(String), + /// A key-value pair, where the key is a string and the value is a combination of static parts, + /// dynamic parts, and comments. KeyValue { key: String, value: Vec<ValuePart> }, } @@ -79,14 +97,22 @@ impl Display for FileLine { } impl FileLine { + /// Create a new empty line. pub fn empty() -> Self { Self::Empty } + /// Create a new full line comment. pub fn comment(comment: impl ToString) -> Self { Self::Comment(comment.to_string()) } + /// Create a new key-value line, where the value may be a combination of static strings, + /// variable references, and comments. + /// + /// Lines constructed this way may contain comments in the middle of strings and references + /// without issue, but will not be able to correctly serialise if writing the non-compliant + /// line to a file. pub fn key_value(key: impl ToString, value: impl ToOwned<Owned = Vec<ValuePart>>) -> Self { Self::KeyValue { key: key.to_string(), @@ -107,8 +133,8 @@ impl FileLine { Self::Comment(span.fragment().to_string()) } - /// Whether this line is entirely self contained. If the line is a kv pair that requires - /// interpolation, it is not considered complete. + /// Returns true if this line can be evaluated by itself. A line cannot be evaluated by itself + /// if it references other environment variables. pub fn is_complete(&self) -> bool { match self { Self::KeyValue { value, .. } => value.iter().all(|part| !matches!(part, &ValuePart::Variable(..))), @@ -116,6 +142,8 @@ impl FileLine { } } + /// Returns a copy of this line without comments. If the entire line is a comment, it is + /// converted to `Line::Empty`. Otherwise, comment parts of key-value lines are removed. pub fn strip_comments(&self) -> Self { match self { Self::Empty => Self::Empty, @@ -131,7 +159,8 @@ impl FileLine { } } - /// Convert the line into a complete value string + /// Convert the line into a complete value string, interpolating dynamic variables and stripping + /// comments. Empty lines or whole line comments will return an empty string. pub fn assemble_value(&self) -> String { match self { Self::Empty => String::new(),