use crate::ctx::{BoxFuture, Ctx, Dep};
use crate::i18n::Text;
use crate::model::ValidationErrors;
use crate::value::Value;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(tag = "type", content = "permission", rename_all = "snake_case")]
pub enum Access {
Public,
Permission(&'static str),
}
#[derive(Clone, Copy, Debug, Serialize)]
pub struct ActionMeta {
pub name: &'static str,
pub access: Access,
pub audit: bool,
pub reauth: bool,
pub doc: &'static str,
pub ai: Ai,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub struct Ai {
pub offered: bool,
pub hint: &'static str,
pub confirm: bool,
}
impl Ai {
pub const DEFAULT: Ai = Ai { offered: true, hint: "", confirm: false };
}
impl Default for Ai {
fn default() -> Self {
Ai::DEFAULT
}
}
pub trait Action: Copy + Send + Sync + 'static {
type Input: Send + 'static;
type Output: Send + 'static;
const META: ActionMeta;
fn deps() -> Vec<Dep>;
fn call(ctx: Ctx, input: Self::Input) -> BoxFuture<Result<Self::Output>>;
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(tag = "error", content = "detail", rename_all = "snake_case")]
pub enum Error {
Forbidden,
Denied(Text),
NotFound,
Conflict,
Validation(ValidationErrors),
Internal(String),
}
impl Error {
pub fn message(&self) -> Text {
match self {
Error::Forbidden => Text::key("zeyn.error.forbidden"),
Error::Denied(reason) => reason.clone(),
Error::NotFound => Text::key("zeyn.error.not_found"),
Error::Conflict => Text::key("zeyn.error.conflict"),
Error::Validation(e) => Text::from(e.to_string()),
Error::Internal(_) => Text::key("zeyn.error.internal"),
}
}
pub fn code(&self) -> &'static str {
match self {
Error::Forbidden | Error::Denied(_) => "forbidden",
Error::NotFound => "not_found",
Error::Conflict => "conflict",
Error::Validation(_) => "validation",
Error::Internal(_) => "internal",
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message().english())
}
}
impl std::error::Error for Error {}
impl From<ValidationErrors> for Error {
fn from(e: ValidationErrors) -> Self {
Error::Validation(e)
}
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
pub const MAX_PAGE_SIZE: u32 = 500;
pub const DEFAULT_PAGE_SIZE: u32 = 50;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Cursor(pub String);
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Page<M> {
pub items: Vec<M>,
pub total: Option<u64>,
pub next: Option<Cursor>,
#[serde(default, skip_deserializing, skip_serializing_if = "Vec::is_empty")]
pub totals: Vec<Total>,
#[serde(default, skip_deserializing, skip_serializing_if = "Vec::is_empty")]
pub groups: Vec<Group>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Group {
pub value: Value,
pub count: u64,
pub totals: Vec<Total>,
}
impl<M> Default for Page<M> {
fn default() -> Self {
Self { items: Vec::new(), total: None, next: None, totals: Vec::new(), groups: Vec::new() }
}
}
impl<M> Page<M> {
pub fn new(items: Vec<M>) -> Self {
Self { items, total: None, next: None, totals: Vec::new(), groups: Vec::new() }
}
pub fn with_total(mut self, field: impl Into<String>, op: Aggregate, value: Value) -> Self {
self.totals.push(Total { field: field.into(), op, value });
self
}
pub fn total(mut self, total: u64) -> Self {
self.total = Some(total);
self
}
pub fn next(mut self, cursor: Cursor) -> Self {
self.next = Some(cursor);
self
}
}
impl<M: Clone> Page<M> {
pub fn from_slice(all: &[M], query: &DataQuery) -> Self {
let limit = query.limit() as usize;
let start = match &query.page {
PageRequest::Offset { offset, .. } => usize::try_from(*offset).unwrap_or(usize::MAX),
PageRequest::After { cursor, .. } => cursor.as_ref().and_then(|c| c.0.parse().ok()).unwrap_or(0),
}
.min(all.len());
let end = start.saturating_add(limit).min(all.len());
let page = Page::new(all[start..end].to_vec()).total(all.len() as u64);
if end < all.len() { page.next(Cursor(end.to_string())) } else { page }
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PageRequest {
Offset { offset: u64, limit: u32 },
After { cursor: Option<Cursor>, limit: u32 },
}
impl Default for PageRequest {
fn default() -> Self {
PageRequest::Offset { offset: 0, limit: DEFAULT_PAGE_SIZE }
}
}
#[deprecated(since = "0.0.2", note = "renamed to `DataQuery`; `zeyn check --fix` rewrites it")]
pub type GridQuery = DataQuery;
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct DataQuery {
#[serde(default)]
pub page: PageRequest,
#[serde(default)]
pub sort: Vec<Sort>,
#[serde(default)]
pub filters: Vec<Filter>,
#[serde(default)]
pub with_total: bool,
#[serde(default)]
pub aggregates: Vec<AggregateRequest>,
#[serde(skip)]
pub time_zone: crate::time::TimeZone,
#[serde(skip)]
pub group: Option<String>,
}
impl DataQuery {
pub fn limit(&self) -> u32 {
let raw = match &self.page {
PageRequest::Offset { limit, .. } | PageRequest::After { limit, .. } => *limit,
};
raw.clamp(1, MAX_PAGE_SIZE)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Aggregate {
Sum,
Avg,
Min,
Max,
Count,
}
impl Aggregate {
pub fn code(self) -> &'static str {
match self {
Aggregate::Sum => "sum",
Aggregate::Avg => "avg",
Aggregate::Min => "min",
Aggregate::Max => "max",
Aggregate::Count => "count",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AggregateRequest {
pub field: String,
pub op: Aggregate,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Total {
pub field: String,
pub op: Aggregate,
pub value: Value,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Sort {
pub field: String,
pub desc: bool,
}
pub const MAX_FILTERS: usize = 32;
pub const MAX_FILTER_TEXT: usize = 200;
pub const MAX_FILTER_CHOICES: usize = 100;
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Filter {
pub field: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub contains: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub starts_with: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub equals: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub from: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub to: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub any_of: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blank: Option<bool>,
}
impl Filter {
pub fn contains(field: impl Into<String>, text: impl Into<String>) -> Self {
Self { field: field.into(), contains: text.into(), ..Self::default() }
}
pub fn range(field: impl Into<String>, from: Option<&str>, to: Option<&str>) -> Self {
Self { field: field.into(), from: from.map(Into::into), to: to.map(Into::into), ..Self::default() }
}
pub fn any_of<I, S>(field: impl Into<String>, values: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self { field: field.into(), any_of: values.into_iter().map(Into::into).collect(), ..Self::default() }
}
pub fn is(field: impl Into<String>, value: bool) -> Self {
Self { field: field.into(), is: Some(value), ..Self::default() }
}
pub fn blank(field: impl Into<String>, empty: bool) -> Self {
Self { field: field.into(), blank: Some(empty), ..Self::default() }
}
pub fn is_empty(&self) -> bool {
let blank = |s: &Option<String>| s.as_deref().is_none_or(|v| v.trim().is_empty());
self.contains.trim().is_empty()
&& blank(&self.starts_with)
&& blank(&self.equals)
&& blank(&self.from)
&& blank(&self.to)
&& self.any_of.is_empty()
&& self.is.is_none()
&& self.blank.is_none()
}
}