Skip to main content

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}