refactor: remove loco-rs deps

This commit is contained in:
2025-03-01 15:21:14 +08:00
parent a68aab1452
commit 2844e1fc32
66 changed files with 2565 additions and 1876 deletions

View File

@@ -1,153 +1,136 @@
use std::{path::Path, sync::Arc};
use std::sync::Arc;
use figment::Figment;
use itertools::Itertools;
use clap::{Parser, command};
use super::{core::App, env::Enviornment};
use crate::{
app::{config::AppConfig, context::create_context, router::create_router},
errors::RResult,
};
use super::{AppContext, core::App, env::Environment};
use crate::{app::config::AppConfig, errors::RResult};
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct MainCliArgs {
/// Explicit config file path
#[arg(short, long)]
config_file: Option<String>,
/// Explicit dotenv file path
#[arg(short, long)]
dotenv_file: Option<String>,
/// Explicit working dir
#[arg(short, long)]
working_dir: Option<String>,
/// Explicit environment
#[arg(short, long)]
environment: Option<Environment>,
}
pub struct AppBuilder {
dotenv_file: Option<String>,
config_file: Option<String>,
working_dir: String,
enviornment: Enviornment,
enviornment: Environment,
}
impl AppBuilder {
pub async fn load_dotenv(&self) -> RResult<()> {
let try_dotenv_file_or_dirs = if self.dotenv_file.is_some() {
vec![self.dotenv_file.as_deref()]
} else {
vec![Some(&self.working_dir as &str)]
};
pub async fn from_main_cli(environment: Option<Environment>) -> RResult<Self> {
let args = MainCliArgs::parse();
let priority_suffix = &AppConfig::priority_suffix(&self.enviornment);
let dotenv_prefix = AppConfig::dotenv_prefix();
let try_filenames = priority_suffix
.iter()
.map(|ps| format!("{}{}", &dotenv_prefix, ps))
.collect_vec();
for try_dotenv_file_or_dir in try_dotenv_file_or_dirs.into_iter().flatten() {
let try_dotenv_file_or_dir_path = Path::new(try_dotenv_file_or_dir);
if try_dotenv_file_or_dir_path.exists() {
if try_dotenv_file_or_dir_path.is_dir() {
for f in try_filenames.iter() {
let p = try_dotenv_file_or_dir_path.join(f);
if p.exists() && p.is_file() {
dotenv::from_path(p)?;
break;
}
}
} else if try_dotenv_file_or_dir_path.is_file() {
dotenv::from_path(try_dotenv_file_or_dir_path)?;
break;
let environment = environment.unwrap_or_else(|| {
args.environment.unwrap_or({
if cfg!(test) {
Environment::Testing
} else if cfg!(debug_assertions) {
Environment::Development
} else {
Environment::Production
}
}
}
Ok(())
}
pub async fn build_config(&self) -> RResult<AppConfig> {
let try_config_file_or_dirs = if self.config_file.is_some() {
vec![self.config_file.as_deref()]
} else {
vec![Some(&self.working_dir as &str)]
};
let allowed_extensions = &AppConfig::allowed_extension();
let priority_suffix = &AppConfig::priority_suffix(&self.enviornment);
let convention_prefix = &AppConfig::config_prefix();
let try_filenames = priority_suffix
.iter()
.flat_map(|ps| {
allowed_extensions
.iter()
.map(move |ext| (format!("{}{}{}", convention_prefix, ps, ext), ext))
})
.collect_vec();
});
let mut fig = Figment::from(AppConfig::default_provider());
let mut builder = Self::default();
for try_config_file_or_dir in try_config_file_or_dirs.into_iter().flatten() {
let try_config_file_or_dir_path = Path::new(try_config_file_or_dir);
if try_config_file_or_dir_path.exists() {
if try_config_file_or_dir_path.is_dir() {
for (f, ext) in try_filenames.iter() {
let p = try_config_file_or_dir_path.join(f);
if p.exists() && p.is_file() {
fig = AppConfig::merge_provider_from_file(fig, &p, ext)?;
break;
}
}
} else if let Some(ext) = try_config_file_or_dir_path
.extension()
.and_then(|s| s.to_str())
&& try_config_file_or_dir_path.is_file()
{
fig =
AppConfig::merge_provider_from_file(fig, try_config_file_or_dir_path, ext)?;
break;
}
}
if let Some(working_dir) = args.working_dir {
builder = builder.working_dir(working_dir);
}
if matches!(
&environment,
Environment::Testing | Environment::Development
) {
builder = builder.working_dir_from_manifest_dir();
}
let app_config: AppConfig = fig.extract()?;
builder = builder
.config_file(args.config_file)
.dotenv_file(args.dotenv_file)
.environment(environment);
Ok(app_config)
Ok(builder)
}
pub async fn build(self) -> RResult<App> {
let _app_name = env!("CARGO_CRATE_NAME");
AppConfig::load_dotenv(
&self.enviornment,
&self.working_dir,
self.dotenv_file.as_deref(),
)
.await?;
let _app_version = format!(
"{} ({})",
env!("CARGO_PKG_VERSION"),
option_env!("BUILD_SHA")
.or(option_env!("GITHUB_SHA"))
.unwrap_or("dev")
let config = AppConfig::load_config(
&self.enviornment,
&self.working_dir,
self.config_file.as_deref(),
)
.await?;
let app_context = Arc::new(
AppContext::new(self.enviornment.clone(), config, self.working_dir.clone()).await?,
);
self.load_dotenv().await?;
let config = self.build_config().await?;
let app_context = Arc::new(create_context(config).await?);
let router = create_router(app_context.clone()).await?;
Ok(App {
context: app_context,
router,
builder: self,
})
}
pub fn set_working_dir(self, working_dir: String) -> Self {
pub fn working_dir(self, working_dir: String) -> Self {
let mut ret = self;
ret.working_dir = working_dir;
ret
}
pub fn set_working_dir_to_manifest_dir(self) -> Self {
let manifest_dir = if cfg!(debug_assertions) {
pub fn environment(self, environment: Environment) -> Self {
let mut ret = self;
ret.enviornment = environment;
ret
}
pub fn config_file(self, config_file: Option<String>) -> Self {
let mut ret = self;
ret.config_file = config_file;
ret
}
pub fn dotenv_file(self, dotenv_file: Option<String>) -> Self {
let mut ret = self;
ret.dotenv_file = dotenv_file;
ret
}
pub fn working_dir_from_manifest_dir(self) -> Self {
let manifest_dir = if cfg!(debug_assertions) || cfg!(test) {
env!("CARGO_MANIFEST_DIR")
} else {
"./apps/recorder"
};
self.set_working_dir(manifest_dir.to_string())
self.working_dir(manifest_dir.to_string())
}
}
impl Default for AppBuilder {
fn default() -> Self {
Self {
enviornment: Enviornment::Production,
enviornment: Environment::Production,
dotenv_file: None,
config_file: None,
working_dir: String::from("."),

View File

@@ -14,3 +14,5 @@ leaky_bucket_refill_interval = 500
[graphql]
depth_limit = inf
complexity_limit = inf
[cache]

View File

@@ -7,21 +7,26 @@ use figment::{
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use super::env::Enviornment;
use super::env::Environment;
use crate::{
auth::AuthConfig, errors::RResult, extract::mikan::AppMikanConfig,
graphql::config::GraphQLConfig, storage::StorageConfig,
auth::AuthConfig, cache::CacheConfig, database::DatabaseConfig, errors::RResult,
extract::mikan::MikanConfig, graphql::GraphQLConfig, logger::LoggerConfig,
storage::StorageConfig, web::WebServerConfig,
};
const DEFAULT_CONFIG_MIXIN: &str = include_str!("./default_mixin.toml");
const CONFIG_ALLOWED_EXTENSIONS: &[&str] = &[".toml", ".json", ".yaml", ".yml"];
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
pub server: WebServerConfig,
pub cache: CacheConfig,
pub auth: AuthConfig,
pub dal: StorageConfig,
pub mikan: AppMikanConfig,
pub storage: StorageConfig,
pub mikan: MikanConfig,
pub graphql: GraphQLConfig,
pub logger: LoggerConfig,
pub database: DatabaseConfig,
}
impl AppConfig {
@@ -40,13 +45,13 @@ impl AppConfig {
.collect_vec()
}
pub fn priority_suffix(enviornment: &Enviornment) -> Vec<String> {
pub fn priority_suffix(environment: &Environment) -> Vec<String> {
vec![
format!(".{}.local", enviornment.full_name()),
format!(".{}.local", enviornment.short_name()),
format!(".{}.local", environment.full_name()),
format!(".{}.local", environment.short_name()),
String::from(".local"),
enviornment.full_name().to_string(),
enviornment.short_name().to_string(),
environment.full_name().to_string(),
environment.short_name().to_string(),
String::from(""),
]
}
@@ -75,4 +80,97 @@ impl AppConfig {
_ => unreachable!("unsupported config extension"),
})
}
pub async fn load_dotenv(
environment: &Environment,
working_dir: &str,
dotenv_file: Option<&str>,
) -> RResult<()> {
let try_dotenv_file_or_dirs = if dotenv_file.is_some() {
vec![dotenv_file]
} else {
vec![Some(working_dir)]
};
let priority_suffix = &AppConfig::priority_suffix(environment);
let dotenv_prefix = AppConfig::dotenv_prefix();
let try_filenames = priority_suffix
.iter()
.map(|ps| format!("{}{}", &dotenv_prefix, ps))
.collect_vec();
for try_dotenv_file_or_dir in try_dotenv_file_or_dirs.into_iter().flatten() {
let try_dotenv_file_or_dir_path = Path::new(try_dotenv_file_or_dir);
if try_dotenv_file_or_dir_path.exists() {
if try_dotenv_file_or_dir_path.is_dir() {
for f in try_filenames.iter() {
let p = try_dotenv_file_or_dir_path.join(f);
if p.exists() && p.is_file() {
dotenv::from_path(p)?;
break;
}
}
} else if try_dotenv_file_or_dir_path.is_file() {
dotenv::from_path(try_dotenv_file_or_dir_path)?;
break;
}
}
}
Ok(())
}
pub async fn load_config(
environment: &Environment,
working_dir: &str,
config_file: Option<&str>,
) -> RResult<AppConfig> {
let try_config_file_or_dirs = if config_file.is_some() {
vec![config_file]
} else {
vec![Some(working_dir)]
};
let allowed_extensions = &AppConfig::allowed_extension();
let priority_suffix = &AppConfig::priority_suffix(environment);
let convention_prefix = &AppConfig::config_prefix();
let try_filenames = priority_suffix
.iter()
.flat_map(|ps| {
allowed_extensions
.iter()
.map(move |ext| (format!("{}{}{}", convention_prefix, ps, ext), ext))
})
.collect_vec();
let mut fig = Figment::from(AppConfig::default_provider());
for try_config_file_or_dir in try_config_file_or_dirs.into_iter().flatten() {
let try_config_file_or_dir_path = Path::new(try_config_file_or_dir);
if try_config_file_or_dir_path.exists() {
if try_config_file_or_dir_path.is_dir() {
for (f, ext) in try_filenames.iter() {
let p = try_config_file_or_dir_path.join(f);
if p.exists() && p.is_file() {
fig = AppConfig::merge_provider_from_file(fig, &p, ext)?;
break;
}
}
} else if let Some(ext) = try_config_file_or_dir_path
.extension()
.and_then(|s| s.to_str())
&& try_config_file_or_dir_path.is_file()
{
fig =
AppConfig::merge_provider_from_file(fig, try_config_file_or_dir_path, ext)?;
break;
}
}
}
let app_config: AppConfig = fig.extract()?;
Ok(app_config)
}
}

View File

@@ -1,13 +1,13 @@
use sea_orm::DatabaseConnection;
use super::config::AppConfig;
use super::{Environment, config::AppConfig};
use crate::{
auth::AuthService, cache::CacheService, errors::RResult, extract::mikan::MikanClient,
graphql::GraphQLService, storage::StorageService,
auth::AuthService, cache::CacheService, database::DatabaseService, errors::RResult,
extract::mikan::MikanClient, graphql::GraphQLService, logger::LoggerService,
storage::StorageService,
};
pub struct AppContext {
pub db: DatabaseConnection,
pub logger: LoggerService,
pub db: DatabaseService,
pub config: AppConfig,
pub cache: CacheService,
pub mikan: MikanClient,
@@ -15,8 +15,36 @@ pub struct AppContext {
pub graphql: GraphQLService,
pub storage: StorageService,
pub working_dir: String,
pub environment: Environment,
}
pub async fn create_context(_config: AppConfig) -> RResult<AppContext> {
todo!()
impl AppContext {
pub async fn new(
environment: Environment,
config: AppConfig,
working_dir: impl ToString,
) -> RResult<Self> {
let config_cloned = config.clone();
let logger = LoggerService::from_config(config.logger).await?;
let cache = CacheService::from_config(config.cache).await?;
let db = DatabaseService::from_config(config.database).await?;
let storage = StorageService::from_config(config.storage).await?;
let auth = AuthService::from_conf(config.auth).await?;
let mikan = MikanClient::from_config(config.mikan).await?;
let graphql = GraphQLService::from_config_and_database(config.graphql, db.clone()).await?;
Ok(AppContext {
config: config_cloned,
environment,
logger,
auth,
cache,
db,
storage,
mikan,
working_dir: working_dir.to_string(),
graphql,
})
}
}

View File

@@ -1,15 +1,89 @@
use std::sync::Arc;
use std::{net::SocketAddr, sync::Arc};
use super::{builder::AppBuilder, context::AppContext, router::AppRouter};
use axum::Router;
use futures::try_join;
use tokio::signal;
use super::{builder::AppBuilder, context::AppContext};
use crate::{
errors::RResult,
web::{
controller::{self, core::ControllerTrait},
middleware::default_middleware_stack,
},
};
pub struct App {
pub context: Arc<AppContext>,
pub builder: AppBuilder,
pub router: AppRouter,
}
impl App {
pub fn builder() -> AppBuilder {
AppBuilder::default()
}
pub async fn serve(&self) -> RResult<()> {
let context = &self.context;
let config = &context.config;
let listener = tokio::net::TcpListener::bind(&format!(
"{}:{}",
config.server.binding, config.server.port
))
.await?;
let mut router = Router::<Arc<AppContext>>::new();
let (graphqlc, oidcc) = try_join!(
controller::graphql::create(context.clone()),
controller::oidc::create(context.clone()),
)?;
for c in [graphqlc, oidcc] {
router = c.apply_to(router);
}
let middlewares = default_middleware_stack(context.clone());
for mid in middlewares {
router = mid.apply(router)?;
tracing::info!(name = mid.name(), "+middleware");
}
let router = router
.with_state(context.clone())
.into_make_service_with_connect_info::<SocketAddr>();
axum::serve(listener, router)
.with_graceful_shutdown(async move {
Self::shutdown_signal().await;
tracing::info!("shutting down...");
})
.await?;
Ok(())
}
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
() = ctrl_c => {},
() = terminate => {},
}
}
}

View File

@@ -1,10 +1,22 @@
pub enum Enviornment {
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "snake_case")]
#[value(rename_all = "snake_case")]
pub enum Environment {
#[serde(alias = "dev")]
#[value(alias = "dev")]
Development,
#[serde(alias = "prod")]
#[value(alias = "prod")]
Production,
#[serde(alias = "test")]
#[value(alias = "test")]
Testing,
}
impl Enviornment {
impl Environment {
pub fn full_name(&self) -> &'static str {
match &self {
Self::Development => "development",

View File

@@ -3,75 +3,10 @@ pub mod config;
pub mod context;
pub mod core;
pub mod env;
pub mod router;
pub use core::App;
use std::path::Path;
use async_trait::async_trait;
pub use builder::AppBuilder;
pub use config::AppConfig;
pub use context::AppContext;
use loco_rs::{
Result,
app::{AppContext as LocoAppContext, Hooks},
boot::{BootResult, StartMode, create_app},
config::Config,
controller::AppRoutes,
db::truncate_table,
environment::Environment,
prelude::*,
task::Tasks,
};
use crate::{migrations::Migrator, models::subscribers};
pub struct App1;
#[async_trait]
impl Hooks for App1 {
fn app_version() -> String {
format!(
"{} ({})",
env!("CARGO_PKG_VERSION"),
option_env!("BUILD_SHA")
.or(option_env!("GITHUB_SHA"))
.unwrap_or("dev")
)
}
fn app_name() -> &'static str {
env!("CARGO_CRATE_NAME")
}
async fn boot(
mode: StartMode,
environment: &Environment,
config: Config,
) -> Result<BootResult> {
create_app::<Self, Migrator>(mode, environment, config).await
}
async fn initializers(_ctx: &LocoAppContext) -> Result<Vec<Box<dyn Initializer>>> {
let initializers: Vec<Box<dyn Initializer>> = vec![];
Ok(initializers)
}
fn routes(_ctx: &LocoAppContext) -> AppRoutes {
AppRoutes::with_default_routes()
}
fn register_tasks(_tasks: &mut Tasks) {}
async fn truncate(ctx: &LocoAppContext) -> Result<()> {
truncate_table(&ctx.db, subscribers::Entity).await?;
Ok(())
}
async fn seed(_ctx: &LocoAppContext, _base: &Path) -> Result<()> {
Ok(())
}
async fn connect_workers(_ctx: &LocoAppContext, _queue: &Queue) -> Result<()> {
Ok(())
}
}
pub use env::Environment;

View File

@@ -1,31 +0,0 @@
use std::sync::Arc;
use axum::Router;
use futures::try_join;
use crate::{
app::AppContext,
controllers::{self, core::ControllerTrait},
errors::RResult,
};
pub struct AppRouter {
pub root: Router<Arc<AppContext>>,
}
pub async fn create_router(context: Arc<AppContext>) -> RResult<AppRouter> {
let mut root_router = Router::<Arc<AppContext>>::new();
let (graphqlc, oidcc) = try_join!(
controllers::graphql::create(context.clone()),
controllers::oidc::create(context.clone()),
)?;
for c in [graphqlc, oidcc] {
root_router = c.apply_to(root_router);
}
root_router = root_router.with_state(context);
Ok(AppRouter { root: root_router })
}