diff --git a/Cargo.lock b/Cargo.lock index a196f6b..c583555 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,5 +3,5 @@ version = 4 [[package]] -name = "no-man-sky" +name = "merlin_env_helper" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index d42df58..8bc07f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "no-man-sky" +name = "merlin_env_helper" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] diff --git a/README.md b/README.md index 2214670..993d4aa 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -#NO Man Sky Wiki +# NO Man Sky Wiki diff --git a/resources/test/test_secret.txt b/resources/test/test_secret.txt new file mode 100644 index 0000000..848c85c --- /dev/null +++ b/resources/test/test_secret.txt @@ -0,0 +1 @@ +my_secret \ No newline at end of file diff --git a/src/config/env.rs b/src/config/env.rs new file mode 100644 index 0000000..958103f --- /dev/null +++ b/src/config/env.rs @@ -0,0 +1,253 @@ +//! Module for reading configuration values from environment variables or secure files. +//! +//! This module provides utilities to retrieve configuration values from environment variables, +//! with support for fallback values and secure file-based secrets. It defines error types for +//! configuration loading and exposes a unified `Result` type for error handling. +//! +//! # Types +//! +//! - [`ConfigError`]: Represents errors that can occur when reading configuration values. +//! - [`ConfigErrorKind`]: Enum describing the kind of configuration error (I/O, file not found, or not found). +//! - [`ConfigErrorMessage`]: Simple error message wrapper. +//! +//! # Functions +//! +//! - [`get_env_value`]: Retrieves a configuration value from the environment or a secure file, with optional fallback. +//! +//! # Usage +//! +//! Use [`get_env_value`] with an [`EnvKey`] to retrieve configuration values, handling errors as needed. +//! +//! # Examples +//! +//! ```rust +//! use no_man_sky::config::EnvKey; +//! use no_man_sky::config::get_env_value; +//! let env_key = EnvKey::key("MY_CONFIG_KEY"); +//! match get_env_value(&env_key) { +//! Ok(value) => println!("Config value: {}", value), +//! Err(e) => eprintln!("Error: {}", e), +//! } +//! ``` +use core::fmt; +use std::{error::Error, fmt::{Display, Formatter}, io::{self, Read}, path::Path}; + +use crate::config::types::EnvKey; + +pub type Result = std::result::Result; + + +#[derive(Debug)] +#[non_exhaustive] +pub struct ConfigError { + pub key: String, + pub kind: ConfigErrorKind, +} + +impl Display for ConfigError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "error reading configuration key `{}`", self.key) + } +} + +impl Error for ConfigError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match &self.kind { + ConfigErrorKind::IO(e) => Some(e), + ConfigErrorKind::FileNotFound(e) => Some(e), + ConfigErrorKind::NotFound => None, + } + } +} + + +/// The kind of error that can occur when reading a configuration key. +#[derive(Debug)] +pub enum ConfigErrorKind { + IO(io::Error), + FileNotFound(ConfigErrorMessage), + NotFound, +} + +/// A simple message that can be used to describe an error. +#[derive(Debug)] +#[non_exhaustive] +pub struct ConfigErrorMessage { + pub msg: String, +} + +impl Display for ConfigErrorMessage { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.msg) + } +} + +impl Error for ConfigErrorMessage { +} + + + +pub fn get_env_value(env_key : &EnvKey) -> Result { + if let Some(sec_key) = &env_key.sec_key { + let key = std::env::var(&sec_key); + + if let Ok(path_file) = key { + return read_secure_value(&env_key.key, &path_file); + } + } + + read_key_value(&env_key.key, &env_key.fallback) +} + +fn read_key_value(key: &str, fallback: &Option) -> Result { + let env_value = std::env::var(key); + + println!("env_value: {:?}", env_value); + if let Ok(value) = env_value { + return Ok(value); + } + + if let Some(fallback) = fallback { + return Ok(fallback.to_string()); + } + + Err( + ConfigError { + key: key.to_string(), + kind: ConfigErrorKind::NotFound, + } + ) +} + + +fn read_secure_value(key: &str, path_file: &str) -> Result { + + let path = Path::new(path_file); + + let exists = path.try_exists().map_err(|e| -> ConfigError { + println!("path.try_exists(): {:?}", e); + ConfigError { + key: format!("{}: {} File not found.", key.to_string(), path_file), + kind: ConfigErrorKind::IO(e), + } + })?; + + if !exists { + return Err( + ConfigError { + key: format!("{}: {} File not found.", key.to_string(), path_file), + kind: ConfigErrorKind::FileNotFound(ConfigErrorMessage { + msg: format!("File not found: {}", path_file), + }), + } + ); + } + + let file = std::fs::File::open(path).map_err(|e| -> ConfigError { + println!("std::fs::File::open(path_file): {:?}", e); + ConfigError { + key: key.to_string(), + kind: ConfigErrorKind::IO(e), + } + })?; + + let mut reader = std::io::BufReader::new(file); + let mut content = String::new(); + + reader.read_to_string(&mut content).map_err(|e| -> ConfigError { + ConfigError { + key: key.to_string(), + kind: ConfigErrorKind::IO(e), + } + })?; + + Ok(content) +} + + +#[cfg(test)] +mod tests { + use super::*; + use std::{panic, path::PathBuf}; + + fn clean_up(reset_value: &Option) { + if let Some(value) = reset_value { + unsafe {std::env::set_var("TEST_KEY", value)}; + } else { + unsafe {std::env::remove_var("TEST_KEY")}; + } + } + + fn run_test(env_key : &str, test: T, clean_up: U) -> () + where T: FnOnce() -> () + std::panic::UnwindSafe + , U: FnOnce(&Option) -> () + { + let old_value = std::env::var(env_key); + let mut reset_value= None; + + if let Ok(value) = old_value { + reset_value = Some(value.clone()); + } + + let result = panic::catch_unwind( || { + test(); + }); + + clean_up(&reset_value); + + assert!(!result.is_err(), "Test failed"); + } + + #[test] + fn test_get_env_value() { + + run_test("TEST_KEY", || { + let key = "TEST_KEY"; + let value = "test_value"; + unsafe {std::env::set_var(key, value)}; + let env_key = EnvKey::key(key); + let result = get_env_value(&env_key); + assert_eq!(result.is_ok(), true); + assert_eq!(result.unwrap(), value); + + }, + clean_up + ); + + } + + #[test] + fn test_get_env_value_with_fallback() { + run_test("TEST_KEY", || { + let key = "TEST_KEY"; + let fallback_value = "fallback_value"; + + let env_key = EnvKey::key_with_fallback(key, fallback_value); + let result = get_env_value(&env_key); + assert_eq!(result.is_ok(), true); + assert_eq!(result.unwrap(), fallback_value); + }, + clean_up); + } + + #[test] + fn test_get_env_value_with_secure_key() { + run_test("TEST_KEY", || { + let key = "TEST_KEY"; + let value = "test_value"; + let secure_key = "TEST_KEY_FILE"; + let secure_value = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/test_secret.txt"); + + unsafe {std::env::set_var(key, value)}; + unsafe {std::env::set_var(secure_key, &secure_value)}; + + assert_eq!(secure_value.try_exists().unwrap(), true); + + let env_key = EnvKey::secure_key(key, secure_key); + let result = get_env_value(&env_key); + assert_eq!(result.is_ok(), true); + assert_eq!(result.unwrap(), "my_secret"); + }, + clean_up); + } +} \ No newline at end of file diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..78cbe2c --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,5 @@ +pub mod env; +pub mod types; + +pub use types::*; +pub use env::*; diff --git a/src/config/types.rs b/src/config/types.rs new file mode 100644 index 0000000..d4ba541 --- /dev/null +++ b/src/config/types.rs @@ -0,0 +1,40 @@ +#[derive(Debug, PartialEq, Eq)] +pub struct EnvKey { + pub key: String, + pub sec_key: Option, + pub fallback: Option, +} + +impl EnvKey { + pub fn key(key: &str) -> Self { + Self { + key: key.to_string(), + sec_key: None, + fallback: None, + } + } + + pub fn key_with_fallback(key: &str, fallback: &str) -> Self { + Self { + key: key.to_string(), + sec_key: None, + fallback: Some(fallback.to_string()), + } + } + + pub fn secure_key(key: &str, sec_key: &str) -> Self { + Self { + key: key.to_string(), + sec_key: Some(sec_key.to_string()), + fallback: None, + } + } + + pub fn secure_with_fallback(key: &str, sec_key: &str, fallback: &str) -> Self { + Self { + key: key.to_string(), + sec_key: Some(sec_key.to_string()), + fallback: Some(fallback.to_string()), + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a105933 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod config; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index e7a11a9..0000000 --- a/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -}