use std::collections::VecDeque;
use std::future::{Future, ready};
use std::sync::Arc;
use std::time::Duration;
use axum::extract::State;
use axum::http::StatusCode;
use axum::routing::{get, post};
use axum::{Json, Router};
use ruststream::codec::JsonCodec;
use ruststream::memory::MemoryPublisher;
use ruststream::memory::prelude::*;
use ruststream::runtime::{HealthProbe, HealthState};
use ruststream::{FixedName, MessageHeaders, NoHeaders, OutgoingDestination};
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
struct OrderPlaced {
id: u64,
item: String,
}
impl OutgoingDestination for OrderPlaced {
type Form = FixedName;
const DESTINATION: &'static str = "orders.placed";
}
impl MessageHeaders for OrderPlaced {
type Contract = NoHeaders;
}
impl MessageInfo for OrderPlaced {
const NAME: &'static str = "OrderPlaced";
const DESCRIPTION: Option<&'static str> =
Some("The integration event the HTTP side hands to the messaging side.");
}
struct Fulfil;
impl Handle<OrderPlaced> for Fulfil {
fn handle(
&self,
order: &OrderPlaced,
_outs: &(),
_ctx: &mut Context<'_>,
) -> impl Future<Output = Result<(), HandlerOutcome>> {
tracing::info!(order.id, order.item, "fulfilling");
ready(Ok(()))
}
}
#[derive(Default)]
struct Store {
orders: Vec<OrderPlaced>,
outbox: VecDeque<OrderPlaced>,
}
async fn place_order(
State(store): State<Arc<Mutex<Store>>>,
Json(order): Json<OrderPlaced>,
) -> &'static str {
let mut store = store.lock().await;
store.orders.push(order.clone());
store.outbox.push_back(order);
"accepted\n"
}
async fn relay_outbox(store: Arc<Mutex<Store>>, egress: MemoryPublisher) {
let mut tick = tokio::time::interval(Duration::from_millis(200));
loop {
tick.tick().await;
loop {
let Some(event) = store.lock().await.outbox.front().cloned() else {
break;
};
if egress
.message(&event)
.with_codec(JsonCodec)
.publish()
.await
.is_err()
{
break;
}
store.lock().await.outbox.pop_front();
}
}
}
async fn healthz(State(health): State<HealthProbe>) -> (StatusCode, String) {
match health.state() {
HealthState::Running => (StatusCode::OK, "running".to_owned()),
state => (StatusCode::SERVICE_UNAVAILABLE, format!("{state:?}")),
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let broker = MemoryBroker::new().bindable();
let egress = broker.bind(Publish);
let app = RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(broker, |b| {
b.include(
subscriber(OrderPlaced::DESTINATION, Fulfil)
.describe(
"The same service consumes what its HTTP endpoints produce; any other \
service subscribed to the broker would see the event too.",
)
.build(),
);
});
let running = app.start().await?;
let egress = running.publisher(egress).await?;
let store = Arc::new(Mutex::new(Store::default()));
tokio::spawn(relay_outbox(store.clone(), egress));
let router = Router::new()
.route("/orders", post(place_order))
.with_state(store)
.route("/healthz", get(healthz).with_state(running.health()));
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await?;
tracing::info!("orders API on http://127.0.0.1:8080/orders");
let stopping = running.stopping();
axum::serve(listener, router)
.with_graceful_shutdown(async move {
tokio::select! {
_ = tokio::signal::ctrl_c() => {}
() = stopping => {}
}
})
.await?;
running.shutdown().await?;
Ok(())
}