Skip to main content

runx_core/
kernel_eval.rs

1use std::fmt;
2
3use runx_contracts::JsonValue;
4
5mod dispatch;
6mod input;
7mod limits;
8
9use dispatch::evaluate_kernel_input;
10use input::{KernelDocument, is_supported_kernel_kind, kernel_document_kind};
11use limits::{validate_kernel_document_shape, validate_kernel_source_size};
12
13#[derive(Clone, Debug, PartialEq, serde::Serialize)]
14#[serde(tag = "kind", rename_all = "snake_case")]
15pub enum KernelEvalOutput {
16    Output { value: JsonValue },
17}
18
19#[derive(Clone, Debug, Eq, PartialEq)]
20pub enum KernelEvalError {
21    InvalidDocument(String),
22    InvalidInput(String),
23    SerializeOutput(String),
24}
25
26impl KernelEvalError {
27    #[must_use]
28    pub fn code(&self) -> &'static str {
29        match self {
30            Self::InvalidDocument(_) => "invalid_document",
31            Self::InvalidInput(_) => "invalid_input",
32            Self::SerializeOutput(_) => "serialize_output",
33        }
34    }
35}
36
37impl fmt::Display for KernelEvalError {
38    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            Self::InvalidDocument(message)
41            | Self::InvalidInput(message)
42            | Self::SerializeOutput(message) => formatter.write_str(message),
43        }
44    }
45}
46
47impl std::error::Error for KernelEvalError {}
48
49pub fn evaluate_kernel_document_str(source: &str) -> Result<KernelEvalOutput, KernelEvalError> {
50    validate_kernel_source_size(source)?;
51    let document = serde_json::from_str::<JsonValue>(source)
52        .map_err(|error| KernelEvalError::InvalidDocument(error.to_string()))?;
53    validate_kernel_document_shape(&document)?;
54    if let Some(kind) = kernel_document_kind(&document)
55        && !is_supported_kernel_kind(kind)
56    {
57        return Err(KernelEvalError::InvalidInput(format!(
58            "unsupported kernel input kind '{kind}'"
59        )));
60    }
61    let input = serde_json::from_str::<KernelDocument>(source)
62        .map_err(|error| KernelEvalError::InvalidInput(error.to_string()))?;
63    Ok(KernelEvalOutput::Output {
64        value: evaluate_kernel_input(input)?,
65    })
66}
67
68#[cfg(test)]
69mod tests {
70    use super::limits::{MAX_KERNEL_EVAL_DOCUMENT_BYTES, MAX_KERNEL_EVAL_JSON_DEPTH};
71    use super::*;
72
73    #[test]
74    fn evaluates_supported_document_under_limits() -> Result<(), KernelEvalError> {
75        let output = evaluate_kernel_document_str(
76            r#"{"kind":"state-machine.fanoutSyncDecisionKey","decision":{"groupId":"group-a","ruleFired":"all_succeeded"}}"#,
77        )?;
78
79        assert_eq!(
80            output,
81            KernelEvalOutput::Output {
82                value: JsonValue::String("group-a:all_succeeded".to_owned())
83            }
84        );
85        Ok(())
86    }
87
88    #[test]
89    fn rejects_oversized_kernel_eval_source_before_parse() {
90        let source = " ".repeat(MAX_KERNEL_EVAL_DOCUMENT_BYTES + 1);
91        let error = assert_invalid_input(&source);
92
93        assert_eq!(error.code(), "invalid_input");
94        assert!(error.to_string().contains("kernel eval input exceeds"));
95    }
96
97    #[test]
98    fn rejects_deep_kernel_eval_json_before_dispatch() {
99        let source = format!(
100            "{}0{}",
101            "[".repeat(MAX_KERNEL_EVAL_JSON_DEPTH),
102            "]".repeat(MAX_KERNEL_EVAL_JSON_DEPTH),
103        );
104        let error = assert_invalid_input(&source);
105
106        assert_eq!(error.code(), "invalid_input");
107        assert!(error.to_string().contains("exceeds JSON depth"));
108    }
109
110    fn assert_invalid_input(source: &str) -> KernelEvalError {
111        match evaluate_kernel_document_str(source) {
112            Err(error @ KernelEvalError::InvalidInput(_)) => error,
113            Err(error) => error,
114            Ok(output) => {
115                KernelEvalError::InvalidInput(format!("expected invalid_input, got {output:?}"))
116            }
117        }
118    }
119}