Skip to content

Commit 65e2aed

Browse files
hyperpolymathclaude
andcommitted
feat(v0.2.0): add live BoJ-server connection module
Create http_client.rs shared async HTTP client (get_json, post_json, check_health) and boj_live.rs with 5 Tauri commands connecting to the real BoJ-server at localhost:7700: - boj_live_health, boj_live_cartridges, boj_live_invoke, boj_live_topology, boj_live_check - BojLiveCmd.res ReScript bindings for all 5 commands This is the first live backend connection — panels with bojRouting=true can now talk to the real BoJ-server instead of returning mock JSON. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2a453ec commit 65e2aed

4 files changed

Lines changed: 405 additions & 1 deletion

File tree

‎src-tauri/src/boj_live.rs‎

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
4+
//! Live BoJ-server connection module (async).
5+
//!
6+
//! Connects to the BoJ-server gateway at localhost:7700 for real
7+
//! cartridge invocation, health checks, and topology queries.
8+
//!
9+
//! This module parallels `boj::commands` but uses async reqwest via the
10+
//! shared [`http_client`] module instead of blocking calls. Panels that
11+
//! need non-blocking BoJ access (v0.2.0+) should prefer these commands.
12+
13+
use crate::http_client::{self, ServiceEndpoint};
14+
use serde::{Deserialize, Serialize};
15+
use serde_json::json;
16+
17+
// panic-attack:allow insecure-protocol — localhost dev endpoint
18+
/// Default BoJ-server base URL.
19+
const DEFAULT_BOJ_URL: &str = "http://localhost:7700";
20+
21+
/// Build the BoJ endpoint, respecting the `BOJ_URL` environment variable
22+
/// if set, otherwise falling back to [`DEFAULT_BOJ_URL`].
23+
fn boj_endpoint() -> ServiceEndpoint {
24+
let url = std::env::var("BOJ_URL").unwrap_or_else(|_| DEFAULT_BOJ_URL.to_string());
25+
ServiceEndpoint::new(&url)
26+
}
27+
28+
/// BoJ health check response shape.
29+
///
30+
/// Mirrors the JSON envelope returned by `GET /health` on the BoJ server.
31+
/// Optional fields are absent when the server is freshly started or running
32+
/// in minimal mode.
33+
#[derive(Debug, Deserialize, Serialize)]
34+
pub struct BojHealth {
35+
/// Top-level status string (e.g. "ok", "degraded").
36+
pub status: String,
37+
/// Semantic version of the running BoJ server.
38+
pub version: Option<String>,
39+
/// Number of loaded cartridges.
40+
pub cartridges: Option<usize>,
41+
/// Human-readable uptime (e.g. "2h 14m").
42+
pub uptime: Option<String>,
43+
}
44+
45+
/// Check BoJ-server health (async).
46+
///
47+
/// `GET /health` — returns the full health envelope as a JSON string.
48+
#[tauri::command]
49+
pub async fn boj_live_health() -> Result<String, String> {
50+
let endpoint = boj_endpoint();
51+
let health: BojHealth = http_client::get_json(&endpoint, "/health")
52+
.await
53+
.map_err(|e| format!("BoJ server unreachable: {}", e))?;
54+
serde_json::to_string(&health).map_err(|e| format!("Serialization failed: {}", e))
55+
}
56+
57+
/// List available cartridges from BoJ-server (async).
58+
///
59+
/// `GET /cartridges` — returns the cartridge array as a JSON string.
60+
#[tauri::command]
61+
pub async fn boj_live_cartridges() -> Result<String, String> {
62+
let endpoint = boj_endpoint();
63+
let cartridges: serde_json::Value = http_client::get_json(&endpoint, "/cartridges")
64+
.await
65+
.map_err(|e| format!("Failed to list cartridges: {}", e))?;
66+
serde_json::to_string(&cartridges).map_err(|e| format!("Serialization failed: {}", e))
67+
}
68+
69+
/// Invoke a BoJ cartridge tool (async).
70+
///
71+
/// `POST /cartridges/{cartridge}/invoke` — sends `{"tool": ..., "args": ...}`
72+
/// and returns the invocation result as a JSON string.
73+
#[tauri::command]
74+
pub async fn boj_live_invoke(
75+
cartridge: String,
76+
tool: String,
77+
params: String,
78+
) -> Result<String, String> {
79+
let endpoint = boj_endpoint();
80+
let parsed_params: serde_json::Value =
81+
serde_json::from_str(&params).unwrap_or(json!({}));
82+
let body = json!({
83+
"tool": tool,
84+
"args": parsed_params,
85+
});
86+
let result: serde_json::Value = http_client::post_json(
87+
&endpoint,
88+
&format!("/cartridges/{}/invoke", cartridge),
89+
&body,
90+
)
91+
.await
92+
.map_err(|e| format!("Cartridge invoke failed: {}", e))?;
93+
serde_json::to_string(&result).map_err(|e| format!("Serialization failed: {}", e))
94+
}
95+
96+
/// Get BoJ topology / cartridge dependency graph (async).
97+
///
98+
/// `GET /topology` — returns the topology matrix as a JSON string.
99+
#[tauri::command]
100+
pub async fn boj_live_topology() -> Result<String, String> {
101+
let endpoint = boj_endpoint();
102+
let topology: serde_json::Value = http_client::get_json(&endpoint, "/topology")
103+
.await
104+
.map_err(|e| format!("Failed to get topology: {}", e))?;
105+
serde_json::to_string(&topology).map_err(|e| format!("Serialization failed: {}", e))
106+
}
107+
108+
/// Check if BoJ-server is reachable (async health probe).
109+
///
110+
/// Returns `{"reachable": bool, "endpoint": "..."}` as a JSON string.
111+
/// Panels use this for connection-dot indicators in the panel bar.
112+
#[tauri::command]
113+
pub async fn boj_live_check() -> Result<String, String> {
114+
let endpoint = boj_endpoint();
115+
let reachable = http_client::check_health(&endpoint, "/health").await?;
116+
let url = std::env::var("BOJ_URL").unwrap_or_else(|_| DEFAULT_BOJ_URL.to_string());
117+
Ok(json!({
118+
"reachable": reachable,
119+
"endpoint": url,
120+
})
121+
.to_string())
122+
}
123+
124+
#[cfg(test)]
125+
mod tests {
126+
use super::*;
127+
128+
/// Verify the endpoint builder respects BOJ_URL and strips trailing slashes.
129+
#[test]
130+
fn boj_endpoint_default_and_override() {
131+
std::env::remove_var("BOJ_URL");
132+
let ep = boj_endpoint();
133+
assert_eq!(ep.base_url, DEFAULT_BOJ_URL);
134+
135+
std::env::set_var("BOJ_URL", "http://boj.local:9999/v2/");
136+
let ep2 = boj_endpoint();
137+
assert_eq!(ep2.base_url, "http://boj.local:9999/v2");
138+
std::env::remove_var("BOJ_URL");
139+
}
140+
}

