use std::collections::HashMap;
use std::future::Future;
use std::sync::Arc;
use async_trait::async_trait;
use futures_util::future::BoxFuture;
use serde::Serialize;
use crate::error::AdminError;
pub type Row = serde_json::Map<String, serde_json::Value>;
pub type Input = HashMap<String, Option<String>>;
pub(crate) type FormData = HashMap<String, String>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteMode {
Replace,
Patch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum FieldKind {
Text,
LongText,
Integer,
Float,
Boolean,
DateTime,
Date,
Time,
Json,
Uuid,
Decimal,
Choice,
List,
Other,
}
#[derive(Debug, Clone, Serialize)]
pub struct Field {
pub name: String,
pub label: String,
pub kind: FieldKind,
pub nullable: bool,
pub primary_key: bool,
pub auto: bool,
pub editable: bool,
pub has_default: bool,
pub reference: Option<Reference>,
pub choices: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Reference {
pub model: String,
pub column: String,
}
type ActionFn =
Arc<dyn Fn(Vec<String>) -> BoxFuture<'static, Result<(), AdminError>> + Send + Sync>;
#[derive(Clone)]
pub(crate) enum ActionKind {
Set { field: String, value: String },
Custom(ActionFn),
}
#[derive(Clone)]
pub(crate) struct BulkAction {
pub(crate) name: String,
pub(crate) label: String,
pub(crate) kind: ActionKind,
}
impl std::fmt::Debug for BulkAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BulkAction")
.field("name", &self.name)
.field("label", &self.label)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Default)]
pub struct ModelOptions {
pub(crate) label: Option<String>,
pub(crate) list_display: Vec<String>,
pub(crate) search_fields: Vec<String>,
pub(crate) readonly_fields: Vec<String>,
pub(crate) hidden_fields: Vec<String>,
pub(crate) per_page: Option<u64>,
pub(crate) display_field: Option<String>,
pub(crate) list_filter: Vec<String>,
pub(crate) actions: Vec<BulkAction>,
pub(crate) file_fields: Vec<String>,
pub(crate) choices: Vec<(String, Vec<String>)>,
pub(crate) many_to_many: Vec<ManyToMany>,
pub(crate) owner_field: Option<String>,
}
#[derive(Clone)]
pub(crate) struct ManyToMany {
pub(crate) name: String,
pub(crate) label: String,
pub(crate) link: Arc<dyn crate::seaorm::LinkTable>,
}
impl std::fmt::Debug for ManyToMany {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ManyToMany")
.field("name", &self.name)
.field("target", &self.link.target())
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct LinkField {
pub name: String,
pub label: String,
pub model: String,
}
impl ModelOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn list_display<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.list_display = columns.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn search_fields<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.search_fields = columns.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn readonly_fields<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.readonly_fields = columns.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn hidden_fields<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.hidden_fields = columns.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn display_field(mut self, column: impl Into<String>) -> Self {
self.display_field = Some(column.into());
self
}
#[must_use]
pub fn list_filter<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.list_filter = columns.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn set_action(
mut self,
name: impl Into<String>,
label: impl Into<String>,
field: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.actions.push(BulkAction {
name: name.into(),
label: label.into(),
kind: ActionKind::Set {
field: field.into(),
value: value.into(),
},
});
self
}
#[must_use]
pub fn action<F, Fut>(
mut self,
name: impl Into<String>,
label: impl Into<String>,
run: F,
) -> Self
where
F: Fn(Vec<String>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<(), AdminError>> + Send + 'static,
{
let run: ActionFn = Arc::new(move |ids| Box::pin(run(ids)));
self.actions.push(BulkAction {
name: name.into(),
label: label.into(),
kind: ActionKind::Custom(run),
});
self
}
#[must_use]
pub fn file_fields<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.file_fields = columns.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn choices<I, S>(mut self, column: impl Into<String>, values: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.choices
.push((column.into(), values.into_iter().map(Into::into).collect()));
self
}
#[must_use]
pub fn owned_by(mut self, column: impl Into<String>) -> Self {
self.owner_field = Some(column.into());
self
}
#[must_use]
pub fn per_page(mut self, per_page: u64) -> Self {
self.per_page = Some(per_page.max(1));
self
}
}
#[derive(Debug, Clone)]
pub struct ListQuery {
pub page: u64,
pub per_page: u64,
pub search: Option<String>,
pub sort: Option<String>,
pub descending: bool,
pub filters: Vec<Filter>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FilterOp {
Equal,
Greater,
GreaterOrEqual,
Less,
LessOrEqual,
IsNull,
IsNotNull,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Filter {
pub field: String,
pub op: FilterOp,
pub value: String,
}
impl ListQuery {
#[must_use]
pub fn first(per_page: u64) -> Self {
Self {
page: 0,
per_page,
search: None,
sort: None,
descending: false,
filters: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Page {
pub rows: Vec<Row>,
pub total: u64,
}
#[async_trait]
pub trait Resource: Send + Sync {
fn slug(&self) -> &str;
fn label(&self) -> &str;
fn fields(&self) -> &[Field];
fn options(&self) -> &ModelOptions;
async fn list(&self, query: &ListQuery) -> Result<Page, AdminError>;
async fn get(&self, id: &str) -> Result<Option<Row>, AdminError>;
async fn get_many(&self, ids: &[String]) -> Result<Vec<Row>, AdminError> {
let mut rows = Vec::with_capacity(ids.len());
for id in ids {
rows.extend(self.get(id).await?);
}
Ok(rows)
}
async fn distinct(&self, _field: &str, _limit: u64) -> Result<Vec<String>, AdminError> {
Ok(Vec::new())
}
async fn create(&self, input: &Input) -> Result<Option<Row>, AdminError>;
async fn update(&self, id: &str, input: &Input, mode: WriteMode) -> Result<(), AdminError>;
async fn delete(&self, id: &str) -> Result<(), AdminError>;
fn links(&self) -> Vec<LinkField> {
Vec::new()
}
}
pub(crate) fn humanize(name: &str) -> String {
let spaced = name.replace('_', " ");
let mut chars = spaced.chars();
match chars.next() {
Some(first) => first.to_uppercase().chain(chars).collect(),
None => spaced,
}
}