use std::collections::HashMap as FxHashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use tokio::sync::RwLock as TokioRwLock;
use tokio::task::JoinHandle;
use yaml_rust::{YamlLoader, Yaml};
#[cfg(feature = "config_hot_reload")]
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
#[cfg(feature = "pyo3")]
use crate::hooks::RiHookKind;
#[cfg(feature = "pyo3")]
use crate::core::RiServiceContext;
#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
#[derive(Clone)]
pub struct RiConfig {
values: FxHashMap<String, String>,
}
impl Default for RiConfig {
fn default() -> Self {
Self::new()
}
}
impl RiConfig {
pub fn new() -> Self {
RiConfig { values: FxHashMap::default() }
}
fn sensitive_key_patterns() -> Vec<&'static str> {
vec![
"password",
"secret",
"key",
"token",
"api_key",
"apikey",
"auth",
"credential",
"private",
"pass",
]
}
pub fn is_sensitive_key(key: &str) -> bool {
let key_lower = key.to_lowercase();
for pattern in Self::sensitive_key_patterns() {
if key_lower.contains(pattern) {
return true;
}
}
false
}
pub fn mask_sensitive_value(value: &str) -> String {
if value.len() <= 4 {
return "*".repeat(value.len().max(4));
}
let first_chars = &value[..2];
let last_chars = &value[value.len()-2..];
let middle_len = value.len() - 4;
format!("{}{}{}", first_chars, "*".repeat(middle_len), last_chars)
}
pub fn get_masked(&self, key: &str) -> Option<String> {
self.values.get(key).map(|v| {
if Self::is_sensitive_key(key) {
Self::mask_sensitive_value(v)
} else {
v.clone()
}
})
}
pub fn all_values_masked(&self) -> FxHashMap<String, String> {
self.values.iter()
.map(|(k, v)| {
if Self::is_sensitive_key(k) {
(k.clone(), Self::mask_sensitive_value(v))
} else {
(k.clone(), v.clone())
}
})
.collect()
}
pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.values.insert(key.into(), value.into());
}
pub fn get(&self, key: &str) -> Option<&String> {
self.values.get(key)
}
pub fn get_str(&self, key: &str) -> Option<&str> {
self.values.get(key).map(|s| s.as_str())
}
pub fn get_bool(&self, key: &str) -> Option<bool> {
self.values.get(key).and_then(|s| {
let v = s.trim().to_ascii_lowercase();
match v.as_str() {
"true" | "1" | "yes" | "on" => Some(true),
"false" | "0" | "no" | "off" => Some(false),
_ => None,
}
})
}
pub fn get_i64(&self, key: &str) -> Option<i64> {
self.values.get(key).and_then(|s| s.trim().parse::<i64>().ok())
}
pub fn get_i64_with_bounds(&self, key: &str, min: i64, max: i64) -> Option<i64> {
self.get_i64(key).filter(|&v| v >= min && v <= max)
}
pub fn get_u64(&self, key: &str) -> Option<u64> {
self.values.get(key).and_then(|s| s.trim().parse::<u64>().ok())
}
pub fn get_u64_with_bounds(&self, key: &str, min: u64, max: u64) -> Option<u64> {
self.get_u64(key).filter(|&v| v >= min && v <= max)
}
pub fn get_positive_u64(&self, key: &str) -> Option<u64> {
self.get_u64(key).filter(|&v| v > 0)
}
pub fn get_port(&self, key: &str) -> Option<u16> {
self.get_u64_with_bounds(key, 1, 65535).map(|v| v as u16)
}
pub fn get_f32(&self, key: &str) -> Option<f32> {
self.values.get(key).and_then(|s| s.trim().parse::<f32>().ok())
}
pub fn get_f32_with_bounds(&self, key: &str, min: f32, max: f32) -> Option<f32> {
self.get_f32(key).filter(|&v| v >= min && v <= max)
}
pub fn get_percentage(&self, key: &str) -> Option<f32> {
self.get_f32_with_bounds(key, 0.0, 100.0)
}
pub fn get_rate(&self, key: &str) -> Option<f32> {
self.get_f32_with_bounds(key, 0.0, 1.0)
}
pub fn merge(&mut self, other: &RiConfig) {
for (k, v) in &other.values {
self.values.insert(k.clone(), v.clone());
}
}
pub fn clear(&mut self) {
self.values.clear();
}
pub fn get_or_default<T>(&self, key: &str, default: T) -> T
where
T: std::str::FromStr,
T::Err: std::fmt::Debug,
{
self.values.get(key).and_then(|s| s.trim().parse::<T>().ok()).unwrap_or(default)
}
pub fn get_f64(&self, key: &str) -> Option<f64> {
self.values.get(key).and_then(|s| s.trim().parse::<f64>().ok())
}
pub fn get_usize(&self, key: &str) -> Option<usize> {
self.values.get(key).and_then(|s| s.trim().parse::<usize>().ok())
}
pub fn get_i32(&self, key: &str) -> Option<i32> {
self.values.get(key).and_then(|s| s.trim().parse::<i32>().ok())
}
pub fn get_u32(&self, key: &str) -> Option<u32> {
self.values.get(key).and_then(|s| s.trim().parse::<u32>().ok())
}
pub fn get_u32_with_bounds(&self, key: &str, min: u32, max: u32) -> Option<u32> {
self.get_u32(key).filter(|&v| v >= min && v <= max)
}
pub fn get_timeout_secs(&self, key: &str) -> Option<u32> {
self.get_u32_with_bounds(key, 1, 86400)
}
pub fn get_retry_count(&self, key: &str) -> Option<u32> {
self.get_u32_with_bounds(key, 0, 100)
}
pub fn keys(&self) -> Vec<&str> {
self.values.keys().map(|s| s.as_str()).collect()
}
pub fn all_values(&self) -> Vec<&str> {
self.values.values().map(|s| s.as_str()).collect()
}
pub fn has_key(&self, key: &str) -> bool {
self.values.contains_key(key)
}
pub fn count(&self) -> usize {
self.values.len()
}
#[cfg(feature = "pyo3")]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
}
#[cfg(feature = "pyo3")]
#[pyo3::prelude::pymethods]
impl RiConfig {
#[new]
fn py_new() -> Self {
Self::new()
}
#[pyo3(name = "set")]
fn set_impl(&mut self, key: String, value: String) {
self.set(key, value);
}
#[pyo3(name = "get")]
fn get_impl(&self, key: String) -> Option<String> {
self.get(&key).cloned()
}
#[pyo3(name = "get_f64")]
fn get_f64_impl(&self, key: String) -> Option<f64> {
self.get_f64(&key)
}
#[pyo3(name = "get_usize")]
fn get_usize_impl(&self, key: String) -> Option<usize> {
self.get_usize(&key)
}
#[pyo3(name = "keys")]
fn py_keys(&self) -> Vec<String> {
self.keys().iter().map(|s| s.to_string()).collect()
}
#[pyo3(name = "values")]
fn py_values(&self) -> Vec<String> {
self.all_values().iter().map(|s| s.to_string()).collect()
}
#[pyo3(name = "contains")]
fn py_contains(&self, key: String) -> bool {
self.has_key(&key)
}
#[pyo3(name = "len")]
fn py_len(&self) -> usize {
self.count()
}
}
#[derive(Clone)]
enum RiConfigSource {
File(PathBuf),
Environment,
}
#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
pub struct RiConfigManager {
config: Arc<RwLock<RiConfig>>,
sources: Vec<RiConfigSource>,
#[cfg(feature = "pyo3")]
hooks: Option<Arc<crate::hooks::RiHookBus>>,
#[cfg(feature = "config_hot_reload")]
watcher: Option<Arc<RecommendedWatcher>>,
watcher_task: Arc<TokioRwLock<Option<JoinHandle<()>>>>,
monitored_paths: Arc<TokioRwLock<Vec<PathBuf>>>,
#[cfg(feature = "config_hot_reload")]
change_callback: Option<Arc<dyn Fn() + Send + Sync>>,
}
impl Default for RiConfigManager {
fn default() -> Self {
Self::new()
}
}
impl Clone for RiConfigManager {
fn clone(&self) -> Self {
RiConfigManager {
config: self.config.clone(),
sources: self.sources.clone(),
#[cfg(feature = "pyo3")]
hooks: self.hooks.clone(),
#[cfg(feature = "config_hot_reload")]
watcher: self.watcher.clone(),
watcher_task: self.watcher_task.clone(),
monitored_paths: self.monitored_paths.clone(),
#[cfg(feature = "config_hot_reload")]
change_callback: self.change_callback.clone(),
}
}
}
impl RiConfigManager {
pub fn new() -> Self {
RiConfigManager {
config: Arc::new(RwLock::new(RiConfig::new())),
sources: Vec::new(),
#[cfg(feature = "pyo3")]
hooks: None,
#[cfg(feature = "config_hot_reload")]
watcher: None,
watcher_task: Arc::new(TokioRwLock::new(None)),
monitored_paths: Arc::new(TokioRwLock::new(Vec::new())),
#[cfg(feature = "config_hot_reload")]
change_callback: None,
}
}
#[cfg(feature = "pyo3")]
pub fn with_hooks(hooks: Arc<crate::hooks::RiHookBus>) -> Self {
RiConfigManager {
config: Arc::new(RwLock::new(RiConfig::new())),
sources: Vec::new(),
hooks: Some(hooks),
#[cfg(feature = "config_hot_reload")]
watcher: None,
watcher_task: Arc::new(TokioRwLock::new(None)),
monitored_paths: Arc::new(TokioRwLock::new(Vec::new())),
#[cfg(feature = "config_hot_reload")]
change_callback: None,
}
}
pub fn add_file_source(&mut self, path: impl AsRef<Path>) {
self.sources.push(RiConfigSource::File(path.as_ref().to_path_buf()));
}
pub fn add_environment_source(&mut self) {
self.sources.push(RiConfigSource::Environment);
}
#[cfg(feature = "pyo3")]
fn notify_config_reload(&self, _path: &str) {
if let Some(hooks) = &self.hooks {
let _ = hooks.emit_with(
&RiHookKind::ConfigReload,
&RiServiceContext::new_default().unwrap_or_else(|_| {
RiServiceContext::new_with(
crate::fs::RiFileSystem::new_auto_root().unwrap_or_else(|_| crate::fs::RiFileSystem::new_with_root(std::env::current_dir().unwrap_or_default())),
crate::log::RiLogger::new(&crate::log::RiLogConfig::default(), crate::fs::RiFileSystem::new_with_root(std::env::current_dir().unwrap_or_default())),
crate::config::RiConfigManager::new(),
crate::hooks::RiHookBus::new(),
None,
)
}),
Some("config_manager"),
None,
);
}
}
pub fn load(&mut self) -> Result<(), crate::core::RiError> {
let mut cfg = RiConfig::new();
for source in &self.sources {
match source {
RiConfigSource::File(path) => {
self.load_file(path, &mut cfg)?;
#[cfg(feature = "pyo3")]
self.notify_config_reload(path.to_str().unwrap_or(""));
}
RiConfigSource::Environment => {
self.load_environment(&mut cfg);
}
}
}
*self.config.write().expect("Failed to lock config for writing") = cfg;
Ok(())
}
pub fn new_default() -> Self {
let mut manager = Self::new();
if let Ok(cwd) = std::env::current_dir() {
let config_dir = cwd.join("config");
manager.add_file_source(config_dir.join("dms.yaml"));
manager.add_file_source(config_dir.join("dms.yml"));
manager.add_file_source(config_dir.join("dms.toml"));
manager.add_file_source(config_dir.join("dms.json"));
}
manager.add_environment_source();
let _ = manager.load();
manager
}
fn load_file(&self, path: &Path, cfg: &mut RiConfig) -> Result<(), crate::core::RiError> {
if !path.exists() {
return Ok(());
}
let text = fs::read_to_string(path)?;
let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
match extension.to_lowercase().as_str() {
"json" => {
if let Ok(map) = serde_json::from_str::<serde_json::Value>(&text) {
self.flatten_json(&map, "", cfg);
}
}
"yaml" | "yml" => {
if let Ok(yaml_docs) = YamlLoader::load_from_str(&text) {
for doc in yaml_docs {
self.flatten_yaml(&doc, "", cfg);
}
}
}
"toml" => {
if let Ok(toml) = toml::from_str(&text) {
self.flatten_toml(&toml, "", cfg);
}
}
_ => {
}
}
Ok(())
}
fn flatten_json(&self, value: &serde_json::Value, prefix: &str, cfg: &mut RiConfig) {
Self::flatten_json_static(value, prefix, cfg);
}
fn flatten_json_static(value: &serde_json::Value, prefix: &str, cfg: &mut RiConfig) {
match value {
serde_json::Value::Object(map) => {
for (k, v) in map {
let new_prefix = if prefix.is_empty() {
k.clone()
} else {
format!("{prefix}.{k}")
};
Self::flatten_json_static(v, &new_prefix, cfg);
}
}
serde_json::Value::Array(arr) => {
for (i, v) in arr.iter().enumerate() {
let new_prefix = format!("{prefix}.{i}");
Self::flatten_json_static(v, &new_prefix, cfg);
}
}
serde_json::Value::String(s) => {
cfg.set(prefix, s);
}
serde_json::Value::Number(n) => {
cfg.set(prefix, n.to_string());
}
serde_json::Value::Bool(b) => {
cfg.set(prefix, b.to_string());
}
serde_json::Value::Null => {
cfg.set(prefix, "");
}
}
}
fn flatten_yaml(&self, value: &Yaml, prefix: &str, cfg: &mut RiConfig) {
Self::flatten_yaml_static(value, prefix, cfg);
}
fn flatten_yaml_static(value: &Yaml, prefix: &str, cfg: &mut RiConfig) {
match value {
Yaml::Hash(map) => {
for (k, v) in map {
if let Yaml::String(key) = k {
let new_prefix = if prefix.is_empty() {
key.clone()
} else {
format!("{prefix}.{key}")
};
Self::flatten_yaml_static(v, &new_prefix, cfg);
}
}
}
Yaml::Array(arr) => {
for (i, v) in arr.iter().enumerate() {
let new_prefix = format!("{prefix}.{i}");
Self::flatten_yaml_static(v, &new_prefix, cfg);
}
}
Yaml::String(s) => {
cfg.set(prefix, s);
}
Yaml::Integer(n) => {
cfg.set(prefix, n.to_string());
}
Yaml::Real(r) => {
cfg.set(prefix, r);
}
Yaml::Boolean(b) => {
cfg.set(prefix, b.to_string());
}
Yaml::Null => {
cfg.set(prefix, "");
}
_ => {
}
}
}
fn flatten_toml(&self, value: &toml::Value, prefix: &str, cfg: &mut RiConfig) {
Self::flatten_toml_static(value, prefix, cfg);
}
fn flatten_toml_static(value: &toml::Value, prefix: &str, cfg: &mut RiConfig) {
match value {
toml::Value::Table(table) => {
for (k, v) in table {
let new_prefix = if prefix.is_empty() {
k.clone()
} else {
format!("{prefix}.{k}")
};
Self::flatten_toml_static(v, &new_prefix, cfg);
}
}
toml::Value::Array(arr) => {
for (i, v) in arr.iter().enumerate() {
let new_prefix = format!("{prefix}.{i}");
Self::flatten_toml_static(v, &new_prefix, cfg);
}
}
toml::Value::String(s) => {
cfg.set(prefix, s);
}
toml::Value::Integer(n) => {
cfg.set(prefix, n.to_string());
}
toml::Value::Float(f) => {
cfg.set(prefix, f.to_string());
}
toml::Value::Boolean(b) => {
cfg.set(prefix, b.to_string());
}
toml::Value::Datetime(dt) => {
cfg.set(prefix, dt.to_string());
}
}
}
fn load_environment(&self, cfg: &mut RiConfig) {
for (name, value) in std::env::vars() {
if let Some(rest) = name.strip_prefix("Ri_") {
let key_parts: Vec<String> = rest
.split("__")
.map(|part| part.to_ascii_lowercase())
.collect();
let key = key_parts.join(".");
if !key.is_empty() {
cfg.set(key, value);
}
}
}
}
#[cfg(feature = "config_hot_reload")]
pub async fn start_watcher(&mut self) -> Result<(), crate::core::RiError> {
self.start_watcher_with_callback::<fn()>(None).await
}
#[cfg(feature = "config_hot_reload")]
pub async fn start_watcher_with_callback<F>(&mut self, callback: Option<Arc<dyn Fn() + Send + Sync>>) -> Result<(), crate::core::RiError> {
let (tx, mut rx) = tokio::sync::mpsc::channel::<notify::Result<notify::Event>>(100);
let mut watcher = RecommendedWatcher::new(
move |res| {
let _ = tx.blocking_send(res);
},
notify::Config::default(),
).map_err(|e| crate::core::RiError::Config(format!("Failed to create config watcher: {}", e)))?;
let mut monitored = Vec::with_capacity(self.sources.len());
for source in &self.sources {
if let RiConfigSource::File(path) = source {
if path.exists() {
watcher.watch(path, RecursiveMode::NonRecursive)
.map_err(|e| crate::core::RiError::Config(format!("Failed to watch config file {}: {}", path.display(), e)))?;
monitored.push(path.clone());
}
}
}
let monitored_paths = self.monitored_paths.clone();
let manager = self.clone();
let change_callback = callback.clone();
let task = tokio::spawn(async move {
while let Some(event) = rx.recv().await {
match event {
Ok(event) => {
if let Some(paths) = event.paths.first() {
let changed_path = paths.clone();
{
let mut paths_guard = monitored_paths.write().await;
if !paths_guard.contains(&changed_path) {
paths_guard.push(changed_path.clone());
}
}
log::info!("Config file changed: {}", changed_path.display());
if let Err(e) = manager.reload_file(&changed_path).await {
log::error!("Failed to reload config file {}: {}", changed_path.display(), e);
}
if let Some(ref cb) = change_callback {
cb();
}
#[cfg(feature = "pyo3")]
manager.notify_config_reload(changed_path.to_str().unwrap_or(""));
}
}
Err(e) => {
log::warn!("Config watcher error: {:?}", e);
}
}
}
});
self.watcher = Some(Arc::new(watcher));
*self.watcher_task.write().await = Some(task);
self.change_callback = callback;
let mut paths_guard = self.monitored_paths.write().await;
*paths_guard = monitored;
Ok(())
}
#[cfg(feature = "config_hot_reload")]
async fn reload_file(&self, path: &PathBuf) -> Result<(), crate::core::RiError> {
let mut new_config = self.config.read().expect("Failed to lock config for reading").clone();
self.load_file(path, &mut new_config)?;
*self.config.write().expect("Failed to lock config for writing") = new_config;
Ok(())
}
#[cfg(feature = "config_hot_reload")]
pub async fn stop_watcher(&mut self) -> Result<(), crate::core::RiError> {
let task = self.watcher_task.write().await.take();
if let Some(task) = task {
task.abort();
}
self.watcher = None;
let mut paths_guard = self.monitored_paths.write().await;
paths_guard.clear();
Ok(())
}
pub async fn get_monitored_paths(&self) -> Vec<PathBuf> {
self.monitored_paths.read().await.clone()
}
#[cfg(not(feature = "config_hot_reload"))]
pub async fn start_watcher(&mut self) -> Result<(), crate::core::RiError> {
Ok(())
}
pub fn config(&self) -> RiConfig {
self.config.read().expect("Failed to lock config for reading").clone()
}
pub fn config_mut(&mut self) -> std::sync::RwLockWriteGuard<'_, RiConfig> {
self.config.write().expect("Failed to lock config for writing")
}
}
#[cfg(feature = "pyo3")]
#[pyo3::prelude::pymethods]
impl RiConfigManager {
#[new]
fn py_new() -> Self {
Self::new()
}
#[pyo3(name = "add_file_source")]
fn add_file_source_impl(&mut self, path: String) {
self.add_file_source(path);
}
#[pyo3(name = "add_environment_source")]
fn add_environment_source_impl(&mut self) {
self.add_environment_source();
}
#[pyo3(name = "get")]
fn get_config_impl(&self, key: String) -> Option<String> {
self.config().get(&key).cloned()
}
}
#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
#[derive(Clone, Debug)]
pub struct RiConfigValidator {
required_keys: Vec<String>,
port_keys: Vec<String>,
timeout_keys: Vec<String>,
secret_keys: Vec<String>,
url_keys: Vec<String>,
positive_int_keys: Vec<String>,
}
impl Default for RiConfigValidator {
fn default() -> Self {
Self::new()
}
}
impl RiConfigValidator {
pub fn new() -> Self {
RiConfigValidator {
required_keys: Vec::new(),
port_keys: vec!["server.port".to_string(), "cache.redis.port".to_string(), "database.port".to_string()],
timeout_keys: vec!["server.timeout".to_string(), "cache.ttl".to_string(), "session.timeout".to_string()],
secret_keys: vec!["auth.jwt.secret".to_string(), "auth.password.salt".to_string(), "encryption.key".to_string()],
url_keys: vec!["database.url".to_string(), "cache.redis.url".to_string(), "mq.url".to_string()],
positive_int_keys: vec!["pool.size".to_string(), "worker.count".to_string(), "retry.max".to_string()],
}
}
pub fn add_required(&mut self, key: String) -> &mut Self {
self.required_keys.push(key);
self
}
pub fn add_port_check(&mut self, key: String) -> &mut Self {
self.port_keys.push(key);
self
}
pub fn add_timeout_check(&mut self, key: String) -> &mut Self {
self.timeout_keys.push(key);
self
}
pub fn add_secret_check(&mut self, key: String) -> &mut Self {
self.secret_keys.push(key);
self
}
pub fn add_url_check(&mut self, key: String) -> &mut Self {
self.url_keys.push(key);
self
}
pub fn add_positive_int_check(&mut self, key: String) -> &mut Self {
self.positive_int_keys.push(key);
self
}
pub fn validate_config(&self, config: &RiConfig) -> Result<(), crate::core::RiError> {
for key in &self.required_keys {
if !config.has_key(key) {
return Err(crate::core::RiError::Config(format!(
"Missing required configuration key: {}", key
)));
}
}
for key in &self.port_keys {
if let Some(port) = config.get_port(key) {
if port == 0 {
return Err(crate::core::RiError::Config(format!(
"Invalid port number for {}: must be between 1 and 65535", key
)));
}
}
}
for key in &self.timeout_keys {
if let Some(timeout) = config.get_timeout_secs(key) {
if timeout == 0 {
return Err(crate::core::RiError::Config(format!(
"Invalid timeout for {}: must be between 1 and 86400 seconds", key
)));
}
}
}
for key in &self.secret_keys {
if let Some(secret) = config.get_str(key) {
if secret.len() < 8 {
return Err(crate::core::RiError::Config(format!(
"Secret key {} is too short: minimum length is 8 characters", key
)));
}
if secret == "secret" || secret == "password" || secret == "123456" {
return Err(crate::core::RiError::Config(format!(
"Insecure secret key detected for {}: using default or weak value", key
)));
}
}
}
for key in &self.url_keys {
if let Some(url) = config.get_str(key) {
if !url.starts_with("http://") && !url.starts_with("https://")
&& !url.starts_with("redis://") && !url.starts_with("postgresql://")
&& !url.starts_with("mysql://") && !url.starts_with("amqp://")
&& !url.starts_with("kafka://") && !url.starts_with("sqlite://")
{
return Err(crate::core::RiError::Config(format!(
"Invalid URL format for {}: {}", key, url
)));
}
}
}
for key in &self.positive_int_keys {
if let Some(value) = config.get_u32(key) {
if value == 0 {
return Err(crate::core::RiError::Config(format!(
"Invalid value for {}: must be a positive integer", key
)));
}
}
}
Ok(())
}
pub fn validate_with_requirements(
&self,
config: &RiConfig,
requirements: &[String],
) -> Result<(), crate::core::RiError> {
for key in requirements {
if !config.has_key(key) {
return Err(crate::core::RiError::Config(format!(
"Missing required configuration key: {}", key
)));
}
}
self.validate_config(config)
}
}
#[cfg(feature = "pyo3")]
#[pyo3::prelude::pymethods]
impl RiConfigValidator {
#[new]
fn py_new() -> Self {
Self::new()
}
#[pyo3(name = "require")]
fn py_add_required(&mut self, key: String) {
self.required_keys.push(key);
}
#[pyo3(name = "require_port")]
fn py_add_port_check(&mut self, key: String) {
self.port_keys.push(key);
}
#[pyo3(name = "require_timeout")]
fn py_add_timeout_check(&mut self, key: String) {
self.timeout_keys.push(key);
}
#[pyo3(name = "require_secret")]
fn py_add_secret_check(&mut self, key: String) {
self.secret_keys.push(key);
}
#[pyo3(name = "require_url")]
fn py_add_url_check(&mut self, key: String) {
self.url_keys.push(key);
}
#[pyo3(name = "require_positive_int")]
fn py_add_positive_int_check(&mut self, key: String) {
self.positive_int_keys.push(key);
}
#[pyo3(name = "validate")]
fn py_validate(&self, config: &RiConfig) -> bool {
self.validate_config(config).is_ok()
}
}