feat(poc/bridge): add flutter_rust_bridge hello spike
Proof-of-concept proving the Flutter/Rust bridge exit criterion from docs/architecture/proof-of-concept-plan.md §2: "Flutter can call Rust and receive event stream data." The spike exposes one synchronous fallible command (greet) returning a typed GreetResult / GreetError DTO, and one async event stream (counter_stream) emitting typed CounterTick events. The Flutter app demonstrates both flows on a Material 3 surface; the headless test suite in test/poc_verification_test.dart exercises the same API directly through dart:ffi. Verified on 2026-05-13 (Linux desktop, Flutter 3.41.9 / Dart 3.11.5, flutter_rust_bridge 2.12.0, Rust 1.95). All three tests pass: - greet() returns typed result for valid input - greet() surfaces typed error for empty input - counterStream() delivers the expected event sequence Authority: PoC plan §2, DEC-014 (typed Flutter/Rust bridge), SAD-068, SDD-079, SysDes-049. Naming note: the PoC plan lists this as flutter-rust-bridge-hello, but Dart pubspec.yaml package names require underscores; the directory uses underscores accordingly. Not product code; not promoted into chanora_bridge. Layout note: includes the full Flutter platform scaffold (android, ios, macos, windows, web, linux). Only the Linux desktop target has been built and verified.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
pub mod simple;
|
||||
@@ -0,0 +1,97 @@
|
||||
//! Chanora PoC API — `flutter_rust_bridge_hello`.
|
||||
//!
|
||||
//! Two flows are proven here:
|
||||
//! 1. **Command:** `greet` — typed input → typed `Result<T, E>` DTO,
|
||||
//! including typed error mapping at the boundary.
|
||||
//! 2. **Event stream:** `counterStream` — Rust pushes typed events to
|
||||
//! Dart via a `StreamSink`, observed in Dart as a `Stream<CounterTick>`.
|
||||
//!
|
||||
//! Together they satisfy the PoC exit criterion from
|
||||
//! `docs/architecture/proof-of-concept-plan.md` §2:
|
||||
//! "Flutter can call Rust and receive event stream data."
|
||||
//!
|
||||
//! This is PoC code only. The Chanora DTO catalogue belongs to the
|
||||
//! `chanora_bridge` crate; this PoC does not stand in for it.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use flutter_rust_bridge::frb;
|
||||
|
||||
use crate::frb_generated::StreamSink;
|
||||
|
||||
// -------- Command DTOs --------
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GreetResult {
|
||||
pub message: String,
|
||||
pub elapsed_micros: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GreetErrorKind {
|
||||
EmptyName,
|
||||
}
|
||||
|
||||
/// Typed error DTO surfaced as a Dart exception by FRB.
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
#[error("greet error: {kind:?}: {detail}")]
|
||||
pub struct GreetError {
|
||||
pub kind: GreetErrorKind,
|
||||
pub detail: String,
|
||||
}
|
||||
|
||||
// -------- Lifecycle --------
|
||||
|
||||
#[frb(init)]
|
||||
pub fn init_app() {
|
||||
flutter_rust_bridge::setup_default_user_utils();
|
||||
}
|
||||
|
||||
// -------- Command --------
|
||||
|
||||
/// Synchronous fallible command. Proves Dart → Rust call with a typed
|
||||
/// `Result` DTO crossing the boundary.
|
||||
#[frb(sync)]
|
||||
pub fn greet(name: String) -> Result<GreetResult, GreetError> {
|
||||
let start = std::time::Instant::now();
|
||||
let trimmed = name.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(GreetError {
|
||||
kind: GreetErrorKind::EmptyName,
|
||||
detail: "name must not be empty".into(),
|
||||
});
|
||||
}
|
||||
Ok(GreetResult {
|
||||
message: format!("Hello, {trimmed}! — greetings from Rust."),
|
||||
elapsed_micros: start.elapsed().as_micros() as u64,
|
||||
})
|
||||
}
|
||||
|
||||
// -------- Event stream --------
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CounterTick {
|
||||
pub seq: u64,
|
||||
pub note: String,
|
||||
}
|
||||
|
||||
/// Rust → Dart event stream.
|
||||
/// `StreamSink` is provided in scope by `flutter_rust_bridge` glue;
|
||||
/// the codegen wires it to a Dart `Stream<CounterTick>`.
|
||||
pub async fn counter_stream(
|
||||
sink: StreamSink<CounterTick>,
|
||||
count: u32,
|
||||
interval_ms: u32,
|
||||
) {
|
||||
let interval = Duration::from_millis(interval_ms.max(1) as u64);
|
||||
for i in 0..count {
|
||||
let tick = CounterTick {
|
||||
seq: i as u64,
|
||||
note: format!("tick {i}/{count}"),
|
||||
};
|
||||
if sink.add(tick).is_err() {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(interval).await;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user