use crate::{
auth_token::AuthToken,
ingest_client::IngestClient,
reflector_config::{
AttrKeyEqValuePair, ConfigLoadError, SemanticErrorExplanation, TomlValue, TopLevelIngest,
TopLevelMutation, CONFIG_ENV_VAR,
},
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{
collections::BTreeMap,
env,
path::{Path, PathBuf},
str::FromStr as _,
time::Duration,
};
use url::Url;
pub struct Config<T> {
pub ingest: TopLevelIngest,
pub mutation: TopLevelMutation,
pub plugin: T,
pub client_timeout: Option<Duration>,
pub run_id: String,
pub time_domain: Option<String>,
}
#[derive(Deserialize)]
struct EnvConfig {
modality_client_timeout: Option<f32>,
modality_run_id: Option<String>,
modality_time_domain: Option<String>,
}
impl Config<()> {
pub fn load_common() -> Result<Config<()>, Box<dyn std::error::Error + Send + Sync>> {
Self::load_custom("__NONE__", |_, _| Ok(None))
}
}
impl<T: Serialize + DeserializeOwned> Config<T> {
pub fn load(env_prefix: &str) -> Result<Config<T>, Box<dyn std::error::Error + Send + Sync>> {
Self::load_custom(env_prefix, |_, _| Ok(None))
}
pub fn load_custom(
env_prefix: &str,
map_env_val: impl Fn(
&str,
&str,
) -> Result<
Option<(String, TomlValue)>,
Box<dyn std::error::Error + Send + Sync>,
>,
) -> Result<Config<T>, Box<dyn std::error::Error + Send + Sync>> {
let mut cfg = None;
if let Ok(env_path) = env::var(CONFIG_ENV_VAR) {
let path = Path::new(&env_path);
let content = &std::fs::read_to_string(path)?;
let mut raw_toml: crate::reflector_config::raw_toml::Config =
toml::from_str(content).map_err(|e| ConfigLoadError::ConfigFileToml {
path: path.to_owned(),
error: e,
})?;
if raw_toml.metadata.is_empty() {
copy_relevant_plugin_section_to_top_level_metadata(&mut raw_toml)?;
}
let r: Result<crate::reflector_config::Config, SemanticErrorExplanation> =
raw_toml.try_into();
cfg = Some(r.map_err(|semantics| ConfigLoadError::DefinitionSemantics {
explanation: semantics.0,
})?);
}
let cfg = cfg.unwrap_or_default();
let mut ingest = cfg.ingest.clone().unwrap_or_default();
override_ingest_config_from_env(&mut ingest)?;
let mut mutation = cfg.mutation.clone().unwrap_or_default();
override_mutation_config_from_env(&mut mutation)?;
let env_config = envy::from_env::<EnvConfig>()?;
let mut plugin_toml = cfg.metadata.clone();
merge_plugin_config_from_env::<T>(env_prefix, map_env_val, &mut plugin_toml)?;
let plugin: T = TomlValue::Table(plugin_toml.into_iter().collect()).try_into()?;
let run_id = env_config
.modality_run_id
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
let client_timeout = env_config
.modality_client_timeout
.map(Duration::from_secs_f32);
Ok(Config {
ingest,
mutation,
plugin,
client_timeout,
run_id,
time_domain: env_config.modality_time_domain,
})
}
#[deprecated = "Prefer the more explicit 'connect_and_authenticate_ingest'"]
pub async fn connect_and_authenticate(
&self,
) -> Result<super::ingest::Client, Box<dyn std::error::Error + Send + Sync>> {
self.connect_and_authenticate_ingest().await
}
pub async fn connect_and_authenticate_ingest(
&self,
) -> Result<super::ingest::Client, Box<dyn std::error::Error + Send + Sync>> {
let protocol_parent_url = if let Some(url) = &self.ingest.protocol_parent_url {
url.clone()
} else {
Url::parse("modality-ingest://127.0.0.1")?
};
let auth_token = AuthToken::load()?;
let client = IngestClient::connect_with_timeout(
&protocol_parent_url,
self.ingest.allow_insecure_tls,
self.client_timeout
.unwrap_or_else(|| Duration::from_secs(1)),
)
.await?
.authenticate(auth_token.into())
.await?;
Ok(super::ingest::Client::new(
client,
self.ingest.timeline_attributes.clone(),
Some(self.run_id.clone()),
self.time_domain.clone(),
)
.await?)
}
#[cfg(feature = "deviant")]
pub async fn connect_and_authenticate_mutation(
&self,
) -> Result<super::mutation::MutatorHost, Box<dyn std::error::Error + Send + Sync>> {
let ingest = self.connect_and_authenticate_ingest().await?;
let protocol_parent_url = if let Some(url) = &self.mutation.protocol_parent_url {
url.clone()
} else {
Url::parse("modality-mutation://127.0.0.1")?
};
let auth_token = AuthToken::load()?;
let client = super::mutation::MutatorHost::connect_and_authenticate(
&protocol_parent_url,
self.mutation.allow_insecure_tls,
auth_token,
Some(ingest),
)
.await?;
Ok(client)
}
}
fn copy_relevant_plugin_section_to_top_level_metadata(
raw_toml: &mut crate::reflector_config::raw_toml::Config,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Some(plugins) = &raw_toml.plugins {
let file_stem = AliasablePluginFileStem::for_current_process()?;
if let Some(ingest) = &plugins.ingest {
let plugins_ingest_member = if file_stem.looks_like_collector() {
ingest
.find_collector_member_by_plugin_name(file_stem.as_str()) .or_else(|| ingest.find_collector_member_by_plugin_name(file_stem.alias()))
} else if file_stem.looks_like_importer() {
ingest
.find_importer_member_by_plugin_name(file_stem.as_str()) .or_else(|| ingest.find_importer_member_by_plugin_name(file_stem.alias()))
} else {
None
};
if let Some(pim) = plugins_ingest_member {
raw_toml.metadata = pim.metadata.clone();
if raw_toml.ingest.is_none() {
raw_toml.ingest = Some(Default::default());
}
raw_toml.ingest.as_mut().unwrap().timeline_attributes =
pim.timeline_attributes.clone();
}
} else if let Some(mutation) = plugins.mutation.as_ref() {
let mutations_ingest_member = if file_stem.looks_like_mutator() {
mutation
.find_mutator_member_by_plugin_name(file_stem.as_str()) .or_else(|| mutation.find_mutator_member_by_plugin_name(file_stem.alias()))
} else {
None
};
if let Some(mim) = mutations_ingest_member {
raw_toml.metadata = mim.metadata.clone();
if raw_toml.ingest.is_none() {
raw_toml.ingest = Some(Default::default());
}
}
}
}
Ok(())
}
fn merge_plugin_config_from_env<T: Serialize + DeserializeOwned>(
env_prefix: &str,
map_env_val: impl Fn(
&str,
&str,
) -> Result<
Option<(String, TomlValue)>,
Box<dyn std::error::Error + Send + Sync>,
>,
plugin_toml: &mut BTreeMap<String, TomlValue>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut auto_vars = vec![];
for (k, v) in env::vars() {
let Some(k) = k.strip_prefix(env_prefix) else {
continue;
};
if let Some((k, toml_val)) = map_env_val(k, &v)? {
plugin_toml.insert(k.to_string(), toml_val);
continue;
} else {
auto_vars.push((k.to_string(), v));
}
}
let env_config = envy::from_iter::<_, T>(auto_vars.into_iter())?;
let env_config_as_toml_str = toml::to_string(&env_config)?;
let env_config_as_toml: BTreeMap<String, TomlValue> = toml::from_str(&env_config_as_toml_str)?;
plugin_toml.extend(env_config_as_toml);
Ok(())
}
#[derive(Deserialize)]
struct IngestEnvOverrides {
modality_ingest_url: Option<Url>,
modality_host: Option<String>,
modality_allow_insecure_tls: Option<bool>,
ingest_protocol_child_port: Option<u16>,
additional_timeline_attributes: Option<Vec<String>>,
override_timeline_attributes: Option<Vec<String>>,
}
fn override_ingest_config_from_env(
ingest: &mut TopLevelIngest,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let ingest_env_overrides = envy::from_env::<IngestEnvOverrides>()?;
if let Some(u) = ingest_env_overrides.modality_ingest_url {
ingest.protocol_parent_url = Some(u);
} else if ingest.protocol_parent_url.is_none() {
if let Some(host) = ingest_env_overrides.modality_host {
let scheme = if host == "localhost" {
"modality-ingest"
} else {
"modality-ingest-tls"
};
ingest.protocol_parent_url =
Some(url::Url::parse(&format!("{scheme}://{host}")).map_err(|e| e.to_string())?);
}
}
if let Some(b) = ingest_env_overrides.modality_allow_insecure_tls {
ingest.allow_insecure_tls = b;
}
if let Some(p) = ingest_env_overrides.ingest_protocol_child_port {
ingest.protocol_child_port = Some(p);
}
if let Some(strs) = ingest_env_overrides.additional_timeline_attributes {
for s in strs {
let kvp = AttrKeyEqValuePair::from_str(&s)?;
ingest
.timeline_attributes
.additional_timeline_attributes
.push(kvp);
}
}
if let Some(strs) = ingest_env_overrides.override_timeline_attributes {
for s in strs {
let kvp = AttrKeyEqValuePair::from_str(&s)?;
ingest
.timeline_attributes
.override_timeline_attributes
.push(kvp);
}
}
Ok(())
}
#[derive(Deserialize)]
struct MutationEnvOverrides {
modality_ingest_url: Option<Url>,
modality_mutation_url: Option<Url>,
modality_host: Option<String>,
modality_allow_insecure_tls: Option<bool>,
additional_mutator_attributes: Option<Vec<String>>,
}
fn override_mutation_config_from_env(
mutation: &mut TopLevelMutation,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mutation_env_overrides = envy::from_env::<MutationEnvOverrides>()?;
if let Some(u) = mutation_env_overrides.modality_mutation_url {
mutation.protocol_parent_url = Some(u);
} else if let Some(u) = mutation_env_overrides.modality_ingest_url {
let scheme = if u.scheme() == "modality-ingest-tls" {
"modality-mutation-tls"
} else {
"modality-mutation"
};
let host = u
.host()
.ok_or_else(|| "Ingest url must have a host component".to_string())?;
mutation.protocol_parent_url =
Some(url::Url::parse(&format!("{scheme}://{host}")).map_err(|e| e.to_string())?);
} else if mutation.protocol_parent_url.is_none() {
if let Some(host) = mutation_env_overrides.modality_host {
let scheme = if host == "localhost" {
"modality-mutation"
} else {
"modality-mutation-tls"
};
mutation.protocol_parent_url =
Some(url::Url::parse(&format!("{scheme}://{host}")).map_err(|e| e.to_string())?);
}
}
if let Some(b) = mutation_env_overrides.modality_allow_insecure_tls {
mutation.allow_insecure_tls = b;
}
if let Some(strs) = mutation_env_overrides.additional_mutator_attributes {
for s in strs {
let kvp = AttrKeyEqValuePair::from_str(&s)?;
mutation
.mutator_attributes
.additional_mutator_attributes
.push(kvp);
}
}
Ok(())
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
struct AliasablePluginFileStem {
filename: String,
path: PathBuf,
}
impl AliasablePluginFileStem {
#[cfg(not(test))]
pub fn for_current_process() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
Self::for_path(std::env::current_exe()?)
}
#[cfg(test)]
pub fn for_current_process() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
if let Ok(path) = std::env::var("TEST_CURRENT_EXE_PATH") {
Self::for_path(path)
} else {
Self::for_path(std::env::current_exe()?)
}
}
pub fn for_path(p: impl AsRef<Path>) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let path = p.as_ref().to_owned();
let filename = path
.file_name()
.ok_or("Plugin does not refer to a file")?
.to_string_lossy()
.to_string();
Ok(Self { path, filename })
}
pub fn alias(&self) -> &str {
self.filename
.trim_start_matches("modality-")
.trim_end_matches("-import")
.trim_end_matches("-importer")
.trim_end_matches("-importers")
.trim_end_matches("-collector")
.trim_end_matches("-collectors")
.trim_end_matches("-mutator")
.trim_end_matches("-mutators")
}
pub fn looks_like_importer(&self) -> bool {
self.filename.ends_with("-import")
|| self.filename.ends_with("-importer")
|| self.filename.ends_with("-importers")
|| self
.path
.parent()
.and_then(|p| p.components().last())
.map(|c| c.as_os_str() == "importers")
.unwrap_or(false)
}
pub fn looks_like_collector(&self) -> bool {
self.filename.ends_with("-collector")
|| self.filename.ends_with("-collectors")
|| self
.path
.parent()
.and_then(|p| p.components().last())
.map(|c| c.as_os_str() == "collectors")
.unwrap_or(false)
}
#[allow(unused)]
pub fn looks_like_mutator(&self) -> bool {
self.filename.ends_with("-mutator")
|| self.filename.ends_with("-mutators")
|| self
.path
.parent()
.and_then(|p| p.components().last())
.map(|c| c.as_os_str() == "mutators")
.unwrap_or(false)
}
pub fn as_str(&self) -> &str {
self.filename.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::{AttrKey, AttrVal};
use std::io::Write;
fn apfs(p: impl AsRef<Path>) -> AliasablePluginFileStem {
AliasablePluginFileStem::for_path(p).unwrap()
}
#[track_caller]
fn check_alias(path: &str, expected: &str) {
assert_eq!(expected, apfs(path).alias());
}
#[test]
fn plugin_alias() {
check_alias("/modality-foo", "foo");
check_alias("/dir/modality-foo", "foo");
check_alias("/dir/foo-import", "foo");
check_alias("/dir/foo-importer", "foo");
check_alias("/dir/foo-importers", "foo");
check_alias("/dir/foo-collector", "foo");
check_alias("/dir/foo-collectors", "foo");
check_alias("/dir/foo-mutator", "foo");
check_alias("/dir/foo-mutators", "foo");
check_alias("/dir/foo", "foo");
}
#[test]
fn type_heuristics() {
assert!(apfs("/dir/foo-import").looks_like_importer());
assert!(apfs("/dir/foo-importer").looks_like_importer());
assert!(apfs("/dir/foo-importers").looks_like_importer());
assert!(apfs("/dir/importers/foo").looks_like_importer());
assert!(!apfs("/dir/collectors/foo").looks_like_importer());
assert!(!apfs("/dir/mutators/foo").looks_like_importer());
assert!(!apfs("/dir/foo-collector").looks_like_importer());
assert!(!apfs("/dir/foo-mutator").looks_like_importer());
assert!(apfs("/dir/foo-collector").looks_like_collector());
assert!(apfs("/dir/foo-collectors").looks_like_collector());
assert!(apfs("/dir/collectors/foo").looks_like_collector());
assert!(!apfs("/dir/foo").looks_like_collector());
assert!(!apfs("/dir/foo-importer").looks_like_collector());
assert!(!apfs("/dir/foo-mutator").looks_like_collector());
assert!(!apfs("/dir/importers/foo").looks_like_collector());
assert!(!apfs("/dir/mutators/foo").looks_like_collector());
assert!(apfs("/dir/foo-mutator").looks_like_mutator());
assert!(apfs("/dir/foo-mutators").looks_like_mutator());
assert!(apfs("/dir/mutators/foo").looks_like_mutator());
assert!(!apfs("/dir/foo").looks_like_mutator());
assert!(!apfs("/dir/foo-collector").looks_like_mutator());
assert!(!apfs("/dir/foo-importer").looks_like_mutator());
assert!(!apfs("/dir/collectors/foo").looks_like_mutator());
assert!(!apfs("/dir/importers/foo").looks_like_mutator());
}
#[derive(Serialize, Deserialize)]
struct CustomConfig {
val: Option<u32>,
}
fn clear_relevant_env_vars() {
env::remove_var("MODALITY_REFLECTOR_CONFIG");
env::remove_var("MODALITY_CLIENT_TIMEOUT");
env::remove_var("MODALITY_RUN_ID");
env::remove_var("MODALITY_HOST");
env::remove_var("MODALITY_INGEST_URL");
env::remove_var("MODALITY_MUTATION_URL");
env::remove_var("ADDITIONAL_TIMELINE_ATTRIBUTES");
env::remove_var("OVERRIDE_TIMELINE_ATTRIBUTES");
env::remove_var("MODALITY_ALLOW_INSECURE_TLS");
}
#[test]
#[serial_test::serial]
fn load_config_from_env() {
env::remove_var("TEST_VAL");
clear_relevant_env_vars();
let cfg = Config::<CustomConfig>::load("TEST_").unwrap();
assert_eq!(cfg.ingest, TopLevelIngest::default());
assert!(cfg.client_timeout.is_none());
assert!(cfg.time_domain.is_none());
assert!(cfg.plugin.val.is_none());
env::set_var("TEST_VAL", "42");
let cfg = Config::<CustomConfig>::load("TEST_").unwrap();
assert_eq!(cfg.plugin.val, Some(42));
env::remove_var("TEST_VAL");
env::set_var("MODALITY_CLIENT_TIMEOUT", "42");
let cfg = Config::<CustomConfig>::load("TEST_").unwrap();
assert_eq!(cfg.client_timeout, Some(Duration::from_secs(42)));
env::remove_var("MODALITY_CLIENT_TIMEOUT");
env::set_var("MODALITY_RUN_ID", "42");
let cfg = Config::<CustomConfig>::load("TEST_").unwrap();
assert_eq!(cfg.run_id, "42");
env::remove_var("MODALITY_RUN_ID");
env::set_var("MODALITY_TIME_DOMAIN", "42");
let cfg = Config::<CustomConfig>::load("TEST_").unwrap();
assert_eq!(cfg.time_domain.unwrap(), "42");
env::remove_var("MODALITY_TIME_DOMAIN");
env::set_var("MODALITY_INGEST_URL", "modality-ingest://foo");
let cfg = Config::<CustomConfig>::load("TEST_").unwrap();
assert_eq!(
cfg.ingest.protocol_parent_url,
Url::parse("modality-ingest://foo").ok()
);
assert_eq!(
cfg.mutation.protocol_parent_url,
Url::parse("modality-mutation://foo").ok()
);
env::remove_var("MODALITY_INGEST_URL");
env::set_var("MODALITY_INGEST_URL", "modality-ingest-tls://foo");
let cfg = Config::<CustomConfig>::load("TEST_").unwrap();
assert_eq!(
cfg.ingest.protocol_parent_url,
Url::parse("modality-ingest-tls://foo").ok()
);
assert_eq!(
cfg.mutation.protocol_parent_url,
Url::parse("modality-mutation-tls://foo").ok()
);
env::remove_var("MODALITY_INGEST_URL");
env::set_var("MODALITY_HOST", "foo");
let cfg = Config::<CustomConfig>::load("TEST_").unwrap();
assert_eq!(
cfg.ingest.protocol_parent_url,
Url::parse("modality-ingest-tls://foo").ok()
);
assert_eq!(
cfg.mutation.protocol_parent_url,
Url::parse("modality-mutation-tls://foo").ok()
);
env::remove_var("MODALITY_HOST");
env::set_var("MODALITY_HOST", "localhost");
let cfg = Config::<CustomConfig>::load("TEST_").unwrap();
assert_eq!(
cfg.ingest.protocol_parent_url,
Url::parse("modality-ingest://localhost").ok()
);
assert_eq!(
cfg.mutation.protocol_parent_url,
Url::parse("modality-mutation://localhost").ok()
);
env::remove_var("MODALITY_HOST");
env::set_var("MODALITY_INGEST_URL", "modality-ingest://foo");
env::set_var("MODALITY_HOST", "bar");
let cfg = Config::<CustomConfig>::load("TEST_").unwrap();
assert_eq!(
cfg.ingest.protocol_parent_url,
Url::parse("modality-ingest://foo").ok()
);
env::remove_var("MODALITY_HOST");
env::remove_var("MODALITY_INGEST_URL");
env::set_var("ADDITIONAL_TIMELINE_ATTRIBUTES", "foo=42,bar='yo'");
env::set_var("OVERRIDE_TIMELINE_ATTRIBUTES", "foo=42,bar='yo'");
let cfg = Config::<CustomConfig>::load("TEST_").unwrap();
assert_eq!(
cfg.ingest
.timeline_attributes
.additional_timeline_attributes,
vec![
(AttrKey::from("foo"), AttrVal::from(42)).into(),
(AttrKey::from("bar"), AttrVal::from("yo")).into(),
]
);
assert_eq!(
cfg.ingest.timeline_attributes.override_timeline_attributes,
vec![
(AttrKey::from("foo"), AttrVal::from(42)).into(),
(AttrKey::from("bar"), AttrVal::from("yo")).into(),
]
);
env::remove_var("ADDITIONAL_TIMELINE_ATTRIBUTES");
env::remove_var("OVERRIDE_TIMELINE_ATTRIBUTES");
clear_relevant_env_vars();
}
#[test]
#[serial_test::serial]
fn load_config_from_file() {
clear_relevant_env_vars();
let content = "
[ingest]
additional-timeline-attributes = ['a = 1']
override-timeline-attributes = ['c = true']
protocol-parent-url = 'modality-ingest-tls://auxon.io:9077'
allow-insecure-tls = true
[mutation]
additional-mutator-attributes = ['a = 1']
override-mutator-attributes = ['c = true']
protocol-parent-url = 'modality-mutation://auxon.io'
allow-insecure-tls = true
[metadata]
val = 42
";
let mut tmpfile = tempfile::NamedTempFile::new().unwrap();
write!(tmpfile, "{content}").unwrap();
env::set_var("MODALITY_REFLECTOR_CONFIG", tmpfile.path());
let cfg = Config::<CustomConfig>::load("TEST_").unwrap();
assert_eq!(
cfg.ingest
.timeline_attributes
.additional_timeline_attributes,
vec![(AttrKey::from("a"), AttrVal::from(1)).into(),]
);
assert_eq!(
cfg.ingest.timeline_attributes.override_timeline_attributes,
vec![(AttrKey::from("c"), AttrVal::from(true)).into(),]
);
assert_eq!(
cfg.ingest.protocol_parent_url,
Url::parse("modality-ingest-tls://auxon.io:9077").ok()
);
assert!(cfg.ingest.allow_insecure_tls);
assert_eq!(cfg.plugin.val, Some(42));
assert_eq!(
cfg.mutation
.mutator_attributes
.additional_mutator_attributes,
vec![(AttrKey::from("a"), AttrVal::from(1)).into(),]
);
assert_eq!(
cfg.mutation.mutator_attributes.override_mutator_attributes,
vec![(AttrKey::from("c"), AttrVal::from(true)).into(),]
);
assert_eq!(
cfg.mutation.protocol_parent_url,
Url::parse("modality-mutation://auxon.io").ok()
);
assert!(cfg.mutation.allow_insecure_tls);
assert_eq!(cfg.plugin.val, Some(42));
env::remove_var("MODALITY_REFLECTOR_CONFIG");
clear_relevant_env_vars();
}
#[test]
#[serial_test::serial]
fn named_ingest_metadata_section_from_config_file() {
clear_relevant_env_vars();
let content = "
[plugins.ingest.collectors.test.metadata]
val = 42
";
let mut tmpfile = tempfile::NamedTempFile::new().unwrap();
write!(tmpfile, "{content}").unwrap();
env::set_var("TEST_CURRENT_EXE_PATH", "/dir/test-collector");
env::set_var("MODALITY_REFLECTOR_CONFIG", tmpfile.path());
let cfg = Config::<CustomConfig>::load("TEST_").unwrap();
assert_eq!(cfg.plugin.val, Some(42));
env::remove_var("MODALITY_REFLECTOR_CONFIG");
env::remove_var("TEST_CURRENT_EXE_PATH");
clear_relevant_env_vars();
}
#[test]
#[serial_test::serial]
fn env_overrides_config_file() {
env::remove_var("TEST_VAL");
clear_relevant_env_vars();
let content = "
[ingest]
additional-timeline-attributes = ['a = 1']
override-timeline-attributes = ['c = true']
protocol-parent-url = 'modality-ingest-tls://auxon.io:9077'
allow-insecure-tls = true
[mutation]
protocol-parent-url = 'modality-mutation://auxon.io'
allow-insecure-tls = true
[metadata]
val = 42
";
let mut tmpfile = tempfile::NamedTempFile::new().unwrap();
write!(tmpfile, "{content}").unwrap();
env::set_var("MODALITY_REFLECTOR_CONFIG", tmpfile.path());
env::set_var("ADDITIONAL_TIMELINE_ATTRIBUTES", "foo=42,bar='yo'");
env::set_var("OVERRIDE_TIMELINE_ATTRIBUTES", "foo=42,bar='yo'");
env::set_var("MODALITY_INGEST_URL", "modality-ingest://foo");
env::set_var("MODALITY_ALLOW_INSECURE_TLS", "false");
env::set_var("MODALITY_MUTATION_URL", "modality-mutation://foo");
env::set_var("TEST_VAL", "99");
let cfg = Config::<CustomConfig>::load("TEST_").unwrap();
assert_eq!(
cfg.ingest
.timeline_attributes
.additional_timeline_attributes,
vec![
(AttrKey::from("a"), AttrVal::from(1)).into(),
(AttrKey::from("foo"), AttrVal::from(42)).into(),
(AttrKey::from("bar"), AttrVal::from("yo")).into(),
]
);
assert_eq!(
cfg.ingest.timeline_attributes.override_timeline_attributes,
vec![
(AttrKey::from("c"), AttrVal::from(true)).into(),
(AttrKey::from("foo"), AttrVal::from(42)).into(),
(AttrKey::from("bar"), AttrVal::from("yo")).into(),
]
);
assert_eq!(
cfg.ingest.protocol_parent_url,
Url::parse("modality-ingest://foo").ok()
);
assert!(!cfg.ingest.allow_insecure_tls);
assert_eq!(
cfg.mutation.protocol_parent_url,
Url::parse("modality-mutation://foo").ok()
);
assert!(!cfg.mutation.allow_insecure_tls);
assert_eq!(cfg.plugin.val, Some(99));
env::remove_var("TEST_VAL");
clear_relevant_env_vars();
}
}