initial git dump
This commit is contained in:
commit
781e25470b
47 changed files with 2361 additions and 0 deletions
29
api/Cargo.toml
Normal file
29
api/Cargo.toml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
[package]
|
||||
name = "spond"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
reqwest = [ "dep:reqwest", "reqwest/json" ]
|
||||
log = [ "tracing/log" ]
|
||||
tracing = [ ]
|
||||
cookies = [ "reqwest?/cookies" ]
|
||||
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.89"
|
||||
bon = "3.9.0"
|
||||
bytes = "1.11.1"
|
||||
chrono = { version = "0.4.43", features = ["serde"] }
|
||||
#macro = { path = "../macro" }
|
||||
reqwest = { version = "0.13.2", optional = true }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
#spond-auth-login = { version = "0.1.0", path = "../login" }
|
||||
thiserror = "2.0.18"
|
||||
tokio = { version = "1.49.0" }
|
||||
tracing = { version = "0.1.44", features = [] }
|
||||
url = "2.5.8"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.49.0", features = ["macros"] }
|
||||
16
api/src/authentication/login/email.rs
Normal file
16
api/src/authentication/login/email.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct Email {
|
||||
email: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
impl Email {
|
||||
pub fn new(email: &str, password: &str) -> Self {
|
||||
Self {
|
||||
email: email.to_string(),
|
||||
password: password.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Credentials for Email {}
|
||||
22
api/src/authentication/login/mod.rs
Normal file
22
api/src/authentication/login/mod.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
mod email;
|
||||
mod phone;
|
||||
|
||||
pub use email::Email;
|
||||
pub use phone::Phone;
|
||||
|
||||
use crate::{
|
||||
Method,
|
||||
authentication::Tokens,
|
||||
traits::{authentication::login::Credentials, endpoint},
|
||||
};
|
||||
|
||||
pub struct Login;
|
||||
|
||||
impl<C: Credentials> endpoint::Public<C> for Login {
|
||||
const METHOD: Method = Method::POST;
|
||||
type Schema = Tokens;
|
||||
|
||||
fn path(&self, _: &C) -> &str {
|
||||
"core/v1/auth2/login"
|
||||
}
|
||||
}
|
||||
16
api/src/authentication/login/phone.rs
Normal file
16
api/src/authentication/login/phone.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct Phone {
|
||||
phone: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
impl Phone {
|
||||
pub fn new(phone: &str, password: &str) -> Self {
|
||||
Self {
|
||||
phone: phone.to_string(),
|
||||
password: password.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Credentials for Phone {}
|
||||
14
api/src/authentication/mod.rs
Normal file
14
api/src/authentication/mod.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
pub mod login;
|
||||
pub mod token;
|
||||
|
||||
pub use login::Login;
|
||||
pub use token::Token;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Tokens {
|
||||
pub access_token: token::Access,
|
||||
pub refresh_token: token::Refresh,
|
||||
}
|
||||
74
api/src/authentication/token.rs
Normal file
74
api/src/authentication/token.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
use crate::traits::Request;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Access(Expire);
|
||||
|
||||
impl Deref for Access {
|
||||
type Target = Expire;
|
||||
|
||||
fn deref(&self) -> &Expire {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Refresh(Expire);
|
||||
|
||||
impl Deref for Refresh {
|
||||
type Target = Expire;
|
||||
|
||||
fn deref(&self) -> &Expire {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Token {
|
||||
token: String,
|
||||
}
|
||||
|
||||
impl AsRef<str> for Token {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.token
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Expire {
|
||||
#[serde(flatten)]
|
||||
token: Token,
|
||||
#[serde(skip_serializing)]
|
||||
expiration: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Expire {
|
||||
pub fn expired(&self) -> bool {
|
||||
Utc::now() <= self.expiration
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Expire {
|
||||
type Target = Token;
|
||||
|
||||
fn deref(&self) -> &Token {
|
||||
&self.token
|
||||
}
|
||||
}
|
||||
|
||||
trait Factory: Serialize {}
|
||||
impl Factory for Refresh {}
|
||||
impl Factory for Token {}
|
||||
|
||||
impl<R: Factory + Request> crate::traits::endpoint::Public<R> for R
|
||||
{
|
||||
const METHOD: crate::Method = crate::Method::POST;
|
||||
type Schema = super::Tokens;
|
||||
|
||||
fn path(&self, _: &R) -> &str {
|
||||
"auth2/refresh"
|
||||
}
|
||||
}
|
||||
2
api/src/authorization.rs
Normal file
2
api/src/authorization.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub struct True;
|
||||
pub struct False;
|
||||
67
api/src/error.rs
Normal file
67
api/src/error.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
use thiserror::Error;
|
||||
|
||||
pub mod api {
|
||||
use thiserror::Error;
|
||||
|
||||
pub type Refresh<Transfer, Decode> = Public<Transfer, Decode>;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Private<Transfer: std::error::Error, Decode: std::error::Error> {
|
||||
#[error("endpoint url")]
|
||||
Url(url::ParseError),
|
||||
|
||||
#[error("json decode")]
|
||||
Decode(Decode),
|
||||
|
||||
#[error("unauthorized")]
|
||||
Unauthorized,
|
||||
|
||||
#[error("token refresh")] // TODO: serde_json::Error -> Schema::...?
|
||||
Refresh(Refresh<Transfer, serde_json::Error>),
|
||||
|
||||
#[error("send error")]
|
||||
Send(Transfer),
|
||||
|
||||
#[error("receive error")]
|
||||
Receive(Transfer),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Public<Transfer: std::error::Error, Decode: std::error::Error> {
|
||||
#[error("endpoint url")]
|
||||
Url(url::ParseError),
|
||||
|
||||
#[error("json decode")]
|
||||
Decode(Decode),
|
||||
|
||||
#[error("send error")]
|
||||
Send(Transfer),
|
||||
|
||||
#[error("receive error")]
|
||||
Receive(Transfer),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Id {
|
||||
#[error("invalid length")]
|
||||
Length(usize),
|
||||
|
||||
#[error("invalid symbol")]
|
||||
Parser(#[from] std::num::ParseIntError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Authorization {
|
||||
#[error("unauthorized")]
|
||||
Unauthorized,
|
||||
}
|
||||
|
||||
//#[derive(Debug, Error)]
|
||||
//pub enum Authentication<A: std::error::Error, B: std::error::Error> {
|
||||
// #[error("unauthorized")]
|
||||
// Public(A),
|
||||
//
|
||||
// #[error("authorized")]
|
||||
// Private(B),
|
||||
//}
|
||||
50
api/src/id.rs
Normal file
50
api/src/id.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
use std::{
|
||||
fmt,
|
||||
str::FromStr,
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
utils::X128,
|
||||
traits::id::Marker,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct Id<T: Marker> {
|
||||
#[serde(flatten)]
|
||||
id: X128,
|
||||
#[serde(skip)]
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: Marker> Id<T> {
|
||||
pub fn new(value: impl Into<X128>) -> Self {
|
||||
Self {
|
||||
id: value.into(),
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Marker> Deref for Id<T> {
|
||||
type Target = X128;
|
||||
|
||||
fn deref(&self) -> &X128 {
|
||||
&self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Marker> fmt::Display for Id<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Marker> FromStr for Id<T> {
|
||||
type Err = <X128 as FromStr>::Err;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
X128::from_str(s).map(Self::new)
|
||||
}
|
||||
}
|
||||
20
api/src/lib.rs
Normal file
20
api/src/lib.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
pub mod authentication;
|
||||
pub mod authorization;
|
||||
pub mod error;
|
||||
pub mod id;
|
||||
pub mod schema;
|
||||
pub mod traits;
|
||||
|
||||
mod utils;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Method {
|
||||
GET,
|
||||
POST,
|
||||
}
|
||||
|
||||
pub use id::Id;
|
||||
|
||||
|
||||
#[cfg(feature = "reqwest")]
|
||||
pub mod reqwest;
|
||||
133
api/src/reqwest/client.rs
Normal file
133
api/src/reqwest/client.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
use async_trait::async_trait;
|
||||
use crate::{
|
||||
authentication,
|
||||
error,
|
||||
};
|
||||
use crate::traits::{
|
||||
client,
|
||||
endpoint,
|
||||
Request,
|
||||
Schema,
|
||||
};
|
||||
|
||||
pub struct Public {
|
||||
client: reqwest::Client,
|
||||
base: url::Url,
|
||||
}
|
||||
|
||||
pub struct Private {
|
||||
public: Public,
|
||||
tokens: tokio::sync::RwLock<authentication::Tokens>,
|
||||
}
|
||||
|
||||
impl Private {
|
||||
async fn current_token(&self) -> authentication::Token {
|
||||
let tokens = self.tokens.read().await;
|
||||
let token: &authentication::Token = (&*tokens).into();
|
||||
token.clone()
|
||||
}
|
||||
|
||||
async fn refresh_tokens(&self) -> Result<authentication::Token, error::api::Refresh<reqwest::Error, serde_json::Error>> {
|
||||
let mut tokens = self.tokens.write().await;
|
||||
*tokens = (*tokens).refresh(self).await?;
|
||||
let token: &authentication::Token = (&*tokens).into();
|
||||
Ok(token.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Client = Private;
|
||||
|
||||
|
||||
type AuthorizedError<S: Schema<R>, R: Request> = error::api::Private<reqwest::Error, S::Error>;
|
||||
type UnauthorizedError<S: Schema<R>, R: Request> = error::api::Public<reqwest::Error, S::Error>;
|
||||
|
||||
async fn authorized<R, S>(client: &reqwest::Client, method: reqwest::Method, base: &url::Url, path: &str, request: &R, token: &authentication::Token) -> Result<S, AuthorizedError<S, R>>
|
||||
where
|
||||
R: Request + Sync,
|
||||
S: Schema<R>,
|
||||
{
|
||||
let url = base.join(path).map_err(AuthorizedError::<S, R>::Url)?;
|
||||
|
||||
let response = client.request(method, url)
|
||||
.json(request)
|
||||
.bearer_auth(token.as_ref())
|
||||
.send()
|
||||
.await
|
||||
.map_err(AuthorizedError::<S, R>::Send)?;
|
||||
|
||||
let body = match response.status() {
|
||||
_ => response.text_with_charset("utf-8")
|
||||
.await
|
||||
.map_err(AuthorizedError::<S, R>::Receive)?,
|
||||
};
|
||||
|
||||
S::deserialize(request, &body).map_err(AuthorizedError::<S, R>::Decode)
|
||||
}
|
||||
|
||||
async fn unauthorized<R, S>(client: &reqwest::Client, method: reqwest::Method, base: &url::Url, path: &str, request: &R) -> Result<S, UnauthorizedError<S, R>>
|
||||
where
|
||||
R: Request + Sync,
|
||||
S: Schema<R>,
|
||||
{
|
||||
let url = base.join(path).map_err(UnauthorizedError::<S, R>::Url)?;
|
||||
|
||||
let response = client.request(method, url)
|
||||
.json(request)
|
||||
.send()
|
||||
.await
|
||||
.map_err(UnauthorizedError::<S, R>::Send)?;
|
||||
|
||||
let body = match response.status() {
|
||||
_ => response.text_with_charset("utf-8")
|
||||
.await
|
||||
.map_err(UnauthorizedError::<S, R>::Receive)?,
|
||||
};
|
||||
|
||||
S::deserialize(request, &body).map_err(UnauthorizedError::<S, R>::Decode)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl client::Public for Public {
|
||||
type Error = reqwest::Error;
|
||||
|
||||
async fn execute<E, R>(&self, endpoint: &E, request: &R) -> Result<E::Schema, error::api::Public<reqwest::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: endpoint::Public<R> + Sync,
|
||||
R: Request + Sync,
|
||||
{
|
||||
unauthorized::<R, E::Schema>(&self.client, E::METHOD.into(), &self.base, endpoint.path(request), &request).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl client::Public for Private {
|
||||
type Error = reqwest::Error;
|
||||
|
||||
async fn execute<E, R>(&self, endpoint: &E, request: &R) -> Result<E::Schema, error::api::Public<Self::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: endpoint::Public<R> + Sync,
|
||||
R: Request + Sync,
|
||||
{
|
||||
self.public.execute::<E, R>(endpoint, request).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl client::Private for Private {
|
||||
type Error = reqwest::Error;
|
||||
|
||||
async fn execute<E, R>(&self, endpoint: &E, request: &R) -> Result<E::Schema, error::api::Private<Self::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: endpoint::Private<R> + Sync,
|
||||
R: Request + Sync,
|
||||
{
|
||||
let token = self.current_token().await;
|
||||
match authorized::<R, E::Schema>(&self.public.client, E::METHOD.into(), &self.public.base, endpoint.path(request), &request, &token).await {
|
||||
Err(AuthorizedError::<E::Schema, R>::Unauthorized) if token.expired() => {
|
||||
let token = self.refresh_tokens().await.map_err(AuthorizedError::<E::Schema, R>::Refresh)?;
|
||||
authorized::<R, E::Schema>(&self.public.client, E::METHOD.into(), &self.public.base, endpoint.path(request), &request, &token).await
|
||||
},
|
||||
result => result,
|
||||
}
|
||||
}
|
||||
}
|
||||
250
api/src/reqwest/mod.rs
Normal file
250
api/src/reqwest/mod.rs
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
use crate::Method;
|
||||
pub use reqwest;
|
||||
|
||||
impl From<Method> for reqwest::Method {
|
||||
fn from(method: Method) -> Self {
|
||||
match method {
|
||||
Method::GET => reqwest::Method::GET,
|
||||
Method::POST => reqwest::Method::POST,
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::traits::{Request, Schema, client, endpoint};
|
||||
use crate::{authentication, error};
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Public {
|
||||
client: reqwest::Client,
|
||||
base: url::Url,
|
||||
}
|
||||
|
||||
impl Public {
|
||||
pub fn new() -> Result<Self, reqwest::Error> {
|
||||
let base = url::Url::parse("https://api.spond.com").unwrap();
|
||||
Self::new_with_base(base)
|
||||
}
|
||||
|
||||
pub fn new_with_base(base: url::Url) -> Result<Self, reqwest::Error> {
|
||||
let client = reqwest::Client::builder()
|
||||
.https_only(true)
|
||||
.redirect(reqwest::redirect::Policy::limited(1));
|
||||
#[cfg(feature = "cookies")]
|
||||
let client = client.cookie_store(true);
|
||||
#[cfg(feature = "tracing")]
|
||||
let client = client.connection_verbose(true);
|
||||
|
||||
let client = client
|
||||
//.user_agent("...")
|
||||
//.read_timeout(std::time::Duration)
|
||||
//.connect_timeout(std::time::Duration)
|
||||
.build()?;
|
||||
Ok(Self { client, base })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl client::Authenticator for Public {
|
||||
type Private = Private;
|
||||
|
||||
fn with_tokens(&self, tokens: authentication::Tokens) -> Self::Private {
|
||||
Private {
|
||||
public: self.clone(),
|
||||
tokens: tokio::sync::RwLock::new(tokens),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Private {
|
||||
public: Public,
|
||||
tokens: tokio::sync::RwLock<authentication::Tokens>,
|
||||
}
|
||||
|
||||
impl Private {
|
||||
async fn current_token(&self) -> authentication::token::Access {
|
||||
let tokens = self.tokens.read().await;
|
||||
tokens.access_token.clone()
|
||||
}
|
||||
|
||||
async fn refresh_tokens(
|
||||
&self,
|
||||
) -> Result<authentication::token::Access, error::api::Refresh<reqwest::Error, serde_json::Error>> {
|
||||
use client::Public;
|
||||
let mut tokens = self.tokens.write().await;
|
||||
*tokens = self
|
||||
.public
|
||||
.execute(&tokens.refresh_token, &tokens.refresh_token)
|
||||
.await?;
|
||||
Ok((*tokens).access_token.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Client = Private;
|
||||
|
||||
type AuthorizedError<S, R> = error::api::Private<reqwest::Error, <S as Schema<R>>::Error>;
|
||||
type UnauthorizedError<S, R> = error::api::Public<reqwest::Error, <S as Schema<R>>::Error>;
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn authorized<R, S>(
|
||||
client: &reqwest::Client,
|
||||
method: reqwest::Method,
|
||||
base: &url::Url,
|
||||
path: &str,
|
||||
request: &R,
|
||||
token: &authentication::token::Access,
|
||||
) -> Result<S, AuthorizedError<S, R>>
|
||||
where
|
||||
R: Request + Sync,
|
||||
S: Schema<R>,
|
||||
{
|
||||
let url = base.join(path).map_err(AuthorizedError::<S, R>::Url)?;
|
||||
|
||||
let builder = client
|
||||
.request(method, url)
|
||||
.json(request)
|
||||
.bearer_auth(token.as_ref());
|
||||
tracing::debug!("request: {builder:?}");
|
||||
|
||||
let response = builder
|
||||
.send()
|
||||
.await
|
||||
.map_err(AuthorizedError::<S, R>::Send)?;
|
||||
tracing::debug!("response: {response:?}");
|
||||
|
||||
let body = match response.status() {
|
||||
_ => response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(AuthorizedError::<S, R>::Receive)?,
|
||||
};
|
||||
tracing::debug!("body: {body:?}");
|
||||
|
||||
let result = S::deserialize(request, &body).map_err(AuthorizedError::<S, R>::Decode)?;
|
||||
tracing::debug!("result: {result:?}");
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn unauthorized<R, S>(
|
||||
client: &reqwest::Client,
|
||||
method: reqwest::Method,
|
||||
base: &url::Url,
|
||||
path: &str,
|
||||
request: &R,
|
||||
) -> Result<S, UnauthorizedError<S, R>>
|
||||
where
|
||||
R: Request + Sync,
|
||||
S: Schema<R>,
|
||||
{
|
||||
let url = base.join(path).map_err(UnauthorizedError::<S, R>::Url)?;
|
||||
|
||||
let builder = client
|
||||
.request(method, url)
|
||||
.json(request);
|
||||
tracing::debug!("request: {builder:?}");
|
||||
let response = builder
|
||||
.send()
|
||||
.await
|
||||
.map_err(UnauthorizedError::<S, R>::Send)?;
|
||||
tracing::debug!("response: {response:?}");
|
||||
|
||||
let body = match response.status() {
|
||||
_ => response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(UnauthorizedError::<S, R>::Receive)?,
|
||||
};
|
||||
tracing::debug!("body: {body:?}");
|
||||
|
||||
let result = S::deserialize(request, &body).map_err(UnauthorizedError::<S, R>::Decode)?;
|
||||
tracing::debug!("result: {result:?}");
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl client::Public for Public {
|
||||
type Error = reqwest::Error;
|
||||
|
||||
async fn execute<E, R>(
|
||||
&self,
|
||||
endpoint: &E,
|
||||
request: &R,
|
||||
) -> Result<E::Schema, error::api::Public<reqwest::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: endpoint::Public<R> + Sync,
|
||||
R: Request + Sync,
|
||||
{
|
||||
unauthorized::<R, E::Schema>(
|
||||
&self.client,
|
||||
E::METHOD.into(),
|
||||
&self.base,
|
||||
endpoint.path(request),
|
||||
&request,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl client::Public for Private {
|
||||
type Error = reqwest::Error;
|
||||
|
||||
async fn execute<E, R>(
|
||||
&self,
|
||||
endpoint: &E,
|
||||
request: &R,
|
||||
) -> Result<E::Schema, error::api::Public<Self::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: endpoint::Public<R> + Sync,
|
||||
R: Request + Sync,
|
||||
{
|
||||
self.public.execute::<E, R>(endpoint, request).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl client::Private for Private {
|
||||
type Error = reqwest::Error;
|
||||
|
||||
async fn execute<E, R>(
|
||||
&self,
|
||||
endpoint: &E,
|
||||
request: &R,
|
||||
) -> Result<E::Schema, error::api::Private<Self::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: endpoint::Private<R> + Sync,
|
||||
R: Request + Sync,
|
||||
{
|
||||
let token = self.current_token().await;
|
||||
match authorized::<R, E::Schema>(
|
||||
&self.public.client,
|
||||
E::METHOD.into(),
|
||||
&self.public.base,
|
||||
endpoint.path(request),
|
||||
&request,
|
||||
&token,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(AuthorizedError::<E::Schema, R>::Unauthorized) if token.expired() => {
|
||||
let token = self
|
||||
.refresh_tokens()
|
||||
.await
|
||||
.map_err(AuthorizedError::<E::Schema, R>::Refresh)?;
|
||||
authorized::<R, E::Schema>(
|
||||
&self.public.client,
|
||||
E::METHOD.into(),
|
||||
&self.public.base,
|
||||
endpoint.path(request),
|
||||
&request,
|
||||
&token,
|
||||
)
|
||||
.await
|
||||
}
|
||||
result => result,
|
||||
}
|
||||
}
|
||||
}
|
||||
1
api/src/schema/mod.rs
Normal file
1
api/src/schema/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod sponds;
|
||||
57
api/src/schema/sponds.rs
Normal file
57
api/src/schema/sponds.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// TODO: use crate::Id;
|
||||
use crate::traits;
|
||||
|
||||
// {
|
||||
// "heading" : "Kraftraum putzen",
|
||||
// "id" : "D678340FAA6341058E368AE2FB6082CA",
|
||||
// "series" : false,
|
||||
// "startTime" : "2026-11-07T08:00:00Z",
|
||||
// "unanswered" : true,
|
||||
// "updated" : 1770872490567
|
||||
// }
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
pub struct Spond {
|
||||
pub id: crate::utils::X128,
|
||||
pub updated: crate::utils::Timestamp,
|
||||
pub start_time: chrono::DateTime<chrono::Utc>,
|
||||
pub heading: String,
|
||||
#[serde(default)]
|
||||
pub unanswered: bool,
|
||||
pub series: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, Default)]
|
||||
pub struct Sponds(Vec<Spond>);
|
||||
|
||||
impl std::ops::Deref for Sponds {
|
||||
type Target = [Spond];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug)]
|
||||
pub struct Upcoming;
|
||||
|
||||
impl traits::endpoint::Private<Upcoming> for Sponds {
|
||||
const METHOD: crate::Method = crate::Method::GET;
|
||||
type Schema = Self;
|
||||
|
||||
fn path(&self, _: &Upcoming) -> &str {
|
||||
"/core/v1/sponds/upcoming"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn decode_upcoming_schema() {
|
||||
const DATA: &[u8] = include_bytes!("./sponds_upcoming.json");
|
||||
let bytes = bytes::Bytes::from_static(DATA);
|
||||
let v: Sponds = <Sponds as super::traits::Schema<Upcoming>>::deserialize(&Upcoming, &bytes).expect("VALID JSON");
|
||||
println!("{v:?}");
|
||||
}
|
||||
}
|
||||
1
api/src/schema/sponds_upcoming.json
Normal file
1
api/src/schema/sponds_upcoming.json
Normal file
File diff suppressed because one or more lines are too long
1
api/src/traits/authentication/login.rs
Normal file
1
api/src/traits/authentication/login.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub trait Credentials: super::super::Request {}
|
||||
2
api/src/traits/authentication/mod.rs
Normal file
2
api/src/traits/authentication/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod login;
|
||||
pub use login::Credentials;
|
||||
11
api/src/traits/authorization.rs
Normal file
11
api/src/traits/authorization.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
pub trait Authorization: private::Seal {}
|
||||
|
||||
mod private {
|
||||
use crate::authorization::{False, True};
|
||||
pub trait Seal {}
|
||||
|
||||
impl Seal for True {}
|
||||
impl Seal for False {}
|
||||
}
|
||||
|
||||
impl<T: private::Seal> Authorization for T {}
|
||||
80
api/src/traits/client.rs
Normal file
80
api/src/traits/client.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
use super::{Authorization, Endpoint, Request, Schema, endpoint};
|
||||
use crate::{authorization, authentication, error};
|
||||
use async_trait::async_trait;
|
||||
|
||||
pub trait Client: Public + Private {}
|
||||
|
||||
impl<C: Public + Private> Client for C {}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Handler<E: Endpoint<R, A>, R: Request, A: Authorization> {
|
||||
type Error: std::error::Error;
|
||||
|
||||
async fn execute(&self, endpoint: &E, request: &R) -> Result<E::Schema, Self::Error>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Authenticator: Public {
|
||||
type Private: Private;
|
||||
|
||||
fn with_tokens(&self, tokens: authentication::Tokens) -> Self::Private;
|
||||
|
||||
async fn authenticate<E, R>(&self, endpoint: &E, request: &R) -> Result<Self::Private, error::api::Public<<Self as Public>::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: endpoint::Public<R, Schema=authentication::Tokens> + Sync,
|
||||
R: Request + Sync,
|
||||
{
|
||||
let tokens = self.execute(endpoint, request).await?;
|
||||
Ok(self.with_tokens(tokens))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Public {
|
||||
type Error: std::error::Error + Send;
|
||||
|
||||
async fn execute<E, R>(
|
||||
&self,
|
||||
endpoint: &E,
|
||||
request: &R,
|
||||
) -> Result<E::Schema, error::api::Public<Self::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: endpoint::Public<R> + Sync,
|
||||
R: Request + Sync;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<P: Public + Sync, E: endpoint::Public<R> + Sync, R: Request + Sync>
|
||||
Handler<E, R, authorization::False> for P
|
||||
{
|
||||
type Error = error::api::Public<P::Error, <E::Schema as Schema<R>>::Error>;
|
||||
|
||||
async fn execute(&self, endpoint: &E, request: &R) -> Result<E::Schema, Self::Error> {
|
||||
self.execute(endpoint, request).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Private {
|
||||
type Error: std::error::Error + Send;
|
||||
|
||||
async fn execute<E, R>(
|
||||
&self,
|
||||
endpoint: &E,
|
||||
request: &R,
|
||||
) -> Result<E::Schema, error::api::Private<Self::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: endpoint::Private<R> + Sync,
|
||||
R: Request + Sync;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<P: Private + Sync, E: endpoint::Private<R> + Sync, R: Request + Sync>
|
||||
Handler<E, R, authorization::True> for P
|
||||
{
|
||||
type Error = error::api::Private<P::Error, <E::Schema as Schema<R>>::Error>;
|
||||
|
||||
async fn execute(&self, endpoint: &E, request: &R) -> Result<E::Schema, Self::Error> {
|
||||
self.execute(endpoint, request).await
|
||||
}
|
||||
}
|
||||
53
api/src/traits/endpoint.rs
Normal file
53
api/src/traits/endpoint.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
use super::{Authorization, Request, Schema};
|
||||
use crate::{Method, authorization};
|
||||
|
||||
pub trait Endpoint<R: Request, A: Authorization>: private::Seal<R, A> {
|
||||
const METHOD: Method;
|
||||
type Schema: Schema<R>;
|
||||
|
||||
fn path(&self, request: &R) -> &str;
|
||||
}
|
||||
|
||||
/// A Endpoint with required authorization
|
||||
pub trait Private<R: Request> {
|
||||
const METHOD: Method;
|
||||
type Schema: Schema<R>;
|
||||
|
||||
fn path(&self, request: &R) -> &str;
|
||||
}
|
||||
|
||||
impl<T: Private<R>, R: Request> Endpoint<R, authorization::True> for T {
|
||||
const METHOD: Method = T::METHOD;
|
||||
type Schema = T::Schema;
|
||||
|
||||
fn path(&self, request: &R) -> &str {
|
||||
self.path(request)
|
||||
}
|
||||
}
|
||||
|
||||
/// A Endpoint without required authorization
|
||||
pub trait Public<R: Request> {
|
||||
const METHOD: Method;
|
||||
type Schema: Schema<R>;
|
||||
|
||||
fn path(&self, request: &R) -> &str;
|
||||
}
|
||||
|
||||
impl<T: Public<R>, R: Request> Endpoint<R, authorization::False> for T {
|
||||
const METHOD: Method = T::METHOD;
|
||||
type Schema = T::Schema;
|
||||
|
||||
fn path(&self, request: &R) -> &str {
|
||||
self.path(request)
|
||||
}
|
||||
}
|
||||
|
||||
mod private {
|
||||
use super::*;
|
||||
|
||||
/// Seal the Endpoint dependency
|
||||
pub trait Seal<R: Request, A: Authorization> {}
|
||||
|
||||
impl<R: Request, P: Private<R>> Seal<R, authorization::True> for P {}
|
||||
impl<R: Request, P: Public<R>> Seal<R, authorization::False> for P {}
|
||||
}
|
||||
22
api/src/traits/id.rs
Normal file
22
api/src/traits/id.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
use super::{
|
||||
client::Handler,
|
||||
Schema,
|
||||
Endpoint,
|
||||
};
|
||||
use crate::{
|
||||
Id,
|
||||
authorization,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[async_trait]
|
||||
pub trait Marker: Schema<Id<Self>> + Endpoint<Id<Self>, authorization::True, Schema=Self> + std::fmt::Debug {
|
||||
async fn resolve<H: Handler<Self, Id<Self>, authorization::True> + Sync>(&self, id: &Id<Self>, handler: &H) -> Result<Self, H::Error>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Schema<Id<T>> + Endpoint<Id<T>, authorization::True, Schema=T> + Sync + std::fmt::Debug> Marker for T {
|
||||
async fn resolve<H: Handler<Self, Id<Self>, authorization::True> + Sync>(&self, id: &Id<Self>, handler: &H) -> Result<Self, H::Error> {
|
||||
handler.execute(&self, id).await
|
||||
}
|
||||
}
|
||||
17
api/src/traits/mod.rs
Normal file
17
api/src/traits/mod.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
pub mod authentication;
|
||||
pub mod authorization;
|
||||
pub mod client;
|
||||
pub mod endpoint;
|
||||
pub mod id;
|
||||
pub mod request;
|
||||
pub mod schema;
|
||||
|
||||
pub use authentication::Credentials;
|
||||
pub use authorization::Authorization;
|
||||
pub use client::Client;
|
||||
pub use endpoint::Endpoint;
|
||||
pub use request::Request;
|
||||
pub use schema::Schema;
|
||||
|
||||
//#[cfg(test)]
|
||||
//mod test;
|
||||
5
api/src/traits/request.rs
Normal file
5
api/src/traits/request.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
use serde::Serialize;
|
||||
|
||||
pub trait Request: Serialize + std::fmt::Debug {}
|
||||
|
||||
impl<T: Serialize + std::fmt::Debug> Request for T {}
|
||||
17
api/src/traits/schema.rs
Normal file
17
api/src/traits/schema.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
use super::Request;
|
||||
use bytes::Bytes;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
pub trait Schema<R: Request>: std::fmt::Debug + Send + Sized {
|
||||
type Error: std::error::Error + Send;
|
||||
|
||||
fn deserialize(request: &R, response: &Bytes) -> Result<Self, Self::Error>;
|
||||
}
|
||||
|
||||
impl<T: DeserializeOwned + Send + std::fmt::Debug, R: Request> Schema<R> for T {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn deserialize(_: &R, response: &Bytes) -> Result<Self, Self::Error> {
|
||||
serde_json::from_slice(&response)
|
||||
}
|
||||
}
|
||||
53
api/src/traits/test.rs
Normal file
53
api/src/traits/test.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
struct Private;
|
||||
struct Endpoint;
|
||||
#[derive(serde::Serialize)]
|
||||
struct Request;
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Schema;
|
||||
|
||||
#[async_trait]
|
||||
impl super::client::Private for Private {
|
||||
type Error = error::Private;
|
||||
|
||||
async fn execute<E, R>(&self, _: &E, _: &R) -> Result<E::Schema, error::api::Private<Self::Error, <E::Schema as Schema<R>>::Error>>
|
||||
where
|
||||
E: super::endpoint::Private<R>,
|
||||
R: super::Request + Sync,
|
||||
{
|
||||
Err(error::Private::Unauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
impl super::endpoint::Private<Request> for Endpoint {
|
||||
const METHOD: reqwest::Method = reqwest::Method::GET;
|
||||
type Schema = Schema;
|
||||
|
||||
fn path(&self, _: &Request) -> &str {
|
||||
"/core/v1/endpoint/private"
|
||||
}
|
||||
}
|
||||
|
||||
mod error {
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Private {
|
||||
#[error("unauthorized")]
|
||||
Unauthorized,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn private_endpoint() {
|
||||
use super::client::Private;
|
||||
let client = Private;
|
||||
let request = Request;
|
||||
let endpoint = Endpoint;
|
||||
let result = match client.execute(&endpoint, &request).await {
|
||||
Err(error::Private::Unauthorized) => false,
|
||||
_ => true,
|
||||
};
|
||||
assert_eq!(result, true)
|
||||
}
|
||||
5
api/src/utils/mod.rs
Normal file
5
api/src/utils/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
mod x128;
|
||||
pub use x128::X128;
|
||||
|
||||
mod timestamp;
|
||||
pub use timestamp::Timestamp;
|
||||
2
api/src/utils/timestamp.rs
Normal file
2
api/src/utils/timestamp.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct Timestamp(u64);
|
||||
78
api/src/utils/x128.rs
Normal file
78
api/src/utils/x128.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
use serde::{Serialize, Deserialize, Serializer, Deserializer};
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
use std::fmt;
|
||||
|
||||
/// Wrapper for u128 that serializes/deserializes as 32-charachter hex
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||
pub struct X128(u128);
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("invalid length: {0}")]
|
||||
Length(usize),
|
||||
|
||||
#[error("parser error: {0}")]
|
||||
Parser(#[from] std::num::ParseIntError),
|
||||
}
|
||||
|
||||
impl X128 {
|
||||
/// construct a new value
|
||||
pub fn new(value: u128) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
/// access the inner value explicitely
|
||||
pub fn value(&self) -> u128 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for X128 {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Error> {
|
||||
match s.len() {
|
||||
32 => u128::from_str_radix(s, 16)
|
||||
.map(Self::new)
|
||||
.map_err(Error::Parser),
|
||||
len => Err(Error::Length(len)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for X128 {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Error> {
|
||||
Self::from_str(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for X128 {
|
||||
type Target = u128;
|
||||
|
||||
fn deref(&self) -> &u128 {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for X128 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:032x}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for X128 {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for X128 {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let s = String::deserialize(deserializer)?;
|
||||
|
||||
Self::from_str(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue