use std::collections::HashMap;
use std::marker::PhantomData;
use std::sync::Arc;
use crate::{
app::App,
capability::{CapHookExt, Capability, HasCapTag},
tag::Tagged,
traits::add::{AddCapability, CapTagAbsent},
};
use anyhow::{Result, anyhow};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use frunk::{HCons, HNil, hlist::HList};
use sea_orm::DatabaseConnection;
pub struct SourceDocTag;
pub trait SourceDocInstance: Send + Sync {
fn source_doc_type(&self) -> &str;
fn source_doc_id(&self) -> i64;
fn display_name(&self) -> String;
fn detail_url(&self) -> String;
fn datetime(&self) -> DateTime<Utc>;
}
#[async_trait]
pub trait SourceDocType: Send + Sync {
fn source_doc_type(&self) -> &str;
fn display_name(&self) -> &str;
fn detail_url(&self, id: i64) -> String;
async fn load_from_id(
&self,
db: &DatabaseConnection,
id: i64,
) -> Result<Arc<dyn SourceDocInstance>>;
}
#[derive(Clone, Default)]
pub struct SourceDocRegistry {
types: HashMap<String, Arc<dyn SourceDocType>>,
}
impl SourceDocRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(mut self, loader: Arc<dyn SourceDocType>) -> Self {
let key = loader.source_doc_type().to_string();
self.types.entry(key).or_insert(loader);
self
}
pub fn get(&self, typ: &str) -> Option<Arc<dyn SourceDocType>> {
self.types.get(typ).cloned()
}
pub fn type_display_name(&self, typ: &str) -> String {
self.get(typ)
.map(|loader| loader.display_name().to_string())
.unwrap_or_else(|| humanize_type_name(typ))
}
pub fn type_detail_url(&self, typ: &str, id: i64) -> Option<String> {
self.get(typ).map(|loader| loader.detail_url(id))
}
pub async fn resolve_instance(
&self,
db: &DatabaseConnection,
typ: &str,
id: i64,
) -> Result<Arc<dyn SourceDocInstance>> {
if typ.is_empty() {
return Err(anyhow!(
"p_finance_accounts: ResolveSourceDocInstance: empty type"
));
}
let loader = self.get(typ).ok_or_else(|| {
anyhow!("p_finance_accounts: ResolveSourceDocInstance: unknown type {typ:?}")
})?;
let inst = loader.load_from_id(db, id).await?;
if inst.source_doc_type() != typ {
return Err(anyhow!(
"p_finance_accounts: ResolveSourceDocInstance: type mismatch: registry key {typ:?}, instance {:?}",
inst.source_doc_type()
));
}
Ok(inst)
}
}
pub trait SourceDocRegistrar: Sized {
fn register_source_docs(self, registry: SourceDocRegistry) -> SourceDocRegistry;
}
#[derive(Clone, Default)]
pub struct SourceDocCap<Hooks> {
pub hooks: Hooks,
pub items: SourceDocRegistry,
_tag: PhantomData<fn() -> SourceDocTag>,
}
impl<Hooks> SourceDocCap<Hooks> {
pub fn new() -> Self
where
Hooks: Default,
{
Self {
hooks: Hooks::default(),
items: SourceDocRegistry::new(),
_tag: PhantomData,
}
}
pub fn add_hook<HTag, H>(self, hook: H) -> SourceDocCap<HCons<Tagged<HTag, H>, Hooks>> {
SourceDocCap {
hooks: HCons {
head: Tagged::new(hook),
tail: self.hooks,
},
items: self.items,
_tag: PhantomData,
}
}
}
impl<Hooks> HasCapTag for SourceDocCap<Hooks> {
type Tag = SourceDocTag;
}
impl<Hooks, Plugin, Hook> CapHookExt<Plugin, Hook> for SourceDocCap<Hooks> {
type Hooked = SourceDocCap<HCons<Tagged<Plugin, Hook>, Hooks>>;
fn prepend_cap_hook(self, hook: Hook) -> Self::Hooked {
self.add_hook::<Plugin, Hook>(hook)
}
}
pub trait FoldSourceDocRegistrarHooks {
fn fold(self, registry: SourceDocRegistry) -> SourceDocRegistry;
}
impl FoldSourceDocRegistrarHooks for HNil {
fn fold(self, registry: SourceDocRegistry) -> SourceDocRegistry {
registry
}
}
impl<Plugin, H, Tail> FoldSourceDocRegistrarHooks for HCons<Tagged<Plugin, H>, Tail>
where
Tail: FoldSourceDocRegistrarHooks,
H: SourceDocRegistrar + Copy,
{
fn fold(self, registry: SourceDocRegistry) -> SourceDocRegistry {
let registry = self.tail.fold(registry);
self.head.value.register_source_docs(registry)
}
}
impl<Hooks> Capability for SourceDocCap<Hooks>
where
Hooks: FoldSourceDocRegistrarHooks,
{
type Value = SourceDocRegistry;
type Output = Tagged<SourceDocTag, SourceDocRegistry>;
type Hooks = Hooks;
type Items = SourceDocRegistry;
fn mount(self) -> Self::Output {
let registry = self.hooks.fold(self.items);
Tagged::new(registry)
}
}
#[derive(Clone, Copy, Default)]
pub struct BaseHook;
impl SourceDocRegistrar for BaseHook {
fn register_source_docs(self, registry: SourceDocRegistry) -> SourceDocRegistry {
registry
}
}
pub fn with_source_docs<L, Proof>(app: App<L>) -> App<HCons<SourceDocCap<HNil>, L>>
where
L: HList + CapTagAbsent<SourceDocTag, Proof>,
{
app.add_capability(SourceDocCap::<HNil>::new())
}
pub fn humanize_type_name(typ: &str) -> String {
let name = typ.rsplit('.').next().unwrap_or(typ);
let mut out = String::new();
for (i, ch) in name.chars().enumerate() {
if ch.is_uppercase() && i > 0 {
out.push(' ');
}
out.extend(ch.to_lowercase());
}
if out.is_empty() {
typ.to_string()
} else {
let mut chars = out.chars();
match chars.next() {
None => typ.to_string(),
Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_type_humanizes() {
assert_eq!(
humanize_type_name("p_example.SomeDocument"),
"Some document"
);
}
#[test]
fn type_display_name_falls_back() {
let reg = SourceDocRegistry::new();
assert_eq!(
reg.type_display_name("p_example.SomeDocument"),
"Some document"
);
}
}