patx/relay-lang
improve body and json param parsing
Commit 3fc4a30 · patx · 2026-02-11T22:26:24-05:00
Comments
No comments yet.
Diff
diff --git a/src/main.rs b/src/main.rs
index 3b1dae6..3492b07 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2496,6 +2496,49 @@ fn install_stdlib(env: &mut Env, evaluator: Arc<Evaluator>) -> RResult<()> {
})),
);
+ let evaluator_for_get_json = evaluator.clone();
+ env.set(
+ "get_json",
+ Value::Builtin(Arc::new(move |_args, _| {
+ let evaluator = evaluator_for_get_json.clone();
+ Box::pin(async move {
+ let request = {
+ let env = evaluator.env.lock().await;
+ env.get("request").ok()
+ };
+
+ match request {
+ Some(Value::Dict(req)) => Ok(req.get("json").cloned().unwrap_or(Value::None)),
+ _ => Ok(Value::None),
+ }
+ })
+ })),
+ );
+
+ let evaluator_for_get_body = evaluator.clone();
+ env.set(
+ "get_body",
+ Value::Builtin(Arc::new(move |_args, _| {
+ let evaluator = evaluator_for_get_body.clone();
+ Box::pin(async move {
+ let request = {
+ let env = evaluator.env.lock().await;
+ env.get("request").ok()
+ };
+
+ match request {
+ Some(Value::Dict(req)) => {
+ Ok(req
+ .get("form")
+ .cloned()
+ .unwrap_or(Value::Dict(IndexMap::new())))
+ }
+ _ => Ok(Value::Dict(IndexMap::new())),
+ }
+ })
+ })),
+ );
+
// sleep(ms, value=None) -> Deferred
// Non-blocking scheduler primitive:
// - returns immediately with a Deferred
@@ -2883,6 +2926,31 @@ fn value_to_json(v: &Value) -> J {
}
}
+fn json_to_value(j: &J) -> Value {
+ match j {
+ J::Null => Value::None,
+ J::Bool(b) => Value::Bool(*b),
+ J::Number(n) => {
+ if let Some(i) = n.as_i64() {
+ Value::Int(i)
+ } else if let Some(f) = n.as_f64() {
+ Value::Float(f)
+ } else {
+ Value::Str(n.to_string())
+ }
+ }
+ J::String(s) => Value::Str(s.clone()),
+ J::Array(items) => Value::List(items.iter().map(json_to_value).collect()),
+ J::Object(obj) => {
+ let mut out = IndexMap::new();
+ for (k, v) in obj {
+ out.insert(k.clone(), json_to_value(v));
+ }
+ Value::Dict(out)
+ }
+ }
+}
+
fn value_to_document(v: Value, sig: &str) -> RResult<Document> {
let json = value_to_json(&v);
match json {
@@ -4136,6 +4204,13 @@ impl Evaluator {
bound.insert(k, Value::Str(v));
}
if let Some(j) = json {
+ // JSON object keys are treated like body params for handler arg binding.
+ if let J::Object(obj) = &j {
+ for (k, v) in obj {
+ bound.insert(k.clone(), json_to_value(v));
+ }
+ }
+
// if handler has a Json param name, bind it (first param typed Json)
let mut json_param_name = None;
for p in &func.params {
@@ -4689,6 +4764,141 @@ mod tests {
}
}
+ #[tokio::test]
+ async fn web_handler_binds_json_object_keys_to_args() {
+ let src = r#"fn submit(name, age: int = 0)
+ return name + ":" + str(age)
+"#;
+ 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()));
+ {
+ let mut env_lock = env.lock().await;
+ install_stdlib(&mut env_lock, evaluator.clone()).expect("stdlib install should work");
+ }
+ evaluator
+ .eval_program(&program)
+ .await
+ .expect("program should evaluate");
+
+ let mut query = HashMap::new();
+ query.insert("name".to_string(), "from-query".to_string());
+
+ let out = evaluator
+ .call_web_handler(
+ WebAppHandle::new(),
+ "submit",
+ RequestParts {
+ method: "POST".to_string(),
+ path: "/".to_string(),
+ path_params: HashMap::new(),
+ query,
+ json: Some(serde_json::json!({
+ "name": "from-json",
+ "age": 33
+ })),
+ form: HashMap::new(),
+ headers: HashMap::new(),
+ cookies: HashMap::new(),
+ session_id: None,
+ websocket: None,
+ },
+ )
+ .await
+ .expect("handler should run");
+
+ match out {
+ Value::Response(resp) => {
+ let text = String::from_utf8(resp.body).expect("response body should be utf-8");
+ assert_eq!(text, "from-json:33");
+ }
+ other => panic!("expected response, got {}", other.repr()),
+ }
+ }
+
+ #[tokio::test]
+ async fn web_handler_helpers_work_without_handler_args() {
+ let src = r#"fn from_json()
+ payload = get_json()
+ return str(payload["name"])
+
+fn from_form()
+ payload = get_body()
+ return payload["name"]
+"#;
+ 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()));
+ {
+ let mut env_lock = env.lock().await;
+ install_stdlib(&mut env_lock, evaluator.clone()).expect("stdlib install should work");
+ }
+ evaluator
+ .eval_program(&program)
+ .await
+ .expect("program should evaluate");
+
+ let out_json = evaluator
+ .call_web_handler(
+ WebAppHandle::new(),
+ "from_json",
+ RequestParts {
+ method: "POST".to_string(),
+ path: "/".to_string(),
+ path_params: HashMap::new(),
+ query: HashMap::new(),
+ json: Some(serde_json::json!({
+ "name": "from-json-helper"
+ })),
+ form: HashMap::new(),
+ headers: HashMap::new(),
+ cookies: HashMap::new(),
+ session_id: None,
+ websocket: None,
+ },
+ )
+ .await
+ .expect("json helper handler should run");
+
+ match out_json {
+ Value::Response(resp) => {
+ let text = String::from_utf8(resp.body).expect("response body should be utf-8");
+ assert_eq!(text, "from-json-helper");
+ }
+ other => panic!("expected response, got {}", other.repr()),
+ }
+
+ let mut form = HashMap::new();
+ form.insert("name".to_string(), "from-form-helper".to_string());
+ let out_form = evaluator
+ .call_web_handler(
+ WebAppHandle::new(),
+ "from_form",
+ RequestParts {
+ method: "POST".to_string(),
+ path: "/".to_string(),
+ path_params: HashMap::new(),
+ query: HashMap::new(),
+ json: None,
+ form,
+ headers: HashMap::new(),
+ cookies: HashMap::new(),
+ session_id: None,
+ websocket: None,
+ },
+ )
+ .await
+ .expect("form helper handler should run");
+
+ match out_form {
+ Value::Response(resp) => {
+ let text = String::from_utf8(resp.body).expect("response body should be utf-8");
+ assert_eq!(text, "from-form-helper");
+ }
+ other => panic!("expected response, got {}", other.repr()),
+ }
+ }
+
#[tokio::test]
async fn supports_index_assignment_and_missing_dict_keys() {
let src = r#"numbers = [1, 2, 3]
diff --git a/test2.ry b/test2.ry
new file mode 100644
index 0000000..2f3859e
--- /dev/null
+++ b/test2.ry
@@ -0,0 +1,36 @@
+app = WebApp()
+server = WebServer()
+
[email protected]("/")
+fn index()
+ return "<h1>Relay handler binding test</h1><p>POST /with-args or /with-helpers</p>"
+
+// Args are bound from query/body/json keys (path params still take highest precedence).
[email protected]("/with-args")
+fn with_args(name, age: int = 0, city = "unknown")
+ return app.json({
+ "mode": "args",
+ "name": name,
+ "age": age,
+ "city": city
+ })
+
+// Zero-arg handlers can read request payloads explicitly.
[email protected]("/with-helpers")
+fn with_helpers()
+ json_data = get_json()
+ if (json_data != None)
+ return app.json({
+ "mode": "helpers-json",
+ "name": str(json_data["name"]),
+ "payload": json_data
+ })
+
+ form_data = get_body()
+ return app.json({
+ "mode": "helpers-form",
+ "name": form_data["name"],
+ "payload": form_data
+ })
+
+server.run(app)