‎src-tauri/src/http_client.rs‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
4+
//! Shared HTTP client for backend service connections.
5+
//!
6+
//! Provides a common reqwest-based async HTTP client that all panel backends
7+
//! use to talk to external services (BoJ-server, VeriSimDB, ECHIDNA, etc.).
8+
//! Centralises timeout, error handling, and connection pooling.
9+
10+
use serde::de::DeserializeOwned;
11+
use std::time::Duration;
12+
13+
/// Default timeout for HTTP requests (10 seconds).
14+
const DEFAULT_TIMEOUT_SECS: u64 = 10;
15+
16+
/// Service endpoint configuration.
17+
///
18+
/// Wraps a base URL and request timeout for a single backend service.
19+
/// Use [`ServiceEndpoint::new`] for defaults or chain [`ServiceEndpoint::with_timeout`]
20+
/// to customise the deadline per-service.
21+
pub struct ServiceEndpoint {
22+
/// Base URL of the service, without trailing slash.
23+
pub base_url: String,
24+
/// Per-request timeout applied to every call through this endpoint.
25+
pub timeout: Duration,
26+
}
27+
28+
impl ServiceEndpoint {
29+
/// Create a new endpoint with the default 10-second timeout.
30+
///
31+
/// Trailing slashes are stripped from `base_url` to prevent double-slash
32+
/// issues when paths are appended.
33+
pub fn new(base_url: &str) -> Self {
34+
Self {
35+
base_url: base_url.trim_end_matches('/').to_string(),
36+
timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
37+
}
38+
}
39+
40+
/// Override the default timeout (builder pattern).
41+
pub fn with_timeout(mut self, secs: u64) -> Self {
42+
self.timeout = Duration::from_secs(secs);
43+
self
44+
}
45+
}
46+
47+
/// Make a GET request to a service endpoint and deserialise the JSON response.
48+
///
49+
/// # Errors
50+
///
51+
/// Returns a human-readable `String` error on:
52+
/// - Client construction failure
53+
/// - Network / timeout errors
54+
/// - Non-2xx HTTP status codes
55+
/// - JSON deserialisation mismatches
56+
pub async fn get_json<T: DeserializeOwned>(
57+
endpoint: &ServiceEndpoint,
58+
path: &str,
59+
) -> Result<T, String> {
60+
let url = format!("{}{}", endpoint.base_url, path);
61+
let client = reqwest::Client::builder()
62+
.timeout(endpoint.timeout)
63+
.build()
64+
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
65+
66+
let response = client
67+
.get(&url)
68+
.send()
69+
.await
70+
.map_err(|e| format!("Request to {} failed: {}", url, e))?;
71+
72+
if !response.status().is_success() {
73+
return Err(format!("HTTP {} from {}", response.status(), url));
74+
}
75+
76+
response
77+
.json::<T>()
78+
.await
79+
.map_err(|e| format!("Failed to parse response from {}: {}", url, e))
80+
}
81+
82+
/// Make a POST request with a JSON body and deserialise the JSON response.
83+
///
84+
/// # Errors
85+
///
86+
/// Same failure modes as [`get_json`], plus serialisation of `body` and
87+
/// inclusion of the upstream response body in non-2xx error messages.
88+
pub async fn post_json<B: serde::Serialize, T: DeserializeOwned>(
89+
endpoint: &ServiceEndpoint,
90+
path: &str,
91+
body: &B,
92+
) -> Result<T, String> {
93+
let url = format!("{}{}", endpoint.base_url, path);
94+
let client = reqwest::Client::builder()
95+
.timeout(endpoint.timeout)
96+
.build()
97+
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
98+
99+
let response = client
100+
.post(&url)
101+
.json(body)
102+
.send()
103+
.await
104+
.map_err(|e| format!("Request to {} failed: {}", url, e))?;
105+
106+
if !response.status().is_success() {
107+
let status = response.status();
108+
let body_text = response.text().await.unwrap_or_default();
109+
return Err(format!("HTTP {} from {}: {}", status, url, body_text));
110+
}
111+
112+
response
113+
.json::<T>()
114+
.await
115+
.map_err(|e| format!("Failed to parse response from {}: {}", url, e))
116+
}
117+
118+
/// Check if a service is reachable (simple health probe).
119+
///
120+
/// Uses a short 5-second timeout regardless of the endpoint's configured
121+
/// timeout — health checks should be fast or not at all.
122+
///
123+
/// Returns `Ok(true)` if the endpoint responds with 2xx, `Ok(false)` if
124+
/// the request fails or returns a non-success status.
125+
pub async fn check_health(
126+
endpoint: &ServiceEndpoint,
127+
health_path: &str,
128+
) -> Result<bool, String> {
129+
let url = format!("{}{}", endpoint.base_url, health_path);
130+
let client = reqwest::Client::builder()
131+
.timeout(Duration::from_secs(5))
132+
.build()
133+
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
134+
135+
match client.get(&url).send().await {
136+
Ok(resp) => Ok(resp.status().is_success()),
137+
Err(_) => Ok(false),
138+
}
139+
}

