use std::future::Future;
use crate::answers::SystemOneResult;
use crate::client::ClientBuilder;
use crate::error::{Error, Result};
use crate::request::SystemOneRequest;
use crate::types::ModelCard;
#[derive(Debug, Clone)]
pub struct Client {
inner: crate::Client,
runtime: std::sync::Arc<OwnedRuntime>,
}
#[derive(Debug)]
struct OwnedRuntime(Option<tokio::runtime::Runtime>);
impl OwnedRuntime {
fn get(&self) -> &tokio::runtime::Runtime {
self.0.as_ref().expect("the runtime lives until drop")
}
}
impl Drop for OwnedRuntime {
fn drop(&mut self) {
let Some(runtime) = self.0.take() else {
return;
};
if tokio::runtime::Handle::try_current().is_ok() {
std::thread::spawn(move || drop(runtime));
}
}
}
impl Client {
pub fn new() -> Result<Self> {
Self::from_async(crate::Client::new()?)
}
pub fn builder() -> BlockingClientBuilder {
BlockingClientBuilder {
inner: crate::Client::builder(),
}
}
pub fn from_async(inner: crate::Client) -> Result<Self> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|err| Error::Config(format!("Could not create a runtime: {err}")))?;
Ok(Self {
inner,
runtime: std::sync::Arc::new(OwnedRuntime(Some(runtime))),
})
}
pub fn as_async(&self) -> &crate::Client {
&self.inner
}
pub fn system_one(&self, request: SystemOneRequest) -> Result<SystemOneResult> {
self.run(self.inner.system_one(request))
}
pub fn models(&self) -> Result<Vec<ModelCard>> {
self.run(self.inner.models().list())
}
fn run<T: Send + 'static>(
&self,
call: impl std::future::IntoFuture<Output = Result<T>, IntoFuture: Future<Output = Result<T>>>,
) -> Result<T> {
if tokio::runtime::Handle::try_current().is_ok() {
return Err(Error::Config(
"kunobi_decision::blocking::Client was called from inside an async runtime, which \
would block the thread driving it. Use kunobi_decision::Client there, or call this \
from a thread with no runtime (for example tokio::task::spawn_blocking)."
.into(),
));
}
self.runtime.get().block_on(call.into_future())
}
}
#[derive(Debug)]
pub struct BlockingClientBuilder {
inner: ClientBuilder,
}
impl BlockingClientBuilder {
#[must_use]
pub fn configure(mut self, configure: impl FnOnce(ClientBuilder) -> ClientBuilder) -> Self {
self.inner = configure(self.inner);
self
}
pub fn build(self) -> Result<Client> {
Client::from_async(self.inner.build()?)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn calling_from_an_async_runtime_is_refused() {
let client = Client::builder()
.configure(|builder| builder.api_key("k").base_url("http://localhost:1234"))
.build()
.unwrap();
let mut questions = crate::Questions::new();
questions.add("q", crate::noul("q?"));
let err = client
.system_one(SystemOneRequest::new("state", questions))
.unwrap_err();
assert!(err.to_string().contains("inside an async runtime"), "{err}");
assert!(err.to_string().contains("kunobi_decision::Client"), "{err}");
}
}