use std::{
collections::BTreeMap,
path::{Path, PathBuf},
};
use fs_err as fs;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::info;
use crate::{
controller::middleware::{remote_ip::RemoteIPConfig, secure_headers::SecureHeadersConfig},
environment::Environment,
logger, Error, Result,
};
lazy_static! {
static ref DEFAULT_FOLDER: PathBuf = PathBuf::from("config");
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
pub logger: Logger,
pub server: Server,
#[cfg(feature = "with-db")]
pub database: Database,
pub queue: Option<Redis>,
pub auth: Option<Auth>,
#[serde(default)]
pub workers: Workers,
pub mailer: Option<Mailer>,
pub initializers: Option<Initializers>,
#[serde(default)]
pub settings: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct Logger {
pub enable: bool,
#[serde(default)]
pub pretty_backtrace: bool,
pub level: logger::LogLevel,
pub format: logger::Format,
pub override_filter: Option<String>,
pub file_appender: Option<LoggerFileAppender>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct LoggerFileAppender {
pub enable: bool,
#[serde(default)]
pub non_blocking: bool,
pub level: logger::LogLevel,
pub format: logger::Format,
pub rotation: logger::Rotation,
pub dir: Option<String>,
pub filename_prefix: Option<String>,
pub filename_suffix: Option<String>,
pub max_log_files: usize,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct Database {
pub uri: String,
pub enable_logging: bool,
pub min_connections: u32,
pub max_connections: u32,
pub connect_timeout: u64,
pub idle_timeout: u64,
pub acquire_timeout: Option<u64>,
#[serde(default)]
pub auto_migrate: bool,
#[serde(default)]
pub dangerously_truncate: bool,
#[serde(default)]
pub dangerously_recreate: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Redis {
pub uri: String,
#[serde(default)]
pub dangerously_flush: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Auth {
pub jwt: Option<JWT>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct JWT {
pub location: Option<JWTLocation>,
pub secret: String,
pub expiration: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "from")]
pub enum JWTLocation {
Bearer,
Query { name: String },
Cookie { name: String },
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Server {
#[serde(default = "default_binding")]
pub binding: String,
pub port: i32,
pub host: String,
pub ident: Option<String>,
pub middlewares: Middlewares,
}
fn default_binding() -> String {
"localhost".to_string()
}
impl Server {
#[must_use]
pub fn full_url(&self) -> String {
format!("{}:{}", self.host, self.port)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct Workers {
pub mode: WorkerMode,
pub queues: Option<Vec<String>>,
}
#[derive(Clone, Default, Serialize, Deserialize, Debug)]
pub enum WorkerMode {
#[default]
BackgroundQueue,
ForegroundBlocking,
BackgroundAsync,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Middlewares {
pub compression: Option<EnableMiddleware>,
pub etag: Option<EnableMiddleware>,
pub limit_payload: Option<LimitPayloadMiddleware>,
pub logger: Option<EnableMiddleware>,
pub catch_panic: Option<EnableMiddleware>,
pub timeout_request: Option<TimeoutRequestMiddleware>,
pub cors: Option<CorsMiddleware>,
#[serde(rename = "static")]
pub static_assets: Option<StaticAssetsMiddleware>,
pub secure_headers: Option<SecureHeadersConfig>,
pub remote_ip: Option<RemoteIPConfig>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct StaticAssetsMiddleware {
pub enable: bool,
pub must_exist: bool,
pub folder: FolderAssetsMiddleware,
pub fallback: String,
#[serde(default = "bool::default")]
pub precompressed: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct FolderAssetsMiddleware {
pub uri: String,
pub path: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CorsMiddleware {
pub enable: bool,
pub allow_origins: Option<Vec<String>>,
pub allow_headers: Option<Vec<String>>,
pub allow_methods: Option<Vec<String>>,
pub max_age: Option<u64>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TimeoutRequestMiddleware {
pub enable: bool,
pub timeout: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LimitPayloadMiddleware {
pub enable: bool,
pub body_limit: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct EnableMiddleware {
pub enable: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Mailer {
pub smtp: Option<SmtpMailer>,
#[serde(default)]
pub stub: bool,
}
pub type Initializers = BTreeMap<String, serde_json::Value>;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SmtpMailer {
pub enable: bool,
pub host: String,
pub port: u16,
pub secure: bool,
pub auth: Option<MailerAuth>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MailerAuth {
pub user: String,
pub password: String,
}
impl Config {
pub fn new(env: &Environment) -> Result<Self> {
let config = Self::from_folder(env, DEFAULT_FOLDER.as_path())?;
Ok(config)
}
pub fn from_folder(env: &Environment, path: &Path) -> Result<Self> {
let files = [
path.join(format!("{env}.local.yaml")),
path.join(format!("{env}.yaml")),
];
let selected_path = files
.iter()
.find(|p| p.exists())
.ok_or_else(|| Error::Message("no configuration file found".to_string()))?;
info!(selected_path =? selected_path, "loading environment from");
let content = fs::read_to_string(selected_path)?;
let rendered = crate::tera::render_string(&content, &json!({}))?;
serde_yaml::from_str(&rendered)
.map_err(|err| Error::YAMLFile(err, selected_path.to_string_lossy().to_string()))
}
pub fn get_jwt_config(&self) -> Result<&JWT> {
self.auth
.as_ref()
.and_then(|auth| auth.jwt.as_ref())
.map_or_else(
|| Err(Error::Any("no JWT config found".to_string().into())),
Ok,
)
}
}