use std::{future::Future, panic::AssertUnwindSafe, sync::Arc};
use futures::FutureExt;
use uuid::Uuid;
use super::{
CreateSceneRequest, CreateSceneResponse, PlayConfig, PlayHttpClient, PlayResult, Query,
QueryRequest, Scene, SceneTemplate,
};
pub struct PlayBuilder {
config: Option<PlayConfig>,
}
impl PlayBuilder {
fn new() -> Self {
Self { config: None }
}
pub fn config(mut self, config: PlayConfig) -> Self {
self.config = Some(config);
self
}
pub async fn run<F, Fut, T>(self, f: F) -> T
where
F: FnOnce(Play) -> Fut,
Fut: Future<Output = T>,
{
let config = self.config.unwrap_or_else(PlayConfig::from_env);
let play = Play::new_internal(config);
let result = AssertUnwindSafe(f(play.clone())).catch_unwind().await;
if let Err(e) = play.clean().await {
tracing::warn!("Play cleanup failed: {:?}", e);
}
match result {
Ok(value) => value,
Err(panic) => std::panic::resume_unwind(panic),
}
}
}
#[derive(Clone)]
pub struct Play {
client: Arc<PlayHttpClient>,
}
impl Play {
pub fn builder() -> PlayBuilder {
PlayBuilder::new()
}
fn new_internal(config: PlayConfig) -> Self {
let play_id = Uuid::new_v4().to_string();
let client = Arc::new(PlayHttpClient::new(play_id, config));
Play { client }
}
pub async fn scene<T>(&self, arguments: &T::Arguments) -> PlayResult<Scene<T>>
where
T: SceneTemplate,
{
let request = CreateSceneRequest {
template: T::template_name(),
arguments,
};
let response: CreateSceneResponse<T::Result> =
self.client.post_seeder("/seed/", &request).await?;
Ok(Scene::new(response.result, response.mangle_map))
}
pub async fn query<Q>(&self, arguments: &Q::Args) -> PlayResult<Q>
where
Q: Query,
{
let request = QueryRequest {
template: Q::template_name(),
arguments,
};
let result: Q::Result = self.client.post_seeder("/seed/query", &request).await?;
Ok(Q::from_result(result))
}
pub async fn clean(&self) -> PlayResult<()> {
self.client
.delete_seeder(&format!("/seed/{}", self.client.play_id()))
.await
}
pub fn play_id(&self) -> &str {
self.client.play_id()
}
pub fn config(&self) -> &PlayConfig {
self.client.config()
}
}
impl Default for PlayBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use serde::{Deserialize, Serialize};
use wiremock::{
Mock, MockServer, ResponseTemplate,
matchers::{method, path},
};
use super::*;
async fn play_with_mock_server() -> (Play, MockServer) {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
let config = PlayConfig::new(
"https://api.example.com",
"https://identity.example.com",
server.uri(),
);
(Play::new_internal(config), server)
}
#[tokio::test]
async fn test_play_instances() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
let config = PlayConfig::new(
"https://api.example.com",
"https://identity.example.com",
server.uri(),
);
Play::builder()
.config(config.clone())
.run(|play1| async move {
assert!(Uuid::parse_str(play1.play_id()).is_ok());
assert_eq!(play1.config().seeder_url, server.uri());
})
.await;
}
#[tokio::test]
async fn test_unique_play_ids() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
let config = PlayConfig::new(
"https://api.example.com",
"https://identity.example.com",
server.uri(),
);
let play1 = Play::new_internal(config.clone());
let play2 = Play::new_internal(config);
assert!(Uuid::parse_str(play1.play_id()).is_ok());
assert!(Uuid::parse_str(play2.play_id()).is_ok());
assert_ne!(play1.play_id(), play2.play_id());
}
struct MockScene;
#[derive(Clone, Serialize)]
struct MockSceneArgs {
name: String,
}
#[derive(Deserialize)]
struct MockSceneResult {
data: String,
}
impl SceneTemplate for MockScene {
type Arguments = MockSceneArgs;
type Result = MockSceneResult;
fn template_name() -> &'static str {
"MockScene"
}
}
#[derive(Debug, Clone)]
struct MockQuery {
args: MockQueryArgs,
value: i32,
}
#[derive(Debug, Clone, Serialize)]
struct MockQueryArgs {
id: String,
}
impl Query for MockQuery {
type Args = MockQueryArgs;
type Result = MockQueryResult;
fn template_name() -> &'static str {
"MockQuery"
}
fn args(&self) -> &Self::Args {
&self.args
}
fn from_result(result: Self::Result) -> Self {
Self {
args: MockQueryArgs { id: String::new() },
value: result.value,
}
}
}
#[derive(Deserialize)]
struct MockQueryResult {
value: i32,
}
#[tokio::test]
async fn test_scene_and_query() {
let (play, server) = play_with_mock_server().await;
Mock::given(method("POST"))
.and(path("/seed/"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"result": { "data": "test-data" },
"mangleMap": { "email@example.com": "mangled@example.com" }
})))
.mount(&server)
.await;
let scene = play
.scene::<MockScene>(&MockSceneArgs {
name: "test".into(),
})
.await
.expect("scene creation should succeed");
assert_eq!(scene.result().data, "test-data");
assert_eq!(
scene.get_mangled("email@example.com"),
"mangled@example.com"
);
Mock::given(method("POST"))
.and(path("/seed/query"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({ "value": 42 })),
)
.mount(&server)
.await;
let result = play
.query::<MockQuery>(&MockQueryArgs { id: "test".into() })
.await
.expect("query should succeed");
assert_eq!(result.value, 42);
}
#[tokio::test]
async fn test_server_error_handling() {
let (play, server) = play_with_mock_server().await;
Mock::given(method("POST"))
.and(path("/seed/"))
.respond_with(ResponseTemplate::new(500))
.mount(&server)
.await;
let result = play
.scene::<MockScene>(&MockSceneArgs {
name: "test".into(),
})
.await;
assert!(matches!(
result,
Err(super::super::PlayError::Response { status: 500, .. })
));
}
#[tokio::test]
async fn test_builder_runs_cleanup() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
let config = PlayConfig::new(
"https://api.example.com",
"https://identity.example.com",
server.uri(),
);
Play::builder()
.config(config)
.run(|_play| async move {
})
.await;
}
#[tokio::test]
async fn test_clean() {
let server = MockServer::start().await;
let config = PlayConfig::new(
"https://api.example.com",
"https://identity.example.com",
server.uri(),
);
let play = Play::new_internal(config);
Mock::given(method("DELETE"))
.and(path(format!("/seed/{}", play.play_id())))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
assert!(play.clean().await.is_ok());
}
}