wasm4pm 26.6.25

High-performance process mining algorithms in WebAssembly for JavaScript/TypeScript
Documentation
//! Structured error handling for WASM exports.
//!
//! All errors returned to JavaScript follow a machine-readable JSON schema:
//! `{"code":"PARSE_ERROR","message":"...","remediation":"..."}`.
//!
//! [`Wasm4pmError`] is the canonical typed error enum. Call [`Wasm4pmError::code`]
//! to get a stable string key for JS `switch` dispatch, and [`Wasm4pmError::remediation`]
//! for optional fix guidance to show to callers.
//!
//! Error codes: `PARSE_ERROR`, `VALIDATION_ERROR`, `BINARY_FORMAT_ERROR`,
//! `ALGORITHM_ERROR`, `HANDLE_NOT_FOUND`.

use serde_json::json;
use wasm_bindgen::prelude::*;

/// Typed error enum for wasm4pm public APIs.
///
/// All `Result<T, String>` public functions should convert to this type at
/// the public boundary using `.map_err(Wasm4pmError::Parse)` etc.
/// Internal helper functions may continue to use `String` and convert at
/// the last mile.
#[derive(Debug, thiserror::Error)]
pub enum Wasm4pmError {
    /// Input could not be parsed (XES, OCEL, POWL text format, etc.).
    #[error("parse error: {0}")]
    Parse(String),
    /// Structural validation failed (partial order, Petri net soundness, etc.).
    #[error("validation error: {0}")]
    Validation(CompatRefusal),
    /// Binary `.pm4bin` format error (magic mismatch, truncated data, etc.).
    #[error("binary format error: {0}")]
    BinaryFormat(String),
    /// An algorithm failed for a named reason.
    #[error("algorithm error in '{algorithm}': {reason}")]
    Algorithm { algorithm: String, reason: String },
    /// A stored-object handle was not found or has the wrong type.
    #[error("handle not found: {0}")]
    HandleNotFound(String),
}

impl Wasm4pmError {
    /// Machine-readable error code for JS switch/dispatch.
    ///
    /// # Examples
    ///
    /// ```
    /// use wasm4pm::error::Wasm4pmError;
    ///
    /// assert_eq!(Wasm4pmError::Parse("bad XES".into()).code(), "PARSE_ERROR");
    /// assert_eq!(
    ///     Wasm4pmError::Algorithm {
    ///         algorithm: "alpha++".into(),
    ///         reason: "empty log".into(),
    ///     }.code(),
    ///     "ALGORITHM_ERROR"
    /// );
    /// ```
    pub fn code(&self) -> &'static str {
        match self {
            Self::Parse(_) => "PARSE_ERROR",
            Self::Validation(_) => "VALIDATION_ERROR",
            Self::BinaryFormat(_) => "BINARY_FORMAT_ERROR",
            Self::Algorithm { .. } => "ALGORITHM_ERROR",
            Self::HandleNotFound(_) => "HANDLE_NOT_FOUND",
        }
    }

    /// Human-readable remediation hint for JS callers, or `None` for self-explanatory errors.
    ///
    /// # Examples
    ///
    /// ```
    /// use wasm4pm::error::Wasm4pmError;
    ///
    /// // Parse errors include fix guidance
    /// assert!(Wasm4pmError::Parse("x".into()).remediation().is_some());
    ///
    /// // Algorithm errors do not (the reason field already explains it)
    /// assert!(Wasm4pmError::Algorithm {
    ///     algorithm: "heuristic".into(),
    ///     reason: "threshold too low".into(),
    /// }.remediation().is_none());
    /// ```
    pub fn remediation(&self) -> Option<&'static str> {
        match self {
            Self::HandleNotFound(_) => Some(
                "Create a fresh handle via load_eventlog_from_xes() or \
                 load_eventlog_from_binary() and pass it here.",
            ),
            Self::Parse(_) => Some(
                "Verify the input is valid syntax. \
                 For POWL: see parse_powl_model_string docs. \
                 For XES: validate against the XES schema.",
            ),
            Self::BinaryFormat(_) => Some(
                "Re-export the event log to binary format via export_eventlog_to_binary() \
                 and retry.",
            ),
            Self::Validation(_) | Self::Algorithm { .. } => None,
        }
    }
}

