use std::sync::Arc;
use std::time::Duration;
use foundation_connectrpc::buffa::bytes::{Buf, BufMut};
use foundation_connectrpc::buffa::encoding::{decode_varint, encode_varint, skip_field, Tag, WireType};
use foundation_connectrpc::buffa::{DecodeContext, DecodeError, DefaultInstance, Message, SizeCache};
use foundation_arrow::arrow_array::{Array, Int32Array, RecordBatch};
use foundation_arrow::arrow_schema::{DataType, Field, Schema};
use foundation_arrow::{ArrowSchema, FromArrow, IpcResult, ToArrow};
use foundation_core::synca::OnSignal;
use foundation_core::valtron::valtron;
use foundation_http::native::server::{HttpServer, ServerConfig};
use foundation_http::shared::app::HttpApp;
use foundation_http::shared::serve::Serve;
use foundation_netio::http::NativeHttpClient;
use foundation_connectrpc::shared::transport::Transport;
use foundation_connectrpc::{
ArrowCodec, Client, ClientOptions, ConnectResult, ConnectRpcServe, Ctx, H1Transport,
HandlerOptions, JsonCodec, ProcedureCodecs, ProtoCodec, Request, Response, Router,
};
const P_PROTO_JSON: &str = "/codecs.EchoService/ProtoJson";
const P_JSON: &str = "/codecs.EchoService/Json";
const P_ARROW: &str = "/codecs.EchoService/Arrow";
const P_ARROW_JSON: &str = "/codecs.EchoService/ArrowJson";
const P_ALL: &str = "/codecs.EchoService/All";
const ALL_PROCEDURES: [&str; 5] = [P_PROTO_JSON, P_JSON, P_ARROW, P_ARROW_JSON, P_ALL];
#[derive(Clone, Default, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
struct Sample {
value: i32,
}
impl DefaultInstance for Sample {
fn default_instance() -> &'static Self {
static I: std::sync::OnceLock<Sample> = std::sync::OnceLock::new();
I.get_or_init(Sample::default)
}
}
impl Message for Sample {
fn compute_size(&self, _c: &mut SizeCache) -> u32 {
0
}
fn write_to(&self, _c: &mut SizeCache, buf: &mut impl BufMut) {
Tag::new(1, WireType::Varint).encode(buf);
encode_varint(self.value as u64, buf);
}
fn merge_field(&mut self, tag: Tag, buf: &mut impl Buf, _c: DecodeContext<'_>) -> Result<(), DecodeError> {
if tag.field_number() == 1 {
self.value = decode_varint(buf)? as i32;
Ok(())
} else {
skip_field(tag, buf)
}
}
fn clear(&mut self) {
*self = Self::default();
}
}
impl ArrowSchema for Sample {
fn schema() -> Schema {
Schema::new(Self::fields())
}
fn fields() -> Vec<Field> {
vec![Field::new("value", DataType::Int32, false)]
}
}
impl ToArrow for Sample {
fn to_arrow(&self) -> IpcResult<RecordBatch> {
RecordBatch::try_new(Self::schema_ref(), vec![Arc::new(Int32Array::from(vec![self.value]))])
}
fn to_arrow_batch(values: &[Self]) -> IpcResult<RecordBatch> {
let col = Int32Array::from(values.iter().map(|s| s.value).collect::<Vec<_>>());
RecordBatch::try_new(Self::schema_ref(), vec![Arc::new(col)])
}
}
impl FromArrow for Sample {
fn from_arrow(batch: &RecordBatch) -> IpcResult<Self> {
let col = batch.column(0).as_any().downcast_ref::<Int32Array>().expect("int32 column");
Ok(Self { value: col.value(0) })
}
fn from_arrow_batch(batch: &RecordBatch) -> IpcResult<Vec<Self>> {
let col = batch.column(0).as_any().downcast_ref::<Int32Array>().expect("int32 column");
Ok((0..col.len()).map(|i| Self { value: col.value(i) }).collect())
}
}
async fn echo(_ctx: Ctx, req: Request<Sample>) -> ConnectResult<Response<Sample>> {
Ok(Response::new(req.msg))
}
fn echo_serve() -> Arc<dyn Serve> {
let mut router = Router::new();
let opts = || HandlerOptions::new();
router.unary(P_PROTO_JSON, ProcedureCodecs::<Sample, Sample>::defaults(), echo, opts());
router.unary(P_JSON, ProcedureCodecs::<Sample, Sample>::of((JsonCodec,)), echo, opts());
router.unary(P_ARROW, ProcedureCodecs::<Sample, Sample>::only(ArrowCodec), echo, opts());
router.unary(P_ARROW_JSON, ProcedureCodecs::<Sample, Sample>::of((JsonCodec, ArrowCodec)), echo, opts());
router.unary(P_ALL, ProcedureCodecs::<Sample, Sample>::of((ProtoCodec, JsonCodec, ArrowCodec)), echo, opts());
Arc::new(ConnectRpcServe::new(router.into_handler()))
}
fn start_server() -> (std::net::SocketAddr, Arc<OnSignal>) {
let mut app = HttpApp::new_serve();
let serve = echo_serve();
for proc in ALL_PROCEDURES {
app.router.add_route_any(proc, &serve);
}
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind loopback");
let addr = listener.local_addr().expect("local addr");
let server =
HttpServer::with_config(app, &format!("127.0.0.1:{}", addr.port()), ServerConfig::defaults());
let shutdown = Arc::new(OnSignal::new());
let shutdown_thread = shutdown.clone();
std::thread::spawn(move || server.serve_with_listener(&listener, &shutdown_thread));
while std::net::TcpStream::connect_timeout(&addr, Duration::from_millis(200)).is_err() {}
(addr, shutdown)
}
async fn round_trip(
transport: Arc<dyn Transport>,
url: String,
codecs: ProcedureCodecs<Sample, Sample>,
wire: &str,
value: i32,
) {
let client = Client::new(transport, &url, codecs, ClientOptions::new().with_codec(wire))
.expect("build client");
let ctx = Ctx::background().with_deadline(Duration::from_secs(10));
let out = client
.unary(ctx, Request::new(Sample { value }))
.await
.expect("unary call");
assert_eq!(out.msg.value, value, "echo mismatch on {wire}");
println!(" {wire:<5} → {url} (value={})", out.msg.value);
}
#[valtron(seed = 1, threads = 4)]
async fn main() {
let (addr, shutdown) = start_server();
let base = format!("http://127.0.0.1:{}", addr.port());
let t = || -> Arc<dyn Transport> { Arc::new(H1Transport::new(Arc::new(NativeHttpClient::from_system()))) };
println!("codec combinations, each verified over a real socket:");
round_trip(t(), format!("{base}{P_PROTO_JSON}"), ProcedureCodecs::defaults(), "proto", 1).await;
round_trip(t(), format!("{base}{P_JSON}"), ProcedureCodecs::of((JsonCodec,)), "json", 2).await;
round_trip(t(), format!("{base}{P_ARROW}"), ProcedureCodecs::only(ArrowCodec), "arrow", 3).await;
round_trip(t(), format!("{base}{P_ARROW_JSON}"), ProcedureCodecs::of((JsonCodec, ArrowCodec)), "arrow", 4).await;
round_trip(t(), format!("{base}{P_ALL}"), ProcedureCodecs::of((ProtoCodec, JsonCodec, ArrowCodec)), "proto", 5).await;
round_trip(t(), format!("{base}{P_ALL}"), ProcedureCodecs::of((ProtoCodec, JsonCodec, ArrowCodec)), "json", 6).await;
round_trip(t(), format!("{base}{P_ALL}"), ProcedureCodecs::of((ProtoCodec, JsonCodec, ArrowCodec)), "arrow", 7).await;
println!("all codec combinations verified ✓");
shutdown.turn_on();
}