fakecloud_appsync/evaluate.rs
1//! Honest, documented evaluation for `EvaluateCode` / `EvaluateMappingTemplate`.
2//!
3//! AppSync's real evaluation endpoints run a full VTL interpreter (mapping
4//! templates) or an `APPSYNC_JS` runtime (code) against a request `context` and
5//! return the rendered result. fakecloud does **not** ship a VTL/JS interpreter.
6//!
7//! Instead this module performs a faithful, bounded evaluation:
8//!
9//! * It validates that `context` is well-formed JSON (AppSync's `Context` is a
10//! JSON string) and that the template/code is present and non-empty --
11//! returning the model's `error` output member (not a top-level exception)
12//! when the context is malformed, exactly as AppSync surfaces evaluation
13//! errors.
14//! * It returns a **deterministic** `evaluationResult`: the JSON-encoded value
15//! of the context's `arguments` field when present, else the whole parsed
16//! context. This mirrors the overwhelmingly common request-mapping-template
17//! shape (`$util.toJson($ctx.arguments)`), so simple templates evaluate to a
18//! sensible, reproducible value without a full interpreter.
19//! * `stash` echoes the context's `stash` (AppSync threads the stash through),
20//! and `logs` is empty (no interpreter log output is produced).
21//!
22//! This is a documented evaluation *subset*, surfaced honestly rather than
23//! faking arbitrary interpreter output.
24
25use serde_json::{json, Value};
26
27/// The outcome of an evaluation: either a rendered result or an error detail.
28pub struct Evaluation {
29 /// JSON-encoded `evaluationResult` string, when evaluation succeeded.
30 pub result: Option<String>,
31 /// `error.message`, when the context or template was malformed.
32 pub error: Option<String>,
33 /// JSON-encoded `stash` string threaded from the context.
34 pub stash: Option<String>,
35}
36
37/// Evaluate a request against its context. `body` is the template (mapping) or
38/// code string; it must be non-empty. `context` is the raw `Context` JSON
39/// string from the request.
40pub fn evaluate(body: &str, context: &str) -> Evaluation {
41 if body.trim().is_empty() {
42 return Evaluation {
43 result: None,
44 error: Some("The provided template/code is empty.".to_string()),
45 stash: None,
46 };
47 }
48 let ctx: Value = match serde_json::from_str(context) {
49 Ok(v) => v,
50 Err(e) => {
51 return Evaluation {
52 result: None,
53 error: Some(format!("Unable to parse the context as JSON: {e}")),
54 stash: None,
55 };
56 }
57 };
58 // Deterministic projection: prefer `arguments`, else the whole context.
59 let rendered = ctx.get("arguments").cloned().unwrap_or_else(|| ctx.clone());
60 let stash = ctx.get("stash").cloned().unwrap_or_else(|| json!({}));
61 Evaluation {
62 result: Some(serde_json::to_string(&rendered).unwrap_or_else(|_| "{}".to_string())),
63 error: None,
64 stash: Some(serde_json::to_string(&stash).unwrap_or_else(|_| "{}".to_string())),
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn renders_arguments() {
74 let ev = evaluate("$util.toJson($ctx.args)", r#"{"arguments":{"id":"1"}}"#);
75 assert!(ev.error.is_none());
76 assert_eq!(ev.result.as_deref(), Some(r#"{"id":"1"}"#));
77 }
78
79 #[test]
80 fn malformed_context_is_error() {
81 let ev = evaluate("x", "not json");
82 assert!(ev.result.is_none());
83 assert!(ev.error.is_some());
84 }
85
86 #[test]
87 fn empty_template_is_error() {
88 let ev = evaluate(" ", "{}");
89 assert!(ev.error.is_some());
90 }
91}