impl From<Wasm4pmError> for JsValue {
    /// Converts to a structured JSON object `{code, message, remediation?}`.
    /// Callers can switch on `code` for typed error handling in JS/TS.
    fn from(e: Wasm4pmError) -> JsValue {
        let mut map = serde_json::Map::new();
        map.insert("code".to_owned(), json!(e.code()));
        map.insert("message".to_owned(), json!(e.to_string()));
        if let Some(r) = e.remediation() {
            map.insert("remediation".to_owned(), json!(r));
        }
        let serialized = serde_json::to_string(&map).unwrap_or_else(|_| {
            r#"{"code":"INTERNAL_ERROR","message":"error serialization failed"}"#.to_owned()
        });
        js_val(&serialized)
    }
}

/// Native-safe JsValue from string.
/// On wasm32, this is a wrapper around JsValue::from_str.
/// On other targets, it returns a zeroed JsValue to avoid panics.
#[inline]
pub fn js_val(s: &str) -> JsValue {
    #[cfg(target_arch = "wasm32")]
    {
        JsValue::from_str(s)
    }
    #[cfg(not(target_arch = "wasm32"))]
    {
        let _ = s;
        JsValue::null()
    }
}

/// Creates a structured error object for JavaScript
/// Returns JSON string: {"code":"CODE","message":"message text"}
pub fn wasm_err(code: &str, message: impl std::fmt::Display) -> JsValue {
    let serialized = serde_json::to_string(&json!({"code": code, "message": message.to_string()}))
        .unwrap_or_else(|_| {
            r#"{"code":"INTERNAL_ERROR","message":"error serialization failed"}"#.to_owned()
        });
    js_val(&serialized)
}

/// Error codes for common failure scenarios
pub mod codes {
    pub const INVALID_HANDLE: &str = "INVALID_HANDLE";
    pub const INVALID_INPUT: &str = "INVALID_INPUT";
    pub const INVALID_XES: &str = "INVALID_XES";
    pub const INVALID_JSON: &str = "INVALID_JSON";
    pub const PARSE_ERROR: &str = "PARSE_ERROR";
    pub const NOT_IMPLEMENTED: &str = "NOT_IMPLEMENTED";
    pub const INTERNAL_ERROR: &str = "INTERNAL_ERROR";
}

/// Helper macros for common error patterns
#[macro_export]
macro_rules! invalid_handle {
    ($handle:expr) => {
        $crate::error::wasm_err(
            $crate::error::codes::INVALID_HANDLE,
            format!("Invalid handle: {}", $handle),
        )
    };
}

#[macro_export]
macro_rules! invalid_input {
    ($msg:expr) => {
        $crate::error::wasm_err($crate::error::codes::INVALID_INPUT, $msg)
    };
}

#[macro_export]
macro_rules! parse_error {
    ($msg:expr) => {
        $crate::error::wasm_err($crate::error::codes::PARSE_ERROR, $msg)
    };
}

#[macro_export]
macro_rules! internal_error {
    ($msg:expr) => {
        $crate::error::wasm_err($crate::error::codes::INTERNAL_ERROR, $msg)
    };
}

/// A wrapper around all refusals from `wasm4pm-compat` to represent named law refusals.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompatRefusal {
    /// A POWL validation refusal.
    Powl(wasm4pm_compat::powl::PowlRefusal),
    /// An EventLog validation refusal.
    EventLog(String),
    /// A DFG validation refusal.
    Dfg(String),
    /// A ProcessTree validation refusal.
    ProcessTree(wasm4pm_compat::process_tree::ProcessTreeRefusal),
    /// A Petri net validation refusal.
    Petri(String),
    /// An OCEL validation refusal.
    Ocel(String),
    /// An OCPQ validation refusal.
    Ocpq(wasm4pm_compat::ocpq::OcpqRefusal),
    /// A Conformance validation refusal.
    Conformance(String),
}

impl std::fmt::Display for CompatRefusal {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Powl(r) => write!(f, "{}", r),
            Self::EventLog(r) => write!(f, "{}", r),
            Self::Dfg(r) => write!(f, "{}", r),
            Self::ProcessTree(r) => write!(f, "{}", r),
            Self::Petri(r) => write!(f, "{}", r),
            Self::Ocel(r) => write!(f, "{}", r),
            Self::Ocpq(r) => write!(f, "{}", r),
            Self::Conformance(r) => write!(f, "{}", r),
        }
    }
}