Initial commit
This commit is contained in:
commit
bd9b07052b
81 changed files with 5516 additions and 0 deletions
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