use std::fmt;
use runx_contracts::JsonValue;
mod dispatch;
mod input;
mod limits;
use dispatch::evaluate_kernel_input;
use input::{KernelDocument, is_supported_kernel_kind, kernel_document_kind};
use limits::{validate_kernel_document_shape, validate_kernel_source_size};
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum KernelEvalOutput {
Output { value: JsonValue },
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum KernelEvalError {
InvalidDocument(String),
InvalidInput(String),
SerializeOutput(String),
}
impl KernelEvalError {
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Self::InvalidDocument(_) => "invalid_document",
Self::InvalidInput(_) => "invalid_input",
Self::SerializeOutput(_) => "serialize_output",
}
}
}
impl fmt::Display for KernelEvalError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidDocument(message)
| Self::InvalidInput(message)
| Self::SerializeOutput(message) => formatter.write_str(message),
}
}
}
impl std::error::Error for KernelEvalError {}
pub fn evaluate_kernel_document_str(source: &str) -> Result<KernelEvalOutput, KernelEvalError> {
validate_kernel_source_size(source)?;
let document = serde_json::from_str::<JsonValue>(source)
.map_err(|error| KernelEvalError::InvalidDocument(error.to_string()))?;
validate_kernel_document_shape(&document)?;
if let Some(kind) = kernel_document_kind(&document)
&& !is_supported_kernel_kind(kind)
{
return Err(KernelEvalError::InvalidInput(format!(
"unsupported kernel input kind '{kind}'"
)));
}
let input = serde_json::from_str::<KernelDocument>(source)
.map_err(|error| KernelEvalError::InvalidInput(error.to_string()))?;
Ok(KernelEvalOutput::Output {
value: evaluate_kernel_input(input)?,
})
}
#[cfg(test)]
mod tests {
use super::limits::{MAX_KERNEL_EVAL_DOCUMENT_BYTES, MAX_KERNEL_EVAL_JSON_DEPTH};
use super::*;
#[test]
fn evaluates_supported_document_under_limits() -> Result<(), KernelEvalError> {
let output = evaluate_kernel_document_str(
r#"{"kind":"state-machine.fanoutSyncDecisionKey","decision":{"groupId":"group-a","ruleFired":"all_succeeded"}}"#,
)?;
assert_eq!(
output,
KernelEvalOutput::Output {
value: JsonValue::String("group-a:all_succeeded".to_owned())
}
);
Ok(())
}
#[test]
fn rejects_oversized_kernel_eval_source_before_parse() {
let source = " ".repeat(MAX_KERNEL_EVAL_DOCUMENT_BYTES + 1);
let error = assert_invalid_input(&source);
assert_eq!(error.code(), "invalid_input");
assert!(error.to_string().contains("kernel eval input exceeds"));
}
#[test]
fn rejects_deep_kernel_eval_json_before_dispatch() {
let source = format!(
"{}0{}",
"[".repeat(MAX_KERNEL_EVAL_JSON_DEPTH),
"]".repeat(MAX_KERNEL_EVAL_JSON_DEPTH),
);
let error = assert_invalid_input(&source);
assert_eq!(error.code(), "invalid_input");
assert!(error.to_string().contains("exceeds JSON depth"));
}
fn assert_invalid_input(source: &str) -> KernelEvalError {
match evaluate_kernel_document_str(source) {
Err(error @ KernelEvalError::InvalidInput(_)) => error,
Err(error) => error,
Ok(output) => {
KernelEvalError::InvalidInput(format!("expected invalid_input, got {output:?}"))
}
}
}
}