use myc_config::{
optional_config::OptionalConfig, secret_resolver::SecretResolver,
};
use myc_core::domain::dtos::{
callback::{Callback, ExecutionMode},
health_check_info::HealthStatus,
http::Protocol,
route::Route,
service::{Service, ServiceHost, ServiceSecret, ServiceType},
};
use mycelium_base::utils::errors::{creation_err, MappedErrors};
use serde::{
de::{MapAccess, Visitor},
Deserialize, Deserializer,
};
use std::{fmt, path::PathBuf};
use toml;
use uuid::Uuid;
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TlsConfig {
pub tls_cert: SecretResolver<String>,
pub tls_key: SecretResolver<String>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum LogFormat {
Ansi,
Jsonl,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum LoggingTarget {
Stdout,
File {
path: String,
},
Collector {
name: String,
protocol: Protocol,
host: String,
port: u32,
},
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LoggingConfig {
pub level: String,
pub format: LogFormat,
pub target: Option<LoggingTarget>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CacheConfig {
pub jwks_ttl: Option<u64>,
pub email_ttl: Option<u64>,
pub profile_ttl: Option<u64>,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
jwks_ttl: Some(60 * 60 * 12), email_ttl: Some(60 * 10), profile_ttl: Some(60 * 10), }
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ServiceIntermediate {
#[serde(default = "default_service_id")]
id: Uuid,
#[serde(alias = "hosts")]
host: ServiceHost,
#[serde(default = "default_service_protocol")]
protocol: Protocol,
#[serde(alias = "path", default)]
routes: Vec<Route>,
#[serde(default = "default_service_health_status")]
health_status: HealthStatus,
health_check_path: String,
#[serde(skip_serializing_if = "Option::is_none")]
discoverable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
service_type: Option<ServiceType>,
#[serde(skip_serializing_if = "Option::is_none")]
is_context_api: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
capabilities: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
openapi_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", alias = "secret")]
secrets: Option<Vec<ServiceSecret>>,
#[serde(skip_serializing_if = "Option::is_none")]
allowed_sources: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
proxy_address: Option<String>,
}
fn default_service_id() -> Uuid {
Uuid::new_v4()
}
fn default_service_protocol() -> Protocol {
Protocol::Http
}
fn default_service_health_status() -> HealthStatus {
HealthStatus::Unknown
}
fn service_from_intermediate(
service_key: String,
intermediate: ServiceIntermediate,
) -> Service {
Service {
id: intermediate.id,
name: service_key, host: intermediate.host,
protocol: intermediate.protocol,
routes: intermediate.routes,
health_status: intermediate.health_status,
health_check_path: intermediate.health_check_path,
discoverable: intermediate.discoverable,
service_type: intermediate.service_type,
is_context_api: intermediate.is_context_api,
capabilities: intermediate.capabilities,
description: intermediate.description,
openapi_path: intermediate.openapi_path,
secrets: intermediate.secrets,
allowed_sources: intermediate.allowed_sources,
proxy_address: intermediate.proxy_address,
}
}
fn deserialize_services<'de, D>(
deserializer: D,
) -> Result<Vec<Service>, D::Error>
where
D: Deserializer<'de>,
{
struct ServicesVisitor;
impl<'de> Visitor<'de> for ServicesVisitor {
type Value = Vec<Service>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a map of service arrays")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut all_services = Vec::new();
while let Some((service_key, services_vec)) =
map.next_entry::<String, Vec<ServiceIntermediate>>()?
{
for intermediate in services_vec {
let service = service_from_intermediate(
service_key.clone(),
intermediate,
);
all_services.push(service);
}
}
Ok(all_services)
}
}
deserializer.deserialize_map(ServicesVisitor)
}
fn deserialize_callbacks<'de, D>(
deserializer: D,
) -> Result<Option<Vec<Callback>>, D::Error>
where
D: Deserializer<'de>,
{
struct CallbacksVisitor;
impl<'de> Visitor<'de> for CallbacksVisitor {
type Value = Option<Vec<Callback>>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a map of callback arrays")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut all_callbacks = Vec::new();
while let Some((_key, callbacks_vec)) =
map.next_entry::<String, Vec<Callback>>()?
{
for callback in callbacks_vec {
all_callbacks.push(callback);
}
}
if all_callbacks.is_empty() {
Ok(None)
} else {
Ok(Some(all_callbacks))
}
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(CallbacksVisitor)
}
}
deserializer.deserialize_option(CallbacksVisitor)
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiConfig {
pub service_ip: String,
pub service_port: u16,
pub allowed_origins: Vec<String>,
pub service_workers: i32,
pub gateway_timeout: u64,
pub logging: LoggingConfig,
pub tls: OptionalConfig<TlsConfig>,
pub cache: Option<CacheConfig>,
pub health_check_interval: Option<u64>,
pub max_retry_count: Option<u32>,
pub max_error_instances: Option<u32>,
#[serde(
deserialize_with = "deserialize_callbacks",
skip_serializing_if = "Option::is_none"
)]
pub callbacks: Option<Vec<Callback>>,
#[serde(default)]
pub callback_execution_mode: ExecutionMode,
#[serde(default, deserialize_with = "deserialize_services")]
pub services: Vec<Service>,
#[serde(skip_serializing_if = "Option::is_none")]
pub openrpc_dev_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub openrpc_prod_url: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TmpConfig {
api: ApiConfig,
}
fn preprocess_toml_services(content: &str) -> String {
let mut result = String::new();
let mut in_services_context = false;
let mut in_callbacks_context = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed == "[api.services]" {
in_services_context = true;
in_callbacks_context = false;
result.push_str(line);
result.push('\n');
continue;
}
if trimmed == "[api.callbacks]" {
in_callbacks_context = true;
in_services_context = false;
result.push_str(line);
result.push('\n');
continue;
}
if in_services_context
&& trimmed.starts_with('[')
&& !trimmed.starts_with("[[")
{
if !trimmed.starts_with("[api.services.")
&& !trimmed.starts_with("[api.")
{
in_services_context = false;
}
}
if in_callbacks_context
&& trimmed.starts_with('[')
&& !trimmed.starts_with("[[")
{
if !trimmed.starts_with("[api.callbacks.")
&& !trimmed.starts_with("[api.")
{
in_callbacks_context = false;
}
}
if in_services_context
&& trimmed.starts_with("[[")
&& !trimmed.starts_with("[[api.services.")
{
if let Some(start) = trimmed.find("[[") {
if let Some(end) = trimmed[start + 2..].find("]]") {
let service_name = &trimmed[start + 2..start + 2 + end];
let new_line = line.replace(
&format!("[[{}", service_name),
&format!("[[api.services.{}", service_name),
);
result.push_str(&new_line);
result.push('\n');
continue;
}
}
}
if in_callbacks_context
&& trimmed.starts_with("[[")
&& !trimmed.starts_with("[[api.callbacks.")
{
if let Some(start) = trimmed.find("[[") {
if let Some(end) = trimmed[start + 2..].find("]]") {
let callback_name = &trimmed[start + 2..start + 2 + end];
let new_line = line.replace(
&format!("[[{}", callback_name),
&format!("[[api.callbacks.{}", callback_name),
);
result.push_str(&new_line);
result.push('\n');
continue;
}
}
}
result.push_str(line);
result.push('\n');
}
result
}
impl ApiConfig {
pub fn from_default_config_file(
file: PathBuf,
) -> Result<Self, MappedErrors> {
if !file.exists() {
return creation_err(format!(
"Could not find config file: {}",
file.to_str().unwrap()
))
.as_error();
}
let file_content =
std::fs::read_to_string(file.as_path()).map_err(|err| {
creation_err(format!("Could not read config file: {err}"))
})?;
let preprocessed_content = preprocess_toml_services(&file_content);
let config: TmpConfig =
toml::from_str(&preprocessed_content).map_err(|err| {
creation_err(format!("Could not parse config file: {err}"))
})?;
Ok(config.api)
}
}