Init
This commit is contained in:
6
.idea/jsLibraryMappings.xml
generated
Normal file
6
.idea/jsLibraryMappings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<includedPredefinedLibrary name="Node.js Core" />
|
||||
</component>
|
||||
</project>
|
||||
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -1659,6 +1659,7 @@ dependencies = [
|
||||
"socket2",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tower-http",
|
||||
"uuid",
|
||||
"validator",
|
||||
]
|
||||
@@ -2973,6 +2974,23 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
|
||||
@@ -40,7 +40,7 @@ axum = { version = "0.8", features = ["macros", "ws"] }
|
||||
#utoipa = "5.4"
|
||||
#utoipa-swagger-ui = { version = "9.0", features = ["axum"] }
|
||||
#tower = "0.5"
|
||||
#tower-http = { version = "0.6", features = ["trace", "cors", "timeout"] }
|
||||
tower-http = { version = "0.6", features = ["trace", "cors", "timeout"] }
|
||||
|
||||
# UDP
|
||||
socket2 = "0.6"
|
||||
|
||||
36
frontend/.gitignore
vendored
Normal file
36
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Cypress
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Vitest
|
||||
__screenshots__/
|
||||
3
frontend/.vscode/extensions.json
vendored
Normal file
3
frontend/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
38
frontend/README.md
Normal file
38
frontend/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# vue-project
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Recommended Browser Setup
|
||||
|
||||
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
||||
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
||||
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
||||
- Firefox:
|
||||
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
||||
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
yarn
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
yarn dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
yarn build
|
||||
```
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
8
frontend/jsconfig.json
Normal file
8
frontend/jsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
22
frontend/package.json
Normal file
22
frontend/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "vue-project",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.25"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"vite": "^7.2.4",
|
||||
"vite-plugin-vue-devtools": "^8.0.5"
|
||||
}
|
||||
}
|
||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
10
frontend/src/App.vue
Normal file
10
frontend/src/App.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<script setup>
|
||||
import Server_list from "@/components/server/server_list.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>Oxspeak</h1>
|
||||
<server_list />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
42
frontend/src/components/server/server_create.vue
Normal file
42
frontend/src/components/server/server_create.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div>
|
||||
<label>
|
||||
Server Name :
|
||||
<input type="text" name="name" placeholder="Server Name" v-model="form_inputs.name">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Password :
|
||||
<input type="password" name="password" placeholder="Password" v-model="form_inputs.password">
|
||||
</label>
|
||||
<button type="submit" @click="create_server">Create</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// defined constants
|
||||
import {ref} from "vue";
|
||||
const emit = defineEmits(['created'])
|
||||
|
||||
let form_inputs = ref({
|
||||
name: '',
|
||||
password: ''
|
||||
})
|
||||
// defined methods
|
||||
async function create_server(){
|
||||
const response = await fetch("/api/server/servers/", {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(form_inputs.value)
|
||||
})
|
||||
if (response.ok) {
|
||||
emit('created', await response.json())
|
||||
} else {
|
||||
console.error("Failed to fetch servers:", response.statusText)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
39
frontend/src/components/server/server_detail.vue
Normal file
39
frontend/src/components/server/server_detail.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div>
|
||||
{{server}}
|
||||
</div>
|
||||
<button @click="remove">Remove</button>/
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
|
||||
// defined constants
|
||||
const props = defineProps({
|
||||
server: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['remove'])
|
||||
|
||||
// defined methods
|
||||
async function remove(){
|
||||
const response = await fetch("/api/server/servers/", {
|
||||
method: 'DELETE'
|
||||
})
|
||||
if (response.ok) {
|
||||
emit('remove')
|
||||
} else {
|
||||
console.error("Failed to fetch servers:", response.statusText)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
39
frontend/src/components/server/server_list.vue
Normal file
39
frontend/src/components/server/server_list.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<h2>Servers</h2>
|
||||
<server_create @created="servers.push($event)"/>
|
||||
<button @click="fetch_servers">Refresh</button>
|
||||
<server_detail
|
||||
v-for="server in servers"
|
||||
:server="server"
|
||||
:key="server.id"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, onMounted} from "vue";
|
||||
import Server_detail from "@/components/server/server_detail.vue";
|
||||
import Server_create from "@/components/server/server_create.vue";
|
||||
|
||||
// defined constants
|
||||
const servers = ref([])
|
||||
|
||||
// defined methods
|
||||
async function fetch_servers(){
|
||||
const response = await fetch("/api/server/servers/")
|
||||
if (response.ok) {
|
||||
servers.value = await response.json()
|
||||
} else {
|
||||
console.error("Failed to fetch servers:", response.statusText)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetch_servers()
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
4
frontend/src/main.js
Normal file
4
frontend/src/main.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
26
frontend/vite.config.js
Normal file
26
frontend/vite.config.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:7000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
1175
frontend/yarn.lock
Normal file
1175
frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,31 @@
|
||||
use crate::app::AppState;
|
||||
use crate::config::Config;
|
||||
use crate::database::Database;
|
||||
use crate::event_bus::EventBus;
|
||||
use crate::network::http::HTTPServer;
|
||||
use crate::network::udp::UDPServer;
|
||||
use crate::repositories::Repositories;
|
||||
|
||||
pub struct App {
|
||||
config: Config,
|
||||
|
||||
event_bus: EventBus,
|
||||
|
||||
db: Database,
|
||||
repositories: Repositories,
|
||||
|
||||
udp_server: UDPServer,
|
||||
http_server: HTTPServer
|
||||
http_server: HTTPServer,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub async fn init(config: Config) -> Self {
|
||||
let event_bus = EventBus::new(1024);
|
||||
|
||||
let db = Database::init(&config.database_url()).await.expect("Failed to initialize database");
|
||||
let repositories = Repositories::new(db.get_connection(), event_bus.clone());
|
||||
|
||||
let state = AppState::new(db.clone());
|
||||
let state = AppState{db: db.clone(), event_bus: event_bus.clone(), repositories: repositories.clone()};
|
||||
// let state = AppState::new();
|
||||
|
||||
let udp_server = UDPServer::new(config.bind_addr());
|
||||
@@ -25,9 +33,11 @@ impl App {
|
||||
|
||||
Self {
|
||||
config,
|
||||
event_bus,
|
||||
db,
|
||||
repositories,
|
||||
udp_server,
|
||||
http_server
|
||||
http_server,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
use crate::database::Database;
|
||||
use crate::event_bus::EventBus;
|
||||
use crate::repositories::Repositories;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub db: Database
|
||||
pub db: Database,
|
||||
pub event_bus: EventBus,
|
||||
pub repositories: Repositories
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(db: Database) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
// pub fn new(db: Database, event_bus: EventBus) -> Self {
|
||||
// Self {
|
||||
// db,
|
||||
// event_bus
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// #[derive(Clone)]
|
||||
|
||||
252
src/event_bus/bus.rs
Normal file
252
src/event_bus/bus.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
use parking_lot::RwLock;
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
pub type DynPayload = Arc<dyn Any + Send + Sync>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EventBus {
|
||||
capacity: usize,
|
||||
topics: Arc<RwLock<HashMap<String, broadcast::Sender<DynPayload>>>>,
|
||||
}
|
||||
|
||||
/// Receiver typé : il ne “voit” que les payloads qui downcastent en `T`.
|
||||
/// Les autres messages du topic sont ignorés.
|
||||
pub struct TypedReceiver<T> {
|
||||
rx: broadcast::Receiver<DynPayload>,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> TypedReceiver<T>
|
||||
where
|
||||
T: Any + Send + Sync + 'static,
|
||||
{
|
||||
/// Attend le prochain message du topic qui est bien de type `T`.
|
||||
///
|
||||
/// - Ignore silencieusement les messages d’un autre type.
|
||||
/// - Peut retourner `Lagged/Closed` comme un receiver broadcast normal.
|
||||
pub async fn recv_typed(&mut self) -> Result<T, broadcast::error::RecvError>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
loop {
|
||||
let payload = self.rx.recv().await?;
|
||||
if let Some(v) = (&*payload).downcast_ref::<T>() {
|
||||
return Ok(v.clone());
|
||||
}
|
||||
// sinon: mauvais type => on ignore et on attend le suivant
|
||||
}
|
||||
}
|
||||
|
||||
/// Accès au receiver brut si tu veux gérer toi-même.
|
||||
pub fn into_inner(self) -> broadcast::Receiver<DynPayload> {
|
||||
self.rx
|
||||
}
|
||||
}
|
||||
|
||||
impl EventBus {
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
Self {
|
||||
capacity,
|
||||
topics: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_or_create_sender(&self, topic: &str) -> broadcast::Sender<DynPayload> {
|
||||
// Fast-path read
|
||||
if let Some(tx) = self.topics.read().get(topic) {
|
||||
return tx.clone();
|
||||
}
|
||||
|
||||
// Slow-path write
|
||||
let mut map = self.topics.write();
|
||||
if let Some(tx) = map.get(topic) {
|
||||
return tx.clone();
|
||||
}
|
||||
|
||||
let (tx, _) = broadcast::channel(self.capacity);
|
||||
map.insert(topic.to_string(), tx.clone());
|
||||
tx
|
||||
}
|
||||
|
||||
/// Emit un évènement sur un topic.
|
||||
///
|
||||
/// # Exemple : emit un model directement (sans DbEvent)
|
||||
/// ```rust
|
||||
/// bus.emit("server-created", server_model);
|
||||
/// ```
|
||||
///
|
||||
/// # Exemple : emit un DbEvent (payload dynamique)
|
||||
/// ```rust
|
||||
/// bus.emit("server-created", DbEvent::new(server_model));
|
||||
/// ```
|
||||
pub fn emit<T>(&self, topic: &str, payload: T)
|
||||
where
|
||||
T: Any + Send + Sync + 'static,
|
||||
{
|
||||
let tx = self.get_or_create_sender(topic);
|
||||
let _ = tx.send(Arc::new(payload));
|
||||
}
|
||||
|
||||
/// Receiver "brut" pour que tu gères toi-même la boucle / erreurs / filtrage.
|
||||
///
|
||||
/// # Exemple (loop custom)
|
||||
/// ```rust
|
||||
/// let mut rx = bus.receiver("websocket-connected");
|
||||
/// tokio::spawn(async move {
|
||||
/// loop {
|
||||
/// match rx.recv().await {
|
||||
/// Ok(payload) => { /* downcast ici */ }
|
||||
/// Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
|
||||
/// Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
|
||||
/// }
|
||||
/// }
|
||||
/// });
|
||||
/// ```
|
||||
pub fn receiver(&self, topic: &str) -> broadcast::Receiver<DynPayload> {
|
||||
self.get_or_create_sender(topic).subscribe()
|
||||
}
|
||||
|
||||
/// S'abonner à un topic (niveau "brut" = payload dynamique).
|
||||
/// Alias de `receiver`.
|
||||
pub fn subscribe(&self, topic: &str) -> broadcast::Receiver<DynPayload> {
|
||||
self.receiver(topic)
|
||||
}
|
||||
|
||||
/// Receiver typé : ne “retourne” que des `T` via `recv_typed().await`.
|
||||
///
|
||||
/// # Exemple
|
||||
/// ```rust
|
||||
/// use crate::event_bus::EventBus;
|
||||
/// use crate::models::server;
|
||||
///
|
||||
/// let bus = EventBus::new(1024);
|
||||
/// let mut rx = bus.receiver_typed::<server::Model>("server-created");
|
||||
///
|
||||
/// tokio::spawn(async move {
|
||||
/// loop {
|
||||
/// match rx.recv_typed().await {
|
||||
/// Ok(srv) => println!("server: {}", srv.name),
|
||||
/// Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
|
||||
/// Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
|
||||
/// }
|
||||
/// }
|
||||
/// });
|
||||
/// ```
|
||||
pub fn receiver_typed<T>(&self, topic: &str) -> TypedReceiver<T>
|
||||
where
|
||||
T: Any + Send + Sync + 'static,
|
||||
{
|
||||
TypedReceiver {
|
||||
rx: self.receiver(topic),
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// API ergonomique : handler appelé seulement si le payload est du type attendu.
|
||||
///
|
||||
/// # Exemple : écouter un model directement
|
||||
/// ```rust
|
||||
/// use crate::models::server;
|
||||
/// bus.on::<server::Model, _>("server-created", |srv| {
|
||||
/// println!("Nouveau server: {}", srv.name);
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// # Exemple : écouter un DbEvent et downcast le model à l'intérieur
|
||||
/// ```rust
|
||||
/// use crate::event_bus::db::DbEvent;
|
||||
/// use crate::models::server;
|
||||
///
|
||||
/// bus.on::<DbEvent, _>("server-created", |ev| {
|
||||
/// if let Some(srv) = ev.model_ref::<server::Model>() {
|
||||
/// println!("Server créé: {}", srv.name);
|
||||
/// }
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// # Exemple : écouter un évènement réseau
|
||||
/// ```rust
|
||||
/// use crate::event_bus::network::NetworkEvent;
|
||||
///
|
||||
/// bus.on::<NetworkEvent, _>("websocket-connected", |ev| {
|
||||
/// if let NetworkEvent::WsConnected { peer } = ev {
|
||||
/// println!("WS connected: {}", peer);
|
||||
/// }
|
||||
/// });
|
||||
/// ```
|
||||
pub fn on<T, F>(&self, topic: &str, handler: F) -> JoinHandle<()>
|
||||
where
|
||||
T: Any + Send + Sync + Clone + 'static,
|
||||
F: Fn(T) + Send + Sync + 'static,
|
||||
{
|
||||
let mut rx = self.receiver(topic);
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(payload) => {
|
||||
if let Some(typed) = (&*payload).downcast_ref::<T>() {
|
||||
handler(typed.clone());
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(_)) => {}
|
||||
Err(broadcast::error::RecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Version async-friendly : le handler est async, tu peux `await` dedans.
|
||||
///
|
||||
/// # Exemple : handler async (ex: requête DB / HTTP / WS broadcast)
|
||||
/// ```rust
|
||||
/// use crate::models::server;
|
||||
///
|
||||
/// bus.on_async::<server::Model, _, _>("server-created", |srv| async move {
|
||||
/// // Ici tu peux faire du async
|
||||
/// println!("(async) Server créé: {}", srv.name);
|
||||
///
|
||||
/// // Exemple fictif :
|
||||
/// // my_http_client.post("/audit").json(&srv).send().await.unwrap();
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// # Exemple : handler async + DbEvent interne
|
||||
/// ```rust
|
||||
/// use crate::event_bus::db::DbEvent;
|
||||
/// use crate::models::server;
|
||||
///
|
||||
/// bus.on_async::<DbEvent, _, _>("server-created", |ev| async move {
|
||||
/// if let Some(srv) = ev.model_ref::<server::Model>() {
|
||||
/// println!("(async) Server créé: {}", srv.name);
|
||||
/// }
|
||||
/// });
|
||||
/// ```
|
||||
pub fn on_async<T, F, Fut>(&self, topic: &str, handler: F) -> JoinHandle<()>
|
||||
where
|
||||
T: Any + Send + Sync + Clone + 'static,
|
||||
F: Fn(T) -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
let mut rx = self.receiver(topic);
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(payload) => {
|
||||
if let Some(typed) = (&*payload).downcast_ref::<T>() {
|
||||
handler(typed.clone()).await;
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(_)) => {}
|
||||
Err(broadcast::error::RecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
24
src/event_bus/db.rs
Normal file
24
src/event_bus/db.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub type DynModel = Arc<dyn Any + Send + Sync>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DbEvent {
|
||||
pub model: DynModel,
|
||||
}
|
||||
|
||||
impl DbEvent {
|
||||
pub fn new<T>(model: T) -> Self
|
||||
where
|
||||
T: Any + Send + Sync + 'static,
|
||||
{
|
||||
Self {
|
||||
model: Arc::new(model),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn model_ref<T: Any>(&self) -> Option<&T> {
|
||||
(&*self.model).downcast_ref::<T>()
|
||||
}
|
||||
}
|
||||
5
src/event_bus/mod.rs
Normal file
5
src/event_bus/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod bus;
|
||||
mod db;
|
||||
mod network;
|
||||
|
||||
pub use bus::{DynPayload, EventBus};
|
||||
9
src/event_bus/network.rs
Normal file
9
src/event_bus/network.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum NetworkEvent {
|
||||
HttpRequest { method: String, path: String },
|
||||
HttpResponse { status: u16, path: String },
|
||||
UdpConnected { peer: String },
|
||||
UdpDisconnected { peer: String },
|
||||
WsConnected { peer: String },
|
||||
WsDisconnected { peer: String },
|
||||
}
|
||||
@@ -6,4 +6,5 @@ pub mod utils;
|
||||
pub mod database;
|
||||
pub mod models;
|
||||
pub mod serializers;
|
||||
pub mod repositories;
|
||||
pub mod repositories;
|
||||
pub mod event_bus;
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{middleware, Router};
|
||||
use tower_http::cors::CorsLayer;
|
||||
use crate::app::AppState;
|
||||
use crate::network::http::middleware::context_middleware;
|
||||
use crate::network::http::{web, AppRouter};
|
||||
@@ -7,6 +8,7 @@ use crate::network::http::{web, AppRouter};
|
||||
pub fn setup_route(app_state: AppState) -> Router {
|
||||
Router::new()
|
||||
.merge(web::setup_route())
|
||||
.layer(CorsLayer::permissive())
|
||||
.layer(middleware::from_fn_with_state(app_state.clone(), context_middleware))
|
||||
.with_state(app_state)
|
||||
}
|
||||
@@ -2,7 +2,7 @@ use axum::Json;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::routing::{delete, get, post, put};
|
||||
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel};
|
||||
use sea_orm::{IntoActiveModel};
|
||||
use uuid::Uuid;
|
||||
use crate::app::AppState;
|
||||
use crate::models::server;
|
||||
@@ -19,21 +19,18 @@ pub fn setup_route() -> AppRouter {
|
||||
}
|
||||
|
||||
pub async fn server_list(
|
||||
State(app_state): State<AppState>
|
||||
State(state): State<AppState>
|
||||
) -> Result<Json<Vec<ServerSerializer>>, HTTPError> {
|
||||
let servers = server::Entity::find()
|
||||
.all(app_state.db.get_connection())
|
||||
.await?;
|
||||
let servers = state.repositories.server.get_all().await?;
|
||||
|
||||
Ok(Json(servers.into_iter().map(ServerSerializer::from).collect()))
|
||||
}
|
||||
|
||||
pub async fn server_detail(
|
||||
State(app_state): State<AppState>,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>
|
||||
) -> Result<Json<ServerSerializer>, HTTPError> {
|
||||
let server = server::Entity::find_by_id(id)
|
||||
.one(app_state.db.get_connection())
|
||||
let server = state.repositories.server.get_by_id(id)
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
|
||||
@@ -41,45 +38,40 @@ pub async fn server_detail(
|
||||
}
|
||||
|
||||
pub async fn server_create(
|
||||
State(app_state): State<AppState>,
|
||||
State(state): State<AppState>,
|
||||
Json(serializer): Json<ServerSerializer>
|
||||
) -> Result<Json<ServerSerializer>, HTTPError> {
|
||||
let active = serializer.into_active_model();
|
||||
let server: server::Model = active.insert(app_state.db.get_connection()).await?;
|
||||
let server = state.repositories.server.create(active).await?;
|
||||
|
||||
Ok(Json(ServerSerializer::from(server)))
|
||||
}
|
||||
|
||||
pub async fn server_update(
|
||||
State(app_state): State<AppState>,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(serializer): Json<ServerSerializer>,
|
||||
) -> Result<Json<ServerSerializer>, HTTPError> {
|
||||
let server = server::Entity::find_by_id(id)
|
||||
.one(app_state.db.get_connection())
|
||||
let am_server = state.repositories.server
|
||||
.get_by_id(id)
|
||||
.await?
|
||||
.ok_or(HTTPError::NotFound)?;
|
||||
.ok_or(HTTPError::NotFound)?
|
||||
.into_active_model();
|
||||
|
||||
let active = server.into_active_model();
|
||||
|
||||
let server: server::Model = serializer.apply_to_active_model(active)
|
||||
.update(app_state.db.get_connection())
|
||||
let server = state.repositories.server
|
||||
.update(serializer.apply_to_active_model(am_server))
|
||||
.await?;
|
||||
|
||||
Ok(Json(ServerSerializer::from(server)))
|
||||
}
|
||||
|
||||
pub async fn server_delete(
|
||||
State(app_state): State<AppState>,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>
|
||||
) -> Result<StatusCode, HTTPError> {
|
||||
let result = server::Entity::delete_by_id(id)
|
||||
.exec(app_state.db.get_connection())
|
||||
.await?;
|
||||
|
||||
if result.rows_affected == 0 {
|
||||
Err(HTTPError::NotFound)
|
||||
} else {
|
||||
if state.repositories.server.delete(id).await? {
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
} else {
|
||||
Err(HTTPError::NotFound)
|
||||
}
|
||||
}
|
||||
5
src/repositories/README.md
Normal file
5
src/repositories/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
Ce module permet de :
|
||||
|
||||
- Gérer/simplifier les interactions avec la base de données
|
||||
- Avoir un système de Signal qui se déclenche lors de modifications
|
||||
dans la base de données (Création, Modification, Suppression)
|
||||
33
src/repositories/category.rs
Normal file
33
src/repositories/category.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use std::sync::Arc;
|
||||
use sea_orm::{DbErr, EntityTrait, ActiveModelTrait};
|
||||
use crate::models::category;
|
||||
use crate::repositories::RepositoryContext;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CategoryRepository {
|
||||
pub context: Arc<RepositoryContext>
|
||||
}
|
||||
|
||||
impl CategoryRepository {
|
||||
pub async fn get_by_id(&self, id: uuid::Uuid) -> Result<Option<category::Model>, DbErr> {
|
||||
category::Entity::find_by_id(id).one(&self.context.db).await
|
||||
}
|
||||
|
||||
pub async fn update(&self, active: category::ActiveModel) -> Result<category::Model, DbErr> {
|
||||
let category = active.update(&self.context.db).await?;
|
||||
self.context.events.emit("Category_updated", category.clone());
|
||||
Ok(category)
|
||||
}
|
||||
|
||||
pub async fn create(&self, active: category::ActiveModel) -> Result<category::Model, DbErr> {
|
||||
let category = active.insert(&self.context.db).await?;
|
||||
self.context.events.emit("Category_created", category.clone());
|
||||
Ok(category)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: uuid::Uuid) -> Result<(), DbErr> {
|
||||
category::Entity::delete_by_id(id).exec(&self.context.db).await?;
|
||||
self.context.events.emit("Category_deleted", id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
33
src/repositories/channel.rs
Normal file
33
src/repositories/channel.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use std::sync::Arc;
|
||||
use sea_orm::{DbErr, EntityTrait, ActiveModelTrait};
|
||||
use crate::models::channel;
|
||||
use crate::repositories::RepositoryContext;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ChannelRepository {
|
||||
pub context: Arc<RepositoryContext>
|
||||
}
|
||||
|
||||
impl ChannelRepository {
|
||||
pub async fn get_by_id(&self, id: uuid::Uuid) -> Result<Option<channel::Model>, DbErr> {
|
||||
channel::Entity::find_by_id(id).one(&self.context.db).await
|
||||
}
|
||||
|
||||
pub async fn update(&self, active: channel::ActiveModel) -> Result<channel::Model, DbErr> {
|
||||
let channel = active.update(&self.context.db).await?;
|
||||
self.context.events.emit("channel_updated", channel.clone());
|
||||
Ok(channel)
|
||||
}
|
||||
|
||||
pub async fn create(&self, active: channel::ActiveModel) -> Result<channel::Model, DbErr> {
|
||||
let channel = active.insert(&self.context.db).await?;
|
||||
self.context.events.emit("channel_created", channel.clone());
|
||||
Ok(channel)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: uuid::Uuid) -> Result<(), DbErr> {
|
||||
channel::Entity::delete_by_id(id).exec(&self.context.db).await?;
|
||||
self.context.events.emit("channel_deleted", id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
33
src/repositories/message.rs
Normal file
33
src/repositories/message.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use std::sync::Arc;
|
||||
use sea_orm::{DbErr, EntityTrait, ActiveModelTrait};
|
||||
use crate::models::message;
|
||||
use crate::repositories::RepositoryContext;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MessageRepository {
|
||||
pub context: Arc<RepositoryContext>
|
||||
}
|
||||
|
||||
impl MessageRepository {
|
||||
pub async fn get_by_id(&self, id: uuid::Uuid) -> Result<Option<message::Model>, DbErr> {
|
||||
message::Entity::find_by_id(id).one(&self.context.db).await
|
||||
}
|
||||
|
||||
pub async fn update(&self, active: message::ActiveModel) -> Result<message::Model, DbErr> {
|
||||
let message = active.update(&self.context.db).await?;
|
||||
self.context.events.emit("message_updated", message.clone());
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
pub async fn create(&self, active: message::ActiveModel) -> Result<message::Model, DbErr> {
|
||||
let message = active.insert(&self.context.db).await?;
|
||||
self.context.events.emit("message_created", message.clone());
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: uuid::Uuid) -> Result<(), DbErr> {
|
||||
message::Entity::delete_by_id(id).exec(&self.context.db).await?;
|
||||
self.context.events.emit("message_deleted", id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
use std::sync::Arc;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use crate::event_bus::EventBus;
|
||||
use crate::repositories::server::ServerRepository;
|
||||
|
||||
mod server;
|
||||
mod category;
|
||||
mod channel;
|
||||
mod message;
|
||||
mod user;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RepositoryContext {
|
||||
db: DatabaseConnection,
|
||||
// pub events: EventBus, // si tu veux publier des events “post-save” plus tard
|
||||
events: EventBus, // si tu veux publier des events “post-save” plus tard
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -16,8 +21,8 @@ pub struct Repositories {
|
||||
}
|
||||
|
||||
impl Repositories {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
let context = Arc::new(RepositoryContext { db });
|
||||
pub fn new(db: &DatabaseConnection, events: EventBus) -> Self {
|
||||
let context = Arc::new(RepositoryContext { db: db.clone(), events });
|
||||
|
||||
Self {
|
||||
server: ServerRepository {context: context.clone()},
|
||||
|
||||
@@ -9,25 +9,29 @@ pub struct ServerRepository {
|
||||
}
|
||||
|
||||
impl ServerRepository {
|
||||
pub async fn get_all(&self) -> Result<Vec<server::Model>, DbErr> {
|
||||
server::Entity::find().all(&self.context.db).await
|
||||
}
|
||||
|
||||
pub async fn get_by_id(&self, id: uuid::Uuid) -> Result<Option<server::Model>, DbErr> {
|
||||
server::Entity::find_by_id(id).one(&self.context.db).await
|
||||
}
|
||||
|
||||
pub async fn update(&self, active: server::ActiveModel) -> Result<server::Model, DbErr> {
|
||||
let model = active.update(&self.context.db).await?;
|
||||
// plus tard: self.context.events.publish(...)
|
||||
Ok(model)
|
||||
let server = active.update(&self.context.db).await?;
|
||||
self.context.events.emit("server_updated", server.clone());
|
||||
Ok(server)
|
||||
}
|
||||
|
||||
pub async fn create(&self, active: server::ActiveModel) -> Result<server::Model, DbErr> {
|
||||
let model = active.insert(&self.context.db).await?;
|
||||
// plus tard: emit post-save
|
||||
Ok(model)
|
||||
let server = active.insert(&self.context.db).await?;
|
||||
self.context.events.emit("server_created", server.clone());
|
||||
Ok(server)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: uuid::Uuid) -> Result<(), DbErr> {
|
||||
server::Entity::delete_by_id(id).exec(&self.context.db).await?;
|
||||
// plus tard: emit post-delete
|
||||
Ok(())
|
||||
pub async fn delete(&self, id: uuid::Uuid) -> Result<bool, DbErr> {
|
||||
let res = server::Entity::delete_by_id(id).exec(&self.context.db).await?;
|
||||
self.context.events.emit("server_deleted", id);
|
||||
Ok(res.rows_affected > 0)
|
||||
}
|
||||
}
|
||||
33
src/repositories/user.rs
Normal file
33
src/repositories/user.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use std::sync::Arc;
|
||||
use sea_orm::{DbErr, EntityTrait, ActiveModelTrait};
|
||||
use crate::models::user;
|
||||
use crate::repositories::RepositoryContext;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UserRepository {
|
||||
pub context: Arc<RepositoryContext>
|
||||
}
|
||||
|
||||
impl UserRepository {
|
||||
pub async fn get_by_id(&self, id: uuid::Uuid) -> Result<Option<user::Model>, DbErr> {
|
||||
user::Entity::find_by_id(id).one(&self.context.db).await
|
||||
}
|
||||
|
||||
pub async fn update(&self, active: user::ActiveModel) -> Result<user::Model, DbErr> {
|
||||
let user = active.update(&self.context.db).await?;
|
||||
self.context.events.emit("user_updated", user.clone());
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn create(&self, active: user::ActiveModel) -> Result<user::Model, DbErr> {
|
||||
let user = active.insert(&self.context.db).await?;
|
||||
self.context.events.emit("user_created", user.clone());
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: uuid::Uuid) -> Result<(), DbErr> {
|
||||
user::Entity::delete_by_id(id).exec(&self.context.db).await?;
|
||||
self.context.events.emit("user_deleted", id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user