diff --git a/apps/ml-service/src/config.rs b/apps/ml-service/src/config.rs index 488dc05..7d693bd 100644 --- a/apps/ml-service/src/config.rs +++ b/apps/ml-service/src/config.rs @@ -1,9 +1,12 @@ +use anyhow::{Context, Result}; +use std::env; use std::path::{Path, PathBuf}; pub const LABELS: [&str; 4] = ["Bercak Daun", "Daun Sehat", "Karat Daun", "Hawar Daun"]; pub const SERVICE_NAME: &str = "zeavis-ml-service"; pub const SERVICE_VERSION: &str = env!("CARGO_PKG_VERSION"); pub const DEFAULT_INPUT_SIZE: u32 = 224; +pub const DEFAULT_MODEL_PATH: &str = "../../Machine_Learning/model/model.onnx"; #[derive(Clone, Debug, PartialEq, Eq)] pub struct Config { @@ -13,6 +16,27 @@ pub struct Config { pub input_size: u32, } +impl Config { + pub fn from_env() -> Result { + let base_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + Self::from_env_with_base_dir(&base_dir) + } + + pub fn from_env_with_base_dir(base_dir: &Path) -> Result { + let host = env::var("ML_SERVICE_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); + let port = parse_env_u16("ML_SERVICE_PORT", 8000)?; + let input_size = parse_env_u32("MODEL_INPUT_SIZE", DEFAULT_INPUT_SIZE)?; + let model_path = env::var("MODEL_PATH").unwrap_or_else(|_| DEFAULT_MODEL_PATH.to_string()); + + Ok(Self { + host, + port, + model_path: resolve_model_path(base_dir, &model_path), + input_size, + }) + } +} + pub fn resolve_model_path(base_dir: &Path, model_path: &str) -> PathBuf { let path = PathBuf::from(model_path); if path.is_absolute() { @@ -22,6 +46,24 @@ pub fn resolve_model_path(base_dir: &Path, model_path: &str) -> PathBuf { } } +fn parse_env_u16(name: &str, default: u16) -> Result { + match env::var(name) { + Ok(value) => value + .parse::() + .with_context(|| format!("{name} must be a valid u16")), + Err(_) => Ok(default), + } +} + +fn parse_env_u32(name: &str, default: u32) -> Result { + match env::var(name) { + Ok(value) => value + .parse::() + .with_context(|| format!("{name} must be a valid u32")), + Err(_) => Ok(default), + } +} + #[cfg(test)] mod tests { use super::*; @@ -44,4 +86,53 @@ mod tests { let resolved = resolve_model_path(base, "/models/model.onnx"); assert_eq!(resolved, PathBuf::from("/models/model.onnx")); } + + #[test] + fn config_uses_default_values_when_env_is_absent() { + temp_env::with_vars_unset( + ["ML_SERVICE_HOST", "ML_SERVICE_PORT", "MODEL_PATH", "MODEL_INPUT_SIZE"], + || { + let config = Config::from_env_with_base_dir(Path::new("/repo/apps/ml-service")).unwrap(); + + assert_eq!(config.host, "0.0.0.0"); + assert_eq!(config.port, 8000); + assert_eq!(config.input_size, 224); + assert_eq!( + config.model_path, + PathBuf::from("/repo/apps/ml-service/../../Machine_Learning/model/model.onnx") + ); + }, + ); + } + + #[test] + fn config_reads_environment_overrides() { + temp_env::with_vars( + [ + ("ML_SERVICE_HOST", Some("127.0.0.1")), + ("ML_SERVICE_PORT", Some("9000")), + ("MODEL_PATH", Some("/tmp/model.onnx")), + ("MODEL_INPUT_SIZE", Some("128")), + ], + || { + let config = Config::from_env_with_base_dir(Path::new("/repo/apps/ml-service")).unwrap(); + + assert_eq!(config.host, "127.0.0.1"); + assert_eq!(config.port, 9000); + assert_eq!(config.input_size, 128); + assert_eq!(config.model_path, PathBuf::from("/tmp/model.onnx")); + }, + ); + } + + #[test] + fn invalid_port_returns_error() { + temp_env::with_vars( + [("ML_SERVICE_PORT", Some("not-a-port"))], + || { + let error = Config::from_env_with_base_dir(Path::new("/repo/apps/ml-service")).unwrap_err(); + assert!(error.to_string().contains("ML_SERVICE_PORT")); + }, + ); + } }