‎src-tauri/src/main.rs‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,19 @@ mod security;
9292
/// Routes to ECHIDNA overlay FFI via V-lang adapter at OVERLAY_URL.
9393
mod overlay;
9494

95-
/// BoJ — Barrel of Jelly cartridge runtime bridge.
95+
/// BoJ — Barrel of Jelly cartridge runtime bridge (blocking).
9696
/// Routes to BoJ server at BOJ_URL (default http://localhost:7700/api/v1).
9797
mod boj;
9898

99+
/// BoJ Live — async BoJ-server connection via shared HTTP client (v0.2.0+).
100+
/// Async alternative to `boj::commands` using the shared `http_client` module.
101+
mod boj_live;
102+
103+
/// Shared async HTTP client for backend service connections.
104+
/// Centralises timeout, error handling, and connection pooling for all
105+
/// panel backends (BoJ, VeriSimDB, ECHIDNA, Fleet, etc.).
106+
mod http_client;
107+
99108
/// TypeLL — Type-Level Language server bridge.
100109
/// Routes to TypeLL server at TYPELL_URL (default http://localhost:7800/api/v1).
101110
mod typell;
@@ -2156,6 +2165,12 @@ fn main() {
21562165
boj::commands::boj_topology,
21572166
boj::commands::boj_invoke,
21582167
boj::commands::boj_umoja_status,
2168+
// BoJ Live — async BoJ-server connection (v0.2.0)
2169+
boj_live::boj_live_health,
2170+
boj_live::boj_live_cartridges,
2171+
boj_live::boj_live_invoke,
2172+
boj_live::boj_live_topology,
2173+
boj_live::boj_live_check,
21592174
// TypeLL — Type-level language server
21602175
typell::commands::typell_health,
21612176
typell::commands::typell_check,

0 commit comments

Comments
 (0)