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}