From 7faa423abaecdf4846f73d23d236da8427ab4d28 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 17 Jun 2024 12:24:06 -0700 Subject: [PATCH] Recursive environments --- src-tauri/src/http_request.rs | 35 +++++------ src-tauri/src/lib.rs | 25 ++++---- src-tauri/src/render.rs | 62 +++++++++++++++----- src-tauri/templates/src/renderer.rs | 32 +++++----- src-web/components/EnvironmentEditDialog.tsx | 2 +- 5 files changed, 89 insertions(+), 67 deletions(-) diff --git a/src-tauri/src/http_request.rs b/src-tauri/src/http_request.rs index 587daff4..928598bc 100644 --- a/src-tauri/src/http_request.rs +++ b/src-tauri/src/http_request.rs @@ -18,6 +18,7 @@ use tauri::{Manager, WebviewWindow}; use tokio::sync::oneshot; use tokio::sync::watch::Receiver; +use crate::render::variables_from_environment; use crate::{models, render, response_err}; pub async fn send_http_request( @@ -33,8 +34,9 @@ pub async fn send_http_request( let workspace = models::get_workspace(window, &request.workspace_id) .await .expect("Failed to get Workspace"); + let vars = variables_from_environment(&workspace, environment_ref); - let mut url_string = render::render(&request.url, &workspace, environment.as_ref()); + let mut url_string = render::render(&request.url, &vars); url_string = ensure_proto(&url_string); if !url_string.starts_with("http://") && !url_string.starts_with("https://") { @@ -144,8 +146,8 @@ pub async fn send_http_request( continue; } - let name = render::render(&h.name, &workspace, environment_ref); - let value = render::render(&h.value, &workspace, environment_ref); + let name = render::render(&h.name, &vars); + let value = render::render(&h.value, &vars); let header_name = match HeaderName::from_bytes(name.as_bytes()) { Ok(n) => n, @@ -180,8 +182,8 @@ pub async fn send_http_request( .unwrap_or(empty_value) .as_str() .unwrap_or(""); - let username = render::render(raw_username, &workspace, environment_ref); - let password = render::render(raw_password, &workspace, environment_ref); + let username = render::render(raw_username, &vars); + let password = render::render(raw_password, &vars); let auth = format!("{username}:{password}"); let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth); @@ -191,7 +193,7 @@ pub async fn send_http_request( ); } else if b == "bearer" { let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or(""); - let token = render::render(raw_token, &workspace, environment_ref); + let token = render::render(raw_token, &vars); headers.insert( "Authorization", HeaderValue::from_str(&format!("Bearer {token}")).unwrap(), @@ -205,8 +207,8 @@ pub async fn send_http_request( continue; } query_params.push(( - render::render(&p.name, &workspace, environment_ref), - render::render(&p.value, &workspace, environment_ref), + render::render(&p.name, &vars), + render::render(&p.value, &vars), )); } request_builder = request_builder.query(&query_params); @@ -222,7 +224,7 @@ pub async fn send_http_request( .unwrap_or(empty_string) .as_str() .unwrap_or(""); - let body = render::render(raw_text, &workspace, environment_ref); + let body = render::render(raw_text, &vars); request_builder = request_builder.body(body); } else if body_type == "application/x-www-form-urlencoded" && request_body.contains_key("form") @@ -249,10 +251,7 @@ pub async fn send_http_request( .unwrap_or(empty_string) .as_str() .unwrap_or_default(); - form_params.push(( - render::render(name, &workspace, environment_ref), - render::render(value, &workspace, environment_ref), - )); + form_params.push((render::render(name, &vars), render::render(value, &vars))); } } request_builder = request_builder.form(&form_params); @@ -301,13 +300,9 @@ pub async fn send_http_request( .as_str() .unwrap_or_default(); - let name = render::render(name_raw, &workspace, environment_ref); + let name = render::render(name_raw, &vars); let mut part = if file_path.is_empty() { - multipart::Part::text(render::render( - value_raw, - &workspace, - environment_ref, - )) + multipart::Part::text(render::render(value_raw, &vars)) } else { match fs::read(file_path) { Ok(f) => multipart::Part::bytes(f), @@ -324,7 +319,7 @@ pub async fn send_http_request( .unwrap_or_default(); if !ct_raw.is_empty() { - let content_type = render::render(ct_raw, &workspace, environment_ref); + let content_type = render::render(ct_raw, &vars); part = part .mime_str(content_type.as_str()) .map_err(|e| e.to_string())?; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 68999c37..455154df 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -57,7 +57,7 @@ use crate::plugin::{ find_plugins, get_plugin, ImportResult, PluginCapability, run_plugin_export_curl, run_plugin_filter, run_plugin_import, }; -use crate::render::render_request; +use crate::render::{render_request, variables_from_environment}; use crate::updates::{UpdateMode, YaakUpdater}; use crate::window_menu::app_menu; @@ -176,6 +176,7 @@ async fn cmd_grpc_go( .await .map_err(|e| e.to_string())?; let mut metadata = HashMap::new(); + let vars = variables_from_environment(&workspace, environment.as_ref()); // Add rest of metadata for h in req.clone().metadata.0 { @@ -187,15 +188,14 @@ async fn cmd_grpc_go( continue; } - let name = render::render(&h.name, &workspace, environment.as_ref()); - let value = render::render(&h.value, &workspace, environment.as_ref()); + let name = render::render(&h.name, &vars); + let value = render::render(&h.value, &vars); metadata.insert(name, value); } if let Some(b) = &req.authentication_type { let req = req.clone(); - let environment_ref = environment.as_ref(); let empty_value = &serde_json::to_value("").unwrap(); let a = req.authentication.0; @@ -210,15 +210,15 @@ async fn cmd_grpc_go( .unwrap_or(empty_value) .as_str() .unwrap_or(""); - let username = render::render(raw_username, &workspace, environment_ref); - let password = render::render(raw_password, &workspace, environment_ref); + let username = render::render(raw_username, &vars); + let password = render::render(raw_password, &vars); let auth = format!("{username}:{password}"); let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth); metadata.insert("Authorization".to_string(), format!("Basic {}", encoded)); } else if b == "bearer" { let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or(""); - let token = render::render(raw_token, &workspace, environment_ref); + let token = render::render(raw_token, &vars); metadata.insert("Authorization".to_string(), format!("Bearer {token}")); } } @@ -290,11 +290,10 @@ async fn cmd_grpc_go( let cb = { let cancelled_rx = cancelled_rx.clone(); - let environment = environment.clone(); - let workspace = workspace.clone(); let w = w.clone(); let base_msg = base_msg.clone(); let method_desc = method_desc.clone(); + let vars = vars.clone(); move |ev: tauri::Event| { if *cancelled_rx.borrow() { @@ -317,9 +316,8 @@ async fn cmd_grpc_go( Ok(IncomingMsg::Message(raw_msg)) => { let w = w.clone(); let base_msg = base_msg.clone(); - let environment_ref = environment.as_ref(); let method_desc = method_desc.clone(); - let msg = render::render(raw_msg.as_str(), &workspace, environment_ref); + let msg = render::render(raw_msg.as_str(), &vars); let d_msg: DynamicMessage = match deserialize_message(msg.as_str(), method_desc) { Ok(d_msg) => d_msg, @@ -371,14 +369,13 @@ async fn cmd_grpc_go( let w = w.clone(); let base_event = base_msg.clone(); let req = req.clone(); - let workspace = workspace.clone(); - let environment = environment.clone(); + let vars = vars.clone(); let raw_msg = if req.message.is_empty() { "{}".to_string() } else { req.message }; - let msg = render::render(&raw_msg, &workspace, environment.as_ref()); + let msg = render::render(&raw_msg, &vars); upsert_grpc_event( &w, diff --git a/src-tauri/src/render.rs b/src-tauri/src/render.rs index 4f9f54bf..8d96285e 100644 --- a/src-tauri/src/render.rs +++ b/src-tauri/src/render.rs @@ -9,16 +9,18 @@ use templates::parse_and_render; pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) -> HttpRequest { let r = r.clone(); + let vars = &variables_from_environment(w, e); + HttpRequest { - url: render(r.url.as_str(), w, e), + url: render(r.url.as_str(), vars), url_parameters: Json( r.url_parameters .0 .iter() .map(|p| HttpUrlParameter { enabled: p.enabled, - name: render(p.name.as_str(), w, e), - value: render(p.value.as_str(), w, e), + name: render(p.name.as_str(), vars), + value: render(p.value.as_str(), vars), }) .collect::>(), ), @@ -28,8 +30,8 @@ pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) - .iter() .map(|p| HttpRequestHeader { enabled: p.enabled, - name: render(p.name.as_str(), w, e), - value: render(p.value.as_str(), w, e), + name: render(p.name.as_str(), vars), + value: render(p.value.as_str(), vars), }) .collect::>(), ), @@ -39,11 +41,11 @@ pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) - .iter() .map(|(k, v)| { let v = if v.is_string() { - render(v.as_str().unwrap(), w, e) + render(v.as_str().unwrap(), vars) } else { v.to_string() }; - (render(k, w, e), JsonValue::from(v)) + (render(k, vars), JsonValue::from(v)) }) .collect::>(), ), @@ -53,11 +55,11 @@ pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) - .iter() .map(|(k, v)| { let v = if v.is_string() { - render(v.as_str().unwrap(), w, e) + render(v.as_str().unwrap(), vars) } else { v.to_string() }; - (render(k, w, e), JsonValue::from(v)) + (render(k, vars), JsonValue::from(v)) }) .collect::>(), ), @@ -65,7 +67,31 @@ pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) - } } -pub fn render(template: &str, workspace: &Workspace, environment: Option<&Environment>) -> String { +pub fn recursively_render_variables<'s>( + m: &HashMap, + render_count: usize, +) -> HashMap { + let mut did_render = false; + let mut new_map = m.clone(); + for (k, v) in m.clone() { + let rendered = render(v.as_str(), m); + if rendered != v { + did_render = true + } + new_map.insert(k, rendered); + } + + if did_render && render_count <= 3 { + new_map = recursively_render_variables(&new_map, render_count + 1); + } + + new_map +} + +pub fn variables_from_environment( + workspace: &Workspace, + environment: Option<&Environment>, +) -> HashMap { let mut variables = HashMap::new(); variables = add_variable_to_map(variables, &workspace.variables.0); @@ -73,13 +99,17 @@ pub fn render(template: &str, workspace: &Workspace, environment: Option<&Enviro variables = add_variable_to_map(variables, &e.variables.0); } - parse_and_render(template, variables, None) + recursively_render_variables(&variables, 0) } -fn add_variable_to_map<'a>( - m: HashMap<&'a str, &'a str>, - variables: &'a Vec, -) -> HashMap<&'a str, &'a str> { +pub fn render(template: &str, vars: &HashMap) -> String { + parse_and_render(template, vars, None) +} + +fn add_variable_to_map( + m: HashMap, + variables: &Vec, +) -> HashMap { let mut map = m.clone(); for variable in variables { if !variable.enabled || variable.value.is_empty() { @@ -87,7 +117,7 @@ fn add_variable_to_map<'a>( } let name = variable.name.as_str(); let value = variable.value.as_str(); - map.insert(name, value); + map.insert(name.into(), value.into()); } map diff --git a/src-tauri/templates/src/renderer.rs b/src-tauri/templates/src/renderer.rs index 9eb89d13..779a58e5 100644 --- a/src-tauri/templates/src/renderer.rs +++ b/src-tauri/templates/src/renderer.rs @@ -6,7 +6,7 @@ type TemplateCallback = fn(name: &str, args: Vec) -> String; pub fn parse_and_render( template: &str, - vars: HashMap<&str, &str>, + vars: &HashMap, cb: Option, ) -> String { let mut p = Parser::new(template); @@ -16,7 +16,7 @@ pub fn parse_and_render( pub fn render( tokens: Vec, - vars: HashMap<&str, &str>, + vars: &HashMap, cb: Option, ) -> String { let mut doc_str: Vec = Vec::new(); @@ -24,7 +24,7 @@ pub fn render( for t in tokens { match t { Token::Raw(s) => doc_str.push(s), - Token::Tag(val) => doc_str.push(render_tag(val, vars.clone(), cb)), + Token::Tag(val) => doc_str.push(render_tag(val, &vars, cb)), Token::Eof => {} } } @@ -32,9 +32,9 @@ pub fn render( return doc_str.join(""); } -fn render_tag<'s>( +fn render_tag( val: Val, - vars: HashMap<&'s str, &'s str>, + vars: &HashMap, cb: Option, ) -> String { match val { @@ -44,13 +44,13 @@ fn render_tag<'s>( None => "".into(), }, Val::Fn { name, args } => { - let empty = &""; + let empty = "".to_string(); let resolved_args = args .iter() .map(|a| match a { Val::Str(s) => s.to_string(), - Val::Var(i) => vars.get(i.as_str()).unwrap_or(empty).to_string(), - val => render_tag(val.clone(), vars.clone(), cb), + Val::Var(i) => vars.get(i.as_str()).unwrap_or(&empty).to_string(), + val => render_tag(val.clone(), vars, cb), }) .collect::>(); match cb { @@ -72,7 +72,7 @@ mod tests { let template = ""; let vars = HashMap::new(); let result = ""; - assert_eq!(parse_and_render(template, vars, None), result.to_string()); + assert_eq!(parse_and_render(template, &vars, None), result.to_string()); } #[test] @@ -80,23 +80,23 @@ mod tests { let template = "Hello World!"; let vars = HashMap::new(); let result = "Hello World!"; - assert_eq!(parse_and_render(template, vars, None), result.to_string()); + assert_eq!(parse_and_render(template, &vars, None), result.to_string()); } #[test] fn render_simple() { let template = "${[ foo ]}"; - let vars = HashMap::from([("foo", "bar")]); + let vars = HashMap::from([("foo".to_string(), "bar".to_string())]); let result = "bar"; - assert_eq!(parse_and_render(template, vars, None), result.to_string()); + assert_eq!(parse_and_render(template, &vars, None), result.to_string()); } #[test] fn render_surrounded() { let template = "hello ${[ word ]} world!"; - let vars = HashMap::from([("word", "cruel")]); + let vars = HashMap::from([("word".to_string(), "cruel".to_string())]); let result = "hello cruel world!"; - assert_eq!(parse_and_render(template, vars, None), result.to_string()); + assert_eq!(parse_and_render(template, &vars, None), result.to_string()); } #[test] @@ -108,7 +108,7 @@ mod tests { fn cb(name: &str, args: Vec) -> String { format!("{name}: {:?}", args) } - assert_eq!(parse_and_render(template, vars, Some(cb)), result); + assert_eq!(parse_and_render(template, &vars, Some(cb)), result); } #[test] @@ -125,7 +125,7 @@ mod tests { } assert_eq!( - parse_and_render(template, vars, Some(cb)), + parse_and_render(template, &vars, Some(cb)), result.to_string() ); } diff --git a/src-web/components/EnvironmentEditDialog.tsx b/src-web/components/EnvironmentEditDialog.tsx index b053390f..c8a9a5c2 100644 --- a/src-web/components/EnvironmentEditDialog.tsx +++ b/src-web/components/EnvironmentEditDialog.tsx @@ -193,7 +193,7 @@ const EnvironmentEditor = function ({ namePlaceholder="VAR_NAME" nameValidate={validateName} valueType={valueVisibility.value ? 'text' : 'password'} - valueAutocompleteVariables={false} + valueAutocompleteVariables={true} forceUpdateKey={environment?.id ?? workspace?.id ?? 'n/a'} pairs={variables} onChange={handleChange}