use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
#[cfg(any(not(feature = "server"), feature = "postgres", feature = "sqlite"))]
use crate::Kind;
use crate::Operation;
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct Notice {
pub title: String,
pub body: String,
pub link: Option<String>,
pub action: Option<NoticeAction>,
pub kind: NoticeKind,
}
impl Notice {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
..Self::default()
}
}
#[must_use]
pub fn body(mut self, body: impl Into<String>) -> Self {
self.body = body.into();
self
}
#[must_use]
pub fn link(mut self, link: impl Into<String>) -> Self {
self.link = Some(link.into());
self
}
#[must_use]
pub fn action(mut self, action: NoticeAction) -> Self {
self.action = Some(action);
self
}
#[must_use]
pub fn kind(mut self, kind: NoticeKind) -> Self {
self.kind = kind;
self
}
}
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum NoticeKind {
#[default]
Info,
Success,
Warning,
Error,
}
impl NoticeKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Info => "info",
Self::Success => "success",
Self::Warning => "warning",
Self::Error => "error",
}
}
#[must_use]
pub fn parse(name: &str) -> Self {
match name {
"success" => Self::Success,
"warning" => Self::Warning,
"error" => Self::Error,
_ => Self::Info,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct NoticeAction {
pub label: String,
pub operation: String,
pub arguments: Value,
}
impl NoticeAction {
#[expect(
clippy::needless_pass_by_value,
reason = "an action is written `NoticeAction::new(\"Retry\", Op { … })`: the arguments \
are built for it"
)]
pub fn new<O: Operation>(label: impl Into<String>, operation: O) -> Self {
Self {
label: label.into(),
operation: O::NAME.to_owned(),
arguments: serde_json::to_value(&operation)
.expect("an operation's arguments serialize, as they must to be sent"),
}
}
#[must_use]
pub fn decode<O: Operation>(&self) -> Option<O> {
if self.operation != O::NAME {
return None;
}
serde_json::from_value(self.arguments.clone()).ok()
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct InboxNotice {
pub id: String,
#[serde(flatten)]
pub notice: Notice,
pub read: bool,
pub created_at: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct Inbox {
pub notices: Vec<InboxNotice>,
pub unread: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct InboxArgs {
pub limit: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct MarkRead {
pub id: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct MarkAllRead {}
#[cfg(any(not(feature = "server"), feature = "postgres", feature = "sqlite"))]
impl Operation for InboxArgs {
#[cfg(any(feature = "postgres", feature = "sqlite"))]
type Context = crate::QueryCx;
type Output = Inbox;
const KIND: Kind = Kind::Query;
const NAME: &'static str = "rpc.inbox";
#[cfg(any(feature = "postgres", feature = "sqlite"))]
fn run(self, cx: &Self::Context) -> impl Future<Output = crate::Result<Self::Output>> + Send {
crate::server::inbox::read(cx, self.limit)
}
}
#[cfg(any(not(feature = "server"), feature = "postgres", feature = "sqlite"))]
impl Operation for MarkRead {
#[cfg(any(feature = "postgres", feature = "sqlite"))]
type Context = crate::MutationCx;
type Output = ();
const KIND: Kind = Kind::Mutation;
const NAME: &'static str = "rpc.mark_read";
#[cfg(any(feature = "postgres", feature = "sqlite"))]
fn run(self, cx: &Self::Context) -> impl Future<Output = crate::Result<Self::Output>> + Send {
crate::server::inbox::mark_read(cx, self.id)
}
}
#[cfg(any(not(feature = "server"), feature = "postgres", feature = "sqlite"))]
impl Operation for MarkAllRead {
#[cfg(any(feature = "postgres", feature = "sqlite"))]
type Context = crate::MutationCx;
type Output = u64;
const KIND: Kind = Kind::Mutation;
const NAME: &'static str = "rpc.mark_all_read";
#[cfg(any(feature = "postgres", feature = "sqlite"))]
fn run(self, cx: &Self::Context) -> impl Future<Output = crate::Result<Self::Output>> + Send {
crate::server::inbox::mark_all_read(cx)
}
}
#[cfg(any(feature = "postgres", feature = "sqlite"))]
use std::future::Future;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_notice_builds_and_its_action_decodes() {
let notice = Notice::new("Payment failed")
.body("The fee was too low.")
.link("/payments/1")
.action(NoticeAction::new("Retry", MarkRead { id: "n1".into() }))
.kind(NoticeKind::Error);
assert_eq!(notice.title, "Payment failed");
assert_eq!(notice.link.as_deref(), Some("/payments/1"));
let action = notice.action.as_ref().unwrap();
assert_eq!(action.operation, "rpc.mark_read");
assert_eq!(
action.decode::<MarkRead>(),
Some(MarkRead { id: "n1".into() })
);
assert_eq!(action.decode::<MarkAllRead>(), None);
for kind in [
NoticeKind::Info,
NoticeKind::Success,
NoticeKind::Warning,
NoticeKind::Error,
] {
assert_eq!(NoticeKind::parse(kind.as_str()), kind);
}
assert_eq!(NoticeKind::parse("shout"), NoticeKind::Info);
}
}