patx/relay-lang

WebApp.render_template() to accept files and kwargs

Commit 8ce36cb · patx · 2026-02-12T22:17:31-05:00

Changeset
8ce36cbdde24c8f4a62be99e6a0d61a50b3ef472
Parents
b1903e14bfb57fb13d06e6fd35cc15fc97857768

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/README.md b/README.md
index 52d36d0..f016d30 100644
--- a/README.md
+++ b/README.md
@@ -1293,11 +1293,11 @@ fn index()
 
 Templates can reference values in the current scope and support MiniJinja expressions/filters.
 
-Use `app.render_template(...)` when you want explicit template rendering without relying on implicit `{{ ... }}` string evaluation:
+Use `app.render_template(path, ...kwargs)` when you want explicit template rendering from a file without relying on implicit `{{ ... }}` string evaluation:
 
 ```relay
 app = WebApp()
-html = app.render_template("<h1>Hello {{ name }}</h1>", {"name": "Ada"})
+html = app.render_template("templates/welcome.html", name="Ada", plan="Pro")
 ```
 
 #### `WebServer()`
diff --git a/examples/templates/upload_form.html b/examples/templates/upload_form.html
new file mode 100644
index 0000000..858a23f
--- /dev/null
+++ b/examples/templates/upload_form.html
@@ -0,0 +1,6 @@
+<h1>Upload</h1>
+<form method="post" enctype="multipart/form-data" action="/upload">
+  <input type="email" name="to" placeholder="[email protected]" />
+  <input type="file" name="file" />
+  <button type="submit">Send</button>
+</form>
diff --git a/examples/upload_email_attachment.ry b/examples/upload_email_attachment.ry
index c42a2fa..334978e 100644
--- a/examples/upload_email_attachment.ry
+++ b/examples/upload_email_attachment.ry
@@ -15,13 +15,7 @@ email = Email(
 
 @app.get("/")
 fn upload_page()
-    return app.render_template(
-        "<h1>Upload</h1><form method=\"post\" enctype=\"multipart/form-data\" action=\"/upload\">\
-<input type=\"email\" name=\"to\" placeholder=\"[email protected]\" />\
-<input type=\"file\" name=\"file\" />\
-<button type=\"submit\">Send</button></form>",
-        {}
-    )
+    return app.render_template("examples/templates/upload_form.html")
 
 @app.post("/upload")
 fn upload(to, file)
diff --git a/src/main.rs b/src/main.rs
index 1e7e9ae..729d7b5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2069,6 +2069,10 @@ impl Evaluator {
                     &**callee,
                     Expr::Member { name, .. } if name == "render_template" || name == "render"
                 );
+                let is_app_render_template_method = matches!(
+                    &**callee,
+                    Expr::Member { name, .. } if name == "render_template"
+                );
 
                 let c = self
                     .resolve_deferred_only(self.eval_expr(callee).await?)
@@ -2102,7 +2106,9 @@ impl Evaluator {
                 // kwargs
                 let mut kw = Vec::new();
                 for (k, x) in kwargs {
-                    let v = if is_explicit_template_method && k == "template" {
+                    let v = if is_explicit_template_method
+                        && (k == "template" || (is_app_render_template_method && k == "path"))
+                    {
                         match x {
                             Expr::Str(s) => Value::Str(s.clone()),
                             _ => self.eval_expr(x).await?,
@@ -6137,11 +6143,27 @@ impl Object {
                 })),
                 "render_template" => Value::Builtin(Arc::new(move |args, kwargs| {
                     Box::pin(async move {
-                        let sig = "app.render_template(template, data=None)";
-                        let template = kwarg_or_arg(&kwargs, &args, "template", 0)
-                            .ok_or_else(|| RelayError::Type(format!("{sig} missing template")))?;
-                        let template = expect_str_value(template, sig, "template")?;
-                        let data = parse_template_locals(kwarg_or_arg(&kwargs, &args, "data", 1), sig)?;
+                        let sig = "app.render_template(path, ...kwargs)";
+                        let path = kwarg_or_arg(&kwargs, &args, "path", 0)
+                            .or_else(|| kwarg_or_arg(&kwargs, &args, "template", 0))
+                            .ok_or_else(|| RelayError::Type(format!("{sig} missing path")))?;
+                        let path = expect_str_value(path, sig, "path")?;
+
+                        let mut data = if args.len() >= 2 {
+                            parse_template_locals(args.get(1).cloned(), sig)?
+                        } else {
+                            HashMap::new()
+                        };
+                        for (k, v) in kwargs {
+                            if k == "path" || k == "template" {
+                                continue;
+                            }
+                            data.insert(k, v);
+                        }
+
+                        let template = tokio::fs::read_to_string(path)
+                            .await
+                            .map_err(|e| RelayError::Runtime(e.to_string()))?;
                         let rendered = render_template(&template, &data)?;
                         Ok(Value::Str(rendered))
                     })
@@ -7179,7 +7201,7 @@ app.uploads(
     allowed_mime_types=["image/png", "application/pdf"]
 )
 "#;
-        let program = parse_src(src).expect("program should parse");
+        let program = parse_src(&src).expect("program should parse");
         let env = Arc::new(tokio::sync::Mutex::new(Env::new_global()));
         let evaluator = Arc::new(Evaluator::new(env.clone()));
         {
@@ -7214,7 +7236,7 @@ app.uploads(
 
 add("5", "10")
 "#;
-        let program = parse_src(src).expect("program should parse");
+        let program = parse_src(&src).expect("program should parse");
         let env = Arc::new(tokio::sync::Mutex::new(Env::new_global()));
         let evaluator = Arc::new(Evaluator::new(env.clone()));
         {
@@ -7242,7 +7264,7 @@ add("5", "10")
 
 greet("ada")
 "#;
-        let program = parse_src(src).expect("program should parse");
+        let program = parse_src(&src).expect("program should parse");
         let env = Arc::new(tokio::sync::Mutex::new(Env::new_global()));
         let evaluator = Arc::new(Evaluator::new(env.clone()));
         {
@@ -7608,10 +7630,23 @@ fn from_query()
 
     #[tokio::test]
     async fn webapp_render_template_method_renders_explicit_template() {
-        let src = r#"app = WebApp()
-rendered = app.render_template("Hello {{ name }}", {"name": "Ada"})
-"#;
-        let program = parse_src(src).expect("program should parse");
+        let file_name = format!(
+            "relay-webapp-template-{}.html",
+            std::time::SystemTime::now()
+                .duration_since(std::time::UNIX_EPOCH)
+                .expect("system time should be after epoch")
+                .as_nanos()
+        );
+        let template_path = std::env::temp_dir().join(file_name);
+        tokio::fs::write(&template_path, "<h1>Hello {{ name }}</h1>")
+            .await
+            .expect("template write should succeed");
+
+        let src = format!(
+            "app = WebApp()\nrendered = app.render_template(\"{}\", name=\"Ada\")\n",
+            template_path.to_string_lossy()
+        );
+        let program = parse_src(&src).expect("program should parse");
         let env = Arc::new(tokio::sync::Mutex::new(Env::new_global()));
         let evaluator = Arc::new(Evaluator::new(env.clone()));
         {
@@ -7626,8 +7661,10 @@ rendered = app.render_template("Hello {{ name }}", {"name": "Ada"})
         let env_lock = env.lock().await;
         assert!(matches!(
             env_lock.get("rendered").expect("rendered should exist"),
-            Value::Str(s) if s == "Hello Ada"
+            Value::Str(s) if s == "<h1>Hello Ada</h1>"
         ));
+
+        let _ = tokio::fs::remove_file(template_path).await;
     }
 
     #[tokio::test]