use std::sync::OnceLock;
use async_trait::async_trait;
use axum::{
body::Bytes,
extract::{FromRequest, Request},
http::{StatusCode, header},
};
use maud::Markup;
use sea_orm::DatabaseConnection;
use serde::de::DeserializeOwned;
use crate::html_form::{FormError, UrlencodedFields};
use crate::plugins::finance_accounts::forms::AccountingPreferencesForm;
static ADDONS: OnceLock<Vec<&'static dyn AccountingPreferencesAddon>> = OnceLock::new();
pub fn str_to_opt_i64(s: &str) -> Option<i64> {
let s = s.trim();
if s.is_empty() { None } else { s.parse().ok() }
}
pub fn str_to_opt_string(s: &str) -> Option<String> {
if s.is_empty() {
None
} else {
Some(s.to_string())
}
}
#[derive(Debug, Clone)]
pub struct AccountingPreferencesPost {
fields: UrlencodedFields,
}
impl AccountingPreferencesPost {
pub fn accounts(&self) -> Result<AccountingPreferencesForm, FormError> {
self.fields.deserialize()
}
pub fn deserialize<T: DeserializeOwned>(&self) -> Result<T, FormError> {
self.fields.deserialize()
}
}
impl<S> FromRequest<S> for AccountingPreferencesPost
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let content_type = req
.headers()
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if !content_type.starts_with("application/x-www-form-urlencoded") {
return Err((
StatusCode::UNSUPPORTED_MEDIA_TYPE,
"Expected `application/x-www-form-urlencoded` request body".into(),
));
}
let bytes = Bytes::from_request(req, state)
.await
.map_err(|err| (StatusCode::BAD_REQUEST, err.to_string()))?;
let fields = UrlencodedFields::parse(&bytes).map_err(|err| {
(
StatusCode::BAD_REQUEST,
format!("Failed to parse form body: {err}"),
)
})?;
Ok(Self { fields })
}
}
#[async_trait]
pub trait AccountingPreferencesAddon: Send + Sync {
fn id(&self) -> &'static str;
async fn render_inputs(&self, db: &DatabaseConnection) -> Markup;
async fn save_from_form(
&self,
db: &DatabaseConnection,
post: &AccountingPreferencesPost,
) -> Result<(), String>;
}
#[derive(Clone, Default)]
pub struct AccountingPreferencesRegistry {
addons: Vec<&'static dyn AccountingPreferencesAddon>,
}
impl AccountingPreferencesRegistry {
pub fn new() -> Self {
Self { addons: Vec::new() }
}
pub fn register_addon(mut self, addon: &'static dyn AccountingPreferencesAddon) -> Self {
let id = addon.id();
if !self.addons.iter().any(|a| a.id() == id) {
self.addons.push(addon);
}
self
}
pub fn addons(&self) -> &[&'static dyn AccountingPreferencesAddon] {
&self.addons
}
}
pub(crate) fn store_accounting_preferences_addons(registry: &AccountingPreferencesRegistry) {
if ADDONS.set(registry.addons.clone()).is_err() {
tracing::error!("accounting preferences ADDONS already initialized");
}
}
pub fn accounting_preferences_addons() -> &'static [&'static dyn AccountingPreferencesAddon] {
ADDONS.get().map(|v| v.as_slice()).unwrap_or(&[])
}
pub async fn render_accounting_preferences_addons(db: &DatabaseConnection) -> Markup {
let mut out = Markup::default();
for addon in accounting_preferences_addons() {
let section = addon.render_inputs(db).await;
out = maud::html! { (out) (section) };
}
out
}
pub async fn save_accounting_preferences_addons(
db: &DatabaseConnection,
post: &AccountingPreferencesPost,
) -> Result<(), String> {
for addon in accounting_preferences_addons() {
addon.save_from_form(db, post).await?;
}
Ok(())
}