refactor: remove loco-rs
This commit is contained in:
156
apps/recorder/src/app/builder.rs
Normal file
156
apps/recorder/src/app/builder.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use figment::Figment;
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::{core::App, env::Enviornment};
|
||||
use crate::{
|
||||
app::{config::AppConfig, context::create_context, router::create_router},
|
||||
errors::RResult,
|
||||
};
|
||||
|
||||
pub struct AppBuilder {
|
||||
dotenv_file: Option<String>,
|
||||
config_file: Option<String>,
|
||||
working_dir: String,
|
||||
enviornment: Enviornment,
|
||||
}
|
||||
|
||||
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)]
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
pub async fn build(self) -> RResult<App> {
|
||||
let _app_name = env!("CARGO_CRATE_NAME");
|
||||
|
||||
let _app_version = format!(
|
||||
"{} ({})",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
option_env!("BUILD_SHA")
|
||||
.or(option_env!("GITHUB_SHA"))
|
||||
.unwrap_or("dev")
|
||||
);
|
||||
|
||||
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 {
|
||||
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) {
|
||||
env!("CARGO_MANIFEST_DIR")
|
||||
} else {
|
||||
"./apps/recorder"
|
||||
};
|
||||
self.set_working_dir(manifest_dir.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AppBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enviornment: Enviornment::Production,
|
||||
dotenv_file: None,
|
||||
config_file: None,
|
||||
working_dir: String::from("."),
|
||||
}
|
||||
}
|
||||
}
|
||||
16
apps/recorder/src/app/config/default_mixin.toml
Normal file
16
apps/recorder/src/app/config/default_mixin.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[storage]
|
||||
data_dir = "./data"
|
||||
|
||||
[mikan]
|
||||
base_url = "https://mikanani.me/"
|
||||
|
||||
[mikan.http_client]
|
||||
exponential_backoff_max_retries = 3
|
||||
leaky_bucket_max_tokens = 2
|
||||
leaky_bucket_initial_tokens = 0
|
||||
leaky_bucket_refill_tokens = 1
|
||||
leaky_bucket_refill_interval = 500
|
||||
|
||||
[graphql]
|
||||
depth_limit = inf
|
||||
complexity_limit = inf
|
||||
78
apps/recorder/src/app/config/mod.rs
Normal file
78
apps/recorder/src/app/config/mod.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use std::{fs, path::Path, str};
|
||||
|
||||
use figment::{
|
||||
Figment, Provider,
|
||||
providers::{Format, Json, Toml, Yaml},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::env::Enviornment;
|
||||
use crate::{
|
||||
auth::AuthConfig, errors::RResult, extract::mikan::AppMikanConfig,
|
||||
graphql::config::GraphQLConfig, storage::StorageConfig,
|
||||
};
|
||||
|
||||
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)]
|
||||
pub struct AppConfig {
|
||||
pub auth: AuthConfig,
|
||||
pub dal: StorageConfig,
|
||||
pub mikan: AppMikanConfig,
|
||||
pub graphql: GraphQLConfig,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub fn config_prefix() -> String {
|
||||
format!("{}.config", env!("CARGO_PKG_NAME"))
|
||||
}
|
||||
|
||||
pub fn dotenv_prefix() -> String {
|
||||
String::from(".env")
|
||||
}
|
||||
|
||||
pub fn allowed_extension() -> Vec<String> {
|
||||
CONFIG_ALLOWED_EXTENSIONS
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect_vec()
|
||||
}
|
||||
|
||||
pub fn priority_suffix(enviornment: &Enviornment) -> Vec<String> {
|
||||
vec![
|
||||
format!(".{}.local", enviornment.full_name()),
|
||||
format!(".{}.local", enviornment.short_name()),
|
||||
String::from(".local"),
|
||||
enviornment.full_name().to_string(),
|
||||
enviornment.short_name().to_string(),
|
||||
String::from(""),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn default_provider() -> impl Provider {
|
||||
Toml::string(DEFAULT_CONFIG_MIXIN)
|
||||
}
|
||||
|
||||
pub fn merge_provider_from_file(
|
||||
fig: Figment,
|
||||
filepath: impl AsRef<Path>,
|
||||
ext: &str,
|
||||
) -> RResult<Figment> {
|
||||
let content = fs::read_to_string(filepath)?;
|
||||
|
||||
let rendered = tera::Tera::one_off(
|
||||
&content,
|
||||
&tera::Context::from_value(serde_json::json!({}))?,
|
||||
false,
|
||||
)?;
|
||||
|
||||
Ok(match ext {
|
||||
".toml" => fig.merge(Toml::string(&rendered)),
|
||||
".json" => fig.merge(Json::string(&rendered)),
|
||||
".yaml" | ".yml" => fig.merge(Yaml::string(&rendered)),
|
||||
_ => unreachable!("unsupported config extension"),
|
||||
})
|
||||
}
|
||||
}
|
||||
22
apps/recorder/src/app/context.rs
Normal file
22
apps/recorder/src/app/context.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use sea_orm::DatabaseConnection;
|
||||
|
||||
use super::config::AppConfig;
|
||||
use crate::{
|
||||
auth::AuthService, cache::CacheService, errors::RResult, extract::mikan::MikanClient,
|
||||
graphql::GraphQLService, storage::StorageService,
|
||||
};
|
||||
|
||||
pub struct AppContext {
|
||||
pub db: DatabaseConnection,
|
||||
pub config: AppConfig,
|
||||
pub cache: CacheService,
|
||||
pub mikan: MikanClient,
|
||||
pub auth: AuthService,
|
||||
pub graphql: GraphQLService,
|
||||
pub storage: StorageService,
|
||||
pub working_dir: String,
|
||||
}
|
||||
|
||||
pub async fn create_context(_config: AppConfig) -> RResult<AppContext> {
|
||||
todo!()
|
||||
}
|
||||
15
apps/recorder/src/app/core.rs
Normal file
15
apps/recorder/src/app/core.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{builder::AppBuilder, context::AppContext, router::AppRouter};
|
||||
|
||||
pub struct App {
|
||||
pub context: Arc<AppContext>,
|
||||
pub builder: AppBuilder,
|
||||
pub router: AppRouter,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn builder() -> AppBuilder {
|
||||
AppBuilder::default()
|
||||
}
|
||||
}
|
||||
23
apps/recorder/src/app/env.rs
Normal file
23
apps/recorder/src/app/env.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
pub enum Enviornment {
|
||||
Development,
|
||||
Production,
|
||||
Testing,
|
||||
}
|
||||
|
||||
impl Enviornment {
|
||||
pub fn full_name(&self) -> &'static str {
|
||||
match &self {
|
||||
Self::Development => "development",
|
||||
Self::Production => "production",
|
||||
Self::Testing => "testing",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short_name(&self) -> &'static str {
|
||||
match &self {
|
||||
Self::Development => "dev",
|
||||
Self::Production => "prod",
|
||||
Self::Testing => "test",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
use loco_rs::{app::AppContext, environment::Environment};
|
||||
|
||||
use crate::{
|
||||
auth::service::AppAuthService, dal::AppDalClient, extract::mikan::AppMikanClient,
|
||||
graphql::service::AppGraphQLService,
|
||||
};
|
||||
|
||||
pub trait AppContextExt {
|
||||
fn get_dal_client(&self) -> &AppDalClient {
|
||||
AppDalClient::app_instance()
|
||||
}
|
||||
|
||||
fn get_mikan_client(&self) -> &AppMikanClient {
|
||||
AppMikanClient::app_instance()
|
||||
}
|
||||
|
||||
fn get_auth_service(&self) -> &AppAuthService {
|
||||
AppAuthService::app_instance()
|
||||
}
|
||||
|
||||
fn get_graphql_service(&self) -> &AppGraphQLService {
|
||||
AppGraphQLService::app_instance()
|
||||
}
|
||||
|
||||
fn get_node_env(&self) -> Environment {
|
||||
let node_env = std::env::var("NODE_ENV");
|
||||
match node_env.as_deref() {
|
||||
Ok("production") => Environment::Production,
|
||||
_ => Environment::Development,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppContextExt for AppContext {}
|
||||
@@ -1,62 +1,33 @@
|
||||
pub mod ext;
|
||||
pub mod builder;
|
||||
pub mod config;
|
||||
pub mod context;
|
||||
pub mod core;
|
||||
pub mod env;
|
||||
pub mod router;
|
||||
|
||||
use std::{
|
||||
fs,
|
||||
path::{self, Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
pub use core::App;
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
pub use ext::AppContextExt;
|
||||
use itertools::Itertools;
|
||||
pub use context::AppContext;
|
||||
use loco_rs::{
|
||||
Result,
|
||||
app::{AppContext, Hooks},
|
||||
app::{AppContext as LocoAppContext, Hooks},
|
||||
boot::{BootResult, StartMode, create_app},
|
||||
cache,
|
||||
config::Config,
|
||||
controller::{AppRoutes, middleware, middleware::MiddlewareLayer},
|
||||
controller::AppRoutes,
|
||||
db::truncate_table,
|
||||
environment::Environment,
|
||||
prelude::*,
|
||||
task::Tasks,
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
use crate::{
|
||||
auth::service::AppAuthServiceInitializer,
|
||||
controllers::{self},
|
||||
dal::AppDalInitalizer,
|
||||
extract::mikan::client::AppMikanClientInitializer,
|
||||
graphql::service::AppGraphQLServiceInitializer,
|
||||
migrations::Migrator,
|
||||
models::subscribers,
|
||||
workers::subscription_worker::SubscriptionWorker,
|
||||
};
|
||||
use crate::{migrations::Migrator, models::subscribers};
|
||||
|
||||
pub const WORKING_ROOT_VAR_NAME: &str = "WORKING_ROOT";
|
||||
|
||||
static APP_WORKING_ROOT: OnceCell<quirks_path::PathBuf> = OnceCell::new();
|
||||
|
||||
pub struct App;
|
||||
|
||||
impl App {
|
||||
pub fn set_working_root(path: PathBuf) {
|
||||
APP_WORKING_ROOT.get_or_init(|| {
|
||||
quirks_path::PathBuf::from(path.as_os_str().to_string_lossy().to_string())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_working_root() -> &'static quirks_path::Path {
|
||||
APP_WORKING_ROOT
|
||||
.get()
|
||||
.map(|p| p.as_path())
|
||||
.expect("working root not set")
|
||||
}
|
||||
}
|
||||
pub struct App1;
|
||||
|
||||
#[async_trait]
|
||||
impl Hooks for App {
|
||||
impl Hooks for App1 {
|
||||
fn app_version() -> String {
|
||||
format!(
|
||||
"{} ({})",
|
||||
@@ -79,130 +50,28 @@ impl Hooks for App {
|
||||
create_app::<Self, Migrator>(mode, environment, config).await
|
||||
}
|
||||
|
||||
async fn load_config(env: &Environment) -> Result<Config> {
|
||||
let working_roots_to_search = [
|
||||
std::env::var(WORKING_ROOT_VAR_NAME).ok(),
|
||||
Some(String::from("./apps/recorder")),
|
||||
Some(String::from(".")),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect_vec();
|
||||
|
||||
for working_root in working_roots_to_search.iter() {
|
||||
let working_root = PathBuf::from(working_root);
|
||||
for env_file in [
|
||||
working_root.join(format!(".env.{env}.local")),
|
||||
working_root.join(format!(".env.{env}")),
|
||||
working_root.join(".env.local"),
|
||||
working_root.join(".env"),
|
||||
] {
|
||||
tracing::info!(env_file =? env_file);
|
||||
if env_file.exists() && env_file.is_file() {
|
||||
dotenv::from_path(&env_file).map_err(loco_rs::Error::wrap)?;
|
||||
tracing::info!("loaded env from {} success.", env_file.to_string_lossy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for working_root in working_roots_to_search.iter() {
|
||||
let working_root = PathBuf::from(working_root);
|
||||
let config_dir = working_root.as_path().join("config");
|
||||
|
||||
for config_file in [
|
||||
config_dir.join(format!("{env}.local.yaml")),
|
||||
config_dir.join(format!("{env}.yaml")),
|
||||
] {
|
||||
if config_file.exists() && config_file.is_file() {
|
||||
let content = fs::read_to_string(config_file.clone())?;
|
||||
|
||||
let rendered = tera::Tera::one_off(
|
||||
&content,
|
||||
&tera::Context::from_value(serde_json::json!({}))?,
|
||||
false,
|
||||
)?;
|
||||
|
||||
App::set_working_root(working_root);
|
||||
|
||||
let config_file = &config_file.to_string_lossy();
|
||||
|
||||
let res = serde_yaml::from_str(&rendered)
|
||||
.map_err(|err| loco_rs::Error::YAMLFile(err, config_file.to_string()))?;
|
||||
|
||||
tracing::info!("loading config from {} success", config_file);
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(loco_rs::Error::Message(format!(
|
||||
"no configuration file found in search paths: {}",
|
||||
working_roots_to_search
|
||||
.iter()
|
||||
.flat_map(|p| path::absolute(PathBuf::from(p)))
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.join(",")
|
||||
)))
|
||||
}
|
||||
|
||||
async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
|
||||
let initializers: Vec<Box<dyn Initializer>> = vec![
|
||||
Box::new(AppDalInitalizer),
|
||||
Box::new(AppMikanClientInitializer),
|
||||
Box::new(AppGraphQLServiceInitializer),
|
||||
Box::new(AppAuthServiceInitializer),
|
||||
];
|
||||
async fn initializers(_ctx: &LocoAppContext) -> Result<Vec<Box<dyn Initializer>>> {
|
||||
let initializers: Vec<Box<dyn Initializer>> = vec![];
|
||||
|
||||
Ok(initializers)
|
||||
}
|
||||
|
||||
fn routes(ctx: &AppContext) -> AppRoutes {
|
||||
let ctx = Arc::new(ctx.clone());
|
||||
fn routes(_ctx: &LocoAppContext) -> AppRoutes {
|
||||
AppRoutes::with_default_routes()
|
||||
.prefix("/api")
|
||||
.add_route(controllers::graphql::routes(ctx.clone()))
|
||||
}
|
||||
|
||||
fn middlewares(ctx: &AppContext) -> Vec<Box<dyn MiddlewareLayer>> {
|
||||
use loco_rs::controller::middleware::static_assets::{FolderConfig, StaticAssets};
|
||||
|
||||
let mut middlewares = middleware::default_middleware_stack(ctx);
|
||||
middlewares.push(Box::new(StaticAssets {
|
||||
enable: true,
|
||||
must_exist: true,
|
||||
folder: FolderConfig {
|
||||
uri: String::from("/api/static"),
|
||||
path: App::get_working_root().join("public").into(),
|
||||
},
|
||||
fallback: App::get_working_root()
|
||||
.join("public/assets/404.html")
|
||||
.into(),
|
||||
precompressed: false,
|
||||
}));
|
||||
middlewares
|
||||
}
|
||||
|
||||
async fn after_context(ctx: AppContext) -> Result<AppContext> {
|
||||
Ok(AppContext {
|
||||
cache: cache::Cache::new(cache::drivers::inmem::new()).into(),
|
||||
..ctx
|
||||
})
|
||||
}
|
||||
|
||||
async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {
|
||||
queue.register(SubscriptionWorker::build(ctx)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_tasks(_tasks: &mut Tasks) {}
|
||||
|
||||
async fn truncate(ctx: &AppContext) -> Result<()> {
|
||||
async fn truncate(ctx: &LocoAppContext) -> Result<()> {
|
||||
truncate_table(&ctx.db, subscribers::Entity).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn seed(_ctx: &AppContext, _base: &Path) -> Result<()> {
|
||||
async fn seed(_ctx: &LocoAppContext, _base: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn connect_workers(_ctx: &LocoAppContext, _queue: &Queue) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
31
apps/recorder/src/app/router.rs
Normal file
31
apps/recorder/src/app/router.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
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 })
|
||||
}
|
||||
Reference in New Issue
Block a user