use napi::{Result, bindgen_prelude::*, Env};
use briefcase_core::{ReplayEngine, ReplayMode, ReplayStatus, ReplayPolicy, ReplayResult, ReplayStats};
use crate::storage::{JsSqliteBackend, JsLakeFSBackend};
#[napi]
pub struct JsReplayEngine {
inner: ReplayEngine,
}
#[napi]
impl JsReplayEngine {
#[napi(constructor)]
pub fn new_with_sqlite(storage: &JsSqliteBackend) -> Self {
Self {
inner: ReplayEngine::new(storage.inner.clone()),
}
}
#[napi(factory)]
pub fn new_with_lakefs(storage: &JsLakeFSBackend) -> Self {
Self {
inner: ReplayEngine::new(storage.inner.clone()),
}
}
#[napi]
pub fn set_default_mode(&mut self, mode: String) -> Result<()> {
let replay_mode = match mode.as_str() {
"strict" => ReplayMode::Strict,
"tolerant" => ReplayMode::Tolerant,
"validation_only" => ReplayMode::ValidationOnly,
_ => return Err(napi::Error::from_reason(
format!("Invalid replay mode: {}. Use 'strict', 'tolerant', or 'validation_only'", mode)
)),
};
self.inner.set_default_mode(replay_mode);
Ok(())
}
#[napi]
pub fn replay(&self, env: Env, snapshot_id: String, mode: Option<String>) -> Result<napi::JsObject> {
let replay_mode = if let Some(mode_str) = mode {
Some(match mode_str.as_str() {
"strict" => ReplayMode::Strict,
"tolerant" => ReplayMode::Tolerant,
"validation_only" => ReplayMode::ValidationOnly,
_ => return Err(napi::Error::from_reason(
format!("Invalid replay mode: {}", mode_str)
)),
})
} else {
None
};
let engine = self.inner.clone();
let task = ReplayTask {
engine,
snapshot_id,
replay_mode,
};
env.spawn(task).map(|async_work| async_work.promise_object())
}
#[napi]
pub fn replay_with_policy(&self, env: Env, snapshot_id: String, policy: &JsReplayPolicy, mode: Option<String>) -> Result<napi::JsObject> {
let replay_mode = if let Some(mode_str) = mode {
Some(match mode_str.as_str() {
"strict" => ReplayMode::Strict,
"tolerant" => ReplayMode::Tolerant,
"validation_only" => ReplayMode::ValidationOnly,
_ => return Err(napi::Error::from_reason(
format!("Invalid replay mode: {}", mode_str)
)),
})
} else {
None
};
let engine = self.inner.clone();
let policy_inner = policy.inner.clone();
let task = ReplayWithPolicyTask {
engine,
snapshot_id,
policy: policy_inner,
replay_mode,
};
env.spawn(task).map(|async_work| async_work.promise_object())
}
}
#[napi]
pub struct JsReplayPolicy {
pub inner: ReplayPolicy,
}
#[napi]
impl JsReplayPolicy {
#[napi(constructor)]
pub fn new(name: String) -> Self {
Self {
inner: ReplayPolicy::new(name),
}
}
#[napi]
pub fn with_exact_match(&mut self, field: String) -> &Self {
self.inner = self.inner.clone().with_exact_match(field);
self
}
#[napi]
pub fn with_similarity_threshold(&mut self, field: String, threshold: f64) -> &Self {
self.inner = self.inner.clone().with_similarity_threshold(field, threshold);
self
}
#[napi(getter)]
pub fn name(&self) -> String {
self.inner.name.clone()
}
#[napi(getter)]
pub fn rule_count(&self) -> u32 {
self.inner.rules.len() as u32
}
}
#[napi]
pub struct JsReplayResult {
inner: ReplayResult,
}
#[napi]
impl JsReplayResult {
#[napi(getter)]
pub fn status(&self) -> String {
match self.inner.status {
ReplayStatus::Success => "success".to_string(),
ReplayStatus::Failed => "failed".to_string(),
ReplayStatus::Partial => "partial".to_string(),
ReplayStatus::Pending => "pending".to_string(),
ReplayStatus::Running => "running".to_string(),
}
}
#[napi(getter)]
pub fn outputs_match(&self) -> bool {
self.inner.outputs_match
}
#[napi(getter)]
pub fn execution_time_ms(&self) -> f64 {
self.inner.execution_time_ms
}
#[napi(getter)]
pub fn replay_output(&self) -> Option<serde_json::Value> {
self.inner.replay_output.clone()
}
#[napi]
pub fn to_object(&self) -> Result<serde_json::Value> {
Ok(serde_json::json!({
"status": self.status(),
"outputs_match": self.inner.outputs_match,
"execution_time_ms": self.inner.execution_time_ms,
"replay_output": self.inner.replay_output,
"policy_violations": self.inner.policy_violations,
}))
}
}
struct ReplayTask {
engine: ReplayEngine,
snapshot_id: String,
replay_mode: Option<ReplayMode>,
}
#[napi]
impl napi::Task for ReplayTask {
type Output = ReplayResult;
type JsValue = JsReplayResult;
fn compute(&mut self) -> Result<Self::Output> {
let rt = tokio::runtime::Runtime::new()
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
rt.block_on(async {
self.engine.replay(&self.snapshot_id, self.replay_mode.clone(), None)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
})
}
fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
Ok(JsReplayResult { inner: output })
}
}
struct ReplayWithPolicyTask {
engine: ReplayEngine,
snapshot_id: String,
policy: ReplayPolicy,
replay_mode: Option<ReplayMode>,
}
#[napi]
impl napi::Task for ReplayWithPolicyTask {
type Output = ReplayResult;
type JsValue = JsReplayResult;
fn compute(&mut self) -> Result<Self::Output> {
let rt = tokio::runtime::Runtime::new()
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
rt.block_on(async {
self.engine.replay_with_policy(&self.snapshot_id, &self.policy, self.replay_mode.clone())
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
})
}
fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
Ok(JsReplayResult { inner: output })
}
}