new(all): Initial import.
This commit is contained in:
parent
87aeb5cbf5
commit
2ba7ba9017
37 changed files with 621 additions and 373 deletions
|
@ -1,27 +1,27 @@
|
|||
[package]
|
||||
name = "gb-api"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
gb-core = { path = "../gb-core" }
|
||||
gb-messaging = { path = "../gb-messaging" }
|
||||
gb-monitoring = { path = "../gb-monitoring" }
|
||||
tokio.workspace = true
|
||||
tokio= { workspace = true }
|
||||
axum = { version = "0.7.9", features = ["ws", "multipart", "macros"] }
|
||||
tower.workspace = true
|
||||
tower= { workspace = true }
|
||||
tower-http = { version = "0.5", features = ["cors", "trace"] }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
uuid.workspace = true
|
||||
tracing.workspace = true
|
||||
async-trait.workspace = true
|
||||
serde= { workspace = true }
|
||||
serde_json= { workspace = true }
|
||||
uuid= { workspace = true }
|
||||
tracing= { workspace = true }
|
||||
async-trait= { workspace = true }
|
||||
futures-util = { version = "0.3", features = ["sink"] }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
tokio-stream = "0.1.17"
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
rstest= { workspace = true }
|
||||
tokio-test = "0.4"
|
||||
|
|
|
@ -13,7 +13,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn test_api_integration() {
|
||||
// Initialize message processor
|
||||
let processor = MessageProcessor::new(100);
|
||||
let processor = MessageProcessor::new();
|
||||
|
||||
// Create router
|
||||
let app: Router = create_router(processor);
|
||||
|
|
|
@ -44,7 +44,7 @@ async fn handle_ws_connection(
|
|||
|
||||
while let Some(Ok(msg)) = receiver.next().await {
|
||||
if let Ok(text) = msg.to_text() {
|
||||
if let Ok(envelope) = serde_json::from_str::<MessageEnvelope>(text) {
|
||||
if let Ok(_envelope) = serde_json::from_str::<MessageEnvelope>(text) {
|
||||
let mut processor = state.message_processor.lock().await;
|
||||
if let Err(e) = processor.process_messages().await {
|
||||
error!("Failed to process message: {}", e);
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
[package]
|
||||
name = "gb-auth"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
gb-core = { path = "../gb-core" }
|
||||
|
@ -18,8 +18,8 @@ tokio-openssl = "0.6"
|
|||
ring = "0.17"
|
||||
|
||||
# Async Runtime
|
||||
tokio.workspace = true
|
||||
async-trait.workspace = true
|
||||
tokio= { workspace = true }
|
||||
async-trait= { workspace = true }
|
||||
|
||||
|
||||
# Database
|
||||
|
@ -34,7 +34,7 @@ serde_json = "1.0"
|
|||
thiserror = "1.0"
|
||||
|
||||
# Logging & Metrics
|
||||
tracing.workspace = true
|
||||
tracing= { workspace = true }
|
||||
|
||||
# Utils
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
mod tests {
|
||||
use gb_auth::services::auth_service::AuthService;
|
||||
use gb_auth::models::LoginRequest;
|
||||
use gb_core::models::User;
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
use rstest::*;
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
[package]
|
||||
name = "gb-automation"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
gb-core = { path = "../gb-core" }
|
||||
image = { version = "0.24", features = ["webp", "jpeg", "png", "gif"] }
|
||||
chromiumoxide = { version = "0.5", features = ["tokio-runtime"] }
|
||||
futures-util = "0.3"
|
||||
async-trait.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
uuid.workspace = true
|
||||
async-trait= { workspace = true }
|
||||
tokio= { workspace = true }
|
||||
serde= { workspace = true }
|
||||
serde_json= { workspace = true }
|
||||
thiserror= { workspace = true }
|
||||
tracing= { workspace = true }
|
||||
uuid= { workspace = true }
|
||||
regex = "1.10"
|
||||
fantoccini = "0.19"
|
||||
headless_chrome = "1.0"
|
||||
async-recursion = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
rstest= { workspace = true }
|
||||
tokio-test = "0.4"
|
||||
mock_instant = "0.2"
|
|
@ -1,25 +1,25 @@
|
|||
[package]
|
||||
name = "gb-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
tokio-tungstenite = "0.18"
|
||||
async-trait.workspace = true
|
||||
serde.workspace = true
|
||||
uuid.workspace = true
|
||||
tokio.workspace = true
|
||||
thiserror.workspace = true
|
||||
chrono.workspace = true
|
||||
sqlx.workspace = true
|
||||
redis.workspace = true
|
||||
tracing.workspace = true
|
||||
async-trait= { workspace = true }
|
||||
serde= { workspace = true }
|
||||
uuid= { workspace = true }
|
||||
tokio= { workspace = true }
|
||||
thiserror= { workspace = true }
|
||||
chrono= { workspace = true }
|
||||
sqlx= { workspace = true }
|
||||
redis= { workspace = true }
|
||||
tracing= { workspace = true }
|
||||
axum = { version = "0.7", features = ["json"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
mockall.workspace = true
|
||||
rstest.workspace = true
|
||||
mockall= { workspace = true }
|
||||
rstest= { workspace = true }
|
||||
tokio-test = "0.4"
|
||||
|
|
|
@ -5,8 +5,7 @@ pub use errors::{Error, ErrorKind, Result};
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::models::{Customer, CustomerStatus, SubscriptionTier};
|
||||
use crate::models::{Customer, SubscriptionTier};
|
||||
use rstest::*;
|
||||
|
||||
#[fixture]
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
[package]
|
||||
name = "gb-document"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
gb-core = { path = "../gb-core" }
|
||||
lopdf = "0.31"
|
||||
docx-rs = "0.4"
|
||||
calamine = "0.21"
|
||||
async-trait.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
async-trait= { workspace = true }
|
||||
tokio= { workspace = true }
|
||||
serde= { workspace = true }
|
||||
serde_json= { workspace = true }
|
||||
thiserror= { workspace = true }
|
||||
tracing= { workspace = true }
|
||||
encoding_rs = "0.8"
|
||||
zip = "0.6"
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
rstest= { workspace = true }
|
||||
tokio-test = "0.4"
|
||||
tempfile = "3.8"
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
[package]
|
||||
name = "gb-image"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
gb-core = { path = "../gb-core" }
|
||||
|
@ -11,16 +11,16 @@ image = { version = "0.24", features = ["webp", "jpeg", "png", "gif"] }
|
|||
imageproc = "0.23"
|
||||
rusttype = "0.9"
|
||||
tesseract = "0.12"
|
||||
async-trait.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
async-trait= { workspace = true }
|
||||
tokio= { workspace = true }
|
||||
serde= { workspace = true }
|
||||
serde_json= { workspace = true }
|
||||
thiserror= { workspace = true }
|
||||
tracing= { workspace = true }
|
||||
tempfile = "3.8"
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
rstest= { workspace = true }
|
||||
tokio-test = "0.4"
|
||||
|
||||
|
||||
|
|
|
@ -29,9 +29,9 @@ mod tests {
|
|||
assert_eq!(cropped.width(), 100);
|
||||
assert_eq!(cropped.height(), 100);
|
||||
|
||||
let blurred = processor.apply_blur(&image, 1.0);
|
||||
let brightened = processor.adjust_brightness(&image, 10);
|
||||
let contrasted = processor.adjust_contrast(&image, 1.2);
|
||||
let _blurred = processor.apply_blur(&image, 1.0);
|
||||
let _brightened = processor.adjust_brightness(&image, 10);
|
||||
let _contrasted = processor.adjust_contrast(&image, 1.2);
|
||||
|
||||
// Test text addition
|
||||
processor.add_text(
|
||||
|
@ -44,10 +44,10 @@ mod tests {
|
|||
)?;
|
||||
|
||||
// Test format conversion
|
||||
let webp_data = ImageConverter::to_webp(&image, 80)?;
|
||||
let jpeg_data = ImageConverter::to_jpeg(&image, 80)?;
|
||||
let png_data = ImageConverter::to_png(&image)?;
|
||||
let gif_data = ImageConverter::to_gif(&image)?;
|
||||
let _webp_data = ImageConverter::to_webp(&image, 80)?;
|
||||
let _jpeg_data = ImageConverter::to_jpeg(&image, 80)?;
|
||||
let _png_data = ImageConverter::to_png(&image)?;
|
||||
let _gif_data = ImageConverter::to_gif(&image)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use gb_core::{Error, Result};
|
||||
use image::{DynamicImage, ImageOutputFormat, Rgba, RgbaImage};
|
||||
use image::{DynamicImage, ImageOutputFormat, Rgba};
|
||||
use imageproc::drawing::draw_text_mut;
|
||||
use rusttype::{Font, Scale};
|
||||
use std::io::Cursor;
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
[package]
|
||||
name = "gb-media"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
gb-core = { path = "../gb-core" }
|
||||
tokio.workspace = true
|
||||
webrtc.workspace = true
|
||||
gstreamer.workspace = true
|
||||
opus.workspace = true
|
||||
tracing.workspace = true
|
||||
async-trait.workspace = true
|
||||
serde.workspace = true
|
||||
uuid.workspace = true
|
||||
anyhow.workspace = true
|
||||
tokio= { workspace = true }
|
||||
webrtc= { workspace = true }
|
||||
gstreamer= { workspace = true }
|
||||
opus= { workspace = true }
|
||||
tracing= { workspace = true }
|
||||
async-trait= { workspace = true }
|
||||
serde= { workspace = true }
|
||||
uuid= { workspace = true }
|
||||
anyhow= { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
mockall.workspace = true
|
||||
rstest= { workspace = true }
|
||||
mockall= { workspace = true }
|
||||
tokio-test = "0.4"
|
||||
|
|
|
@ -12,9 +12,7 @@ impl MediaProcessor {
|
|||
pub fn new() -> Result<Self> {
|
||||
gst::init().map_err(|e| Error::internal(format!("Failed to initialize GStreamer: {}", e)))?;
|
||||
|
||||
let pipeline = gst::Pipeline::new()
|
||||
.map_err(|e| Error::internal(format!("Failed to create pipeline: {}", e)))?;
|
||||
|
||||
let pipeline = gst::Pipeline::new();
|
||||
Ok(Self { pipeline })
|
||||
}
|
||||
|
||||
|
@ -57,21 +55,23 @@ impl MediaProcessor {
|
|||
format: &str
|
||||
) -> Result<()> {
|
||||
let source = gst::ElementFactory::make("filesrc")
|
||||
.build()
|
||||
.map_err(|e| Error::internal(format!("Failed to create source element: {}", e)))?;
|
||||
source.set_property("location", input_path.to_str().unwrap());
|
||||
|
||||
let sink = gst::ElementFactory::make("filesink")
|
||||
.build()
|
||||
.map_err(|e| Error::internal(format!("Failed to create sink element: {}", e)))?;
|
||||
sink.set_property("location", output_path.to_str().unwrap());
|
||||
|
||||
let decoder = match format.to_lowercase().as_str() {
|
||||
"mp4" => gst::ElementFactory::make("qtdemux"),
|
||||
"webm" => gst::ElementFactory::make("matroskademux"),
|
||||
"mp4" => gst::ElementFactory::make("qtdemux").build(),
|
||||
"webm" => gst::ElementFactory::make("matroskademux").build(),
|
||||
_ => return Err(Error::internal(format!("Unsupported format: {}", format)))
|
||||
}.map_err(|e| Error::internal(format!("Failed to create decoder: {}", e)))?;
|
||||
|
||||
self.pipeline.add_many(&[&source, &decoder, &sink])
|
||||
.map_err(|e| Error::internal(format!("Failed to add elements: {}", e)))?;
|
||||
.map_err(|e| Error::internal(format!("Failed to add elements: {}", e)))?;
|
||||
|
||||
gst::Element::link_many(&[&source, &decoder, &sink])
|
||||
.map_err(|e| Error::internal(format!("Failed to link elements: {}", e)))?;
|
||||
|
|
|
@ -3,7 +3,6 @@ use webrtc::{
|
|||
api::{API, APIBuilder},
|
||||
peer_connection::{
|
||||
RTCPeerConnection,
|
||||
peer_connection_state::RTCPeerConnectionState,
|
||||
configuration::RTCConfiguration,
|
||||
},
|
||||
track::{
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
[package]
|
||||
name = "gb-messaging"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
gb-core = { path = "../gb-core" }
|
||||
tokio.workspace = true
|
||||
rdkafka.workspace = true
|
||||
redis.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
uuid.workspace = true
|
||||
async-trait.workspace = true
|
||||
tracing.workspace = true
|
||||
futures.workspace = true
|
||||
tokio= { workspace = true }
|
||||
rdkafka= { workspace = true }
|
||||
redis= { workspace = true }
|
||||
serde= { workspace = true }
|
||||
serde_json= { workspace = true }
|
||||
uuid= { workspace = true }
|
||||
async-trait= { workspace = true }
|
||||
tracing= { workspace = true }
|
||||
futures= { workspace = true }
|
||||
futures-util = "0.3"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
lapin = "2.3"
|
||||
tokio-tungstenite = { version = "0.20", features = ["native-tls"] }
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
tokio-test = "0.4"
|
||||
rstest= { workspace = true }
|
||||
tokio-test = "0.4"
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
[package]
|
||||
name = "gb-migrations"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
name = "migrations"
|
||||
path = "src/bin/migrations.rs"
|
||||
|
||||
[dependencies]
|
||||
tokio.workspace = true
|
||||
sqlx.workspace = true
|
||||
tracing.workspace = true
|
||||
uuid.workspace = true
|
||||
chrono.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio= { workspace = true }
|
||||
sqlx= { workspace = true }
|
||||
tracing= { workspace = true }
|
||||
uuid= { workspace = true }
|
||||
chrono= { workspace = true }
|
||||
serde_json= { workspace = true }
|
||||
gb-core = { path = "../gb-core" }
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
rstest= { workspace = true }
|
49
gb-messaging/src/broker.rs
Normal file
49
gb-messaging/src/broker.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use gb_core::Error;
|
||||
use rdkafka::producer::{FutureProducer, FutureRecord};
|
||||
use rdkafka::config::ClientConfig;
|
||||
use std::time::Duration;
|
||||
use serde::Serialize;
|
||||
|
||||
pub struct KafkaBroker {
|
||||
producer: FutureProducer,
|
||||
broker_address: String,
|
||||
group_id: String,
|
||||
}
|
||||
|
||||
impl KafkaBroker {
|
||||
pub fn new(broker_address: &str, group_id: &str) -> Self {
|
||||
let producer: FutureProducer = ClientConfig::new()
|
||||
.set("bootstrap.servers", broker_address)
|
||||
.set("message.timeout.ms", "5000")
|
||||
.create()
|
||||
.expect("Producer creation failed");
|
||||
|
||||
Self {
|
||||
producer,
|
||||
broker_address: broker_address.to_string(),
|
||||
group_id: group_id.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn publish<T: Serialize>(
|
||||
&self,
|
||||
topic: &str,
|
||||
key: &str,
|
||||
message: &T,
|
||||
) -> Result<(), Error> {
|
||||
let payload = serde_json::to_string(message)
|
||||
.map_err(|e| Error::internal(format!("Serialization failed: {}", e)))?;
|
||||
|
||||
self.producer
|
||||
.send(
|
||||
FutureRecord::to(topic)
|
||||
.key(key)
|
||||
.payload(&payload),
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await
|
||||
.map_err(|(e, _)| Error::internal(format!("Failed to publish message: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -3,27 +3,34 @@ use rdkafka::producer::{FutureProducer, FutureRecord};
|
|||
use rdkafka::consumer::{StreamConsumer, Consumer};
|
||||
use rdkafka::ClientConfig;
|
||||
use std::time::Duration;
|
||||
use tracing::{instrument, error};
|
||||
use serde::Serialize;
|
||||
use super::kafka;
|
||||
|
||||
pub struct Kafka {
|
||||
broker_address: String,
|
||||
group_id: String,
|
||||
|
||||
producer: FutureProducer,
|
||||
consumer: StreamConsumer,
|
||||
}
|
||||
|
||||
impl Kafka {
|
||||
pub async fn new(brokers: &str) -> Result<Self> {
|
||||
pub async fn new(broker_address: &str, group_id: &str) -> Result<Self> {
|
||||
let producer = ClientConfig::new()
|
||||
.set("bootstrap.servers", brokers)
|
||||
.set("bootstrap.servers", broker_address)
|
||||
.create()
|
||||
.map_err(|e| Error::kafka(format!("Failed to create producer: {}", e)))?;
|
||||
|
||||
let consumer = ClientConfig::new()
|
||||
.set("bootstrap.servers", brokers)
|
||||
.set("group.id", "my-group")
|
||||
|
||||
let consumer = ClientConfig::new()
|
||||
.set("bootstrap.servers", broker_address)
|
||||
.set("group.id", group_id)
|
||||
.create()
|
||||
.map_err(|e| Error::kafka(format!("Failed to create consumer: {}", e)))?;
|
||||
|
||||
Ok(Self {
|
||||
broker_address: broker_address.to_string(),
|
||||
group_id: group_id.to_string(),
|
||||
producer,
|
||||
consumer,
|
||||
})
|
||||
|
@ -54,12 +61,16 @@ impl Kafka {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rstest::*;
|
||||
use tokio;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
use std::future::Future;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
struct TestMessage {
|
||||
|
@ -67,11 +78,6 @@ mod tests {
|
|||
content: String,
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
async fn kafka_broker() -> Kafka {
|
||||
Kafka::new("localhost:9092").await.unwrap()
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn test_message() -> TestMessage {
|
||||
TestMessage {
|
||||
|
@ -79,16 +85,28 @@ mod tests {
|
|||
content: "test message".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[fixture]
|
||||
async fn kafka() -> Kafka {
|
||||
Kafka::new(
|
||||
"localhost:9092",
|
||||
"test-group",
|
||||
).await.unwrap()
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_publish_subscribe(#[future] kafka_broker: Kafka, test_message: TestMessage) {
|
||||
async fn test_publish_subscribe(
|
||||
#[future] kafka: Kafka,
|
||||
test_message: TestMessage
|
||||
) {
|
||||
let topic = "test-topic";
|
||||
kafka_broker.publish(topic, &test_message)
|
||||
let kafka = kafka.await;
|
||||
kafka.publish(topic, &test_message)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
kafka_broker.subscribe(topic)
|
||||
kafka.subscribe(topic)
|
||||
.await
|
||||
.unwrap();
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
|
|
|
@ -11,15 +11,20 @@ pub use redis_pubsub::RedisPubSub;
|
|||
pub use websocket::WebSocketClient;
|
||||
pub use processor::MessageProcessor;
|
||||
pub use models::MessageEnvelope;
|
||||
mod broker;
|
||||
pub use broker::KafkaBroker;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gb_core::models::Message;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use uuid::Uuid;
|
||||
|
||||
use std::sync::Arc;
|
||||
use redis::Client;
|
||||
use tokio::sync::broadcast;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
struct TestMessage {
|
||||
id: Uuid,
|
||||
|
@ -31,12 +36,10 @@ mod tests {
|
|||
let kafka = KafkaBroker::new(
|
||||
"localhost:9092",
|
||||
"test-group",
|
||||
).unwrap();
|
||||
|
||||
let redis = RedisPubSub::new("redis://localhost")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
);
|
||||
let redis_client = Client::open("redis://localhost")
|
||||
.expect("Failed to create Redis client");
|
||||
let redis = RedisPubSub::new(Arc::new(redis_client));
|
||||
let rabbitmq = RabbitMQ::new("amqp://localhost:5672")
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -62,11 +65,11 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
websocket.send_message(serde_json::to_string(&test_message).unwrap())
|
||||
websocket.send_message(&serde_json::to_string(&test_message).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut processor = MessageProcessor::new(100);
|
||||
let mut processor = MessageProcessor::new();
|
||||
|
||||
processor.register_handler("test", |envelope| {
|
||||
println!("Processed message: {}", envelope.message.content);
|
||||
|
@ -92,6 +95,6 @@ mod tests {
|
|||
metadata: std::collections::HashMap::new(),
|
||||
};
|
||||
|
||||
processor.sender().send(envelope).await.unwrap();
|
||||
processor.sender().send(envelope).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,49 +1,60 @@
|
|||
use gb_core::{Result, Error, models::Message};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{instrument, error};
|
||||
use uuid::Uuid;
|
||||
use gb_core::Result;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MessageEnvelope {
|
||||
pub id: Uuid,
|
||||
pub message: Message,
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
use tracing::{error, instrument};
|
||||
use uuid::Uuid;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::broadcast;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::MessageEnvelope;
|
||||
|
||||
pub struct MessageProcessor {
|
||||
tx: mpsc::Sender<MessageEnvelope>,
|
||||
rx: mpsc::Receiver<MessageEnvelope>,
|
||||
handlers: HashMap<String, Box<dyn Fn(MessageEnvelope) -> Result<()> + Send + Sync>>,
|
||||
tx: broadcast::Sender<MessageEnvelope>,
|
||||
rx: broadcast::Receiver<MessageEnvelope>,
|
||||
handlers: Arc<HashMap<String, Box<dyn Fn(MessageEnvelope) -> Result<()> + Send + Sync>>>,
|
||||
}
|
||||
|
||||
impl Clone for MessageProcessor {
|
||||
fn clone(&self) -> Self {
|
||||
MessageProcessor {
|
||||
tx: self.tx.clone(),
|
||||
rx: self.tx.subscribe(),
|
||||
handlers: Arc::clone(&self.handlers),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageProcessor {
|
||||
pub fn new(buffer_size: usize) -> Self {
|
||||
let (tx, rx) = mpsc::channel(buffer_size);
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self::new_with_buffer_size(100)
|
||||
}
|
||||
|
||||
pub fn new_with_buffer_size(buffer_size: usize) -> Self {
|
||||
let (tx, rx) = broadcast::channel(buffer_size);
|
||||
Self {
|
||||
tx,
|
||||
rx,
|
||||
handlers: HashMap::new(),
|
||||
handlers: Arc::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sender(&self) -> mpsc::Sender<MessageEnvelope> {
|
||||
pub fn sender(&self) -> broadcast::Sender<MessageEnvelope> {
|
||||
self.tx.clone()
|
||||
}
|
||||
|
||||
#[instrument(skip(self, handler))]
|
||||
pub fn register_handler<F>(&mut self, kind: &str, handler: F)
|
||||
pub fn register_handler<F>(&mut self, kind: &str, handler: F)
|
||||
where
|
||||
F: Fn(MessageEnvelope) -> Result<()> + Send + Sync + 'static,
|
||||
{
|
||||
self.handlers.insert(kind.to_string(), Box::new(handler));
|
||||
Arc::get_mut(&mut self.handlers)
|
||||
.expect("Cannot modify handlers")
|
||||
.insert(kind.to_string(), Box::new(handler));
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn process_messages(&mut self) -> Result<()> {
|
||||
while let Some(envelope) = self.rx.recv().await {
|
||||
while let Ok(envelope) = self.rx.recv().await {
|
||||
if let Some(handler) = self.handlers.get(&envelope.message.kind) {
|
||||
if let Err(e) = handler(envelope.clone()) {
|
||||
error!("Handler error for message {}: {}", envelope.id, e);
|
||||
|
@ -52,7 +63,6 @@ impl MessageProcessor {
|
|||
error!("No handler registered for message kind: {}", envelope.message.kind);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -60,8 +70,9 @@ impl MessageProcessor {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gb_core::models::Message;
|
||||
use rstest::*;
|
||||
use std::sync::Arc;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[fixture]
|
||||
|
@ -83,11 +94,10 @@ mod tests {
|
|||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_message_processor(test_message: Message) {
|
||||
let mut processor = MessageProcessor::new(100);
|
||||
let mut processor = MessageProcessor::new();
|
||||
let processed = Arc::new(Mutex::new(false));
|
||||
let processed_clone = processed.clone();
|
||||
|
||||
// Register handler
|
||||
processor.register_handler("test", move |envelope| {
|
||||
assert_eq!(envelope.message.content, "test content");
|
||||
let mut processed = processed_clone.blocking_lock();
|
||||
|
@ -95,25 +105,21 @@ mod tests {
|
|||
Ok(())
|
||||
});
|
||||
|
||||
// Start processing in background
|
||||
let mut processor_clone = processor.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
processor_clone.process_messages().await.unwrap();
|
||||
});
|
||||
|
||||
// Send test message
|
||||
let envelope = MessageEnvelope {
|
||||
id: Uuid::new_v4(),
|
||||
message: test_message,
|
||||
metadata: HashMap::new(),
|
||||
};
|
||||
|
||||
processor.sender().send(envelope).await.unwrap();
|
||||
processor.sender().send(envelope).unwrap();
|
||||
|
||||
// Wait for processing
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
|
||||
// Verify message was processed
|
||||
assert!(*processed.lock().await);
|
||||
|
||||
handle.abort();
|
||||
|
|
|
@ -157,19 +157,24 @@ mod tests {
|
|||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_publish_subscribe(
|
||||
rabbitmq: RabbitMQ,
|
||||
#[future] rabbitmq: RabbitMQ,
|
||||
test_message: TestMessage,
|
||||
) {
|
||||
let queue = "test_queue";
|
||||
let routing_key = "test_routing_key";
|
||||
|
||||
let rabbitmq = rabbitmq.await;
|
||||
let rabbitmq_clone = rabbitmq.clone();
|
||||
let test_message_clone = test_message.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let handler = |msg: TestMessage| async move {
|
||||
assert_eq!(msg, test_message_clone);
|
||||
Ok(())
|
||||
let test_message_ref = test_message_clone.clone();
|
||||
let handler = move |msg: TestMessage| {
|
||||
let expected_msg = test_message_ref.clone();
|
||||
async move {
|
||||
assert_eq!(msg, expected_msg);
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
rabbitmq_clone.subscribe(queue, handler).await.unwrap();
|
||||
|
|
|
@ -23,9 +23,9 @@ impl WebSocketClient {
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn send_message(&mut self, payload: String) -> Result<()> {
|
||||
pub async fn send_message(&mut self, payload: &str) -> Result<()> {
|
||||
self.stream
|
||||
.send(Message::Text(payload))
|
||||
.send(Message::Text(payload.to_string()))
|
||||
.await
|
||||
.map_err(Self::to_gb_error)?;
|
||||
Ok(())
|
||||
|
@ -35,8 +35,10 @@ impl WebSocketClient {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures::StreamExt;
|
||||
use rstest::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio_tungstenite::tungstenite::WebSocket;
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpListener;
|
||||
use uuid::Uuid;
|
||||
|
@ -79,8 +81,8 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn test_websocket(test_message: TestMessage) {
|
||||
let server_url = create_test_server().await;
|
||||
let mut client = WebSocket::new(&server_url).await.unwrap();
|
||||
let mut client = WebSocketClient::connect(&server_url).await.unwrap();
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
client.send(&test_message).await.unwrap();
|
||||
client.send_message(&serde_json::to_string(&test_message).unwrap()).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
[package]
|
||||
name = "gb-migrations"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
name = "migrations"
|
||||
path = "src/bin/migrations.rs"
|
||||
|
||||
[dependencies]
|
||||
tokio.workspace = true
|
||||
sqlx.workspace = true
|
||||
tracing.workspace = true
|
||||
uuid.workspace = true
|
||||
chrono.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio= { workspace = true }
|
||||
sqlx= { workspace = true }
|
||||
tracing= { workspace = true }
|
||||
uuid= { workspace = true }
|
||||
chrono= { workspace = true }
|
||||
serde_json= { workspace = true }
|
||||
gb-core = { path = "../gb-core" }
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
rstest= { workspace = true }
|
|
@ -1,21 +1,21 @@
|
|||
[package]
|
||||
name = "gb-monitoring"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
version= { workspace = true }
|
||||
edition= { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
opentelemetry = { version = "0.19", features = ["rt-tokio"] }
|
||||
opentelemetry-otlp = { version = "0.12", features = ["tonic"] }
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
thiserror.workspace = true
|
||||
prometheus.workspace = true
|
||||
tracing= { workspace = true }
|
||||
tracing-subscriber= { workspace = true }
|
||||
thiserror= { workspace = true }
|
||||
prometheus= { workspace = true }
|
||||
gb-core = { path = "../gb-core" }
|
||||
lazy_static = "1.4"
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio= { workspace = true }
|
||||
serde= { workspace = true }
|
||||
serde_json= { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
rstest= { workspace = true }
|
||||
tokio-test = "0.4"
|
|
@ -15,13 +15,13 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn test_monitoring_integration() {
|
||||
// Initialize logging
|
||||
init_logging().unwrap();
|
||||
init_logging("gb").unwrap();
|
||||
|
||||
// Initialize metrics
|
||||
let metrics = Metrics::new().unwrap();
|
||||
let metrics = Metrics::new();
|
||||
|
||||
// Initialize telemetry
|
||||
let telemetry = Telemetry::new("test-service").unwrap();
|
||||
let telemetry = Telemetry::new("test-service").await.unwrap();
|
||||
|
||||
// Test logging with metrics
|
||||
info!(
|
||||
|
@ -30,11 +30,11 @@ mod tests {
|
|||
);
|
||||
|
||||
// Simulate some activity
|
||||
metrics.increment_connections();
|
||||
metrics.increment_messages();
|
||||
metrics.observe_request_duration(0.1);
|
||||
metrics.set_active_connections(1);
|
||||
metrics.increment_message_count();
|
||||
metrics.observe_processing_time(0.1);
|
||||
|
||||
// Verify metrics
|
||||
assert_eq!(metrics.active_connections.get(), 1.0);
|
||||
assert_eq!(metrics.active_connections.get(), 1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use tracing_subscriber::{
|
|||
Registry,
|
||||
};
|
||||
|
||||
pub fn init_logging(service_name: &str) {
|
||||
pub fn init_logging(service_name: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let env_filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("info"));
|
||||
|
||||
|
@ -22,7 +22,8 @@ pub fn init_logging(service_name: &str) {
|
|||
.with(env_filter)
|
||||
.with(formatting_layer);
|
||||
|
||||
set_global_default(subscriber).expect("Failed to set tracing subscriber");
|
||||
set_global_default(subscriber)?; // Use ? instead of expect
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -32,8 +33,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_logging_initialization() {
|
||||
assert!(init_logging().is_ok());
|
||||
|
||||
init_logging("gb"); // Just call the function
|
||||
info!("Test log message");
|
||||
// Add assertions to verify the log was actually written if needed
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use prometheus::{IntCounter, IntGauge, Histogram, Registry};
|
|||
pub struct Metrics {
|
||||
registry: Registry,
|
||||
message_counter: IntCounter,
|
||||
active_connections: IntGauge,
|
||||
pub active_connections: IntGauge,
|
||||
message_processing_time: Histogram,
|
||||
}
|
||||
|
||||
|
@ -59,6 +59,8 @@ impl Metrics {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use prometheus::Encoder as _;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
[package]
|
||||
name = "gb-storage"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
gb-core = { path = "../gb-core" }
|
||||
tokio.workspace = true
|
||||
sqlx.workspace = true
|
||||
redis.workspace = true
|
||||
tikv-client.workspace = true
|
||||
tracing.workspace = true
|
||||
async-trait.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
uuid.workspace = true
|
||||
chrono.workspace = true
|
||||
tokio= { workspace = true }
|
||||
sqlx= { workspace = true }
|
||||
redis= { workspace = true }
|
||||
tikv-client= { workspace = true }
|
||||
tracing= { workspace = true }
|
||||
async-trait= { workspace = true }
|
||||
serde= { workspace = true }
|
||||
serde_json= { workspace = true }
|
||||
uuid= { workspace = true }
|
||||
chrono= { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
mockall.workspace = true
|
||||
rstest= { workspace = true }
|
||||
mockall= { workspace = true }
|
||||
tokio-test = "0.4"
|
||||
|
|
|
@ -2,6 +2,6 @@ mod postgres;
|
|||
mod redis;
|
||||
mod tikv;
|
||||
|
||||
pub use postgres::{CustomerRepository, PostgresCustomerRepository};
|
||||
pub use postgres::PostgresCustomerRepository;
|
||||
pub use redis::RedisStorage;
|
||||
pub use tikv::TiKVStorage;
|
21
gb-storage/src/models.rs
Normal file
21
gb-storage/src/models.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
// or wherever SubscriptionTier is defined
|
||||
|
||||
impl From<SubscriptionTier> for String {
|
||||
fn from(tier: SubscriptionTier) -> Self {
|
||||
match tier {
|
||||
SubscriptionTier::Free => "free".to_string(),
|
||||
SubscriptionTier::Pro => "pro".to_string(),
|
||||
SubscriptionTier::Enterprise => "enterprise".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CustomerStatus> for String {
|
||||
fn from(status: CustomerStatus) -> Self {
|
||||
match status {
|
||||
CustomerStatus::Active => "active".to_string(),
|
||||
CustomerStatus::Inactive => "inactive".to_string(),
|
||||
CustomerStatus::Suspended => "suspended".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,80 +2,226 @@ use gb_core::{
|
|||
Result, Error,
|
||||
models::{Customer, CustomerStatus, SubscriptionTier},
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
use sqlx::{PgPool, Row, postgres::PgRow};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait CustomerRepository: Send + Sync {
|
||||
async fn create(&self, customer: Customer) -> Result<Customer>;
|
||||
async fn get_customer_by_id(&self, id: &str) -> Result<Option<Customer>>;
|
||||
async fn update(&self, customer: Customer) -> Result<Customer>;
|
||||
async fn delete(&self, id: &str) -> Result<()>;
|
||||
}
|
||||
|
||||
trait ToDbString {
|
||||
fn to_db_string(&self) -> String;
|
||||
}
|
||||
|
||||
trait FromDbString: Sized {
|
||||
fn from_db_string(s: &str) -> Result<Self>;
|
||||
}
|
||||
|
||||
impl ToDbString for SubscriptionTier {
|
||||
fn to_db_string(&self) -> String {
|
||||
match self {
|
||||
SubscriptionTier::Free => "free".to_string(),
|
||||
SubscriptionTier::Pro => "pro".to_string(),
|
||||
SubscriptionTier::Enterprise => "enterprise".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToDbString for CustomerStatus {
|
||||
fn to_db_string(&self) -> String {
|
||||
match self {
|
||||
CustomerStatus::Active => "active".to_string(),
|
||||
CustomerStatus::Inactive => "inactive".to_string(),
|
||||
CustomerStatus::Suspended => "suspended".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromDbString for SubscriptionTier {
|
||||
fn from_db_string(s: &str) -> Result<Self> {
|
||||
match s {
|
||||
"free" => Ok(SubscriptionTier::Free),
|
||||
"pro" => Ok(SubscriptionTier::Pro),
|
||||
"enterprise" => Ok(SubscriptionTier::Enterprise),
|
||||
_ => Err(Error::internal(format!("Invalid subscription tier: {}", s))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromDbString for CustomerStatus {
|
||||
fn from_db_string(s: &str) -> Result<Self> {
|
||||
match s {
|
||||
"active" => Ok(CustomerStatus::Active),
|
||||
"inactive" => Ok(CustomerStatus::Inactive),
|
||||
"suspended" => Ok(CustomerStatus::Suspended),
|
||||
_ => Err(Error::internal(format!("Invalid customer status: {}", s))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PostgresCustomerRepository {
|
||||
pool: Arc<PgPool>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl CustomerRepository for PostgresCustomerRepository {
|
||||
async fn create(&self, customer: Customer) -> Result<Customer> {
|
||||
let subscription_tier = customer.subscription_tier.to_db_string();
|
||||
let status = customer.status.to_db_string();
|
||||
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO customers (
|
||||
id, name, email,
|
||||
subscription_tier, status,
|
||||
created_at, updated_at,
|
||||
max_instances
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING *
|
||||
"#
|
||||
)
|
||||
.bind(&customer.id)
|
||||
.bind(&customer.name)
|
||||
.bind(&customer.email)
|
||||
.bind(&subscription_tier)
|
||||
.bind(&status)
|
||||
.bind(&customer.created_at)
|
||||
.bind(&customer.updated_at)
|
||||
.bind(customer.max_instances as i32)
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| Error::internal(format!("Database error: {}", e)))?;
|
||||
|
||||
Self::row_to_customer(&row).await
|
||||
}
|
||||
|
||||
async fn get_customer_by_id(&self, id: &str) -> Result<Option<Customer>> {
|
||||
let maybe_row = sqlx::query(
|
||||
"SELECT * FROM customers WHERE id = $1"
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| Error::internal(format!("Database error: {}", e)))?;
|
||||
|
||||
if let Some(row) = maybe_row {
|
||||
Ok(Some(Self::row_to_customer(&row).await?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn update(&self, customer: Customer) -> Result<Customer> {
|
||||
let subscription_tier = customer.subscription_tier.to_db_string();
|
||||
let status = customer.status.to_db_string();
|
||||
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
UPDATE customers
|
||||
SET name = $2,
|
||||
email = $3,
|
||||
subscription_tier = $4,
|
||||
status = $5,
|
||||
updated_at = $6,
|
||||
max_instances = $7
|
||||
WHERE id = $1
|
||||
RETURNING *
|
||||
"#
|
||||
)
|
||||
.bind(&customer.id)
|
||||
.bind(&customer.name)
|
||||
.bind(&customer.email)
|
||||
.bind(&subscription_tier)
|
||||
.bind(&status)
|
||||
.bind(Utc::now())
|
||||
.bind(customer.max_instances as i32)
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| Error::internal(format!("Database error: {}", e)))?;
|
||||
|
||||
Self::row_to_customer(&row).await
|
||||
}
|
||||
|
||||
async fn delete(&self, id: &str) -> Result<()> {
|
||||
sqlx::query("DELETE FROM customers WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| Error::internal(format!("Database error: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PostgresCustomerRepository {
|
||||
pub fn new(pool: Arc<PgPool>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
pub async fn create(&self, customer: Customer) -> Result<Customer> {
|
||||
let subscription_tier: String = customer.subscription_tier.clone().into();
|
||||
let status: String = customer.status.clone().into();
|
||||
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO customers (
|
||||
id, name, email, max_instances,
|
||||
subscription_tier, status,
|
||||
created_at, updated_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING *
|
||||
"#,
|
||||
customer.id,
|
||||
customer.name,
|
||||
customer.email,
|
||||
customer.max_instances as i32,
|
||||
subscription_tier,
|
||||
status,
|
||||
customer.created_at,
|
||||
customer.updated_at,
|
||||
)
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| Error::internal(format!("Database error: {}", e)))?;
|
||||
|
||||
async fn row_to_customer(row: &PgRow) -> Result<Customer> {
|
||||
Ok(Customer {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
email: row.email,
|
||||
max_instances: row.max_instances as u32,
|
||||
subscription_tier: SubscriptionTier::from(row.subscription_tier),
|
||||
status: CustomerStatus::from(row.status),
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
id: row.try_get("id").map_err(|e| Error::internal(e.to_string()))?,
|
||||
name: row.try_get("name").map_err(|e| Error::internal(e.to_string()))?,
|
||||
email: row.try_get("email").map_err(|e| Error::internal(e.to_string()))?,
|
||||
subscription_tier: SubscriptionTier::from_db_string(
|
||||
row.try_get("subscription_tier").map_err(|e| Error::internal(e.to_string()))?
|
||||
)?,
|
||||
status: CustomerStatus::from_db_string(
|
||||
row.try_get("status").map_err(|e| Error::internal(e.to_string()))?
|
||||
)?,
|
||||
created_at: row.try_get("created_at").map_err(|e| Error::internal(e.to_string()))?,
|
||||
updated_at: row.try_get("updated_at").map_err(|e| Error::internal(e.to_string()))?,
|
||||
max_instances: {
|
||||
let value: i32 = row.try_get("max_instances")
|
||||
.map_err(|e| Error::internal(e.to_string()))?;
|
||||
if value < 0 {
|
||||
return Err(Error::internal("max_instances cannot be negative"));
|
||||
}
|
||||
value as u32
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(&self, id: Uuid) -> Result<Option<Customer>> {
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
SELECT *
|
||||
FROM customers
|
||||
WHERE id = $1
|
||||
"#,
|
||||
id
|
||||
)
|
||||
.fetch_optional(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| Error::internal(format!("Database error: {}", e)))?;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::Utc;
|
||||
|
||||
Ok(row.map(|row| Customer {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
email: row.email,
|
||||
max_instances: row.max_instances as u32,
|
||||
subscription_tier: SubscriptionTier::from(row.subscription_tier),
|
||||
status: CustomerStatus::from(row.status),
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
}))
|
||||
fn create_test_customer() -> Customer {
|
||||
Customer {
|
||||
id: Uuid::new_v4(),
|
||||
name: "Test Customer".to_string(),
|
||||
email: "test@example.com".to_string(),
|
||||
subscription_tier: SubscriptionTier::Free,
|
||||
status: CustomerStatus::Active,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
max_instances: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// Add your tests here
|
||||
// Example:
|
||||
/*
|
||||
#[sqlx::test]
|
||||
async fn test_create_customer() {
|
||||
let pool = setup_test_db().await;
|
||||
let repo = PostgresCustomerRepository::new(Arc::new(pool));
|
||||
|
||||
let customer = create_test_customer();
|
||||
let created = repo.create(customer.clone()).await.unwrap();
|
||||
|
||||
assert_eq!(created.id, customer.id);
|
||||
assert_eq!(created.name, customer.name);
|
||||
// ... more assertions
|
||||
}
|
||||
*/
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
[package]
|
||||
name = "gb-testing"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
gb-core = { path = "../gb-core" }
|
||||
|
@ -19,8 +19,8 @@ k8s-openapi = { version = "0.18", features = ["v1_26"] }
|
|||
kube = { version = "0.82", features = ["runtime", "derive"] }
|
||||
|
||||
# Async Runtime
|
||||
tokio.workspace = true
|
||||
async-trait.workspace = true
|
||||
tokio= { workspace = true }
|
||||
async-trait= { workspace = true }
|
||||
|
||||
# HTTP Client
|
||||
reqwest = { version = "0.11", features = ["json", "stream"] }
|
||||
|
@ -31,17 +31,17 @@ tokio-tungstenite = "0.20"
|
|||
tungstenite = "0.20"
|
||||
|
||||
# Database
|
||||
sqlx.workspace = true
|
||||
redis.workspace = true
|
||||
sqlx= { workspace = true }
|
||||
redis= { workspace = true }
|
||||
|
||||
# Metrics & Monitoring
|
||||
prometheus = { version = "0.13.0", features = ["process"] }
|
||||
tracing.workspace = true
|
||||
opentelemetry.workspace = true
|
||||
tracing= { workspace = true }
|
||||
opentelemetry= { workspace = true }
|
||||
|
||||
# Serialization
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde= { workspace = true }
|
||||
serde_json= { workspace = true }
|
||||
|
||||
# Utils
|
||||
futures = "0.3"
|
||||
|
|
|
@ -3,7 +3,7 @@ use sqlx::PgPool;
|
|||
use testcontainers::clients::Cli;
|
||||
|
||||
pub struct IntegrationTest {
|
||||
pub docker: Cli,
|
||||
docker: Cli,
|
||||
pub db_pool: PgPool,
|
||||
}
|
||||
|
||||
|
@ -14,16 +14,15 @@ pub trait IntegrationTestCase {
|
|||
async fn teardown(&mut self) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
//pub struct TestEnvironment {
|
||||
//pub postgres: testcontainers::Container<'static, testcontainers::images::postgres::Postgres>,
|
||||
//pub redis: testcontainers::Container<'static, testcontainers::images::redis::Redis>,
|
||||
// pub kafka: testcontainers::Container<'static, testcontainers::images::kafka::Kafka>,
|
||||
//
|
||||
pub struct TestEnvironment {
|
||||
pub postgres: testcontainers::Container<'static, testcontainers::images::postgres::Postgres>,
|
||||
pub redis: testcontainers::Container<'static, testcontainers::images::redis::Redis>,
|
||||
pub kafka: testcontainers::Container<'static, testcontainers::images::kafka::Kafka>,
|
||||
}
|
||||
|
||||
impl TestEnvironment {
|
||||
pub async fn new() -> anyhow::Result<Self> {
|
||||
impl IntegrationTest {
|
||||
pub fn new() -> Self {
|
||||
let docker = Cli::default();
|
||||
|
||||
// Start PostgreSQL
|
||||
let postgres = docker.run(testcontainers::images::postgres::Postgres::default());
|
||||
|
||||
|
@ -33,10 +32,9 @@ impl TestEnvironment {
|
|||
// Start Kafka
|
||||
let kafka = docker.run(testcontainers::images::kafka::Kafka::default());
|
||||
|
||||
Ok(Self {
|
||||
postgres,
|
||||
redis,
|
||||
kafka,
|
||||
})
|
||||
Self {
|
||||
docker,
|
||||
db_pool: todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +1,35 @@
|
|||
use goose::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use goose::goose::TransactionError;
|
||||
|
||||
use goose::prelude::*;
|
||||
|
||||
fn get_default_name() -> &'static str {
|
||||
"default"
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LoadTestConfig {
|
||||
pub users: usize,
|
||||
pub duration: std::time::Duration,
|
||||
pub ramp_up: std::time::Duration,
|
||||
pub scenarios: Vec<String>,
|
||||
pub ramp_up: usize,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
pub struct LoadTest {
|
||||
pub config: LoadTestConfig,
|
||||
pub metrics: crate::metrics::TestMetrics,
|
||||
config: LoadTestConfig,
|
||||
}
|
||||
|
||||
impl LoadTest {
|
||||
pub fn new(config: LoadTestConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
metrics: crate::metrics::TestMetrics::new(),
|
||||
}
|
||||
Self { config }
|
||||
}
|
||||
|
||||
pub async fn run(&self) -> anyhow::Result<crate::reports::TestReport> {
|
||||
pub fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut goose = GooseAttack::initialize()?;
|
||||
|
||||
goose
|
||||
.set_default_host("http://localhost:8080")?
|
||||
.set_default(GooseDefault::Host, &format!("http://localhost:{}", self.config.port).as_str())?
|
||||
.set_users(self.config.users)?
|
||||
.set_startup_time(self.config.ramp_up)?
|
||||
.set_run_time(self.config.duration)?;
|
||||
.set_startup_time(self.config.ramp_up)?;
|
||||
|
||||
for scenario in &self.config.scenarios {
|
||||
match scenario.as_str() {
|
||||
"auth" => goose.register_scenario(auth_scenario()),
|
||||
"api" => goose.register_scenario(api_scenario()),
|
||||
"webrtc" => goose.register_scenario(webrtc_scenario()),
|
||||
_ => continue,
|
||||
}?;
|
||||
}
|
||||
|
||||
let metrics = goose.execute().await?;
|
||||
Ok(crate::reports::TestReport::from(metrics))
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,7 +48,8 @@ async fn login(user: &mut GooseUser) -> TransactionResult {
|
|||
let _response = user
|
||||
.post_json("/auth/login", &payload)
|
||||
.await?
|
||||
.response?;
|
||||
.response
|
||||
.map_err(|e| Box::new(TransactionError::RequestError(e.to_string())))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -69,7 +58,8 @@ async fn logout(user: &mut GooseUser) -> TransactionResult {
|
|||
let _response = user
|
||||
.post("/auth/logout")
|
||||
.await?
|
||||
.response?;
|
||||
.response
|
||||
.map_err(|e| Box::new(TransactionError::RequestError(e.to_string())))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -92,7 +82,8 @@ async fn create_instance(user: &mut GooseUser) -> TransactionResult {
|
|||
let _response = user
|
||||
.post_json("/api/instances", &payload)
|
||||
.await?
|
||||
.response?;
|
||||
.response
|
||||
.map_err(|e| Box::new(TransactionError::RequestFailed(e.to_string())))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -101,7 +92,8 @@ async fn list_instances(user: &mut GooseUser) -> TransactionResult {
|
|||
let _response = user
|
||||
.get("/api/instances")
|
||||
.await?
|
||||
.response?;
|
||||
.response
|
||||
.map_err(|e| Box::new(TransactionError::RequestError(e.to_string())))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -121,7 +113,8 @@ async fn join_room(user: &mut GooseUser) -> TransactionResult {
|
|||
let _response = user
|
||||
.post_json("/webrtc/rooms/join", &payload)
|
||||
.await?
|
||||
.response?;
|
||||
.response
|
||||
.map_err(|e| Box::new(TransactionError::RequestError(e.to_string())))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -135,7 +128,14 @@ async fn send_message(user: &mut GooseUser) -> TransactionResult {
|
|||
let _response = user
|
||||
.post_json("/webrtc/messages", &payload)
|
||||
.await?
|
||||
.response?;
|
||||
.response
|
||||
.map_err(|e| Box::new(TransactionError::RequestError(e.to_string())))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for TransactionError {
|
||||
fn from(error: reqwest::Error) -> Self {
|
||||
TransactionError::RequestError(error.to_string())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
[package]
|
||||
name = "gb-utils"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
gb-core = { path = "../gb-core" }
|
||||
gb-document = { path = "../gb-document" }
|
||||
gb-image = { path = "../gb-image" }
|
||||
async-trait.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
async-trait= { workspace = true }
|
||||
tokio= { workspace = true }
|
||||
serde= { workspace = true }
|
||||
serde_json= { workspace = true }
|
||||
thiserror= { workspace = true }
|
||||
tracing= { workspace = true }
|
||||
mime = "0.3"
|
||||
mime_guess = "2.0"
|
||||
uuid = { version = "1.6", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
rstest= { workspace = true }
|
||||
tokio-test = "0.4"
|
||||
|
|
0
postgres.rs
Normal file
0
postgres.rs
Normal file
Loading…
Add table
Reference in a new issue