Initial commit
This commit is contained in:
commit
bd9b07052b
81 changed files with 5516 additions and 0 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
|||
use flake .
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
.direnv
|
||||
Cargo.lock
|
32
Cargo.toml
Normal file
32
Cargo.toml
Normal file
|
@ -0,0 +1,32 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"core/",
|
||||
"http/"
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.dependencies]
|
||||
eva = { git = "ssh://forgejo@git.viende.su:61488/VienDesu/eva.git" }
|
||||
|
||||
futures = "0.3.31"
|
||||
tokio = "1.44.2"
|
||||
eyre = "0.6.12"
|
||||
|
||||
serde_json = "1.0.137"
|
||||
serde_with = { version = "3.12.0", default-features = false, features = ["alloc", "macros"] }
|
||||
serde = "1"
|
||||
schemars = { git = "ssh://forgejo@git.viende.su:61488/VienDesu/schemars.git", features = [
|
||||
"compact_str08",
|
||||
"smallvec1",
|
||||
"bytes1",
|
||||
"url2",
|
||||
"hashbrown015"
|
||||
] }
|
||||
|
||||
viendesu-core = { path = "core/" }
|
||||
|
||||
# TODO: upstream.
|
||||
[patch.crates-io]
|
||||
schemars = { git = "ssh://forgejo@git.viende.su:61488/VienDesu/schemars.git", version = "1.0.0-alpha.17" }
|
||||
|
||||
|
21
core/Cargo.toml
Normal file
21
core/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "viendesu-core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
test-util = ["dep:tokio"]
|
||||
|
||||
[dependencies]
|
||||
eva.workspace = true
|
||||
serde.workspace = true
|
||||
schemars.workspace = true
|
||||
|
||||
eyre.workspace = true
|
||||
|
||||
serde_with.workspace = true
|
||||
futures.workspace = true
|
||||
lazy_static = "1.5.0"
|
||||
|
||||
tokio = { workspace = true, features = ["rt"], optional = true }
|
141
core/src/errors.rs
Normal file
141
core/src/errors.rs
Normal file
|
@ -0,0 +1,141 @@
|
|||
use eva::{data, str::CompactString};
|
||||
use std::fmt;
|
||||
|
||||
pub mod ext;
|
||||
|
||||
pub mod boards;
|
||||
pub mod messages;
|
||||
pub mod threads;
|
||||
|
||||
pub mod auth;
|
||||
pub mod authors;
|
||||
pub mod games;
|
||||
pub mod uploads;
|
||||
pub mod users;
|
||||
|
||||
pub trait FromReport {
|
||||
fn from_report(report: eyre::Report) -> Self;
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! bail {
|
||||
($($tt:tt)*) => {
|
||||
return ::core::result::Result::Err($crate::report!($($tt)*))
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! report {
|
||||
($($tt:tt)*) => {
|
||||
$crate::errors::FromReport::from_report($crate::_priv::eyre::eyre!($($tt)*))
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! mk_error {
|
||||
($ident:ident) => {
|
||||
#[derive(Debug)]
|
||||
#[repr(transparent)]
|
||||
pub struct $ident($crate::_priv::eyre::Report);
|
||||
|
||||
const _: () = {
|
||||
use std::error::Error as StdError;
|
||||
use $crate::{_priv::eyre, errors::FromReport};
|
||||
|
||||
impl $ident {
|
||||
pub fn map_report(self, f: impl FnOnce(eyre::Report) -> eyre::Report) -> Self {
|
||||
Self(f(self.0))
|
||||
}
|
||||
|
||||
pub const fn from_report(report: eyre::Report) -> Self {
|
||||
Self(report)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromReport for $ident {
|
||||
fn from_report(report: eyre::Report) -> Self {
|
||||
Self(report)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$ident> for eyre::Report {
|
||||
fn from(value: $ident) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: StdError + Send + Sync + 'static> From<E> for $ident {
|
||||
#[track_caller]
|
||||
fn from(value: E) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
pub type Result<O, E> = ::core::result::Result<O, Generic<E>>;
|
||||
pub type AuxResult<O> = ::core::result::Result<O, Aux>;
|
||||
|
||||
#[data(copy, ord, display("impossible"), error)]
|
||||
#[derive(Hash)]
|
||||
pub enum Impossible {}
|
||||
|
||||
/// Error from auxiliary system.
|
||||
#[data(error)]
|
||||
pub enum Aux {
|
||||
/// Database failure.
|
||||
#[display("{_0}")]
|
||||
Db(CompactString),
|
||||
/// Captcha failure.
|
||||
#[display("{_0}")]
|
||||
Captcha(CompactString),
|
||||
/// Mail failure.
|
||||
#[display("{_0}")]
|
||||
Mail(CompactString),
|
||||
/// Authentication failure.
|
||||
#[display("unauthenticated")]
|
||||
Unauthenticated,
|
||||
#[display("{_0}")]
|
||||
InvalidSession(#[from] auth::InvalidSession),
|
||||
/// Authorization failure.
|
||||
#[display("{_0}")]
|
||||
InvalidRole(#[from] auth::InvalidRole),
|
||||
/// Internal failure.
|
||||
#[display("{_0}")]
|
||||
InternalError(CompactString),
|
||||
/// object store failure.
|
||||
#[display("{_0}")]
|
||||
ObjectStore(CompactString),
|
||||
#[display("{_0}")]
|
||||
Deserialization(String),
|
||||
}
|
||||
|
||||
/// Generic error.
|
||||
#[data]
|
||||
#[serde(untagged)]
|
||||
pub enum Generic<S> {
|
||||
/// Auxiliary system failure.
|
||||
Aux(#[from] Aux),
|
||||
/// Specific for this handler error.
|
||||
Spec(S),
|
||||
}
|
||||
|
||||
impl<S> From<Impossible> for Generic<S> {
|
||||
fn from(value: Impossible) -> Self {
|
||||
match value {}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add ability to specify bounds for error.
|
||||
impl<S: fmt::Display + std::error::Error> std::error::Error for Generic<S> {}
|
||||
|
||||
// TODO: add ability to specify bounds in display attr.
|
||||
impl<S: fmt::Display> fmt::Display for Generic<S> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Aux(aux) => aux.fmt(f),
|
||||
Self::Spec(spec) => spec.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
18
core/src/errors/auth.rs
Normal file
18
core/src/errors/auth.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use eva::data;
|
||||
|
||||
use crate::types::{session, user::Role};
|
||||
|
||||
#[data(
|
||||
error,
|
||||
display(
|
||||
"invalid role, it's required to be at least {required_at_least} to perform this action"
|
||||
)
|
||||
)]
|
||||
pub struct InvalidRole {
|
||||
pub required_at_least: Role,
|
||||
}
|
||||
|
||||
#[data(error, display("session {token} does not exists or is expired"))]
|
||||
pub struct InvalidSession {
|
||||
pub token: session::Token,
|
||||
}
|
16
core/src/errors/authors.rs
Normal file
16
core/src/errors/authors.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
use eva::data;
|
||||
|
||||
use crate::types::author::{Selector, Slug};
|
||||
|
||||
#[data(error, display("this action will orphan games belonged to the author"))]
|
||||
pub struct WillOrphanGames;
|
||||
|
||||
#[data(error, copy, display("the {author} was not found"))]
|
||||
pub struct NotFound {
|
||||
pub author: Selector,
|
||||
}
|
||||
|
||||
#[data(error, display("@{slug} already exists"))]
|
||||
pub struct AlreadyExists {
|
||||
pub slug: Slug,
|
||||
}
|
14
core/src/errors/boards.rs
Normal file
14
core/src/errors/boards.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
use crate::types::board;
|
||||
|
||||
use eva::data;
|
||||
|
||||
#[data(error)]
|
||||
pub enum AlreadyExists {
|
||||
#[display("the slug {_0} already exists")]
|
||||
Slug(#[from] board::Slug),
|
||||
}
|
||||
|
||||
#[data(error, display("the {board} was not found"))]
|
||||
pub struct NotFound {
|
||||
pub board: board::Selector,
|
||||
}
|
65
core/src/errors/ext.rs
Normal file
65
core/src/errors/ext.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use crate::{
|
||||
errors::{Aux, Generic, auth::InvalidRole},
|
||||
types::user::Role,
|
||||
};
|
||||
|
||||
pub trait AuthzRoleExt {
|
||||
fn require_at_least(self, role: Role) -> Result<(), InvalidRole>;
|
||||
}
|
||||
|
||||
impl AuthzRoleExt for Role {
|
||||
fn require_at_least(self, role: Role) -> Result<(), InvalidRole> {
|
||||
if self >= role {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(InvalidRole {
|
||||
required_at_least: role,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ResultExt: Sized {
|
||||
type Ok;
|
||||
type Err;
|
||||
|
||||
fn aux_err(self) -> Result<Self::Ok, Aux>
|
||||
where
|
||||
Self::Err: Into<Aux>;
|
||||
}
|
||||
|
||||
impl<O, E> ResultExt for Result<O, E> {
|
||||
type Ok = O;
|
||||
type Err = E;
|
||||
|
||||
#[track_caller]
|
||||
fn aux_err(self) -> Result<O, Aux>
|
||||
where
|
||||
E: Into<Aux>,
|
||||
{
|
||||
match self {
|
||||
Self::Ok(o) => Ok(o),
|
||||
Self::Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ErrExt: Sized {
|
||||
#[track_caller]
|
||||
fn aux(self) -> Aux
|
||||
where
|
||||
Self: Into<Aux>,
|
||||
{
|
||||
self.into()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn spec<S>(self) -> Generic<S>
|
||||
where
|
||||
S: From<Self>,
|
||||
{
|
||||
Generic::Spec(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ErrExt for T {}
|
19
core/src/errors/games.rs
Normal file
19
core/src/errors/games.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use eva::data;
|
||||
|
||||
use crate::types::{author, game};
|
||||
|
||||
#[data(error, display("{author} is not an owner of the {game}"))]
|
||||
pub struct NotAnOwner {
|
||||
pub author: author::Id,
|
||||
pub game: game::Id,
|
||||
}
|
||||
|
||||
#[data(error, display("the @{slug} is already taken for that author"))]
|
||||
pub struct AlreadyTaken {
|
||||
pub slug: game::Slug,
|
||||
}
|
||||
|
||||
#[data(error, copy, display("the {game} was not found"))]
|
||||
pub struct NotFound {
|
||||
pub game: game::Selector,
|
||||
}
|
8
core/src/errors/messages.rs
Normal file
8
core/src/errors/messages.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
use eva::data;
|
||||
|
||||
use crate::types::message;
|
||||
|
||||
#[data(error, display("message {what} was not found"))]
|
||||
pub struct NotFound {
|
||||
pub what: message::Selector,
|
||||
}
|
13
core/src/errors/threads.rs
Normal file
13
core/src/errors/threads.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use eva::data;
|
||||
|
||||
use crate::types::thread;
|
||||
|
||||
#[data(error, display("thread {what} was not found"))]
|
||||
pub struct NotFound {
|
||||
pub what: thread::Selector,
|
||||
}
|
||||
|
||||
#[data(error, copy, display("you don't own the {thread} thread"))]
|
||||
pub struct NotAnOwner {
|
||||
pub thread: thread::Id,
|
||||
}
|
51
core/src/errors/uploads.rs
Normal file
51
core/src/errors/uploads.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
use eva::{bytesize::ByteSize, data};
|
||||
|
||||
use crate::types::{file, upload};
|
||||
|
||||
#[data(error, copy, display("someone is finishing file already"))]
|
||||
#[derive(Default)]
|
||||
pub enum ConcurrentUploadInProgress {
|
||||
#[serde(rename = "concurrent_upload_in_progress")]
|
||||
#[default]
|
||||
Value,
|
||||
}
|
||||
|
||||
#[data(error, display("hash mismatch, expected: {expected}, got: {got}"))]
|
||||
pub struct HashMismatch {
|
||||
pub expected: file::Hash,
|
||||
pub got: file::Hash,
|
||||
}
|
||||
|
||||
#[data(
|
||||
error,
|
||||
copy,
|
||||
display("uploading more than requested ({expected} bytes)")
|
||||
)]
|
||||
pub struct Overuploading {
|
||||
pub expected: u64,
|
||||
}
|
||||
|
||||
#[data(error, copy, display("simultaneous upload quota exceeded: {}/{}", ByteSize::b(*in_progress), ByteSize::b(*quota)))]
|
||||
pub struct SimUpQuotaExceeded {
|
||||
pub in_progress: u64,
|
||||
pub quota: u64,
|
||||
}
|
||||
|
||||
#[data(
|
||||
error,
|
||||
copy,
|
||||
display(
|
||||
"file uploading quota exceeded({}/{})",
|
||||
ByteSize::b(*uploaded),
|
||||
ByteSize::b(*quota)
|
||||
)
|
||||
)]
|
||||
pub struct QuotaExceeded {
|
||||
pub uploaded: u64,
|
||||
pub quota: u64,
|
||||
}
|
||||
|
||||
#[data(error, copy, display("{id} was not found"))]
|
||||
pub struct NotFound {
|
||||
pub id: upload::Id,
|
||||
}
|
37
core/src/errors/users.rs
Normal file
37
core/src/errors/users.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
use eva::data;
|
||||
|
||||
use crate::types::user;
|
||||
|
||||
#[data(copy, display(name))]
|
||||
pub enum WhatTaken {
|
||||
Nickname,
|
||||
Email,
|
||||
}
|
||||
|
||||
#[data(error, display("passed invalid sign up token"))]
|
||||
pub struct InvalidSignUpToken;
|
||||
|
||||
#[data(error, display("the {what} is already taken"))]
|
||||
pub struct AlreadyTaken {
|
||||
pub what: WhatTaken,
|
||||
}
|
||||
|
||||
#[data(
|
||||
error,
|
||||
display("sign up for specified user was already completed or the user was not found")
|
||||
)]
|
||||
pub struct NotFoundOrCompleted;
|
||||
|
||||
#[data(error, display("user {user} was not found"))]
|
||||
pub struct NotFound {
|
||||
pub user: user::Selector,
|
||||
}
|
||||
|
||||
#[data(error, display("invalid password specified"))]
|
||||
pub struct InvalidPassword;
|
||||
|
||||
#[data(error, display("the session is required for this request"))]
|
||||
pub struct NoSession;
|
||||
|
||||
#[data(error, display("you must complete sign up first"))]
|
||||
pub struct MustCompleteSignUp;
|
16
core/src/lib.rs
Normal file
16
core/src/lib.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
pub mod requests;
|
||||
pub mod types;
|
||||
pub mod uploads;
|
||||
|
||||
pub mod service;
|
||||
|
||||
pub mod errors;
|
||||
pub mod world;
|
||||
|
||||
pub mod rt;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub mod _priv {
|
||||
pub use eva::paste;
|
||||
pub use eyre;
|
||||
}
|
93
core/src/requests/authors.rs
Normal file
93
core/src/requests/authors.rs
Normal file
|
@ -0,0 +1,93 @@
|
|||
use eva::data;
|
||||
|
||||
use crate::{
|
||||
errors::{
|
||||
authors::{AlreadyExists, NotFound},
|
||||
users as users_errors,
|
||||
},
|
||||
types::{Patch, True, author, file, user},
|
||||
};
|
||||
|
||||
pub mod update {
|
||||
///! # Update author.
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
/// Who to update.
|
||||
pub author: author::Selector,
|
||||
/// Specific update to apply.
|
||||
pub update: Update,
|
||||
}
|
||||
|
||||
#[serde_with::apply(Patch => #[serde(default)])]
|
||||
#[data]
|
||||
pub struct Update {
|
||||
pub title: Patch<author::Title>,
|
||||
pub description: Patch<Option<author::Description>>,
|
||||
pub pfp: Patch<Option<file::Id>>,
|
||||
pub slug: Patch<author::Slug>,
|
||||
pub verified: Patch<bool>,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok(pub True);
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NotFound(#[from] NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod get {
|
||||
///! # Get author by selector.
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub author: author::Selector,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok {
|
||||
pub author: author::Author,
|
||||
}
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NotFound(#[from] NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod create {
|
||||
///! # Create author.
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub title: author::Title,
|
||||
pub slug: author::Slug,
|
||||
pub description: Option<author::Description>,
|
||||
/// Owner of the author, `None` for current user. Creating
|
||||
/// authors for different user than currently authenticated
|
||||
/// requires at least `admin` role.
|
||||
pub owner: Option<user::Id>,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok {
|
||||
pub id: author::Id,
|
||||
}
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NotFound(#[from] NotFound),
|
||||
#[display("{_0}")]
|
||||
AlreadyExists(#[from] AlreadyExists),
|
||||
#[display("{_0}")]
|
||||
NoSuchUser(#[from] users_errors::NotFound),
|
||||
}
|
||||
}
|
86
core/src/requests/boards.rs
Normal file
86
core/src/requests/boards.rs
Normal file
|
@ -0,0 +1,86 @@
|
|||
use eva::data;
|
||||
|
||||
use crate::{
|
||||
errors,
|
||||
types::{Patch, True, board, message},
|
||||
};
|
||||
|
||||
pub mod edit {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub board: board::Selector,
|
||||
pub text: Patch<message::Text>,
|
||||
pub slug: Patch<Option<board::Slug>>,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok(pub True);
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NotFound(#[from] errors::boards::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod delete {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub board: board::Selector,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok(pub True);
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NotFound(#[from] errors::boards::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod create {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub slug: board::Slug,
|
||||
pub initial_message: message::Text,
|
||||
pub by: Option<message::ById>,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok {
|
||||
pub id: board::Id,
|
||||
}
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
AlreadyExists(#[from] errors::boards::AlreadyExists),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod get {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub board: board::Selector,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok {
|
||||
pub board: board::Board,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NotFound(#[from] errors::boards::NotFound),
|
||||
}
|
||||
}
|
1
core/src/requests/files.rs
Normal file
1
core/src/requests/files.rs
Normal file
|
@ -0,0 +1 @@
|
|||
|
187
core/src/requests/games.rs
Normal file
187
core/src/requests/games.rs
Normal file
|
@ -0,0 +1,187 @@
|
|||
use crate::{
|
||||
errors,
|
||||
types::{Patch, True, author, file, game},
|
||||
};
|
||||
|
||||
use eva::{array, data, int, perfect_derive, str, time};
|
||||
|
||||
pub mod update {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub id: game::Id,
|
||||
pub update: Update,
|
||||
}
|
||||
|
||||
#[data]
|
||||
#[serde_with::apply(Patch => #[serde(default)])]
|
||||
pub struct Update {
|
||||
pub title: Patch<game::Title>,
|
||||
pub description: Patch<Option<game::Description>>,
|
||||
pub slug: Patch<game::Slug>,
|
||||
pub thumbnail: Patch<Option<file::Id>>,
|
||||
pub genres: Patch<game::Genres>,
|
||||
pub badges: Patch<game::Badges>,
|
||||
pub tags: Patch<game::Tags>,
|
||||
pub screenshots: Patch<game::Screenshots>,
|
||||
pub published: Patch<bool>,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok(pub True);
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NotFound(#[from] errors::games::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod search {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
#[perfect_derive(Default)]
|
||||
pub struct Logic<T, const LIMIT: usize> {
|
||||
pub all: array::ImmutableHeap<T, LIMIT>,
|
||||
pub any: array::ImmutableHeap<T, LIMIT>,
|
||||
}
|
||||
|
||||
#[data]
|
||||
#[derive(Default)]
|
||||
pub struct Marks {
|
||||
pub tags: Logic<game::Tag, 16>,
|
||||
pub genres: Logic<game::Genre, 16>,
|
||||
pub badges: Logic<game::Badge, 16>,
|
||||
}
|
||||
|
||||
#[data(copy, display(name))]
|
||||
pub enum SortBy {
|
||||
/// By game release date.
|
||||
ReleaseDate {
|
||||
after: Option<(time::Date, game::Id)>,
|
||||
},
|
||||
/// Sort by game creation date.
|
||||
CreatedAt { after: Option<game::Id> },
|
||||
/// By publish on site date.
|
||||
PublishedAt {
|
||||
after: Option<(time::Date, game::Id)>,
|
||||
},
|
||||
/// By game rating.
|
||||
Rating {
|
||||
after: Option<(game::RatingValue, game::Id)>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for SortBy {
|
||||
fn default() -> Self {
|
||||
Self::CreatedAt { after: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[data(copy, display(name))]
|
||||
#[derive(Default)]
|
||||
pub enum Order {
|
||||
/// From lowest to highest.
|
||||
Asc,
|
||||
/// From highest to lowest.
|
||||
#[default]
|
||||
Desc,
|
||||
}
|
||||
|
||||
#[int(u8, 1..=32)]
|
||||
pub enum Limit {}
|
||||
|
||||
impl Default for Limit {
|
||||
fn default() -> Self {
|
||||
Self::POS16
|
||||
}
|
||||
}
|
||||
|
||||
#[data]
|
||||
#[derive(Default)]
|
||||
pub struct Condition {
|
||||
pub marks: Marks,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub query: Option<game::SearchQuery>,
|
||||
pub author: Option<author::Selector>,
|
||||
#[serde(default)]
|
||||
pub include: Condition,
|
||||
#[serde(default)]
|
||||
pub exclude: Condition,
|
||||
#[serde(default)]
|
||||
pub order: Order,
|
||||
#[serde(default)]
|
||||
pub sort_by: SortBy,
|
||||
pub limit: Option<Limit>,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok {
|
||||
pub found: Vec<game::Game>,
|
||||
}
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NoSuchAuthor(#[from] errors::authors::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod create {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub title: game::Title,
|
||||
pub description: Option<game::Description>,
|
||||
pub thumbnail: Option<file::Id>,
|
||||
pub author: author::Id,
|
||||
pub slug: Option<game::Slug>,
|
||||
pub vndb: Option<game::VndbId>,
|
||||
pub release_date: Option<game::ReleaseDate>,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok {
|
||||
pub id: game::Id,
|
||||
}
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NoSuchAuthor(#[from] errors::authors::NotFound),
|
||||
#[display("{_0}")]
|
||||
AlreadyTaken(#[from] errors::games::AlreadyTaken),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod get {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub game: game::Selector,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok {
|
||||
// TODO: include if requested
|
||||
// - translation maps.
|
||||
// - comments
|
||||
// - downloads
|
||||
pub game: game::Game,
|
||||
}
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NotFound(#[from] errors::games::NotFound),
|
||||
#[display("{_0}")]
|
||||
NoSuchAuthor(#[from] errors::authors::NotFound),
|
||||
}
|
||||
}
|
84
core/src/requests/messages.rs
Normal file
84
core/src/requests/messages.rs
Normal file
|
@ -0,0 +1,84 @@
|
|||
use eva::data;
|
||||
|
||||
use crate::{
|
||||
errors,
|
||||
types::{True, message, thread},
|
||||
};
|
||||
|
||||
pub mod edit {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub message: message::Id,
|
||||
pub text: message::Text,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok;
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NotFound(#[from] errors::messages::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod delete {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub message: message::Id,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok(pub True);
|
||||
|
||||
#[data]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NotFound(#[from] errors::messages::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod post {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub thread: thread::Selector,
|
||||
pub text: message::Text,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok {
|
||||
pub id: message::Id,
|
||||
}
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NoSuchThread(#[from] errors::threads::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod get {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub message: message::Selector,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok {
|
||||
pub message: message::Message,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NotFound(#[from] errors::messages::NotFound),
|
||||
}
|
||||
}
|
13
core/src/requests/mod.rs
Normal file
13
core/src/requests/mod.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use crate::errors::Generic;
|
||||
|
||||
pub type Response<O, E> = Result<O, Generic<E>>;
|
||||
|
||||
pub mod boards;
|
||||
pub mod messages;
|
||||
pub mod threads;
|
||||
|
||||
pub mod authors;
|
||||
pub mod files;
|
||||
pub mod games;
|
||||
pub mod uploads;
|
||||
pub mod users;
|
114
core/src/requests/threads.rs
Normal file
114
core/src/requests/threads.rs
Normal file
|
@ -0,0 +1,114 @@
|
|||
use eva::{data, int, str};
|
||||
|
||||
use crate::{
|
||||
errors,
|
||||
types::{Patch, True, board, message, thread},
|
||||
};
|
||||
|
||||
pub mod search {
|
||||
use super::*;
|
||||
|
||||
#[int(u8, 1..=64)]
|
||||
pub enum Limit {}
|
||||
|
||||
impl Default for Limit {
|
||||
fn default() -> Self {
|
||||
Self::POS24
|
||||
}
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
#[serde(default)]
|
||||
pub limit: Limit,
|
||||
// TODO(MKS-6): implement better pagination.
|
||||
pub after: Option<thread::Id>,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok {
|
||||
pub results: Vec<thread::Thread>,
|
||||
}
|
||||
|
||||
#[data(error, display("_"))]
|
||||
pub enum Err {}
|
||||
}
|
||||
|
||||
pub mod delete {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub thread: thread::Id,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok(pub True);
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NotFound(#[from] errors::threads::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod edit {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub thread: thread::Id,
|
||||
pub text: Patch<message::Text>,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok(pub True);
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NotFound(#[from] errors::threads::NotFound),
|
||||
#[display("{_0}")]
|
||||
NotAnOwner(#[from] errors::threads::NotAnOwner),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod get {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub thread: thread::Selector,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok {
|
||||
pub thread: thread::Thread,
|
||||
}
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NotFound(#[from] errors::threads::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod create {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub board: board::Selector,
|
||||
pub initial_message: message::Text,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok {
|
||||
pub id: thread::Id,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub enum Err {
|
||||
NoSuchBoard(#[from] errors::boards::NotFound),
|
||||
}
|
||||
}
|
107
core/src/requests/uploads.rs
Normal file
107
core/src/requests/uploads.rs
Normal file
|
@ -0,0 +1,107 @@
|
|||
use eva::{data, str};
|
||||
|
||||
use crate::{
|
||||
errors,
|
||||
types::{True, file, upload},
|
||||
uploads::UploadStream,
|
||||
};
|
||||
|
||||
pub mod start {
|
||||
//! # Upload file to the server.
|
||||
//!
|
||||
//! The following quotas apply:
|
||||
//! - Rate limiting - by bytes, temporal and permanent
|
||||
//! - Concurrent uploads limit - also by bytes
|
||||
//!
|
||||
//! Quotas are applied per-class.
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
/// Name of the file. Mainly serves as a hint to user to not
|
||||
/// download files with "scary" names. Don't set if that doesn't
|
||||
/// matter.
|
||||
pub name: Option<file::BaseName>,
|
||||
|
||||
/// Hash of the file.
|
||||
pub hash: file::Hash,
|
||||
|
||||
/// Class of the file.
|
||||
pub class: file::Class,
|
||||
|
||||
/// Size of the file to upload. Must be known
|
||||
/// prior to upload, streaming is not supported.
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok {
|
||||
pub upload: upload::Id,
|
||||
}
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
QuotaExceeded(#[from] errors::uploads::QuotaExceeded),
|
||||
#[display("{_0}")]
|
||||
SimUpQuotaExceeded(#[from] errors::uploads::SimUpQuotaExceeded),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod finish {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args<S: UploadStream> {
|
||||
pub id: upload::Id,
|
||||
pub stream: S,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok {
|
||||
pub file: file::Id,
|
||||
}
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NotFound(#[from] errors::uploads::NotFound),
|
||||
#[display("{_0}")]
|
||||
ConcurrentUploadInProgress(#[from] errors::uploads::ConcurrentUploadInProgress),
|
||||
#[display("{_0}")]
|
||||
HashMismatch(#[from] errors::uploads::HashMismatch),
|
||||
#[display("{_0}")]
|
||||
Overuploading(#[from] errors::uploads::Overuploading),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod cancel {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub upload: file::Id,
|
||||
}
|
||||
|
||||
pub type Ok = True;
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NotFound(#[from] errors::uploads::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod list_pending {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {}
|
||||
|
||||
#[data]
|
||||
pub struct Ok {
|
||||
pub uploads: Vec<upload::Upload>,
|
||||
}
|
||||
|
||||
pub type Err = errors::Impossible;
|
||||
}
|
149
core/src/requests/users.rs
Normal file
149
core/src/requests/users.rs
Normal file
|
@ -0,0 +1,149 @@
|
|||
///! # Users functionality
|
||||
///!
|
||||
///! Subject for actions.
|
||||
use eva::{data, time::Timestamp};
|
||||
|
||||
use crate::{
|
||||
errors::users as errors,
|
||||
types::{True, file, patch::Patch, session, user},
|
||||
};
|
||||
|
||||
pub mod check_auth {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {}
|
||||
|
||||
#[data(copy)]
|
||||
pub struct Ok {
|
||||
pub user: user::Id,
|
||||
pub role: user::Role,
|
||||
}
|
||||
|
||||
#[data(error, display(""))]
|
||||
pub enum Err {}
|
||||
}
|
||||
|
||||
pub mod update {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub user: Option<user::Selector>,
|
||||
pub update: Update,
|
||||
}
|
||||
|
||||
#[serde_with::apply(
|
||||
Patch => #[serde(default)]
|
||||
)]
|
||||
#[data]
|
||||
#[derive(Default)]
|
||||
pub struct Update {
|
||||
pub nickname: Patch<user::Nickname>,
|
||||
pub display_name: Patch<Option<user::DisplayName>>,
|
||||
pub bio: Patch<Option<user::Bio>>,
|
||||
pub password: Patch<user::Password>,
|
||||
pub role: Patch<user::Role>,
|
||||
pub pfp: Patch<Option<file::Id>>,
|
||||
pub email: Patch<user::Email>,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok(pub True);
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NotFound(#[from] errors::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod get {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub user: Option<user::Selector>,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok {
|
||||
pub user: user::User,
|
||||
}
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NotFound(#[from] errors::NotFound),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod confirm_sign_up {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub user: user::Id,
|
||||
pub token: user::SignUpCompletionToken,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok(pub True);
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
InvalidSignUpToken(#[from] errors::InvalidSignUpToken),
|
||||
#[display("{_0}")]
|
||||
NotFoundOrCompleted(#[from] errors::NotFoundOrCompleted),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod sign_in {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub nickname: user::Nickname,
|
||||
pub password: user::Password,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok {
|
||||
pub token: session::Token,
|
||||
pub expires_at: Timestamp,
|
||||
}
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
NotFound(#[from] errors::NotFound),
|
||||
#[display("{_0}")]
|
||||
InvalidPassword(#[from] errors::InvalidPassword),
|
||||
#[display("{_0}")]
|
||||
MustCompleteSignUp(#[from] errors::MustCompleteSignUp),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod sign_up {
|
||||
use super::*;
|
||||
|
||||
#[data]
|
||||
pub struct Args {
|
||||
pub nickname: user::Nickname,
|
||||
pub email: user::Email,
|
||||
pub display_name: Option<user::DisplayName>,
|
||||
pub password: user::Password,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ok {
|
||||
pub id: user::Id,
|
||||
}
|
||||
|
||||
#[data(error)]
|
||||
pub enum Err {
|
||||
#[display("{_0}")]
|
||||
AlreadyTaken(#[from] errors::AlreadyTaken),
|
||||
}
|
||||
}
|
22
core/src/rt.rs
Normal file
22
core/src/rt.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use eva::{auto_impl, fut::Fut};
|
||||
|
||||
#[auto_impl(&, &mut)]
|
||||
pub trait RtRef: Send + Sync {}
|
||||
|
||||
pub trait BlockingFn: FnOnce() -> Self::Ret + Send + 'static {
|
||||
type Ret: Send + 'static;
|
||||
}
|
||||
|
||||
impl<F, R> BlockingFn for F
|
||||
where
|
||||
F: FnOnce() -> R + Send + 'static,
|
||||
R: Send + 'static,
|
||||
{
|
||||
type Ret = R;
|
||||
}
|
||||
|
||||
#[auto_impl(&mut)]
|
||||
pub trait RtMut: RtRef {
|
||||
fn spawn_blocking<F: BlockingFn>(&mut self, task: F) -> impl Fut<Output = F::Ret>;
|
||||
fn spawn<F: Fut<Output: Send> + 'static>(&mut self, task: F);
|
||||
}
|
17
core/src/service/authors.rs
Normal file
17
core/src/service/authors.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
use crate::{
|
||||
requests::authors::{create, get, update},
|
||||
service::CallStep,
|
||||
};
|
||||
|
||||
use eva::auto_impl;
|
||||
|
||||
#[auto_impl(&mut, Box)]
|
||||
pub trait AuthorsRef: Send + Sync {
|
||||
fn get(&mut self) -> impl CallStep<get::Args, Ok = get::Ok, Err = get::Err>;
|
||||
}
|
||||
|
||||
#[auto_impl(&mut, Box)]
|
||||
pub trait AuthorsMut: AuthorsRef {
|
||||
fn create(&mut self) -> impl CallStep<create::Args, Ok = create::Ok, Err = create::Err>;
|
||||
fn update(&mut self) -> impl CallStep<update::Args, Ok = update::Ok, Err = update::Err>;
|
||||
}
|
9
core/src/service/authz.rs
Normal file
9
core/src/service/authz.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
use eva::auto_impl;
|
||||
|
||||
use crate::{service::AuxFut, types::session};
|
||||
|
||||
#[auto_impl(&mut)]
|
||||
pub trait AuthenticationMut: Send + Sync {
|
||||
fn authenticate(&mut self, session: session::Token) -> impl AuxFut<()>;
|
||||
fn clear(&mut self);
|
||||
}
|
18
core/src/service/boards.rs
Normal file
18
core/src/service/boards.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use eva::auto_impl;
|
||||
|
||||
use crate::{
|
||||
requests::boards::{create, delete, edit, get},
|
||||
service::CallStep,
|
||||
};
|
||||
|
||||
#[auto_impl(&mut, Box)]
|
||||
pub trait BoardsRef: Send + Sync {
|
||||
fn get(&mut self) -> impl CallStep<get::Args, Ok = get::Ok, Err = get::Err>;
|
||||
}
|
||||
|
||||
#[auto_impl(&mut, Box)]
|
||||
pub trait BoardsMut: BoardsRef {
|
||||
fn create(&mut self) -> impl CallStep<create::Args, Ok = create::Ok, Err = create::Err>;
|
||||
fn delete(&mut self) -> impl CallStep<delete::Args, Ok = delete::Ok, Err = delete::Err>;
|
||||
fn edit(&mut self) -> impl CallStep<edit::Args, Ok = edit::Ok, Err = edit::Err>;
|
||||
}
|
18
core/src/service/games.rs
Normal file
18
core/src/service/games.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use eva::auto_impl;
|
||||
|
||||
use crate::{
|
||||
requests::games::{create, get, search, update},
|
||||
service::CallStep,
|
||||
};
|
||||
|
||||
#[auto_impl(&mut, Box)]
|
||||
pub trait GamesRef: Send + Sync {
|
||||
fn get(&mut self) -> impl CallStep<get::Args, Ok = get::Ok, Err = get::Err>;
|
||||
fn search(&mut self) -> impl CallStep<search::Args, Ok = search::Ok, Err = search::Err>;
|
||||
}
|
||||
|
||||
#[auto_impl(&mut, Box)]
|
||||
pub trait GamesMut: GamesRef {
|
||||
fn create(&mut self) -> impl CallStep<create::Args, Ok = create::Ok, Err = create::Err>;
|
||||
fn update(&mut self) -> impl CallStep<update::Args, Ok = update::Ok, Err = update::Err>;
|
||||
}
|
18
core/src/service/messages.rs
Normal file
18
core/src/service/messages.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use eva::auto_impl;
|
||||
|
||||
use crate::{
|
||||
requests::messages::{delete, edit, get, post},
|
||||
service::CallStep,
|
||||
};
|
||||
|
||||
#[auto_impl(&mut, Box)]
|
||||
pub trait MessagesRef: Send + Sync {
|
||||
fn get(&mut self) -> impl CallStep<get::Args, Ok = get::Ok, Err = get::Err>;
|
||||
}
|
||||
|
||||
#[auto_impl(&mut, Box)]
|
||||
pub trait MessagesMut: Send + Sync {
|
||||
fn post(&mut self) -> impl CallStep<post::Args, Ok = post::Ok, Err = post::Err>;
|
||||
fn delete(&mut self) -> impl CallStep<delete::Args, Ok = delete::Ok, Err = delete::Err>;
|
||||
fn edit(&mut self) -> impl CallStep<edit::Args, Ok = edit::Ok, Err = edit::Err>;
|
||||
}
|
75
core/src/service/mod.rs
Normal file
75
core/src/service/mod.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
use eva::{auto_impl, handling, trait_set};
|
||||
|
||||
use crate::{errors::Aux, requests::Response};
|
||||
|
||||
pub mod boards;
|
||||
pub mod messages;
|
||||
pub mod threads;
|
||||
|
||||
pub mod authors;
|
||||
pub mod games;
|
||||
pub mod users;
|
||||
|
||||
pub mod authz;
|
||||
|
||||
pub type SessionOf<T> = <T as SessionMaker>::Session;
|
||||
|
||||
trait_set! {
|
||||
pub trait RespFut<O, E> = Future<Output = Response<O, E>> + Send;
|
||||
pub trait AuxFut<O> = Future<Output = Result<O, Aux>> + Send;
|
||||
}
|
||||
|
||||
pub trait CallStep<I>: Send + Sync {
|
||||
type Ok;
|
||||
type Err;
|
||||
|
||||
fn call(&mut self, args: I) -> impl RespFut<Self::Ok, Self::Err>;
|
||||
}
|
||||
|
||||
pub trait IsEndpoint<I, S>:
|
||||
handling::Endpoint<I, S, Output = Response<Self::Ok, Self::Err>>
|
||||
{
|
||||
type Ok;
|
||||
type Err;
|
||||
}
|
||||
|
||||
trait_set! {
|
||||
pub trait IsState = Send + Sync;
|
||||
}
|
||||
|
||||
impl<I, S, O, E, Ep> IsEndpoint<I, S> for Ep
|
||||
where
|
||||
Ep: handling::Endpoint<I, S, Output = Response<O, E>>,
|
||||
{
|
||||
type Ok = O;
|
||||
type Err = E;
|
||||
}
|
||||
|
||||
#[auto_impl(&, &mut, Arc)]
|
||||
pub trait SessionMaker: Send + Sync {
|
||||
type Session: Session;
|
||||
|
||||
fn make_session(&self) -> impl AuxFut<Self::Session>;
|
||||
}
|
||||
|
||||
trait_set! {
|
||||
pub trait Service = SessionMaker;
|
||||
}
|
||||
|
||||
#[auto_impl(&mut)]
|
||||
pub trait Session: Send + Sync {
|
||||
fn users(&mut self) -> impl users::UsersRef;
|
||||
fn users_mut(&mut self) -> impl users::UsersMut;
|
||||
|
||||
fn authors(&mut self) -> impl authors::AuthorsRef;
|
||||
fn authors_mut(&mut self) -> impl authors::AuthorsMut;
|
||||
|
||||
fn games(&mut self) -> impl games::GamesRef;
|
||||
fn games_mut(&mut self) -> impl games::GamesMut;
|
||||
|
||||
fn authentication_mut(&mut self) -> impl authz::AuthenticationMut;
|
||||
|
||||
fn boards(&mut self) -> impl boards::BoardsMut;
|
||||
fn threads(&mut self) -> impl threads::ThreadsMut;
|
||||
fn messages(&mut self) -> impl messages::MessagesMut;
|
||||
}
|
19
core/src/service/threads.rs
Normal file
19
core/src/service/threads.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use eva::auto_impl;
|
||||
|
||||
use crate::{
|
||||
requests::threads::{create, delete, edit, get, search},
|
||||
service::CallStep,
|
||||
};
|
||||
|
||||
#[auto_impl(&mut, Box)]
|
||||
pub trait ThreadsRef: Send + Sync {
|
||||
fn get(&mut self) -> impl CallStep<get::Args, Ok = get::Ok, Err = get::Err>;
|
||||
fn search(&mut self) -> impl CallStep<search::Args, Ok = search::Ok, Err = search::Err>;
|
||||
}
|
||||
|
||||
#[auto_impl(&mut, Box)]
|
||||
pub trait ThreadsMut: ThreadsRef {
|
||||
fn delete(&mut self) -> impl CallStep<delete::Args, Ok = delete::Ok, Err = delete::Err>;
|
||||
fn edit(&mut self) -> impl CallStep<edit::Args, Ok = edit::Ok, Err = edit::Err>;
|
||||
fn create(&mut self) -> impl CallStep<create::Args, Ok = create::Ok, Err = create::Err>;
|
||||
}
|
26
core/src/service/users.rs
Normal file
26
core/src/service/users.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use eva::auto_impl;
|
||||
|
||||
use crate::{
|
||||
requests::users::{check_auth, confirm_sign_up, get, sign_in, sign_up, update},
|
||||
service::CallStep,
|
||||
};
|
||||
|
||||
#[auto_impl(&mut)]
|
||||
pub trait UsersRef: Send + Sync {
|
||||
fn get(&mut self) -> impl CallStep<get::Args, Ok = get::Ok, Err = get::Err>;
|
||||
fn check_auth(
|
||||
&mut self,
|
||||
) -> impl CallStep<check_auth::Args, Ok = check_auth::Ok, Err = check_auth::Err>;
|
||||
}
|
||||
|
||||
#[auto_impl(&mut)]
|
||||
pub trait UsersMut: UsersRef {
|
||||
fn sign_in(&mut self) -> impl CallStep<sign_in::Args, Ok = sign_in::Ok, Err = sign_in::Err>;
|
||||
fn sign_up(&mut self) -> impl CallStep<sign_up::Args, Ok = sign_up::Ok, Err = sign_up::Err>;
|
||||
|
||||
fn update(&mut self) -> impl CallStep<update::Args, Ok = update::Ok, Err = update::Err>;
|
||||
|
||||
fn confirm_sign_up(
|
||||
&mut self,
|
||||
) -> impl CallStep<confirm_sign_up::Args, Ok = confirm_sign_up::Ok, Err = confirm_sign_up::Err>;
|
||||
}
|
114
core/src/types/author.rs
Normal file
114
core/src/types/author.rs
Normal file
|
@ -0,0 +1,114 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use eva::{data, str, str::CompactString, time::Date, zst_error};
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize, de};
|
||||
|
||||
use crate::types::{entity, file, slug, user};
|
||||
|
||||
/// Author selector.
|
||||
#[data(copy, ord)]
|
||||
#[serde(untagged)]
|
||||
pub enum Selector {
|
||||
#[display("{_0}")]
|
||||
Id(#[from] Id),
|
||||
#[display("@{_0}")]
|
||||
#[serde(with = "SlugStr")]
|
||||
Slug(#[from] Slug),
|
||||
}
|
||||
|
||||
struct SlugStr;
|
||||
|
||||
impl JsonSchema for SlugStr {
|
||||
fn schema_id() -> Cow<'static, str> {
|
||||
Cow::Borrowed(concat!(module_path!(), "::SlugStr"))
|
||||
}
|
||||
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
Cow::Borrowed("SlugStr")
|
||||
}
|
||||
|
||||
fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
let mut pat = String::from("^@");
|
||||
<Slug as str::HasPattern>::pat_into(&mut pat);
|
||||
pat.push('$');
|
||||
|
||||
schemars::json_schema!({
|
||||
"type": "string",
|
||||
"pattern": pat,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SlugStr {
|
||||
fn serialize<S: serde::Serializer>(slug: &Slug, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
str::format_compact!("@{slug}").serialize(serializer)
|
||||
}
|
||||
|
||||
fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Slug, D::Error> {
|
||||
let s = <&'de str as Deserialize<'de>>::deserialize(deserializer)?;
|
||||
if let Some(slug) = s.strip_prefix('@') {
|
||||
slug.parse().map_err(de::Error::custom)
|
||||
} else {
|
||||
Err(de::Error::custom(zst_error!("slug must start with a @")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Author miniature.
|
||||
#[data]
|
||||
pub struct Mini {
|
||||
pub id: Id,
|
||||
pub title: Title,
|
||||
pub slug: Slug,
|
||||
pub owner: user::Id,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pfp: Option<file::Id>,
|
||||
}
|
||||
|
||||
/// Author.
|
||||
#[data]
|
||||
pub struct Author {
|
||||
pub id: Id,
|
||||
pub slug: Slug,
|
||||
pub title: Title,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<Description>,
|
||||
pub owner: user::Mini,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pfp: Option<file::Id>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub verification: Option<Verification>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub contact_email: Option<ContactEmail>,
|
||||
pub created_at: Date,
|
||||
}
|
||||
|
||||
/// Information about author verification.
|
||||
#[data]
|
||||
pub struct Verification {
|
||||
pub by: user::Mini,
|
||||
pub at: Date,
|
||||
}
|
||||
|
||||
/// Email for contacting the author.
|
||||
#[data]
|
||||
pub struct ContactEmail(pub CompactString);
|
||||
|
||||
entity::define_eid! {
|
||||
/// ID of the author.
|
||||
pub struct Id(Author);
|
||||
}
|
||||
|
||||
/// Author's slug.
|
||||
#[str(newtype, copy)]
|
||||
pub struct Slug(pub slug::Slug<23>);
|
||||
|
||||
/// Author's title.
|
||||
#[str(newtype)]
|
||||
pub struct Title(pub CompactString);
|
||||
|
||||
/// Author's description.
|
||||
#[str(newtype)]
|
||||
pub struct Description(pub CompactString);
|
106
core/src/types/board.rs
Normal file
106
core/src/types/board.rs
Normal file
|
@ -0,0 +1,106 @@
|
|||
use crate::types::{entity, game, slug, user};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use eva::{
|
||||
data, str,
|
||||
str::{HasPattern, format_compact},
|
||||
time::Timestamp,
|
||||
zst_error,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize, de};
|
||||
|
||||
#[data(copy)]
|
||||
#[serde(untagged)]
|
||||
pub enum Selector {
|
||||
#[display("@{_0}")]
|
||||
#[serde(with = "Slugged")]
|
||||
Slug(#[from] Slug),
|
||||
#[display("{_0}")]
|
||||
Id(#[from] Id),
|
||||
}
|
||||
|
||||
struct Slugged;
|
||||
|
||||
impl JsonSchema for Slugged {
|
||||
fn schema_id() -> Cow<'static, str> {
|
||||
Cow::Borrowed(concat!(module_path!(), "::Slugged"))
|
||||
}
|
||||
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
Cow::Borrowed("Slugged")
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
let mut pat = String::from("^@");
|
||||
Slug::pat_into(&mut pat);
|
||||
pat.push('$');
|
||||
schemars::json_schema!({
|
||||
"type": "string",
|
||||
"pattern": pat,
|
||||
"description": "slug of the board"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Slugged {
|
||||
fn serialize<S>(slug: &Slug, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
format_compact!("@{slug}").serialize(serializer)
|
||||
}
|
||||
|
||||
fn deserialize<'de, D>(deserializer: D) -> Result<Slug, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = <&'de str as Deserialize<'de>>::deserialize(deserializer)?;
|
||||
if let Some(rest) = s.strip_prefix('@') {
|
||||
let slug: Slug = rest.parse().map_err(de::Error::custom)?;
|
||||
Ok(slug)
|
||||
} else {
|
||||
Err(de::Error::custom(zst_error!("slug must start with a @")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entity::define_eid! {
|
||||
pub struct Id(Board);
|
||||
}
|
||||
|
||||
entity::define_eid! {
|
||||
pub struct Op(User | Game);
|
||||
}
|
||||
|
||||
impl Op {
|
||||
pub const fn user(user: user::Id) -> Self {
|
||||
Self(user.raw_id())
|
||||
}
|
||||
|
||||
pub const fn game(game: game::Id) -> Self {
|
||||
Self(game.raw_id())
|
||||
}
|
||||
}
|
||||
|
||||
#[str(newtype, copy)]
|
||||
pub struct Slug(slug::LowerSlug<7>);
|
||||
|
||||
#[str(newtype)]
|
||||
pub struct Title(str::CompactString);
|
||||
|
||||
#[data]
|
||||
pub struct Brief {
|
||||
pub id: Id,
|
||||
pub slug: Option<Slug>,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Board {
|
||||
pub id: Id,
|
||||
pub title: Option<Title>,
|
||||
pub slug: Option<Slug>,
|
||||
pub op: Op,
|
||||
pub created_at: Timestamp,
|
||||
}
|
5
core/src/types/captcha.rs
Normal file
5
core/src/types/captcha.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
use eva::str;
|
||||
|
||||
/// String that proves successful captcha solution.
|
||||
#[str(newtype)]
|
||||
pub struct Token(pub str::CompactString);
|
843
core/src/types/entity.rs
Normal file
843
core/src/types/entity.rs
Normal file
|
@ -0,0 +1,843 @@
|
|||
//! # Generic entity identifier
|
||||
//!
|
||||
//! Used to identify entities in the system. Should be treated as 128bits of randomness. Unique and stable, thus
|
||||
//! it should be preferred over slugs (such as nicknames) for referring to entities.
|
||||
//!
|
||||
//! # Schema
|
||||
//!
|
||||
//! Schema is not guaranteed, can be changed any time. From highest to lowest bits.
|
||||
//! ```plaintext
|
||||
//! Bits
|
||||
//! 48 72 8
|
||||
//! +---+---+---+
|
||||
//! | T | R | M |
|
||||
//! +---+---+---+
|
||||
//! ```
|
||||
//!
|
||||
//! 1. T - timestamp in milliseconds
|
||||
//! 2. R - randomness, non-zero
|
||||
//! 3. M - metadata
|
||||
//!
|
||||
//! Thus, identifier has some kind of monotonicity and never zero. Strictly speaking, two generated IDs are
|
||||
//! not guaranteed to be in order "first created > created after", it's actually "first >= after".
|
||||
//!
|
||||
//! The format takes inspiration from the ULID, slightly modifying it for our purposes. The exact modification is
|
||||
//! metadata at the lowest bits, instead of additional 16bits of randomness, we take that bits for storing metadata, 72bits
|
||||
//! is still enormous amount of possible values in a millisecond. Especially since identifiers comparison includes
|
||||
//! comparing metadata, thus identifier is equal only if all of 128bits matches.
|
||||
//!
|
||||
//! Metadata
|
||||
//! ```plaintext
|
||||
//! Bits
|
||||
//! 3 5
|
||||
//! +---+---+
|
||||
//! | D | K |
|
||||
//! +---+---+
|
||||
//! ```
|
||||
//! 1. D - data specific to each kind, can be randomness.
|
||||
//! 2. K - Kind. Entity kind to which ID refers.
|
||||
//!
|
||||
//! # Encoding
|
||||
//!
|
||||
//! TODO.
|
||||
|
||||
use eva::{
|
||||
data, int,
|
||||
rand::Rng as _,
|
||||
str,
|
||||
str::{FixedUtf8, HasPattern, Seq},
|
||||
time::{Clock as _, Timestamp},
|
||||
};
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt, mem,
|
||||
num::NonZeroU128,
|
||||
slice,
|
||||
str::{FromStr, from_utf8_unchecked},
|
||||
};
|
||||
|
||||
use crate::world::{World, WorldMut};
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize, de};
|
||||
|
||||
#[data(
|
||||
copy,
|
||||
display("provided number of steps would overflow capacity of the identifier")
|
||||
)]
|
||||
pub struct IdCapacityOverflow;
|
||||
|
||||
pub trait IsEntityId:
|
||||
for<'de> Deserialize<'de> + Serialize + fmt::Display + FromStr<Err = ParseError> + Copy + Eq + Ord
|
||||
{
|
||||
fn from_generic(generic: Id) -> Option<Self>;
|
||||
fn to_str(&self) -> StrId;
|
||||
fn into_inner(&self) -> NonZeroU128;
|
||||
}
|
||||
|
||||
/// Entity kind.
|
||||
#[data(ord, copy, display(name))]
|
||||
#[derive(Hash)]
|
||||
#[repr(u8)]
|
||||
pub enum Kind {
|
||||
Session = 0,
|
||||
File,
|
||||
Upload,
|
||||
User,
|
||||
Author,
|
||||
Game,
|
||||
Tag,
|
||||
Badge,
|
||||
Message,
|
||||
Thread,
|
||||
Board,
|
||||
}
|
||||
|
||||
const _: () = {
|
||||
if Kind::MAX as u8 >= (1 << 5_u8) {
|
||||
panic!("Number of kinds exceed limits of 5bits");
|
||||
}
|
||||
};
|
||||
|
||||
impl Kind {
|
||||
pub const MIN: Self = Self::Session;
|
||||
pub const MAX: Self = Self::Board;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Metadata(u8);
|
||||
|
||||
impl Metadata {
|
||||
const fn validate(v: u8) -> bool {
|
||||
let kind = v & 0b1111;
|
||||
kind <= Kind::MAX as u8
|
||||
}
|
||||
|
||||
pub const fn new(kind: Kind, data: u8) -> Self {
|
||||
let kind = kind as u8;
|
||||
let data = data & !0b1111_u8;
|
||||
|
||||
Self((data << 4) | kind)
|
||||
}
|
||||
|
||||
pub const fn repr(self) -> u8 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub const fn kind(self) -> Kind {
|
||||
unsafe { mem::transmute::<u8, Kind>(self.0 & 0b1111) }
|
||||
}
|
||||
|
||||
pub const fn data(self) -> u8 {
|
||||
self.0 >> 4
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct DisplayArray<'a, T>(pub &'a [T]);
|
||||
|
||||
impl<T: fmt::Display> fmt::Display for DisplayArray<'_, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("[")?;
|
||||
|
||||
for value in self.0 {
|
||||
fmt::Display::fmt(value, f)?;
|
||||
}
|
||||
|
||||
f.write_str("]")
|
||||
}
|
||||
}
|
||||
|
||||
#[data(copy, error)]
|
||||
pub enum ParseError {
|
||||
#[display("unexpected entity kinds {got}, expected {}", DisplayArray(&[expected]))]
|
||||
UnexpectedKind { expected: Kind, got: Kind },
|
||||
#[display("provided identifier is malformed")]
|
||||
Malformed,
|
||||
#[display("invalid string length")]
|
||||
Length,
|
||||
#[display("invalid char")]
|
||||
Char,
|
||||
}
|
||||
|
||||
impl From<str::ParseError> for ParseError {
|
||||
fn from(value: str::ParseError) -> Self {
|
||||
match value {
|
||||
str::ParseError::Length => Self::Length,
|
||||
str::ParseError::Char => Self::Char,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[int(u8, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_')]
|
||||
pub enum Char {}
|
||||
eva::single_ascii_char!(Char);
|
||||
|
||||
impl Char {
|
||||
pub const NULL: Self = Self::VARIANTS[0];
|
||||
}
|
||||
|
||||
unsafe impl FixedUtf8 for Char {}
|
||||
|
||||
#[str(fixed(error = eva::str::ParseError))]
|
||||
struct StrIdBuf(Char, Seq<22, Char>);
|
||||
|
||||
impl StrIdBuf {
|
||||
const fn to_array(self) -> [Char; 23] {
|
||||
unsafe { mem::transmute::<Self, [Char; 23]>(self) }
|
||||
}
|
||||
|
||||
const fn as_mut_array<'this>(&'this mut self) -> &'this mut [Char; 23] {
|
||||
unsafe { mem::transmute::<&'this mut Self, &'this mut [Char; 23]>(self) }
|
||||
}
|
||||
}
|
||||
|
||||
/// String representation of the [`Id`].
|
||||
#[str(custom, copy)]
|
||||
pub struct StrId {
|
||||
buf: StrIdBuf,
|
||||
len: u8,
|
||||
}
|
||||
|
||||
impl HasPattern for StrId {
|
||||
#[inline]
|
||||
fn pat_into(buf: &mut String) {
|
||||
Char::pat_into(buf);
|
||||
buf.push_str("{1,23}");
|
||||
}
|
||||
}
|
||||
|
||||
impl StrId {
|
||||
pub const fn len(&self) -> u8 {
|
||||
self.len
|
||||
}
|
||||
|
||||
pub const fn is_empty(&self) -> bool {
|
||||
self.len == 0
|
||||
}
|
||||
|
||||
pub const fn as_str(&self) -> &str {
|
||||
unsafe {
|
||||
from_utf8_unchecked(slice::from_raw_parts(
|
||||
(&raw const self.buf).cast(),
|
||||
self.len as usize,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for StrId {
|
||||
type Err = ParseError;
|
||||
|
||||
#[inline]
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
use ParseError as E;
|
||||
|
||||
let bytes = s.as_bytes();
|
||||
if bytes.is_empty() || bytes.len() > 23 {
|
||||
return Err(E::Length);
|
||||
}
|
||||
|
||||
let mut buf = StrIdBuf(Char::NULL, Seq([Char::NULL; 22]));
|
||||
let array = buf.as_mut_array();
|
||||
let mut idx = 0_usize;
|
||||
let mut len = 0_u8;
|
||||
|
||||
for &byte in bytes {
|
||||
array[idx] = Char::new(byte).ok_or(E::Char)?;
|
||||
idx += 1;
|
||||
len += 1;
|
||||
}
|
||||
|
||||
Ok(Self { buf, len })
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(inline)]
|
||||
pub use crate::_define_eid as define_eid;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use crate::_match_kind as match_;
|
||||
|
||||
#[macro_export]
|
||||
#[doc(hidden)]
|
||||
macro_rules! _if_tail_exists {
|
||||
([$($Inp:ident)+] => {$($True:tt)*}; _ => {$($False:tt)*};) => {$($True)*};
|
||||
([] => {$($True:tt)*}; _ => {$($False:tt)*};) => {$($False)*};
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! _use_match_arms_ty {
|
||||
(@ [$($Acc:ident)*] $Last:ident ;; as $As:ident) => {$crate::_priv::paste! { use $($Acc ::)* [<$Last _Match>] as $As; }};
|
||||
(@ [$($Acc:ident)*] $Hd:ident $($Tail:ident)+ ;; as $As:ident) => { $crate::_use_match_arms_ty!(@ [$($Acc)* $Hd] $($Tail)+ ;; as $As) };
|
||||
($($List:ident)+ ;; as $As:ident) => { $crate::_use_match_arms_ty!(@ [] $($List)+ ;; as $As) };
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
#[doc(hidden)]
|
||||
macro_rules! _match_kind {
|
||||
($id:expr, $($IdT:ident)::+ => {$(
|
||||
$Kind:ident => $Code:expr
|
||||
),* $(,)?}) => {$crate::_priv::paste! {
|
||||
{
|
||||
$crate::_use_match_arms_ty!($($IdT)* ;; as __Match);
|
||||
|
||||
{$(
|
||||
use $crate::types::entity::Kind::$Kind as _;
|
||||
)*}
|
||||
|
||||
$id.match_(__Match {$(
|
||||
[<$Kind:snake>]: move || $Code
|
||||
),*})
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
#[doc(hidden)]
|
||||
macro_rules! _define_eid {
|
||||
($(
|
||||
$(#[$outer_meta:meta])*
|
||||
$vis:vis struct $Name:ident($KindHd:ident $(| $KindTs:ident)*);
|
||||
)*) => {$crate::_priv::paste! {$(
|
||||
$(#[$outer_meta])*
|
||||
#[::eva::data(copy, ord, not(Deserialize), display("{_0}"))]
|
||||
#[derive(Hash)]
|
||||
#[repr(transparent)]
|
||||
$vis struct $Name($crate::types::entity::Id);
|
||||
|
||||
#[allow(non_camel_case_types, dead_code)]
|
||||
$vis struct [<$Name _Match>] <$KindHd $(, $KindTs)*> {
|
||||
$vis [<$KindHd:snake>]: $KindHd,
|
||||
$(
|
||||
$vis [<$KindTs:snake>]: $KindTs
|
||||
),*
|
||||
}
|
||||
|
||||
const _: () = {
|
||||
use $crate::{
|
||||
world::{World, WorldMut},
|
||||
types::entity,
|
||||
_if_tail_exists
|
||||
};
|
||||
use ::core::{
|
||||
result::Result,
|
||||
cmp::PartialEq,
|
||||
num::NonZeroU128,
|
||||
ops::Deref,
|
||||
str::FromStr,
|
||||
};
|
||||
use ::eva::{
|
||||
zst_error,
|
||||
_priv::serde::{
|
||||
Deserialize,
|
||||
Deserializer,
|
||||
de,
|
||||
},
|
||||
};
|
||||
|
||||
impl entity::IsEntityId for $Name {
|
||||
fn to_str(&self) -> entity::StrId {
|
||||
self.0.to_str()
|
||||
}
|
||||
|
||||
fn into_inner(&self) -> NonZeroU128 {
|
||||
self.0.into_inner()
|
||||
}
|
||||
|
||||
fn from_generic(v: entity::Id) -> Option<Self> {
|
||||
Self::from_generic(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for $Name {
|
||||
type Target = entity::Id;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for $Name {
|
||||
type Err = entity::ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let id: entity::Id = s.parse()?;
|
||||
Self::from_generic(id).ok_or(entity::ParseError::UnexpectedKind {
|
||||
// TODO(MKS-5): enrich.
|
||||
expected: entity::Kind::$KindHd,
|
||||
got: id.metadata().kind(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<NonZeroU128> for $Name {
|
||||
fn eq(&self, other: &NonZeroU128) -> bool {
|
||||
self.0.into_inner() == *other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<u128> for $Name {
|
||||
fn eq(&self, other: &u128) -> bool {
|
||||
self.0.into_inner().get() == *other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<entity::Id> for $Name {
|
||||
fn eq(&self, other: &entity::Id) -> bool {
|
||||
self.0 == *other
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for $Name {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let id = entity::Id::deserialize(deserializer)?;
|
||||
Self::from_generic(id).ok_or_else(|| de::Error::custom(zst_error!(
|
||||
"unexpected entity kind, expected {}",
|
||||
entity::DisplayArray(&[entity::Kind::$KindHd $(, entity::Kind::$KindTs)*]),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl $Name {
|
||||
pub const fn raw_id(self) -> entity::Id {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub const fn is_valid_kind(kind: entity::Kind) -> bool {
|
||||
matches!(kind, entity::Kind::$KindHd $(| entity::Kind::$KindTs)*)
|
||||
}
|
||||
|
||||
/// Create identifier from the generic entity id.
|
||||
pub const fn from_generic(entity: entity::Id) -> Option<Self> {
|
||||
if Self::is_valid_kind(entity.metadata().kind()) {
|
||||
Some(Self(entity))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub const fn next_gt(self, steps: u128) -> Self {
|
||||
match self.try_next_gt(steps) {
|
||||
Ok(r) => r,
|
||||
Err(..) => panic!("id capacity overflow")
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn try_next_gt(self, steps: u128) -> Result<Self, entity::IdCapacityOverflow> {
|
||||
let raw = self.raw_id();
|
||||
match raw.try_next_gt(steps) {
|
||||
Ok(r) => Ok(Self(r)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_<__FnOut, $KindHd $(, $KindTs)*>(self, arms: [<$Name _Match>] <$KindHd $(, $KindTs)*>) -> __FnOut
|
||||
where
|
||||
$KindHd: FnOnce() -> __FnOut,
|
||||
$($KindTs: FnOnce() -> __FnOut),*
|
||||
{
|
||||
use entity::Kind as K;
|
||||
|
||||
match self.raw_id().metadata().kind() {
|
||||
K::$KindHd => (arms.[<$KindHd:snake>])()
|
||||
$(, K::$KindTs => (arms.[<$KindTs:snake>])())*
|
||||
,
|
||||
_ => unsafe { core::hint::unreachable_unchecked() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert identifier to the str.
|
||||
pub const fn to_str(self) -> entity::StrId {
|
||||
self.0.to_str()
|
||||
}
|
||||
|
||||
_if_tail_exists! {
|
||||
[$($KindTs)*] => {
|
||||
/// Generate new identifier.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// panics if passed wrong kind.
|
||||
#[track_caller]
|
||||
pub fn generate<W: WorldMut>(w: World<W>, kind: entity::Kind) -> Self {
|
||||
if Self::is_valid_kind(kind) {
|
||||
let id = entity::Id::generate(w, entity::Metadata::new(kind, 0));
|
||||
Self(id)
|
||||
} else {
|
||||
panic!(
|
||||
"passed wrong kind for `{}`, available: [{}]",
|
||||
stringify!($Name),
|
||||
entity::DisplayArray(&[entity::Kind::$KindHd $(, entity::Kind::$KindTs)*])
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
_ => {
|
||||
/// Minimal ID in sense of ordering. No ID is lesser than this one.
|
||||
pub const MIN: Self = Self(entity::Id::from_parts(0, 0, entity::Metadata::new(entity::Kind::$KindHd, 0)));
|
||||
|
||||
/// Maximal ID in sense of ordering. No ID could be greater than this one.
|
||||
pub const MAX: Self = Self(entity::Id::from_parts(u64::MAX, u128::MAX, entity::Metadata::new(entity::Kind::$KindHd, u8::MAX)));
|
||||
|
||||
/// Generate new identifier.
|
||||
pub fn generate<W: WorldMut>(w: World<W>) -> Self {
|
||||
let id = entity::Id::generate(w, entity::Metadata::new(entity::Kind::$KindHd, 0));
|
||||
Self(id)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
)*}};
|
||||
}
|
||||
|
||||
/// Generic entity identifier.
|
||||
#[data(copy, ord, not(serde, schemars, Debug), display("{}:{}", self.metadata().kind(), self.to_str()))]
|
||||
#[derive(Hash)]
|
||||
pub struct Id(NonZeroU128);
|
||||
|
||||
impl IsEntityId for Id {
|
||||
fn from_generic(generic: Id) -> Option<Self> {
|
||||
Some(generic)
|
||||
}
|
||||
|
||||
fn to_str(&self) -> StrId {
|
||||
Id::to_str(*self)
|
||||
}
|
||||
|
||||
fn into_inner(&self) -> NonZeroU128 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Id {
|
||||
pub const TIMESTAMP_OFFSET: u64 = 1735678800000;
|
||||
}
|
||||
|
||||
impl fmt::Debug for Id {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("Id").field(&self.to_str().as_str()).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Id {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
if serializer.is_human_readable() {
|
||||
self.to_str().serialize(serializer)
|
||||
} else {
|
||||
self.0.serialize(serializer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Id {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let res = if deserializer.is_human_readable() {
|
||||
let s = <&'de str as Deserialize<'de>>::deserialize(deserializer)?;
|
||||
Self::parse(StrId::from_str(s).map_err(de::Error::custom)?)
|
||||
.map_err(de::Error::custom)?
|
||||
} else {
|
||||
let repr = u128::deserialize(deserializer)?;
|
||||
Self::from_repr(repr).ok_or_else(|| serde::de::Error::custom(ParseError::Malformed))?
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for Id {
|
||||
fn schema_id() -> Cow<'static, str> {
|
||||
Cow::Borrowed(concat!(module_path!(), "::Id"))
|
||||
}
|
||||
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
Cow::Borrowed("Id")
|
||||
}
|
||||
|
||||
fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
schemars::json_schema!({
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": u128::MAX,
|
||||
"description": "integer representation of the identifier, only in binary formats"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": StrId::regex_pat_fullmatch(),
|
||||
"minLength": 1,
|
||||
"maxLength": 23,
|
||||
"description": "unique entity identifier, only in human-readable formats"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Id {
|
||||
type Err = ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let s: StrId = s.parse()?;
|
||||
Ok(Self::parse(s)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Id {
|
||||
pub const fn to_str(self) -> StrId {
|
||||
let mut buf = StrIdBuf(Char::NULL, Seq([Char::NULL; 22]));
|
||||
let array = buf.as_mut_array();
|
||||
let mut len = 0_u8;
|
||||
|
||||
let mut value = self.0.get();
|
||||
let base = Char::VARIANTS.len() as u128;
|
||||
let idx = value.ilog(base) as usize;
|
||||
|
||||
loop {
|
||||
array[idx - len as usize] = Char::VARIANTS[(value % base) as usize];
|
||||
value /= base;
|
||||
len += 1;
|
||||
if value == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
StrId { buf, len }
|
||||
}
|
||||
|
||||
pub const fn from_repr(x: u128) -> Option<Self> {
|
||||
let Some(x) = NonZeroU128::new(x) else {
|
||||
return None;
|
||||
};
|
||||
if !Metadata::validate((x.get() & 0xFF) as u8) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Self(x))
|
||||
}
|
||||
|
||||
pub fn parse(s: StrId) -> Result<Self, ParseError> {
|
||||
let mut res = 0_u128;
|
||||
let mut idx = 0_usize;
|
||||
let array = s.buf.to_array();
|
||||
|
||||
while idx != s.len() as usize {
|
||||
res = res
|
||||
.checked_mul(Char::VARIANTS.len() as u128)
|
||||
.ok_or(ParseError::Malformed)?;
|
||||
res = res
|
||||
.checked_add(array[idx].nth() as u128)
|
||||
.ok_or(ParseError::Malformed)?;
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
Self::from_repr(res).ok_or(ParseError::Malformed)
|
||||
}
|
||||
|
||||
pub const fn timestamp(self) -> Timestamp {
|
||||
Timestamp::from_millis(self.timestamp_ms())
|
||||
}
|
||||
|
||||
pub const fn timestamp_ms_rel(self) -> u64 {
|
||||
self.timestamp_ms() - Self::TIMESTAMP_OFFSET
|
||||
}
|
||||
|
||||
pub const fn timestamp_ms(self) -> u64 {
|
||||
(self.0.get() >> 80) as u64 + Self::TIMESTAMP_OFFSET
|
||||
}
|
||||
|
||||
pub const fn metadata(self) -> Metadata {
|
||||
Metadata((self.0.get() & 0xFF) as u8)
|
||||
}
|
||||
|
||||
pub const fn with_metadata(self, new: Metadata) -> Self {
|
||||
Self(unsafe { NonZeroU128::new_unchecked((self.0.get() & !0xFF) | new.repr() as u128) })
|
||||
}
|
||||
|
||||
/// Same as [`Id::try_next_gt`], but panics on overflow.
|
||||
#[track_caller]
|
||||
pub const fn next_gt(self, steps: u128) -> Self {
|
||||
if let Ok(this) = self.try_next_gt(steps) {
|
||||
this
|
||||
} else {
|
||||
panic!("steps would overflow")
|
||||
}
|
||||
}
|
||||
|
||||
/// Get entity id which'd be greater than `steps` of next identifiers.
|
||||
pub const fn try_next_gt(self, mut steps: u128) -> Result<Self, IdCapacityOverflow> {
|
||||
const RAND_QUOTA: u128 = (1 << 72) - 1;
|
||||
|
||||
let mut ts = self.timestamp_ms_rel();
|
||||
let rand = self.random().get();
|
||||
let meta = self.metadata();
|
||||
let rand_quota_left = RAND_QUOTA - rand;
|
||||
|
||||
if rand_quota_left >= steps {
|
||||
return Ok(Self::from_parts(ts, rand + steps, meta));
|
||||
}
|
||||
|
||||
// rand_quota_left < steps
|
||||
ts += 1;
|
||||
steps -= rand_quota_left;
|
||||
|
||||
if RAND_QUOTA >= steps {
|
||||
return Ok(Self::from_parts(ts, steps, meta));
|
||||
}
|
||||
|
||||
// RAND_QUOTA < steps
|
||||
// So, at minimum we're `2^72 + 1` steps ahead, one more
|
||||
// jump and we got the maximum value.
|
||||
|
||||
steps -= RAND_QUOTA;
|
||||
ts += 1;
|
||||
|
||||
if RAND_QUOTA >= steps {
|
||||
Ok(Self::from_parts(ts, steps, meta))
|
||||
} else {
|
||||
// We're already `2^144 + 1` steps ahead, there's no more
|
||||
// values.
|
||||
Err(IdCapacityOverflow)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get random bits of the identifier.
|
||||
pub const fn random(self) -> NonZeroU128 {
|
||||
unsafe { NonZeroU128::new_unchecked((self.0.get() & (((1 << 72) - 1) << 8)) >> 8) }
|
||||
}
|
||||
|
||||
/// Construct ID from parts.
|
||||
pub const fn from_parts(mut millis: u64, mut random: u128, metadata: Metadata) -> Self {
|
||||
millis &= (1 << 48) - 1;
|
||||
random &= (1 << 72) - 1;
|
||||
if random == 0 {
|
||||
random = 1;
|
||||
}
|
||||
|
||||
let mut result = metadata.repr() as u128;
|
||||
result |= random << 8;
|
||||
result |= (millis as u128) << 80;
|
||||
|
||||
Self(unsafe { NonZeroU128::new_unchecked(result) })
|
||||
}
|
||||
|
||||
/// Generate new identifier.
|
||||
pub fn generate<W: WorldMut>(mut world: World<W>, metadata: Metadata) -> Self {
|
||||
let millis = world
|
||||
.clock()
|
||||
.get()
|
||||
.as_millis()
|
||||
.saturating_sub(Self::TIMESTAMP_OFFSET);
|
||||
let random: u128 = world.rng().random();
|
||||
|
||||
Self::from_parts(millis, random, metadata)
|
||||
}
|
||||
|
||||
/// Convert ID into its raw representation.
|
||||
pub const fn into_inner(self) -> NonZeroU128 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const KIND: Kind = Kind::MIN;
|
||||
const META: Metadata = Metadata::new(KIND, 0);
|
||||
|
||||
#[test]
|
||||
fn steps_simple_ordering() {
|
||||
let fst = Id::from_parts(0, 1, META);
|
||||
assert_eq!(fst.random().get(), 1);
|
||||
|
||||
// 0 steps = same id.
|
||||
assert_eq!(fst.next_gt(0), fst);
|
||||
|
||||
// Only random should increment.
|
||||
{
|
||||
let next = fst.next_gt(1);
|
||||
assert_eq!(next.timestamp_ms_rel(), 0);
|
||||
assert_eq!(next.random().get(), 2);
|
||||
assert_eq!(next.metadata(), META);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steps_borderline_ordering() {
|
||||
const QUOTA: u128 = (1 << 72) - 1;
|
||||
|
||||
let fst = Id::from_parts(0, QUOTA - 1, META);
|
||||
|
||||
// random() = QUOTA is ok, should be preserved.
|
||||
{
|
||||
let next = fst.next_gt(1);
|
||||
assert_eq!(next.timestamp_ms_rel(), 0);
|
||||
assert_eq!(next.random().get(), QUOTA);
|
||||
assert_eq!(next.metadata(), META);
|
||||
}
|
||||
|
||||
// Timestamp increment.
|
||||
{
|
||||
let next = fst.next_gt(2);
|
||||
assert_eq!(next.timestamp_ms_rel(), 1);
|
||||
assert_eq!(next.random().get(), 1);
|
||||
assert_eq!(next.metadata(), META);
|
||||
}
|
||||
|
||||
// Random should be 2, when getting next greater id, 1 = 0
|
||||
// shouldn't be visible.
|
||||
{
|
||||
let next = fst.next_gt(3);
|
||||
assert_eq!(next.timestamp_ms_rel(), 1);
|
||||
// May be faulty due to `1 = 0` in random, MUST be worked-around
|
||||
// by implementation by removing `0` from the set of possible values
|
||||
// here.
|
||||
assert_eq!(next.random().get(), 2);
|
||||
assert_eq!(next.metadata(), META);
|
||||
}
|
||||
|
||||
// Make third jump, should be correct as well.
|
||||
{
|
||||
let next = fst.next_gt(2 + QUOTA);
|
||||
assert_eq!(next.timestamp_ms_rel(), 2);
|
||||
assert_eq!(next.random().get(), 1);
|
||||
assert_eq!(next.metadata(), META);
|
||||
}
|
||||
|
||||
// Same for third jump, 1 = 0 check.
|
||||
{
|
||||
let next = fst.next_gt(2 + QUOTA + 1);
|
||||
assert_eq!(next.timestamp_ms_rel(), 2);
|
||||
assert_eq!(next.random().get(), 2);
|
||||
assert_eq!(next.metadata(), META);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_ordering() {
|
||||
assert!(Id::from_parts(0, 0, META) < Id::from_parts(1, 0, META));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_back() {
|
||||
let id: Id = "nigger".parse().unwrap();
|
||||
let back = id.to_str().to_string();
|
||||
|
||||
assert_eq!(back, "nigger");
|
||||
}
|
||||
}
|
129
core/src/types/file.rs
Normal file
129
core/src/types/file.rs
Normal file
|
@ -0,0 +1,129 @@
|
|||
//! # File identifier
|
||||
//!
|
||||
//! Basically same as [`entity::Id`], but additionally stores server where file is located in
|
||||
//! two lowest bits of random.
|
||||
|
||||
use std::{mem, num::NonZeroU128, str::FromStr};
|
||||
|
||||
use crate::types::entity;
|
||||
use eva::{data, hash::blake3, int, str, str::ToCompactString};
|
||||
|
||||
pub type Hash = blake3::Hash;
|
||||
|
||||
#[data(copy, ord, display(name))]
|
||||
pub enum ImageFormat {
|
||||
Png,
|
||||
Jpeg,
|
||||
Bmp,
|
||||
Gif,
|
||||
Webp,
|
||||
}
|
||||
|
||||
#[data(copy, ord, display(name))]
|
||||
pub enum Class {
|
||||
Image,
|
||||
GameFile,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref BASE_NAME_CHAR_PAT: String = {
|
||||
let mut out = String::from("^");
|
||||
|
||||
eva::push_ascii_pat!(BaseNameBoundChar, &mut out);
|
||||
|
||||
eva::push_ascii_pat!(BaseNameChar, &mut out);
|
||||
out.push_str("{0,");
|
||||
out.push_str(&str::format_compact!("{}", const { BaseName::MAX_LEN - 2 }));
|
||||
out.push('}');
|
||||
|
||||
eva::push_ascii_pat!(BaseNameBoundChar, &mut out);
|
||||
|
||||
out.push('$');
|
||||
|
||||
out
|
||||
};
|
||||
}
|
||||
|
||||
#[int(u8, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_')]
|
||||
enum BaseNameBoundChar {}
|
||||
|
||||
#[int(u8, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'.' | b'-' | b'_')]
|
||||
enum BaseNameChar {}
|
||||
|
||||
/// Base file name.
|
||||
#[str(custom)]
|
||||
#[derive(schemars::JsonSchema)]
|
||||
pub struct BaseName(#[schemars(regex(pattern = BASE_NAME_CHAR_PAT))] str::CompactString);
|
||||
|
||||
impl BaseName {
|
||||
pub const MAX_LEN: usize = 256;
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for BaseName {
|
||||
type Err = str::ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s.len() >= Self::MAX_LEN {
|
||||
return Err(str::ParseError::Length);
|
||||
}
|
||||
|
||||
let bytes = s.as_bytes();
|
||||
if let Some((&first, &last)) = bytes.first().zip(bytes.last()) {
|
||||
_ = BaseNameBoundChar::new(first).ok_or(str::ParseError::Char)?;
|
||||
_ = BaseNameBoundChar::new(last).ok_or(str::ParseError::Char)?;
|
||||
}
|
||||
|
||||
for b in s.as_bytes().iter().copied() {
|
||||
_ = BaseNameChar::new(b).ok_or(str::ParseError::Char)?;
|
||||
}
|
||||
|
||||
Ok(Self(s.to_compact_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// ID of the server.
|
||||
#[int(u8, 0..8)]
|
||||
#[derive(Hash)]
|
||||
pub enum Server {}
|
||||
|
||||
impl Server {
|
||||
pub const fn try_from_ascii_repr(val: u8) -> Option<Self> {
|
||||
if val < b'a' {
|
||||
return None;
|
||||
}
|
||||
|
||||
Self::new(val - b'a')
|
||||
}
|
||||
|
||||
pub const fn ascii_repr(self) -> u8 {
|
||||
b'a' + (self as u8)
|
||||
}
|
||||
}
|
||||
|
||||
entity::define_eid! {
|
||||
pub struct Id(File);
|
||||
}
|
||||
|
||||
impl Id {
|
||||
pub const fn from_parts(millis: u64, random: u128, server: Server) -> Self {
|
||||
let id = entity::Id::from_parts(
|
||||
millis,
|
||||
random,
|
||||
entity::Metadata::new(entity::Kind::File, server as u8),
|
||||
);
|
||||
Self(id)
|
||||
}
|
||||
|
||||
pub const fn server(self) -> Server {
|
||||
// It's still possible that ID is invalid because of deserialization.
|
||||
unsafe { mem::transmute::<u8, Server>(self.0.metadata().data() & 0b111) }
|
||||
}
|
||||
|
||||
pub const fn into_inner(self) -> NonZeroU128 {
|
||||
self.0.into_inner()
|
||||
}
|
||||
}
|
161
core/src/types/game.rs
Normal file
161
core/src/types/game.rs
Normal file
|
@ -0,0 +1,161 @@
|
|||
use eva::{array, data, int, str, str::CompactString, time::Date};
|
||||
|
||||
use crate::types::{author, entity::define_eid, file, slug, user};
|
||||
|
||||
#[data]
|
||||
#[derive(Default)]
|
||||
pub struct Tags(pub array::ImmutableHeap<Tag, 64>);
|
||||
|
||||
#[data]
|
||||
#[derive(Default)]
|
||||
pub struct Genres(pub array::ImmutableHeap<Genre, 64>);
|
||||
|
||||
#[data]
|
||||
#[derive(Default)]
|
||||
pub struct Badges(pub array::ImmutableHeap<Badge, 64>);
|
||||
|
||||
#[data]
|
||||
#[derive(Default)]
|
||||
pub struct Screenshots(pub array::ImmutableHeap<file::Id, 8>);
|
||||
|
||||
/// Query for searching.
|
||||
#[str(newtype)]
|
||||
pub struct SearchQuery(pub CompactString);
|
||||
|
||||
/// Fully qualified game path.
|
||||
#[data(copy, display("{author}/@{slug}"))]
|
||||
pub struct FullyQualified {
|
||||
pub author: author::Selector,
|
||||
pub slug: Slug,
|
||||
}
|
||||
|
||||
/// How game can be selected.
|
||||
#[data(copy)]
|
||||
pub enum Selector {
|
||||
#[display("{_0}")]
|
||||
FullyQualified(#[from] FullyQualified),
|
||||
#[display("{_0}")]
|
||||
Id(#[from] Id),
|
||||
}
|
||||
|
||||
/// Date precision.
|
||||
#[data(copy, ord, display("name"))]
|
||||
pub enum DatePrecision {
|
||||
/// Precise to day, month and year.
|
||||
Day,
|
||||
/// Precise to month and year.
|
||||
Month,
|
||||
/// Precise to year.
|
||||
Year,
|
||||
}
|
||||
|
||||
/// Date when game is released.
|
||||
#[data]
|
||||
pub struct ReleaseDate {
|
||||
/// Date.
|
||||
pub date: Date,
|
||||
/// Precision of the date.
|
||||
pub precision: DatePrecision,
|
||||
}
|
||||
|
||||
/// ID of the vndb entry.
|
||||
#[data(copy, ord, display("v{_0}"))]
|
||||
pub struct VndbId(pub u64);
|
||||
|
||||
#[data]
|
||||
pub struct PubModerated {
|
||||
pub by: user::Id,
|
||||
pub at: Date,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct PubVerified {
|
||||
pub by: user::Id,
|
||||
pub at: Date,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub enum Publication {
|
||||
/// Game was published after a manual moderation.
|
||||
Moderated(PubModerated),
|
||||
/// Game was published immediately after publication
|
||||
/// request, since author is verified.
|
||||
Verified(PubVerified),
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Game {
|
||||
pub id: Id,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub slug: Option<Slug>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub thumbnail: Option<file::Id>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub vndb: Option<VndbId>,
|
||||
pub title: Title,
|
||||
pub description: Option<Description>,
|
||||
pub mean_rating: MeanRating,
|
||||
pub author: author::Mini,
|
||||
pub release_date: Option<ReleaseDate>,
|
||||
pub publication: Option<Publication>,
|
||||
|
||||
pub screenshots: Screenshots,
|
||||
pub tags: Tags,
|
||||
pub genres: Genres,
|
||||
pub badges: Badges,
|
||||
}
|
||||
|
||||
#[data(copy)]
|
||||
pub struct MeanRating {
|
||||
/// Mean value of all ratings.
|
||||
pub mean: RatingValue,
|
||||
pub votes: Votes,
|
||||
}
|
||||
|
||||
impl Default for MeanRating {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mean: RatingValue::POS0,
|
||||
votes: Votes(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of votes from users.
|
||||
#[data(copy, ord, display("{_0}"))]
|
||||
#[derive(Hash)]
|
||||
pub struct Votes(pub u32);
|
||||
|
||||
define_eid! {
|
||||
/// ID of the game.
|
||||
pub struct Id(Game);
|
||||
|
||||
/// ID of the tag.
|
||||
pub struct Tag(Tag);
|
||||
|
||||
/// Game badge.
|
||||
pub struct Badge(Badge);
|
||||
}
|
||||
|
||||
#[str(newtype, copy)]
|
||||
pub struct Genre(pub slug::LowerSlug<15>);
|
||||
|
||||
/// Estimated time to read.
|
||||
#[int(u8, 0..=100)]
|
||||
pub enum TimeToReadHours {}
|
||||
|
||||
/// Numerical rating of the game.
|
||||
#[int(u8, 0..=100)]
|
||||
pub enum RatingValue {}
|
||||
|
||||
/// Short, url-safe human-readable identifier of the game.
|
||||
#[str(newtype, copy)]
|
||||
pub struct Slug(pub slug::Slug<31>);
|
||||
|
||||
/// Game title.
|
||||
#[str(newtype)]
|
||||
pub struct Title(CompactString);
|
||||
|
||||
/// Game description.
|
||||
#[str(newtype)]
|
||||
pub struct Description(CompactString);
|
51
core/src/types/message.rs
Normal file
51
core/src/types/message.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
use eva::{data, int, str, time::Timestamp};
|
||||
|
||||
use crate::types::{author, entity, file, thread, user};
|
||||
|
||||
#[int(u8, 1..=128)]
|
||||
pub enum PaginationLimit {}
|
||||
|
||||
#[data(copy)]
|
||||
#[serde(untagged)]
|
||||
pub enum Selector {
|
||||
#[display("{_0}")]
|
||||
Id(#[from] Id),
|
||||
}
|
||||
|
||||
entity::define_eid! {
|
||||
pub struct Id(Message);
|
||||
}
|
||||
|
||||
entity::define_eid! {
|
||||
pub struct ById(User | Author);
|
||||
}
|
||||
|
||||
impl ById {
|
||||
pub const fn user(user: user::Id) -> Self {
|
||||
Self(user.raw_id())
|
||||
}
|
||||
|
||||
pub const fn author(author: author::Id) -> Self {
|
||||
Self(author.raw_id())
|
||||
}
|
||||
}
|
||||
|
||||
/// Text of the message.
|
||||
#[str(newtype)]
|
||||
pub struct Text(str::CompactString);
|
||||
|
||||
#[data]
|
||||
pub struct By {
|
||||
pub id: ById,
|
||||
pub display_name: str::CompactString,
|
||||
pub pfp: file::Id,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Message {
|
||||
pub id: Id,
|
||||
pub by: By,
|
||||
pub thread: thread::Id,
|
||||
pub text: Text,
|
||||
pub created_at: Timestamp,
|
||||
}
|
22
core/src/types/mod.rs
Normal file
22
core/src/types/mod.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
pub use self::{patch::Patch, r#true::True};
|
||||
|
||||
pub mod r#true;
|
||||
|
||||
pub mod board;
|
||||
pub mod message;
|
||||
pub mod thread;
|
||||
|
||||
pub mod author;
|
||||
pub mod user;
|
||||
|
||||
pub mod game;
|
||||
|
||||
pub mod entity;
|
||||
pub mod file;
|
||||
pub mod upload;
|
||||
|
||||
pub mod patch;
|
||||
pub mod slug;
|
||||
|
||||
pub mod captcha;
|
||||
pub mod session;
|
33
core/src/types/patch.rs
Normal file
33
core/src/types/patch.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use eva::data;
|
||||
|
||||
#[data(copy)]
|
||||
#[derive(Default)]
|
||||
pub enum Patch<T> {
|
||||
#[default]
|
||||
Keep,
|
||||
Change(T),
|
||||
}
|
||||
|
||||
impl<T> Patch<T> {
|
||||
pub fn option(self) -> Option<T> {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Option<T>> for Patch<T> {
|
||||
fn from(value: Option<T>) -> Self {
|
||||
match value {
|
||||
Some(x) => Self::Change(x),
|
||||
None => Self::Keep,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Patch<T>> for Option<T> {
|
||||
fn from(value: Patch<T>) -> Self {
|
||||
match value {
|
||||
Patch::Keep => None,
|
||||
Patch::Change(v) => Some(v),
|
||||
}
|
||||
}
|
||||
}
|
5
core/src/types/session.rs
Normal file
5
core/src/types/session.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
use crate::types::entity;
|
||||
|
||||
entity::define_eid! {
|
||||
pub struct Token(Session);
|
||||
}
|
181
core/src/types/slug.rs
Normal file
181
core/src/types/slug.rs
Normal file
|
@ -0,0 +1,181 @@
|
|||
use std::{borrow::Cow, num::NonZeroU8, slice, str::FromStr};
|
||||
|
||||
use eva::{data, int, str, str::HasPattern};
|
||||
use schemars::JsonSchema;
|
||||
|
||||
pub type LowerSlug<const MAX: usize> = Slug<MAX, LowerSlugStart, LowerSlugRest>;
|
||||
|
||||
pub unsafe trait SlugPart: Default + Copy + Eq + Ord {
|
||||
const TYPE_NAME: &str;
|
||||
|
||||
fn from_u8(u: u8) -> Option<Self>;
|
||||
fn push_pat(into: &mut String);
|
||||
}
|
||||
|
||||
#[data(copy, error, display(doc))]
|
||||
pub enum ParseError {
|
||||
/// Invalid char at start.
|
||||
CharAtStart,
|
||||
/// Invalid char at rest.
|
||||
CharAtRest { pos: u8 },
|
||||
/// Invalid length.
|
||||
Length,
|
||||
}
|
||||
|
||||
macro_rules! slug_part {
|
||||
($ty:ident: $default:literal) => {
|
||||
impl Default for $ty {
|
||||
fn default() -> Self {
|
||||
Self::new($default).expect("shit happens")
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl SlugPart for $ty {
|
||||
const TYPE_NAME: &str = stringify!($ty);
|
||||
|
||||
fn push_pat(into: &mut String) {
|
||||
eva::push_ascii_pat!($ty, into);
|
||||
}
|
||||
|
||||
fn from_u8(u: u8) -> Option<Self> {
|
||||
Self::new(u)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[int(u8, b'a'..=b'z')]
|
||||
pub enum LowerSlugStart {}
|
||||
|
||||
slug_part!(LowerSlugStart: b'z');
|
||||
|
||||
#[int(u8, b'a'..=b'z' | b'0'..=b'9' | b'_' | b'-')]
|
||||
pub enum LowerSlugRest {}
|
||||
|
||||
slug_part!(LowerSlugRest: b'z');
|
||||
|
||||
#[int(u8, b'A'..=b'Z' | b'a'..=b'z')]
|
||||
pub enum SlugStart {}
|
||||
|
||||
slug_part!(SlugStart: b'Z');
|
||||
|
||||
#[int(u8, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_')]
|
||||
pub enum SlugRest {}
|
||||
|
||||
slug_part!(SlugRest: b'Z');
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[repr(C, align(1))]
|
||||
struct SlugBuf<const MAX: usize, Start, Rest>(Start, [Rest; MAX]);
|
||||
|
||||
#[str(custom, copy)]
|
||||
pub struct Slug<const MAX: usize, Start: SlugPart = SlugStart, Rest: SlugPart = SlugRest> {
|
||||
buf: SlugBuf<MAX, Start, Rest>,
|
||||
len: NonZeroU8,
|
||||
}
|
||||
|
||||
impl<const MAX: usize> Slug<MAX> {
|
||||
pub const MAX: usize = MAX;
|
||||
|
||||
pub const fn lower(mut self) -> LowerSlug<MAX> {
|
||||
unsafe {
|
||||
self.buf.0 = SlugStart::new_unchecked((self.buf.0 as u8).to_ascii_lowercase());
|
||||
let mut idx = 0;
|
||||
|
||||
while idx != MAX {
|
||||
let rest = self.buf.1[idx] as u8;
|
||||
self.buf.1[idx] = SlugRest::new_unchecked(rest.to_ascii_lowercase());
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
unsafe { std::mem::transmute_copy::<Slug<MAX>, LowerSlug<MAX>>(&self) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<const MAX: usize, Start: SlugPart, Rest: SlugPart> FromStr for Slug<MAX, Start, Rest> {
|
||||
type Err = ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
use ParseError as E;
|
||||
|
||||
#[expect(non_snake_case)]
|
||||
let Z = Rest::default();
|
||||
|
||||
let Some((head, tail_str)) = s.split_at_checked(1) else {
|
||||
return Err(E::Length);
|
||||
};
|
||||
|
||||
let head = Start::from_u8(head.as_bytes()[0]).ok_or(E::CharAtStart)?;
|
||||
let mut tail = [Z; MAX];
|
||||
let mut len = 1_u8;
|
||||
for (idx, b) in tail_str.as_bytes().iter().copied().enumerate() {
|
||||
tail[idx] = Rest::from_u8(b).ok_or(E::CharAtRest {
|
||||
pos: (idx + 1) as u8,
|
||||
})?;
|
||||
len = len.checked_add(1).ok_or(E::Length)?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
buf: SlugBuf(head, tail),
|
||||
len: unsafe { NonZeroU8::new_unchecked(len) },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<const MAX: usize, Start: SlugPart, Rest: SlugPart> JsonSchema for Slug<MAX, Start, Rest> {
|
||||
fn schema_id() -> Cow<'static, str> {
|
||||
format!(
|
||||
"{}::Slug<{}, {}, {}>",
|
||||
module_path!(),
|
||||
MAX + 1,
|
||||
Start::TYPE_NAME,
|
||||
Rest::TYPE_NAME
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
Cow::Owned(format!(
|
||||
"Slug_{}_{}_{}",
|
||||
MAX + 1,
|
||||
Start::TYPE_NAME,
|
||||
Rest::TYPE_NAME
|
||||
))
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
let mut pat = String::from("^");
|
||||
Self::pat_into(&mut pat);
|
||||
pat.push('$');
|
||||
|
||||
schemars::json_schema!({
|
||||
"type": "string",
|
||||
"description": "human and machine readable identifier",
|
||||
"minLength": 1,
|
||||
"maxLength": MAX + 1,
|
||||
"pattern": pat,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<const MAX: usize, Start: SlugPart, Rest: SlugPart> Slug<MAX, Start, Rest> {
|
||||
pub const fn as_str(&self) -> &str {
|
||||
unsafe {
|
||||
std::str::from_utf8_unchecked(slice::from_raw_parts(
|
||||
(&raw const self.buf).cast(),
|
||||
self.len.get() as usize,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const MAX: usize, Start: SlugPart, Rest: SlugPart> HasPattern for Slug<MAX, Start, Rest> {
|
||||
fn pat_into(buf: &mut String) {
|
||||
Start::push_pat(buf);
|
||||
Rest::push_pat(buf);
|
||||
|
||||
buf.push_str("{0,");
|
||||
buf.push_str(&MAX.to_string());
|
||||
buf.push_str("}");
|
||||
}
|
||||
}
|
22
core/src/types/thread.rs
Normal file
22
core/src/types/thread.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use eva::data;
|
||||
|
||||
use crate::types::{board, entity, message};
|
||||
|
||||
entity::define_eid! {
|
||||
pub struct Id(Thread);
|
||||
}
|
||||
|
||||
#[data(copy)]
|
||||
pub enum Selector {
|
||||
#[display("{_0}")]
|
||||
Id(#[from] Id),
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Thread {
|
||||
pub id: Id,
|
||||
pub by: message::By,
|
||||
pub board: board::Brief,
|
||||
|
||||
pub text: message::Text,
|
||||
}
|
60
core/src/types/true.rs
Normal file
60
core/src/types/true.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use eva::data;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize, de};
|
||||
|
||||
#[data(copy, ord, error, display("must be `true`"))]
|
||||
pub struct MustBeTrue;
|
||||
|
||||
#[data(copy, ord, not(serde, schemars), display(doc))]
|
||||
#[derive(Default)]
|
||||
pub enum True {
|
||||
/// true.
|
||||
#[default]
|
||||
Value,
|
||||
}
|
||||
|
||||
impl JsonSchema for True {
|
||||
fn schema_id() -> Cow<'static, str> {
|
||||
bool::schema_id()
|
||||
}
|
||||
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
bool::schema_name()
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
schemars::json_schema!({"const": true})
|
||||
}
|
||||
}
|
||||
|
||||
impl True {
|
||||
pub const fn new() -> Self {
|
||||
Self::Value
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for True {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let res = bool::deserialize(deserializer)?;
|
||||
if res {
|
||||
Ok(True::Value)
|
||||
} else {
|
||||
Err(de::Error::custom(MustBeTrue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for True {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
true.serialize(serializer)
|
||||
}
|
||||
}
|
17
core/src/types/upload.rs
Normal file
17
core/src/types/upload.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
use eva::data;
|
||||
|
||||
use crate::types::{entity, file};
|
||||
|
||||
entity::define_eid! {
|
||||
pub struct Id(Upload);
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Upload {
|
||||
pub id: Id,
|
||||
pub file_name: Option<file::BaseName>,
|
||||
pub uploaded: u64,
|
||||
pub size: u64,
|
||||
pub class: file::Class,
|
||||
pub hash: file::Hash,
|
||||
}
|
157
core/src/types/user.rs
Normal file
157
core/src/types/user.rs
Normal file
|
@ -0,0 +1,157 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use serde::{Serialize, de};
|
||||
|
||||
use eva::{
|
||||
data, rand, str,
|
||||
str::{CompactString, HasPattern, ParseError, Seq, ascii},
|
||||
zst_error,
|
||||
};
|
||||
|
||||
use crate::types::{entity, file, slug::Slug};
|
||||
|
||||
/// Miniature for the user.
|
||||
#[data]
|
||||
pub struct Mini {
|
||||
pub id: Id,
|
||||
pub nickname: Nickname,
|
||||
pub role: Role,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pfp: Option<file::Id>,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct User {
|
||||
pub id: Id,
|
||||
pub nickname: Nickname,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub email: Option<Email>,
|
||||
pub role: Role,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bio: Option<Bio>,
|
||||
pub pfp: Option<file::Id>,
|
||||
}
|
||||
|
||||
#[data(copy)]
|
||||
#[serde(untagged)]
|
||||
pub enum Selector {
|
||||
#[serde(with = "NicknameStr")]
|
||||
#[display("@{_0}")]
|
||||
Nickname(#[from] Nickname),
|
||||
#[display("{_0}")]
|
||||
Id(#[from] Id),
|
||||
}
|
||||
|
||||
struct NicknameStr;
|
||||
|
||||
impl schemars::JsonSchema for NicknameStr {
|
||||
fn schema_id() -> Cow<'static, str> {
|
||||
Cow::Borrowed(concat!(module_path!(), "::NicknameStr"))
|
||||
}
|
||||
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
Cow::Borrowed("NicknameStr")
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
let mut pat = String::from("^@");
|
||||
Nickname::pat_into(&mut pat);
|
||||
pat.push('$');
|
||||
schemars::json_schema!({
|
||||
"type": "string",
|
||||
"pattern": pat,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl NicknameStr {
|
||||
pub fn deserialize<'de, D: serde::Deserializer<'de>>(des: D) -> Result<Nickname, D::Error> {
|
||||
let s = <&'de str as serde::Deserialize<'de>>::deserialize(des)?;
|
||||
if let Some(nickname) = s.strip_prefix('@') {
|
||||
nickname.parse().map_err(de::Error::custom)
|
||||
} else {
|
||||
Err(de::Error::custom(zst_error!(
|
||||
"nickname must start with a '@'"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize<S: serde::Serializer>(nick: &Nickname, ser: S) -> Result<S::Ok, S::Error> {
|
||||
eva::str::format_compact!("@{nick}").serialize(ser)
|
||||
}
|
||||
}
|
||||
|
||||
#[str(fixed(error = ParseError))]
|
||||
pub struct SignUpCompletionToken(Seq<16, ascii::Printable>);
|
||||
|
||||
// TODO: impl this from macro.
|
||||
impl rand::distr::Distribution<SignUpCompletionToken> for rand::distr::StandardUniform {
|
||||
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> SignUpCompletionToken {
|
||||
let seq = std::array::from_fn(|_| rng.random());
|
||||
SignUpCompletionToken(Seq(seq))
|
||||
}
|
||||
}
|
||||
|
||||
/// Email of the user.
|
||||
#[str(newtype)]
|
||||
pub struct Email(pub CompactString);
|
||||
|
||||
/// Hashed user password.
|
||||
#[str(newtype)]
|
||||
pub struct PasswordHash(pub CompactString);
|
||||
|
||||
/// Cleartext user password.
|
||||
#[str(newtype)]
|
||||
pub struct Password(pub CompactString);
|
||||
|
||||
entity::define_eid! {
|
||||
/// ID of the user.
|
||||
pub struct Id(User);
|
||||
}
|
||||
|
||||
/// Machine and user-readable identifier.
|
||||
#[str(newtype, copy)]
|
||||
pub struct Nickname(pub Slug<23>);
|
||||
|
||||
/// Preferred name for display.
|
||||
#[str(newtype)]
|
||||
pub struct DisplayName(pub CompactString);
|
||||
|
||||
/// User's bio.
|
||||
#[str(newtype)]
|
||||
pub struct Bio(pub CompactString);
|
||||
|
||||
#[data(copy, ord, display(name))]
|
||||
#[derive(Default)]
|
||||
pub enum Role {
|
||||
/// The least powerful.
|
||||
Slave,
|
||||
/// Plain user.
|
||||
#[default]
|
||||
User,
|
||||
/// Moderator.
|
||||
Mod,
|
||||
/// Admin of the site.
|
||||
Admin,
|
||||
/// The most powerful user.
|
||||
Master,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Simple role ordering test.
|
||||
#[test]
|
||||
fn role_ordering() {
|
||||
let ordering = [Role::Admin, Role::Mod, Role::User, Role::Slave];
|
||||
let mut prev_role = Role::Master;
|
||||
for role in ordering {
|
||||
assert!(
|
||||
prev_role > role,
|
||||
"{prev_role} <= {role}, must be {prev_role} > {role}",
|
||||
);
|
||||
prev_role = role;
|
||||
}
|
||||
}
|
||||
}
|
13
core/src/uploads.rs
Normal file
13
core/src/uploads.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use eva::{bytes::Bytes, data, trait_set};
|
||||
|
||||
use futures::stream::Stream;
|
||||
|
||||
#[data]
|
||||
pub enum Action {
|
||||
Push(Bytes),
|
||||
Abort,
|
||||
}
|
||||
|
||||
trait_set! {
|
||||
pub trait UploadStream = Stream<Item = Action> + Unpin + Send;
|
||||
}
|
70
core/src/world.rs
Normal file
70
core/src/world.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
use eva::{auto_impl, rand, time::Clock};
|
||||
|
||||
use crate::rt;
|
||||
|
||||
#[cfg(feature = "test-util")]
|
||||
pub mod testing;
|
||||
|
||||
eva::trait_set! {
|
||||
pub trait Traits = Send + Sync;
|
||||
}
|
||||
|
||||
pub trait Demiurge: Traits {
|
||||
type World: WorldMut + 'static;
|
||||
|
||||
fn make_world(&self) -> World<Self::World>;
|
||||
}
|
||||
|
||||
#[auto_impl(&, &mut)]
|
||||
pub trait WorldRef: Traits + Clock {
|
||||
fn rt(&self) -> impl rt::RtRef;
|
||||
}
|
||||
|
||||
#[auto_impl(&mut)]
|
||||
pub trait WorldMut: WorldRef {
|
||||
fn rng(&mut self) -> impl rand::Rng;
|
||||
|
||||
fn rt_mut(&mut self) -> impl rt::RtMut;
|
||||
}
|
||||
|
||||
pub struct World<W>(pub W);
|
||||
|
||||
macro_rules! narrow {
|
||||
($($method:ident : $Trait:ty),* $(,)?) => {eva::paste! {$(
|
||||
impl<W: WorldMut> World<W> {
|
||||
pub const fn [<$method _mut>](&mut self) -> &mut impl $Trait {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: WorldRef> World<W> {
|
||||
pub const fn $method(&self) -> &impl $Trait {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
)*}};
|
||||
}
|
||||
|
||||
narrow! {
|
||||
clock: Clock,
|
||||
}
|
||||
|
||||
impl<W: WorldRef> World<W> {
|
||||
pub fn rt(&self) -> impl rt::RtRef {
|
||||
self.0.rt()
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: WorldMut> World<W> {
|
||||
pub fn rng(&mut self) -> impl rand::Rng {
|
||||
self.0.rng()
|
||||
}
|
||||
|
||||
pub fn rt_mut(&mut self) -> impl rt::RtMut {
|
||||
self.0.rt_mut()
|
||||
}
|
||||
|
||||
pub fn mut_(&mut self) -> World<&mut W> {
|
||||
World(&mut self.0)
|
||||
}
|
||||
}
|
87
core/src/world/testing.rs
Normal file
87
core/src/world/testing.rs
Normal file
|
@ -0,0 +1,87 @@
|
|||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use eva::{
|
||||
fut::Fut,
|
||||
rand::{self, xoshiro},
|
||||
time,
|
||||
};
|
||||
|
||||
use crate::rt;
|
||||
|
||||
pub struct World {
|
||||
rng: xoshiro::Xoshiro256StarStar,
|
||||
clock: time::Mock,
|
||||
}
|
||||
|
||||
impl time::Clock for World {
|
||||
fn get(&self) -> time::Timestamp {
|
||||
self.clock.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl super::WorldRef for World {
|
||||
fn rt(&self) -> impl rt::RtRef {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl super::WorldMut for World {
|
||||
fn rt_mut(&mut self) -> impl rt::RtMut {
|
||||
self
|
||||
}
|
||||
|
||||
fn rng(&mut self) -> impl rand::Rng {
|
||||
&mut self.rng
|
||||
}
|
||||
}
|
||||
|
||||
impl rt::RtRef for World {}
|
||||
impl rt::RtMut for World {
|
||||
fn spawn<F: Fut<Output: Send> + 'static>(&mut self, task: F) {
|
||||
tokio::spawn(task);
|
||||
}
|
||||
|
||||
async fn spawn_blocking<F: rt::BlockingFn>(&mut self, task: F) -> F::Ret {
|
||||
tokio::task::spawn_blocking(task)
|
||||
.await
|
||||
.expect("underlying task panicked")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Demiurge {
|
||||
rng: Arc<Mutex<xoshiro::Xoshiro256StarStar>>,
|
||||
pub clock: time::Mock,
|
||||
}
|
||||
|
||||
impl Demiurge {
|
||||
pub fn from_os_rng() -> Self {
|
||||
Self {
|
||||
rng: Arc::new(Mutex::new(rand::SeedableRng::from_os_rng())),
|
||||
clock: time::Mock::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(seed: u64) -> Self {
|
||||
Self {
|
||||
rng: Arc::new(Mutex::new(rand::SeedableRng::seed_from_u64(seed))),
|
||||
clock: time::Mock::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demiurge for Demiurge {
|
||||
type World = World;
|
||||
|
||||
fn make_world(&self) -> super::World<Self::World> {
|
||||
super::World(World {
|
||||
rng: {
|
||||
let mut global = self.rng.lock().unwrap();
|
||||
let rng = (*global).clone();
|
||||
// Jump to avoid overlapping.
|
||||
global.jump();
|
||||
rng
|
||||
},
|
||||
clock: self.clock.clone(),
|
||||
})
|
||||
}
|
||||
}
|
99
flake.lock
generated
Normal file
99
flake.lock
generated
Normal file
|
@ -0,0 +1,99 @@
|
|||
{
|
||||
"nodes": {
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1758264155,
|
||||
"narHash": "sha256-sgg1sd/pYO9C7ccY9tAvR392CDscx8sqXrHkxspjIH0=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "a7d9df0179fcc48259a68b358768024f8e5a6372",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1758198701,
|
||||
"narHash": "sha256-7To75JlpekfUmdkUZewnT6MoBANS0XVypW6kjUOXQwc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "0147c2f1d54b30b5dd6d4a8c8542e8d7edf93b5d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"ref": "nixos-unstable",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"fenix": "fenix",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1758224093,
|
||||
"narHash": "sha256-buZMH6NgzSLowTda+aArct5ISsMR/S888EdFaqUvbog=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "958a8d06e3e5ba7dca7cc23b0639335071d65f2a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
41
flake.nix
Normal file
41
flake.nix
Normal file
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
inputs = {
|
||||
fenix = {
|
||||
url = "github:nix-community/fenix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs = { flake-utils, fenix, nixpkgs, ... }:
|
||||
flake-utils.lib.eachDefaultSystem(system:
|
||||
let
|
||||
overlays = [ fenix.overlays.default ];
|
||||
pkgs = import nixpkgs { inherit system overlays; };
|
||||
rust = with pkgs.fenix; combine [
|
||||
((fromToolchainName { name = "1.87"; sha256 = "sha256-KUm16pHj+cRedf8vxs/Hd2YWxpOrWZ7UOrwhILdSJBU="; }).withComponents [
|
||||
"cargo"
|
||||
"rustc"
|
||||
"rust-src"
|
||||
"rust-analyzer"
|
||||
"clippy"
|
||||
"llvm-tools-preview"
|
||||
])
|
||||
default.rustfmt
|
||||
];
|
||||
llvm = pkgs.llvmPackages_20;
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
|
||||
packages = [ rust ] ++ (with pkgs; [
|
||||
libclang.lib
|
||||
]);
|
||||
buildInputs = with pkgs; [
|
||||
stdenv.cc.cc.lib
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
33
http/Cargo.toml
Normal file
33
http/Cargo.toml
Normal file
|
@ -0,0 +1,33 @@
|
|||
[package]
|
||||
name = "viendesu-http"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
client = ["dep:reqwest"]
|
||||
server = [
|
||||
"dep:axum",
|
||||
"dep:http-body-util",
|
||||
"dep:tower-http",
|
||||
"dep:serde_urlencoded",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
viendesu-core.workspace = true
|
||||
http = "1.3.1"
|
||||
|
||||
serde.workspace = true
|
||||
serde_with.workspace = true
|
||||
serde_json.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
eva.workspace = true
|
||||
eyre.workspace = true
|
||||
|
||||
axum = { optional = true, version = "0.8.4", default-features = false, features = ["http2", "ws", "macros"] }
|
||||
http-body-util = { version = "0.1.3", optional = true }
|
||||
tower-http = { optional = true, version = "0.6.6", default-features = false, features = ["limit"] }
|
||||
serde_urlencoded = { version = "0.7.1", optional = true }
|
||||
|
||||
reqwest = { optional = true, version = "0.12.23", default-features = false }
|
15
http/src/client/auth.rs
Normal file
15
http/src/client/auth.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use viendesu_core::errors::AuxResult;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl AuthenticationMut for Hidden<'_> {
|
||||
async fn authenticate(&mut self, session: session::Token) -> AuxResult<()> {
|
||||
self.0.session = Some(session);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.0.session = None;
|
||||
}
|
||||
}
|
23
http/src/client/authors.rs
Normal file
23
http/src/client/authors.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
use super::*;
|
||||
|
||||
use viendesu_core::requests::authors::{create, get, update};
|
||||
|
||||
use crate::requests::authors as requests;
|
||||
|
||||
impl AuthorsRef for Hidden<'_> {
|
||||
fn get(&mut self) -> impl CallStep<get::Args, Ok = get::Ok, Err = get::Err> {
|
||||
self.do_call(Method::GET, |get::Args { author }| {
|
||||
(format_compact!("/authors/{author}"), requests::Get {})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthorsMut for Hidden<'_> {
|
||||
fn create(&mut self) -> impl CallStep<create::Args, Ok = create::Ok, Err = create::Err> {
|
||||
self.do_call(Method::POST, todo::<_, requests::Create>())
|
||||
}
|
||||
|
||||
fn update(&mut self) -> impl CallStep<update::Args, Ok = update::Ok, Err = update::Err> {
|
||||
self.do_call(Method::PATCH, todo::<_, requests::Update>())
|
||||
}
|
||||
}
|
25
http/src/client/boards.rs
Normal file
25
http/src/client/boards.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use super::*;
|
||||
|
||||
use viendesu_core::requests::boards::{create, delete, edit, get};
|
||||
|
||||
use crate::requests::boards as requests;
|
||||
|
||||
impl BoardsRef for Hidden<'_> {
|
||||
fn get(&mut self) -> impl CallStep<get::Args, Ok = get::Ok, Err = get::Err> {
|
||||
self.do_call(Method::GET, todo::<_, requests::Get>())
|
||||
}
|
||||
}
|
||||
|
||||
impl BoardsMut for Hidden<'_> {
|
||||
fn create(&mut self) -> impl CallStep<create::Args, Ok = create::Ok, Err = create::Err> {
|
||||
self.do_call(Method::POST, todo::<_, requests::Create>())
|
||||
}
|
||||
|
||||
fn delete(&mut self) -> impl CallStep<delete::Args, Ok = delete::Ok, Err = delete::Err> {
|
||||
self.do_call(Method::DELETE, todo::<_, requests::Delete>())
|
||||
}
|
||||
|
||||
fn edit(&mut self) -> impl CallStep<edit::Args, Ok = edit::Ok, Err = edit::Err> {
|
||||
self.do_call(Method::PATCH, todo::<_, requests::Edit>())
|
||||
}
|
||||
}
|
25
http/src/client/games.rs
Normal file
25
http/src/client/games.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use super::*;
|
||||
|
||||
use viendesu_core::requests::games::{create, get, search, update};
|
||||
|
||||
use crate::requests::games as requests;
|
||||
|
||||
impl GamesRef for Hidden<'_> {
|
||||
fn get(&mut self) -> impl CallStep<get::Args, Ok = get::Ok, Err = get::Err> {
|
||||
self.do_call(Method::GET, todo::<_, requests::Get>())
|
||||
}
|
||||
|
||||
fn search(&mut self) -> impl CallStep<search::Args, Ok = search::Ok, Err = search::Err> {
|
||||
self.do_call(Method::POST, todo::<_, requests::Search>())
|
||||
}
|
||||
}
|
||||
|
||||
impl GamesMut for Hidden<'_> {
|
||||
fn create(&mut self) -> impl CallStep<create::Args, Ok = create::Ok, Err = create::Err> {
|
||||
self.do_call(Method::POST, todo::<_, requests::Create>())
|
||||
}
|
||||
|
||||
fn update(&mut self) -> impl CallStep<update::Args, Ok = update::Ok, Err = update::Err> {
|
||||
self.do_call(Method::PATCH, todo::<_, requests::Update>())
|
||||
}
|
||||
}
|
25
http/src/client/messages.rs
Normal file
25
http/src/client/messages.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use super::*;
|
||||
|
||||
use viendesu_core::requests::messages::{delete, edit, get, post};
|
||||
|
||||
use crate::requests::messages as requests;
|
||||
|
||||
impl MessagesRef for Hidden<'_> {
|
||||
fn get(&mut self) -> impl CallStep<get::Args, Ok = get::Ok, Err = get::Err> {
|
||||
self.do_call(Method::GET, todo::<_, requests::Get>())
|
||||
}
|
||||
}
|
||||
|
||||
impl MessagesMut for Hidden<'_> {
|
||||
fn post(&mut self) -> impl CallStep<post::Args, Ok = post::Ok, Err = post::Err> {
|
||||
self.do_call(Method::POST, todo::<_, requests::Post>())
|
||||
}
|
||||
|
||||
fn delete(&mut self) -> impl CallStep<delete::Args, Ok = delete::Ok, Err = delete::Err> {
|
||||
self.do_call(Method::DELETE, todo::<_, requests::Delete>())
|
||||
}
|
||||
|
||||
fn edit(&mut self) -> impl CallStep<edit::Args, Ok = edit::Ok, Err = edit::Err> {
|
||||
self.do_call(Method::PATCH, todo::<_, requests::Edit>())
|
||||
}
|
||||
}
|
165
http/src/client/mod.rs
Normal file
165
http/src/client/mod.rs
Normal file
|
@ -0,0 +1,165 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use eva::{
|
||||
error::ShitHappens,
|
||||
str::{CompactString, format_compact},
|
||||
};
|
||||
|
||||
use http::Method;
|
||||
use viendesu_core::{
|
||||
errors,
|
||||
requests::Response,
|
||||
service::{
|
||||
CallStep, Session, SessionMaker,
|
||||
authors::{AuthorsMut, AuthorsRef},
|
||||
authz::AuthenticationMut,
|
||||
boards::{BoardsMut, BoardsRef},
|
||||
games::{GamesMut, GamesRef},
|
||||
messages::{MessagesMut, MessagesRef},
|
||||
threads::{ThreadsMut, ThreadsRef},
|
||||
users::{UsersMut, UsersRef},
|
||||
},
|
||||
types::session,
|
||||
};
|
||||
|
||||
use crate::{format::Format, requests::Request};
|
||||
|
||||
mod boards;
|
||||
mod messages;
|
||||
mod threads;
|
||||
|
||||
mod auth;
|
||||
|
||||
mod authors;
|
||||
mod games;
|
||||
mod users;
|
||||
|
||||
struct DoRequest<'c, P> {
|
||||
client: &'c mut HttpClient,
|
||||
method: Method,
|
||||
map_payload: P,
|
||||
}
|
||||
|
||||
impl<'c, P, C, H> CallStep<C> for DoRequest<'c, P>
|
||||
where
|
||||
C: Send + Sync,
|
||||
P: Send + Sync + FnMut(C) -> (CompactString, H),
|
||||
H: Request,
|
||||
{
|
||||
type Ok = H::Response;
|
||||
type Err = H::Error;
|
||||
|
||||
async fn call(&mut self, req: C) -> Response<Self::Ok, Self::Err> {
|
||||
let (path, request) = (self.map_payload)(req);
|
||||
self.client
|
||||
.do_request(self.method.clone(), &path, request)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
struct Hidden<'a>(&'a mut HttpClient);
|
||||
|
||||
fn todo<C, O>() -> impl Send + Sync + FnMut(C) -> (CompactString, O) {
|
||||
|_| todo!()
|
||||
}
|
||||
|
||||
impl<'t> Hidden<'t> {
|
||||
fn do_call<'this, P>(&'this mut self, method: Method, map_payload: P) -> DoRequest<'this, P> {
|
||||
DoRequest {
|
||||
client: self.0,
|
||||
method,
|
||||
map_payload,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! fwd {
|
||||
($($method:ident -> $Ret:ident;)*) => {$(
|
||||
fn $method(&mut self) -> impl $Ret {
|
||||
Hidden(self)
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
pub struct HttpClient {
|
||||
options: Arc<ClientOptions>,
|
||||
client: reqwest::Client,
|
||||
session: Option<session::Token>,
|
||||
}
|
||||
|
||||
impl HttpClient {
|
||||
async fn do_request<R>(
|
||||
&self,
|
||||
method: Method,
|
||||
path: &str,
|
||||
request: R,
|
||||
) -> Response<R::Response, R::Error>
|
||||
where
|
||||
R: Request,
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Session for HttpClient {
|
||||
fwd! {
|
||||
users -> UsersRef;
|
||||
users_mut -> UsersMut;
|
||||
authors -> AuthorsRef;
|
||||
authors_mut -> AuthorsMut;
|
||||
games -> GamesRef;
|
||||
games_mut -> GamesMut;
|
||||
boards -> BoardsMut;
|
||||
threads -> ThreadsMut;
|
||||
messages -> MessagesMut;
|
||||
authentication_mut -> AuthenticationMut;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClientOptions {
|
||||
pub format: Format,
|
||||
pub endpoint: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HttpService {
|
||||
options: Arc<ClientOptions>,
|
||||
inner: reqwest::Client,
|
||||
}
|
||||
|
||||
impl SessionMaker for HttpService {
|
||||
type Session = HttpClient;
|
||||
|
||||
async fn make_session(&self) -> errors::AuxResult<Self::Session> {
|
||||
Ok(HttpClient {
|
||||
options: Arc::clone(&self.options),
|
||||
client: self.inner.clone(),
|
||||
session: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpService {
|
||||
pub fn new(options: ClientOptions) -> Self {
|
||||
let inner = reqwest::Client::builder()
|
||||
.user_agent("Makise/HTTPP")
|
||||
.default_headers({
|
||||
use reqwest::header;
|
||||
|
||||
let mut headers = header::HeaderMap::new();
|
||||
headers.insert(
|
||||
"Accept",
|
||||
header::HeaderValue::from_static(options.format.mime_type()),
|
||||
);
|
||||
|
||||
headers
|
||||
})
|
||||
.build()
|
||||
.shit_happens();
|
||||
|
||||
Self {
|
||||
inner,
|
||||
options: Arc::new(options),
|
||||
}
|
||||
}
|
||||
}
|
29
http/src/client/threads.rs
Normal file
29
http/src/client/threads.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
use super::*;
|
||||
|
||||
use viendesu_core::requests::threads::{create, delete, edit, get, search};
|
||||
|
||||
use crate::requests::threads as requests;
|
||||
|
||||
impl ThreadsRef for Hidden<'_> {
|
||||
fn get(&mut self) -> impl CallStep<get::Args, Ok = get::Ok, Err = get::Err> {
|
||||
self.do_call(Method::GET, todo::<_, requests::Get>())
|
||||
}
|
||||
|
||||
fn search(&mut self) -> impl CallStep<search::Args, Ok = search::Ok, Err = search::Err> {
|
||||
self.do_call(Method::POST, todo::<_, requests::Search>())
|
||||
}
|
||||
}
|
||||
|
||||
impl ThreadsMut for Hidden<'_> {
|
||||
fn delete(&mut self) -> impl CallStep<delete::Args, Ok = delete::Ok, Err = delete::Err> {
|
||||
self.do_call(Method::DELETE, todo::<_, requests::Delete>())
|
||||
}
|
||||
|
||||
fn edit(&mut self) -> impl CallStep<edit::Args, Ok = edit::Ok, Err = edit::Err> {
|
||||
self.do_call(Method::PATCH, todo::<_, requests::Edit>())
|
||||
}
|
||||
|
||||
fn create(&mut self) -> impl CallStep<create::Args, Ok = create::Ok, Err = create::Err> {
|
||||
self.do_call(Method::POST, todo::<_, requests::Create>())
|
||||
}
|
||||
}
|
74
http/src/client/users.rs
Normal file
74
http/src/client/users.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
use super::*;
|
||||
|
||||
use eva::str::format_compact;
|
||||
|
||||
use viendesu_core::requests::users::{check_auth, confirm_sign_up, get, sign_in, sign_up, update};
|
||||
|
||||
use crate::requests::users as requests;
|
||||
|
||||
impl UsersRef for Hidden<'_> {
|
||||
fn get(&mut self) -> impl CallStep<get::Args, Ok = get::Ok, Err = get::Err> {
|
||||
self.do_call(Method::GET, |get::Args { user }| match user {
|
||||
Some(u) => (format_compact!("/users/{u}"), requests::Get {}),
|
||||
None => ("/users/me".into(), requests::Get {}),
|
||||
})
|
||||
}
|
||||
|
||||
fn check_auth(
|
||||
&mut self,
|
||||
) -> impl CallStep<check_auth::Args, Ok = check_auth::Ok, Err = check_auth::Err> {
|
||||
self.do_call(Method::GET, |check_auth::Args {}| {
|
||||
("/users/check_auth".into(), requests::CheckAuth {})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl UsersMut for Hidden<'_> {
|
||||
fn confirm_sign_up(
|
||||
&mut self,
|
||||
) -> impl CallStep<confirm_sign_up::Args, Ok = confirm_sign_up::Ok, Err = confirm_sign_up::Err>
|
||||
{
|
||||
self.do_call(Method::POST, todo::<_, requests::ConfirmSignUp>())
|
||||
}
|
||||
|
||||
fn update(&mut self) -> impl CallStep<update::Args, Ok = update::Ok, Err = update::Err> {
|
||||
self.do_call(Method::PATCH, |update::Args { user, update }| match user {
|
||||
Some(u) => (
|
||||
format_compact!("/users/{u}"),
|
||||
requests::Update::from(update),
|
||||
),
|
||||
None => ("/users/me".into(), requests::Update::from(update)),
|
||||
})
|
||||
}
|
||||
|
||||
fn sign_up(&mut self) -> impl CallStep<sign_up::Args, Ok = sign_up::Ok, Err = sign_up::Err> {
|
||||
self.do_call(
|
||||
Method::POST,
|
||||
|sign_up::Args {
|
||||
nickname,
|
||||
email,
|
||||
display_name,
|
||||
password,
|
||||
}| {
|
||||
(
|
||||
"/users/sign_up".into(),
|
||||
requests::SignUp {
|
||||
nickname,
|
||||
email,
|
||||
password,
|
||||
display_name,
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn sign_in(&mut self) -> impl CallStep<sign_in::Args, Ok = sign_in::Ok, Err = sign_in::Err> {
|
||||
self.do_call(Method::POST, |sign_in::Args { nickname, password }| {
|
||||
(
|
||||
"/users/sign_in".into(),
|
||||
requests::SignIn { nickname, password },
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
63
http/src/format.rs
Normal file
63
http/src/format.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
use eva::{data, error::ShitHappens};
|
||||
use eyre::Context;
|
||||
|
||||
#[data(copy, display("got unsupported mime type"), error)]
|
||||
pub struct UnknownMimeType;
|
||||
|
||||
#[data(copy, display("{}", self.mime_type()))]
|
||||
#[derive(Default)]
|
||||
pub enum Format {
|
||||
#[default]
|
||||
Json,
|
||||
}
|
||||
|
||||
impl Format {
|
||||
pub fn from_mime_type(mime: &str) -> Result<Self, UnknownMimeType> {
|
||||
use Format::*;
|
||||
use UnknownMimeType as E;
|
||||
|
||||
match mime {
|
||||
"application/json" => Ok(Json),
|
||||
_ => Err(E),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn mime_type(self) -> &'static str {
|
||||
use Format::*;
|
||||
|
||||
match self {
|
||||
Json => "application/json",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[data]
|
||||
#[derive(Default)]
|
||||
pub struct DumpParams {
|
||||
pub pretty: bool,
|
||||
}
|
||||
|
||||
impl Format {
|
||||
pub fn dump<T: ?Sized>(self, params: DumpParams, what: &T, dst: &mut Vec<u8>)
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
match self {
|
||||
Self::Json => if params.pretty {
|
||||
serde_json::to_writer_pretty(dst, what)
|
||||
} else {
|
||||
serde_json::to_writer(dst, what)
|
||||
}
|
||||
.shit_happens(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load<'de, T>(&self, buf: &'de [u8]) -> eyre::Result<T>
|
||||
where
|
||||
T: serde::Deserialize<'de>,
|
||||
{
|
||||
match self {
|
||||
Self::Json => serde_json::from_slice(buf).wrap_err("failed to deserialize JSON"),
|
||||
}
|
||||
}
|
||||
}
|
7
http/src/lib.rs
Normal file
7
http/src/lib.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
#[cfg(feature = "client")]
|
||||
pub mod client;
|
||||
#[cfg(feature = "server")]
|
||||
pub mod server;
|
||||
|
||||
pub mod format;
|
||||
pub mod requests;
|
53
http/src/requests/authors.rs
Normal file
53
http/src/requests/authors.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
use eva::data;
|
||||
|
||||
use viendesu_core::{
|
||||
errors,
|
||||
requests::authors as reqs,
|
||||
types::{Patch, author, file, user},
|
||||
};
|
||||
|
||||
use crate::requests::status_code;
|
||||
|
||||
#[serde_with::apply(Patch => #[serde(default)])]
|
||||
#[data]
|
||||
pub struct Update {
|
||||
pub title: Patch<author::Title>,
|
||||
pub description: Patch<Option<author::Description>>,
|
||||
pub pfp: Patch<Option<file::Id>>,
|
||||
pub slug: Patch<author::Slug>,
|
||||
pub verified: Patch<bool>,
|
||||
}
|
||||
|
||||
impl_req!(Update => [reqs::update::Ok; reqs::update::Err]);
|
||||
|
||||
status_code::direct!(reqs::update::Ok => OK);
|
||||
status_code::map!(reqs::update::Err => [NotFound]);
|
||||
|
||||
#[data]
|
||||
pub struct Create {
|
||||
pub title: author::Title,
|
||||
pub slug: author::Slug,
|
||||
pub description: Option<author::Description>,
|
||||
pub owner: Option<user::Id>,
|
||||
}
|
||||
|
||||
impl_req!(Create => [reqs::create::Ok; reqs::create::Err]);
|
||||
|
||||
status_code::direct!(reqs::create::Ok => OK);
|
||||
status_code::map!(reqs::create::Err => [NotFound, AlreadyExists, NoSuchUser]);
|
||||
|
||||
#[data]
|
||||
pub struct Get {}
|
||||
|
||||
impl_req!(Get => [reqs::get::Ok; reqs::get::Err]);
|
||||
|
||||
status_code::direct!(reqs::get::Ok => OK);
|
||||
status_code::map!(reqs::get::Err => [NotFound]);
|
||||
|
||||
const _: () = {
|
||||
use errors::authors::*;
|
||||
use status_code::direct;
|
||||
|
||||
direct!(NotFound => NOT_FOUND);
|
||||
direct!(AlreadyExists => BAD_REQUEST);
|
||||
};
|
56
http/src/requests/boards.rs
Normal file
56
http/src/requests/boards.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use eva::data;
|
||||
|
||||
use crate::requests::status_code;
|
||||
|
||||
use viendesu_core::{
|
||||
errors,
|
||||
requests::boards as reqs,
|
||||
types::{Patch, board, message},
|
||||
};
|
||||
|
||||
#[data]
|
||||
pub struct Get {}
|
||||
|
||||
impl_req!(Get => [reqs::get::Ok; reqs::get::Err]);
|
||||
|
||||
status_code::direct!(reqs::get::Ok => OK);
|
||||
status_code::map!(reqs::get::Err => [NotFound]);
|
||||
|
||||
#[data]
|
||||
pub struct Edit {
|
||||
pub text: Patch<message::Text>,
|
||||
pub slug: Patch<Option<board::Slug>>,
|
||||
}
|
||||
|
||||
impl_req!(Edit => [reqs::edit::Ok; reqs::edit::Err]);
|
||||
|
||||
status_code::direct!(reqs::edit::Ok => OK);
|
||||
status_code::map!(reqs::edit::Err => [NotFound]);
|
||||
|
||||
#[data]
|
||||
pub struct Delete {}
|
||||
|
||||
impl_req!(Delete => [reqs::delete::Ok; reqs::delete::Err]);
|
||||
|
||||
status_code::direct!(reqs::delete::Ok => OK);
|
||||
status_code::map!(reqs::delete::Err => [NotFound]);
|
||||
|
||||
#[data]
|
||||
pub struct Create {
|
||||
pub slug: board::Slug,
|
||||
pub initial_message: message::Text,
|
||||
pub by: Option<message::ById>,
|
||||
}
|
||||
|
||||
impl_req!(Create => [reqs::create::Ok; reqs::create::Err]);
|
||||
|
||||
status_code::direct!(reqs::create::Ok => OK);
|
||||
status_code::map!(reqs::create::Err => [AlreadyExists]);
|
||||
|
||||
const _: () = {
|
||||
use errors::boards::*;
|
||||
use status_code::direct;
|
||||
|
||||
direct!(AlreadyExists => BAD_REQUEST);
|
||||
direct!(NotFound => NOT_FOUND);
|
||||
};
|
81
http/src/requests/games.rs
Normal file
81
http/src/requests/games.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
use eva::data;
|
||||
|
||||
use crate::requests::status_code;
|
||||
|
||||
use viendesu_core::{
|
||||
errors,
|
||||
requests::games as reqs,
|
||||
types::{Patch, author, file, game},
|
||||
};
|
||||
|
||||
#[data]
|
||||
#[serde_with::apply(Patch => #[serde(default)])]
|
||||
pub struct Update {
|
||||
pub title: Patch<game::Title>,
|
||||
pub description: Patch<Option<game::Description>>,
|
||||
pub slug: Patch<game::Slug>,
|
||||
pub thumbnail: Patch<Option<file::Id>>,
|
||||
pub genres: Patch<game::Genres>,
|
||||
pub badges: Patch<game::Badges>,
|
||||
pub tags: Patch<game::Tags>,
|
||||
pub screenshots: Patch<game::Screenshots>,
|
||||
pub published: Patch<bool>,
|
||||
}
|
||||
|
||||
impl_req!(Update => [reqs::update::Ok; reqs::update::Err]);
|
||||
|
||||
status_code::direct!(reqs::update::Ok => OK);
|
||||
status_code::map!(reqs::update::Err => [NotFound]);
|
||||
|
||||
#[data]
|
||||
pub struct Search {
|
||||
pub query: Option<game::SearchQuery>,
|
||||
pub author: Option<author::Selector>,
|
||||
#[serde(default)]
|
||||
pub include: reqs::search::Condition,
|
||||
#[serde(default)]
|
||||
pub exclude: reqs::search::Condition,
|
||||
#[serde(default)]
|
||||
pub order: reqs::search::Order,
|
||||
#[serde(default)]
|
||||
pub sort_by: reqs::search::SortBy,
|
||||
pub limit: Option<reqs::search::Limit>,
|
||||
}
|
||||
|
||||
impl_req!(Search => [reqs::search::Ok; reqs::search::Err]);
|
||||
|
||||
status_code::direct!(reqs::search::Ok => OK);
|
||||
status_code::map!(reqs::search::Err => [NoSuchAuthor]);
|
||||
|
||||
#[data]
|
||||
pub struct Get {}
|
||||
|
||||
impl_req!(Get => [reqs::get::Ok; reqs::get::Err]);
|
||||
|
||||
status_code::direct!(reqs::get::Ok => OK);
|
||||
status_code::map!(reqs::get::Err => [NotFound, NoSuchAuthor]);
|
||||
|
||||
#[data]
|
||||
pub struct Create {
|
||||
pub title: game::Title,
|
||||
pub description: Option<game::Description>,
|
||||
pub thumbnail: Option<file::Id>,
|
||||
pub author: author::Id,
|
||||
pub slug: Option<game::Slug>,
|
||||
pub vndb: Option<game::VndbId>,
|
||||
pub release_date: Option<game::ReleaseDate>,
|
||||
}
|
||||
|
||||
impl_req!(Create => [reqs::create::Ok; reqs::create::Err]);
|
||||
|
||||
status_code::direct!(reqs::create::Ok => CREATED);
|
||||
status_code::map!(reqs::create::Err => [AlreadyTaken, NoSuchAuthor]);
|
||||
|
||||
const _: () = {
|
||||
use errors::games::*;
|
||||
use status_code::direct;
|
||||
|
||||
direct!(NotFound => NOT_FOUND);
|
||||
direct!(NotAnOwner => FORBIDDEN);
|
||||
direct!(AlreadyTaken => BAD_REQUEST);
|
||||
};
|
53
http/src/requests/messages.rs
Normal file
53
http/src/requests/messages.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
use eva::data;
|
||||
|
||||
use crate::requests::status_code;
|
||||
|
||||
use viendesu_core::{
|
||||
errors,
|
||||
requests::messages as reqs,
|
||||
types::{message, thread},
|
||||
};
|
||||
|
||||
#[data]
|
||||
pub struct Edit {
|
||||
pub text: message::Text,
|
||||
}
|
||||
|
||||
impl_req!(Edit => [reqs::edit::Ok; reqs::edit::Err]);
|
||||
|
||||
status_code::direct!(reqs::edit::Ok => OK);
|
||||
status_code::map!(reqs::edit::Err => [NotFound]);
|
||||
|
||||
#[data]
|
||||
pub struct Delete {}
|
||||
|
||||
impl_req!(Delete => [reqs::delete::Ok; reqs::delete::Err]);
|
||||
|
||||
status_code::direct!(reqs::delete::Ok => OK);
|
||||
status_code::map!(reqs::delete::Err => [NotFound]);
|
||||
|
||||
#[data]
|
||||
pub struct Post {
|
||||
pub thread: thread::Selector,
|
||||
pub text: message::Text,
|
||||
}
|
||||
|
||||
impl_req!(Post => [reqs::post::Ok; reqs::post::Err]);
|
||||
|
||||
status_code::direct!(reqs::post::Ok => OK);
|
||||
status_code::map!(reqs::post::Err => [NoSuchThread]);
|
||||
|
||||
#[data]
|
||||
pub struct Get {}
|
||||
|
||||
impl_req!(Get => [reqs::get::Ok; reqs::get::Err]);
|
||||
|
||||
status_code::direct!(reqs::get::Ok => OK);
|
||||
status_code::map!(reqs::get::Err => [NotFound]);
|
||||
|
||||
const _: () = {
|
||||
use errors::messages::*;
|
||||
use status_code::direct;
|
||||
|
||||
direct!(NotFound => NOT_FOUND);
|
||||
};
|
32
http/src/requests/mod.rs
Normal file
32
http/src/requests/mod.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
pub mod status_code;
|
||||
|
||||
pub trait Request: Send + Sync + 'static + for<'de> serde::Deserialize<'de> {
|
||||
type Response: IsResponse;
|
||||
type Error: IsResponse;
|
||||
}
|
||||
|
||||
eva::trait_set! {
|
||||
pub trait IsResponse = status_code::HasStatusCode + serde::Serialize + Send + Sync + 'static;
|
||||
}
|
||||
|
||||
macro_rules! impl_req {
|
||||
($Input:ty => [$Ok:ty; $Err:ty]) => {
|
||||
const _: () = {
|
||||
use $crate::requests::Request;
|
||||
|
||||
impl Request for $Input {
|
||||
type Response = $Ok;
|
||||
type Error = $Err;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
pub mod users;
|
||||
|
||||
pub mod authors;
|
||||
pub mod games;
|
||||
|
||||
pub mod boards;
|
||||
pub mod messages;
|
||||
pub mod threads;
|
81
http/src/requests/status_code.rs
Normal file
81
http/src/requests/status_code.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
use http::status::StatusCode;
|
||||
|
||||
use viendesu_core::errors;
|
||||
|
||||
pub trait HasStatusCode {
|
||||
fn status_code(&self) -> StatusCode;
|
||||
}
|
||||
|
||||
impl<O: HasStatusCode, E: HasStatusCode> HasStatusCode for Result<O, E> {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Ok(o) => o.status_code(),
|
||||
Err(e) => e.status_code(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: HasStatusCode> HasStatusCode for errors::Generic<S> {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Self::Aux(aux) => aux.status_code(),
|
||||
Self::Spec(spec) => spec.status_code(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HasStatusCode for errors::Aux {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
use StatusCode as C;
|
||||
use errors::Aux::*;
|
||||
|
||||
match self {
|
||||
Unauthenticated => C::UNAUTHORIZED,
|
||||
InvalidRole(e) => e.status_code(),
|
||||
InvalidSession(e) => e.status_code(),
|
||||
Captcha(..) | Deserialization(..) => C::BAD_REQUEST,
|
||||
Db(..) | InternalError(..) | ObjectStore(..) | Mail(..) => C::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! map {
|
||||
($Ty:ty => [$($Item:ident),* $(,)?]) => {
|
||||
const _: () = {
|
||||
use $crate::requests::status_code::HasStatusCode;
|
||||
use ::http::status::StatusCode;
|
||||
|
||||
impl HasStatusCode for $Ty {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {$(
|
||||
Self::$Item(ref e) => e.status_code(),
|
||||
)*}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use map;
|
||||
|
||||
macro_rules! direct {
|
||||
($($ty:ty => $code:ident),* $(,)?) => {$(
|
||||
const _: () = {
|
||||
use $crate::requests::status_code::HasStatusCode;
|
||||
use ::http::status::StatusCode;
|
||||
|
||||
impl HasStatusCode for $ty {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
StatusCode::$code
|
||||
}
|
||||
}
|
||||
};
|
||||
)*};
|
||||
}
|
||||
|
||||
pub(crate) use direct;
|
||||
|
||||
direct! {
|
||||
errors::auth::InvalidRole => FORBIDDEN,
|
||||
errors::auth::InvalidSession => BAD_REQUEST,
|
||||
}
|
66
http/src/requests/threads.rs
Normal file
66
http/src/requests/threads.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use eva::data;
|
||||
|
||||
use crate::requests::status_code;
|
||||
|
||||
use viendesu_core::{
|
||||
errors,
|
||||
requests::threads as reqs,
|
||||
types::{Patch, board, message, thread},
|
||||
};
|
||||
|
||||
#[data]
|
||||
pub struct Get {}
|
||||
|
||||
impl_req!(Get => [reqs::get::Ok; reqs::get::Err]);
|
||||
|
||||
status_code::direct!(reqs::get::Ok => OK);
|
||||
status_code::map!(reqs::get::Err => [NotFound]);
|
||||
|
||||
#[data]
|
||||
pub struct Delete {}
|
||||
|
||||
impl_req!(Delete => [reqs::delete::Ok; reqs::delete::Err]);
|
||||
|
||||
status_code::direct!(reqs::delete::Ok => OK);
|
||||
status_code::map!(reqs::delete::Err => [NotFound]);
|
||||
|
||||
#[data]
|
||||
pub struct Edit {
|
||||
pub text: Patch<message::Text>,
|
||||
}
|
||||
|
||||
impl_req!(Edit => [reqs::edit::Ok; reqs::edit::Err]);
|
||||
|
||||
status_code::direct!(reqs::edit::Ok => OK);
|
||||
status_code::map!(reqs::edit::Err => [NotFound, NotAnOwner]);
|
||||
|
||||
#[data]
|
||||
pub struct Search {
|
||||
#[serde(default)]
|
||||
pub limit: reqs::search::Limit,
|
||||
pub after: Option<thread::Id>,
|
||||
}
|
||||
|
||||
impl_req!(Search => [reqs::search::Ok; reqs::search::Err]);
|
||||
|
||||
status_code::direct!(reqs::search::Ok => OK);
|
||||
status_code::map!(reqs::search::Err => []);
|
||||
|
||||
#[data]
|
||||
pub struct Create {
|
||||
pub board: board::Selector,
|
||||
pub initial_message: message::Text,
|
||||
}
|
||||
|
||||
impl_req!(Create => [reqs::create::Ok; reqs::create::Err]);
|
||||
|
||||
status_code::direct!(reqs::create::Ok => CREATED);
|
||||
status_code::map!(reqs::create::Err => [NoSuchBoard]);
|
||||
|
||||
const _: () = {
|
||||
use errors::threads::*;
|
||||
use status_code::direct;
|
||||
|
||||
direct!(NotAnOwner => FORBIDDEN);
|
||||
direct!(NotFound => NOT_FOUND);
|
||||
};
|
102
http/src/requests/users.rs
Normal file
102
http/src/requests/users.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
use eva::data;
|
||||
|
||||
use viendesu_core::{
|
||||
errors,
|
||||
requests::users as reqs,
|
||||
types::{Patch, file, user as core},
|
||||
};
|
||||
|
||||
use super::status_code;
|
||||
|
||||
#[data]
|
||||
pub struct CheckAuth {}
|
||||
|
||||
impl_req!(CheckAuth => [reqs::check_auth::Ok; reqs::check_auth::Err]);
|
||||
status_code::direct!(reqs::check_auth::Ok => OK);
|
||||
status_code::map!(reqs::check_auth::Err => []);
|
||||
|
||||
#[data]
|
||||
#[non_exhaustive]
|
||||
#[derive(Default)]
|
||||
pub struct Get {}
|
||||
|
||||
impl_req!(Get => [reqs::get::Ok; reqs::get::Err]);
|
||||
status_code::map!(reqs::get::Err => [NotFound]);
|
||||
status_code::direct!(reqs::get::Ok => OK);
|
||||
|
||||
#[data]
|
||||
pub struct SignIn {
|
||||
pub nickname: core::Nickname,
|
||||
pub password: core::Password,
|
||||
}
|
||||
|
||||
impl_req!(SignIn => [reqs::sign_in::Ok; reqs::sign_in::Err]);
|
||||
status_code::map!(reqs::sign_in::Err => [NotFound, InvalidPassword, MustCompleteSignUp]);
|
||||
status_code::direct!(reqs::sign_in::Ok => OK);
|
||||
|
||||
#[data]
|
||||
#[non_exhaustive]
|
||||
#[derive(Default)]
|
||||
pub struct ConfirmSignUp {}
|
||||
|
||||
impl_req!(ConfirmSignUp => [reqs::confirm_sign_up::Ok; reqs::confirm_sign_up::Err]);
|
||||
status_code::direct!(reqs::confirm_sign_up::Ok => OK);
|
||||
status_code::map!(reqs::confirm_sign_up::Err => [NotFoundOrCompleted, InvalidSignUpToken]);
|
||||
|
||||
#[data]
|
||||
pub struct SignUp {
|
||||
pub nickname: core::Nickname,
|
||||
pub email: core::Email,
|
||||
pub password: core::Password,
|
||||
pub display_name: Option<core::DisplayName>,
|
||||
}
|
||||
|
||||
impl_req!(SignUp => [reqs::sign_up::Ok; reqs::sign_up::Err]);
|
||||
status_code::direct!(reqs::sign_up::Ok => CREATED);
|
||||
status_code::map!(reqs::sign_up::Err => [AlreadyTaken]);
|
||||
|
||||
#[serde_with::apply(
|
||||
Patch => #[serde(default)]
|
||||
)]
|
||||
#[data]
|
||||
#[non_exhaustive]
|
||||
#[derive(Default)]
|
||||
pub struct Update {
|
||||
pub nickname: Patch<core::Nickname>,
|
||||
pub display_name: Patch<Option<core::DisplayName>>,
|
||||
pub bio: Patch<Option<core::Bio>>,
|
||||
pub password: Patch<core::Password>,
|
||||
pub role: Patch<core::Role>,
|
||||
pub pfp: Patch<Option<file::Id>>,
|
||||
pub email: Patch<core::Email>,
|
||||
}
|
||||
|
||||
impl From<reqs::update::Update> for Update {
|
||||
fn from(u: reqs::update::Update) -> Self {
|
||||
Self {
|
||||
nickname: u.nickname,
|
||||
display_name: u.display_name,
|
||||
bio: u.bio,
|
||||
password: u.password,
|
||||
role: u.role,
|
||||
pfp: u.pfp,
|
||||
email: u.email,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_req!(Update => [reqs::update::Ok; reqs::update::Err]);
|
||||
status_code::direct!(reqs::update::Ok => OK);
|
||||
status_code::map!(reqs::update::Err => [NotFound]);
|
||||
|
||||
const _: () = {
|
||||
use errors::users::*;
|
||||
use status_code::direct;
|
||||
|
||||
direct!(NotFound => NOT_FOUND);
|
||||
direct!(InvalidPassword => FORBIDDEN);
|
||||
direct!(MustCompleteSignUp => FORBIDDEN);
|
||||
direct!(AlreadyTaken => BAD_REQUEST);
|
||||
direct!(NotFoundOrCompleted => BAD_REQUEST);
|
||||
direct!(InvalidSignUpToken => BAD_REQUEST);
|
||||
};
|
21
http/src/server/config.rs
Normal file
21
http/src/server/config.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
use std::net::SocketAddr;
|
||||
|
||||
use eva::data;
|
||||
|
||||
#[data]
|
||||
pub struct Http {
|
||||
pub enable: bool,
|
||||
pub listen: SocketAddr,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Ssl {
|
||||
pub enable: bool,
|
||||
pub listen: SocketAddr,
|
||||
}
|
||||
|
||||
#[data]
|
||||
pub struct Config {
|
||||
pub unencrypted: Option<Http>,
|
||||
pub ssl: Option<Ssl>,
|
||||
}
|
85
http/src/server/context.rs
Normal file
85
http/src/server/context.rs
Normal file
|
@ -0,0 +1,85 @@
|
|||
use crate::{
|
||||
format::Format,
|
||||
server::{
|
||||
request::{ServerRequest, extract},
|
||||
response,
|
||||
},
|
||||
};
|
||||
|
||||
use axum::{
|
||||
RequestExt,
|
||||
extract::Request as AxumRequest,
|
||||
http::{Method, request::Parts},
|
||||
response::Response as AxumResponse,
|
||||
};
|
||||
|
||||
use futures::StreamExt;
|
||||
use serde::Deserialize;
|
||||
|
||||
use viendesu_core::errors::{Aux, AuxResult};
|
||||
|
||||
#[non_exhaustive]
|
||||
pub struct Context<R: ServerRequest> {
|
||||
pub request: R,
|
||||
pub parts: Parts,
|
||||
pub response_format: Format,
|
||||
}
|
||||
|
||||
impl<R: ServerRequest> Context<R> {
|
||||
pub async fn path<P>(&mut self) -> AuxResult<P>
|
||||
where
|
||||
P: for<'de> Deserialize<'de> + Send + 'static,
|
||||
{
|
||||
extract::path(&mut self.parts).await
|
||||
}
|
||||
|
||||
pub fn query<'this, T: serde::Deserialize<'this>>(&'this self) -> AuxResult<T> {
|
||||
extract::query(&self.parts)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn load_args<R: ServerRequest>(req: AxumRequest) -> Result<Context<R>, AxumResponse> {
|
||||
let (parts, body) = req.with_limited_body().into_parts();
|
||||
|
||||
let response_format =
|
||||
extract::response_format(&parts).map_err(|e| response::err(Format::default(), e))?;
|
||||
let request_format =
|
||||
extract::request_format(&parts).map_err(|e| response::err(Format::default(), e))?;
|
||||
|
||||
let request: R = if parts.method == Method::GET {
|
||||
let query = parts.uri.query().unwrap_or("");
|
||||
serde_urlencoded::from_str(query)
|
||||
.map_err(|e| Aux::Deserialization(format!("failed to decode query: {e}")))
|
||||
.map_err(|e| response::err(response_format, e))?
|
||||
} else {
|
||||
let content_length =
|
||||
extract::content_length(&parts).map_err(|e| response::err(response_format, e))?;
|
||||
let mut data_stream = body.into_data_stream();
|
||||
let mut buffer = Vec::with_capacity(content_length);
|
||||
|
||||
while let Some(frame) = data_stream.next().await {
|
||||
let frame = match frame {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
return Err(response::err(
|
||||
response_format,
|
||||
Aux::Deserialization(format!("failed to read body frame: {e}")),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
buffer.extend_from_slice(&frame);
|
||||
}
|
||||
|
||||
request_format
|
||||
.load(&buffer)
|
||||
.map_err(|e| Aux::Deserialization(format!("failed to deserialize body: {e}")))
|
||||
.map_err(|e| response::err(response_format, e))?
|
||||
};
|
||||
|
||||
Ok(Context {
|
||||
request,
|
||||
parts,
|
||||
response_format,
|
||||
})
|
||||
}
|
250
http/src/server/handler.rs
Normal file
250
http/src/server/handler.rs
Normal file
|
@ -0,0 +1,250 @@
|
|||
use eva::fut::Fut;
|
||||
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
|
||||
use axum::{
|
||||
Router as AxumRouter,
|
||||
extract::Request as AxumRequest,
|
||||
response::Response as AxumResponse,
|
||||
routing::{MethodFilter, method_routing},
|
||||
};
|
||||
|
||||
use viendesu_core::{errors as core_errors, service::SessionOf};
|
||||
|
||||
use crate::{
|
||||
format::Format,
|
||||
server::{
|
||||
State, Types,
|
||||
context::{Context, load_args},
|
||||
request::{ServerRequest, extract},
|
||||
response,
|
||||
},
|
||||
};
|
||||
|
||||
struct Inner<R: ServerRequest, Cx> {
|
||||
make_context: Cx,
|
||||
method: MethodFilter,
|
||||
_phantom: PhantomData<R>,
|
||||
}
|
||||
|
||||
pub struct RouterScope<T: Types> {
|
||||
router: AxumRouter,
|
||||
state: State<T>,
|
||||
}
|
||||
|
||||
impl<T: Types> RouterScope<T> {
|
||||
pub fn root(state: State<T>) -> Self {
|
||||
Self {
|
||||
router: AxumRouter::new(),
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn include(self, f: impl FnOnce(RouterScope<T>) -> RouterScope<T>) -> RouterScope<T> {
|
||||
f(self)
|
||||
}
|
||||
|
||||
pub fn route<R: ServerRequest, M, Cx>(
|
||||
self,
|
||||
path: &str,
|
||||
handler: FinishedHandler<R, M, T, Cx>,
|
||||
) -> RouterScope<T>
|
||||
where
|
||||
M: MakeRequest<T, R>,
|
||||
Cx: MakeContext<R>,
|
||||
{
|
||||
let Self { mut router, state } = self;
|
||||
router = router.route(
|
||||
path,
|
||||
method_routing::on(
|
||||
handler.inner.method,
|
||||
handler.into_axum_handler(state.clone()),
|
||||
),
|
||||
);
|
||||
|
||||
Self { router, state }
|
||||
}
|
||||
|
||||
pub fn nest(
|
||||
self,
|
||||
path: &str,
|
||||
f: impl FnOnce(RouterScope<T>) -> RouterScope<T>,
|
||||
) -> RouterScope<T> {
|
||||
let nested = f(RouterScope {
|
||||
router: AxumRouter::new(),
|
||||
state: self.state.clone(),
|
||||
});
|
||||
|
||||
let Self { mut router, state } = self;
|
||||
router = router.nest(path, nested.router);
|
||||
|
||||
Self { router, state }
|
||||
}
|
||||
|
||||
pub fn into_axum(self) -> AxumRouter {
|
||||
self.router
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FinishedHandler<R: ServerRequest, M, T: Types, Cx> {
|
||||
inner: Inner<R, Cx>,
|
||||
make_request: M,
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<R: ServerRequest, M, T: Types, Cx: MakeContext<R>> FinishedHandler<R, M, T, Cx> {
|
||||
fn into_axum_handler(self, state: State<T>) -> impl AxumHandler
|
||||
where
|
||||
M: MakeRequest<T, R>,
|
||||
{
|
||||
let Self {
|
||||
inner,
|
||||
make_request,
|
||||
_phantom: _,
|
||||
} = self;
|
||||
|
||||
let captures = Arc::new((make_request, inner, state));
|
||||
|
||||
move |req: AxumRequest| {
|
||||
let captures = captures.clone();
|
||||
async move {
|
||||
let (make_request, inner, state) = Arc::as_ref(&captures);
|
||||
let Inner {
|
||||
make_context,
|
||||
method: _,
|
||||
_phantom: _,
|
||||
} = inner;
|
||||
|
||||
let (parts, body) = req.into_parts();
|
||||
let context = make_context(AxumRequest::from_parts(parts, body)).await?;
|
||||
let resp_format = context.response_format;
|
||||
let session = state
|
||||
.make_session()
|
||||
.await
|
||||
.map_err(|e| response::err(resp_format, e))?;
|
||||
|
||||
match make_request(session, context).await {
|
||||
Ok(r) => Ok(response::ok(resp_format, r)),
|
||||
Err(e) => Err(response::err(resp_format, e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get<R, T, M>(make_request: M) -> FinishedHandler<R, M, T, impl MakeContext<R>>
|
||||
where
|
||||
R: ServerRequest,
|
||||
T: Types,
|
||||
M: MakeRequest<T, R>,
|
||||
{
|
||||
Handler::get(load_args::<R>).exec(make_request)
|
||||
}
|
||||
|
||||
pub fn post<R, T, M>(make_request: M) -> FinishedHandler<R, M, T, impl MakeContext<R>>
|
||||
where
|
||||
R: ServerRequest,
|
||||
T: Types,
|
||||
M: MakeRequest<T, R>,
|
||||
{
|
||||
Handler::post(load_args::<R>).exec(make_request)
|
||||
}
|
||||
|
||||
pub fn patch<R, T, M>(make_request: M) -> FinishedHandler<R, M, T, impl MakeContext<R>>
|
||||
where
|
||||
R: ServerRequest,
|
||||
T: Types,
|
||||
M: MakeRequest<T, R>,
|
||||
{
|
||||
Handler::patch(load_args::<R>).exec(make_request)
|
||||
}
|
||||
|
||||
pub struct Handler<R: ServerRequest, Cx>(Inner<R, Cx>);
|
||||
|
||||
impl<R: ServerRequest, Cx: MakeContext<R>> Handler<R, Cx> {
|
||||
pub fn get(make_context: Cx) -> Handler<R, Cx> {
|
||||
Self(Inner {
|
||||
make_context,
|
||||
method: MethodFilter::GET,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn post(make_context: Cx) -> Handler<R, Cx> {
|
||||
Self(Inner {
|
||||
make_context,
|
||||
method: MethodFilter::POST,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn patch(make_context: Cx) -> Handler<R, Cx> {
|
||||
Self(Inner {
|
||||
make_context,
|
||||
method: MethodFilter::PATCH,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn exec<T, M>(self, make_request: M) -> FinishedHandler<R, M, T, Cx>
|
||||
where
|
||||
T: Types,
|
||||
M: MakeRequest<T, R>,
|
||||
{
|
||||
let Self(inner) = self;
|
||||
FinishedHandler {
|
||||
inner,
|
||||
make_request,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// == Complex trait magic ==
|
||||
|
||||
pub trait AxumHandler: Send + Sync + 'static + Clone + Fn(AxumRequest) -> Self::AFut {
|
||||
#[doc(hidden)]
|
||||
type AFut: Fut<Output = Result<AxumResponse, AxumResponse>>;
|
||||
}
|
||||
|
||||
impl<H, F> AxumHandler for H
|
||||
where
|
||||
H: Clone + Send + Sync + 'static + Fn(AxumRequest) -> F,
|
||||
F: Fut<Output = Result<AxumResponse, AxumResponse>>,
|
||||
{
|
||||
type AFut = F;
|
||||
}
|
||||
|
||||
pub trait Captures<T: ?Sized> {}
|
||||
impl<T: ?Sized, U: ?Sized> Captures<T> for U {}
|
||||
|
||||
pub trait MakeContext<R: ServerRequest>:
|
||||
Send + Sync + 'static + Fn(AxumRequest) -> Self::MkFut
|
||||
{
|
||||
type MkFut: Fut<Output = Result<Context<R>, AxumResponse>>;
|
||||
}
|
||||
|
||||
impl<R: ServerRequest, H, F> MakeContext<R> for H
|
||||
where
|
||||
H: Send + Sync + 'static + Fn(AxumRequest) -> F,
|
||||
F: Fut<Output = Result<Context<R>, AxumResponse>>,
|
||||
{
|
||||
type MkFut = F;
|
||||
}
|
||||
|
||||
pub trait MakeRequest<T: Types, R: ServerRequest>:
|
||||
Send + Sync + 'static + Fn(SessionOf<T::Service>, Context<R>) -> Self::HFut
|
||||
{
|
||||
type HFut: Fut<Output = core_errors::Result<R::Response, R::Error>>
|
||||
+ Captures<SessionOf<T::Service>>;
|
||||
}
|
||||
|
||||
impl<T, R, F, H> MakeRequest<T, R> for H
|
||||
where
|
||||
H: Send + Sync + 'static + Fn(SessionOf<T::Service>, Context<R>) -> F,
|
||||
F: Fut<Output = core_errors::Result<R::Response, R::Error>> + Captures<SessionOf<T::Service>>,
|
||||
T: Types,
|
||||
R: ServerRequest,
|
||||
{
|
||||
type HFut = F;
|
||||
}
|
35
http/src/server/mod.rs
Normal file
35
http/src/server/mod.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use eva::perfect_derive;
|
||||
|
||||
use viendesu_core::{
|
||||
errors::AuxResult,
|
||||
service::{Service, SessionMaker, SessionOf},
|
||||
};
|
||||
|
||||
pub mod config;
|
||||
|
||||
mod context;
|
||||
mod handler;
|
||||
mod request;
|
||||
mod response;
|
||||
|
||||
mod routes;
|
||||
|
||||
pub trait Types: Send + Sync + 'static {
|
||||
type Service: Service + Clone;
|
||||
}
|
||||
|
||||
pub fn make_router<T: Types>(service: T::Service) -> axum::Router {
|
||||
let scope = handler::RouterScope::root(State::<T> { service });
|
||||
routes::make(scope).into_axum()
|
||||
}
|
||||
|
||||
#[perfect_derive(Clone)]
|
||||
struct State<T: Types> {
|
||||
service: T::Service,
|
||||
}
|
||||
|
||||
impl<T: Types> State<T> {
|
||||
async fn make_session(&self) -> AuxResult<SessionOf<T::Service>> {
|
||||
self.service.make_session().await
|
||||
}
|
||||
}
|
26
http/src/server/request.rs
Normal file
26
http/src/server/request.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use eva::{data, trait_set};
|
||||
|
||||
use axum::http::request::Parts;
|
||||
|
||||
use viendesu_core::errors::AuxResult;
|
||||
|
||||
use crate::{format::Format, requests::Request};
|
||||
|
||||
pub mod extract;
|
||||
|
||||
#[data]
|
||||
pub struct MetaInfo {
|
||||
pub request_format: Format,
|
||||
}
|
||||
|
||||
impl MetaInfo {
|
||||
pub fn gather(parts: &Parts) -> AuxResult<Self> {
|
||||
let request_format = extract::request_format(parts)?;
|
||||
|
||||
Ok(Self { request_format })
|
||||
}
|
||||
}
|
||||
|
||||
trait_set! {
|
||||
pub trait ServerRequest = Request;
|
||||
}
|
132
http/src/server/request/extract.rs
Normal file
132
http/src/server/request/extract.rs
Normal file
|
@ -0,0 +1,132 @@
|
|||
use axum::{
|
||||
extract::{FromRequest, FromRequestParts, Path, Request},
|
||||
http::{HeaderValue, request::Parts},
|
||||
};
|
||||
|
||||
use eva::bytes::Bytes;
|
||||
use viendesu_core::{
|
||||
errors::{Aux, AuxResult},
|
||||
types::{captcha, session},
|
||||
};
|
||||
|
||||
use crate::format::Format;
|
||||
|
||||
pub async fn read_body<R: serde::de::DeserializeOwned>(
|
||||
format: Format,
|
||||
request: Request,
|
||||
) -> AuxResult<R> {
|
||||
let raw_body = read_raw_body(request).await?;
|
||||
format
|
||||
.load(&raw_body)
|
||||
.map_err(|e| Aux::Deserialization(format!("failed to deserialize body: {e:#}")))
|
||||
}
|
||||
|
||||
pub fn raw_query(parts: &Parts) -> &str {
|
||||
parts.uri.query().unwrap_or("")
|
||||
}
|
||||
|
||||
pub fn query<'de, T: serde::de::Deserialize<'de>>(parts: &'de Parts) -> AuxResult<T> {
|
||||
let raw = raw_query(parts);
|
||||
serde_urlencoded::from_str(raw)
|
||||
.map_err(|e| Aux::Deserialization(format!("failed to decode query string: {e}")))
|
||||
}
|
||||
|
||||
pub async fn read_raw_body(request: Request) -> AuxResult<Bytes> {
|
||||
match Bytes::from_request(request, &()).await {
|
||||
Ok(r) => Ok(r),
|
||||
Err(e) => Err(Aux::Deserialization(format!("failed to read body: {e}"))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_token(parts: &Parts) -> AuxResult<Option<session::Token>> {
|
||||
let Some(val) = str_header(parts, "authorization")? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some((scheme, rest)) = val.split_once(' ') else {
|
||||
return Err(Aux::Deserialization(format!(
|
||||
"invalid Authorization header format, expected `<scheme> <rest>`"
|
||||
)));
|
||||
};
|
||||
|
||||
match scheme {
|
||||
"Bearer" => rest
|
||||
.parse()
|
||||
.map(Some)
|
||||
.map_err(|e| Aux::Deserialization(format!("invalid session token: {e}"))),
|
||||
_ => Err(Aux::Deserialization(format!(
|
||||
"scheme {scheme:?} is not supported"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn captcha(parts: &Parts) -> AuxResult<Option<captcha::Token>> {
|
||||
let Some(raw_token) = str_header(parts, "x-challenge")? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
match raw_token.parse() {
|
||||
Ok(r) => Ok(Some(r)),
|
||||
Err(e) => Err(Aux::Deserialization(format!(
|
||||
"invalid X-Challenge header: {e}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn str_header<'p>(parts: &'p Parts, header: &str) -> AuxResult<Option<&'p str>> {
|
||||
let Some(value) = raw_header(parts, header) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let value = value.to_str().map_err(|e| {
|
||||
Aux::Deserialization(format!(
|
||||
"failed to decode UTF-8 content of header {header:?}: {e}"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(Some(value))
|
||||
}
|
||||
|
||||
pub fn raw_header<'p>(parts: &'p Parts, header: &str) -> Option<&'p HeaderValue> {
|
||||
parts.headers.get(header)
|
||||
}
|
||||
|
||||
pub fn request_format(parts: &Parts) -> AuxResult<Format> {
|
||||
let Some(raw) = str_header(parts, "content-type")? else {
|
||||
return Err(Aux::Deserialization(
|
||||
"`Content-Type` header is required".into(),
|
||||
));
|
||||
};
|
||||
Format::from_mime_type(raw)
|
||||
.map_err(|e| Aux::Deserialization(format!("failed to parse `Content-Type` header: {e}")))
|
||||
}
|
||||
|
||||
pub fn response_format(parts: &Parts) -> AuxResult<Format> {
|
||||
let Some(raw) = str_header(parts, "accept")? else {
|
||||
return Ok(Format::Json);
|
||||
};
|
||||
|
||||
Format::from_mime_type(raw)
|
||||
.map_err(|e| Aux::Deserialization(format!("failed to parse `Accept` header: {e}")))
|
||||
}
|
||||
|
||||
pub fn content_length(parts: &Parts) -> AuxResult<usize> {
|
||||
let Some(raw) = str_header(parts, "content-length")? else {
|
||||
return Ok(0);
|
||||
};
|
||||
|
||||
let content_length: usize = raw
|
||||
.parse()
|
||||
.map_err(|e| Aux::Deserialization(format!("failed to decode content length: {e}")))?;
|
||||
|
||||
Ok(content_length)
|
||||
}
|
||||
|
||||
pub async fn path<T>(parts: &mut Parts) -> AuxResult<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned + Send + 'static,
|
||||
{
|
||||
let response = Path::<T>::from_request_parts(parts, &()).await;
|
||||
match response {
|
||||
Ok(Path(r)) => Ok(r),
|
||||
Err(rej) => Err(Aux::Deserialization(format!("failed to parse path: {rej}"))),
|
||||
}
|
||||
}
|
64
http/src/server/response/mod.rs
Normal file
64
http/src/server/response/mod.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::format::{DumpParams, Format};
|
||||
use crate::requests::{IsResponse, status_code::HasStatusCode};
|
||||
|
||||
macro_rules! header {
|
||||
($name:expr => $value:expr) => {
|
||||
(
|
||||
axum::http::HeaderName::from_static($name),
|
||||
axum::http::HeaderValue::from_static($value),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn err<E: IsResponse>(format: Format, error: E) -> AxumResponse {
|
||||
#[derive(Serialize)]
|
||||
struct Failure<E> {
|
||||
pub error: E,
|
||||
}
|
||||
|
||||
impl<T: HasStatusCode> HasStatusCode for Failure<T> {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
self.error.status_code()
|
||||
}
|
||||
}
|
||||
|
||||
respond(format, Failure { error })
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn ok<O: IsResponse>(format: Format, ok: O) -> AxumResponse {
|
||||
#[derive(Serialize)]
|
||||
struct Success<T> {
|
||||
ok: T,
|
||||
}
|
||||
|
||||
impl<T: HasStatusCode> HasStatusCode for Success<T> {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
self.ok.status_code()
|
||||
}
|
||||
}
|
||||
|
||||
respond(format, Success { ok })
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn respond<O: IsResponse>(format: Format, res: O) -> AxumResponse {
|
||||
let status_code = res.status_code();
|
||||
let headers = [
|
||||
header!("Server" => "Kurisu-desu"),
|
||||
header!("Content-Type" => format.mime_type()),
|
||||
];
|
||||
|
||||
let mut body = Vec::with_capacity(128);
|
||||
let dump_params = DumpParams { pretty: false };
|
||||
format.dump(dump_params, &res, &mut body);
|
||||
|
||||
(status_code, headers, body).into_response()
|
||||
}
|
167
http/src/server/routes.rs
Normal file
167
http/src/server/routes.rs
Normal file
|
@ -0,0 +1,167 @@
|
|||
use crate::server::{
|
||||
Types,
|
||||
context::Context as Ctx,
|
||||
handler::{RouterScope, get, patch, post},
|
||||
};
|
||||
|
||||
use viendesu_core::{
|
||||
service::{
|
||||
CallStep, Session, SessionOf as SessionOfService,
|
||||
users::{UsersMut, UsersRef},
|
||||
},
|
||||
types::user,
|
||||
};
|
||||
|
||||
type SessionOf<T> = SessionOfService<<T as Types>::Service>;
|
||||
|
||||
pub fn make<T: Types>(router: RouterScope<T>) -> RouterScope<T> {
|
||||
router
|
||||
.nest("/users", users)
|
||||
.nest("/authors", authors)
|
||||
.nest("/games", games)
|
||||
.nest("/boards", boards)
|
||||
.nest("/threads", threads)
|
||||
.nest("/messages", messages)
|
||||
}
|
||||
|
||||
// == Routes ==
|
||||
|
||||
fn users<T: Types>(router: RouterScope<T>) -> RouterScope<T> {
|
||||
use crate::requests::users::{CheckAuth, ConfirmSignUp, Get, SignIn, SignUp, Update};
|
||||
use viendesu_core::requests::users::{
|
||||
check_auth, confirm_sign_up, get, sign_in, sign_up, update,
|
||||
};
|
||||
|
||||
fn convert_update(u: Update) -> update::Update {
|
||||
update::Update {
|
||||
nickname: u.nickname,
|
||||
display_name: u.display_name,
|
||||
bio: u.bio,
|
||||
password: u.password,
|
||||
role: u.role,
|
||||
pfp: u.pfp,
|
||||
email: u.email,
|
||||
}
|
||||
}
|
||||
|
||||
router
|
||||
.route(
|
||||
"/{sel}",
|
||||
get(async |mut session: SessionOf<T>, mut ctx: Ctx<Get>| {
|
||||
let selector: user::Selector = ctx.path().await?;
|
||||
session
|
||||
.users()
|
||||
.get()
|
||||
.call(get::Args {
|
||||
user: Some(selector),
|
||||
})
|
||||
.await
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/check_auth",
|
||||
get(async |mut session: SessionOf<T>, _ctx: Ctx<CheckAuth>| {
|
||||
session.users().check_auth().call(check_auth::Args {}).await
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/me",
|
||||
get(async |mut session: SessionOf<T>, _ctx: Ctx<Get>| {
|
||||
session.users().get().call(get::Args { user: None }).await
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/sign_in",
|
||||
post(async |mut session: SessionOf<T>, ctx: Ctx<SignIn>| {
|
||||
session
|
||||
.users_mut()
|
||||
.sign_in()
|
||||
.call(sign_in::Args {
|
||||
nickname: ctx.request.nickname,
|
||||
password: ctx.request.password,
|
||||
})
|
||||
.await
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/sign_up",
|
||||
post(async |mut session: SessionOf<T>, ctx: Ctx<SignUp>| {
|
||||
let SignUp {
|
||||
nickname,
|
||||
email,
|
||||
password,
|
||||
display_name,
|
||||
} = ctx.request;
|
||||
session
|
||||
.users_mut()
|
||||
.sign_up()
|
||||
.call(sign_up::Args {
|
||||
nickname,
|
||||
email,
|
||||
display_name,
|
||||
password,
|
||||
})
|
||||
.await
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/{sel}",
|
||||
patch(async |mut session: SessionOf<T>, mut ctx: Ctx<Update>| {
|
||||
let user: user::Selector = ctx.path().await?;
|
||||
session
|
||||
.users_mut()
|
||||
.update()
|
||||
.call(update::Args {
|
||||
user: Some(user),
|
||||
update: convert_update(ctx.request),
|
||||
})
|
||||
.await
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/me",
|
||||
patch(async |mut session: SessionOf<T>, ctx: Ctx<Update>| {
|
||||
session
|
||||
.users_mut()
|
||||
.update()
|
||||
.call(update::Args {
|
||||
user: None,
|
||||
update: convert_update(ctx.request),
|
||||
})
|
||||
.await
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/{user}/confirm/{token}",
|
||||
post(
|
||||
async |mut session: SessionOf<T>, mut ctx: Ctx<ConfirmSignUp>| {
|
||||
let (user, token) = ctx.path().await?;
|
||||
session
|
||||
.users_mut()
|
||||
.confirm_sign_up()
|
||||
.call(confirm_sign_up::Args { user, token })
|
||||
.await
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn authors<T: Types>(router: RouterScope<T>) -> RouterScope<T> {
|
||||
router
|
||||
}
|
||||
|
||||
fn games<T: Types>(router: RouterScope<T>) -> RouterScope<T> {
|
||||
router
|
||||
}
|
||||
|
||||
fn boards<T: Types>(router: RouterScope<T>) -> RouterScope<T> {
|
||||
router
|
||||
}
|
||||
|
||||
fn threads<T: Types>(router: RouterScope<T>) -> RouterScope<T> {
|
||||
router
|
||||
}
|
||||
|
||||
fn messages<T: Types>(router: RouterScope<T>) -> RouterScope<T> {
|
||||
router
|
||||
}
|
1
http/tests/it/main.rs
Normal file
1
http/tests/it/main.rs
Normal file
|
@ -0,0 +1 @@
|
|||
|
Loading…
Add table
Add a link
Reference in a new issue