From 11caf44498a0498b4b18beaf1254363406485041 Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Sat, 9 Dec 2023 19:32:30 +0100 Subject: [PATCH 01/22] Add support for parsing variables --- rq-cli/src/app.rs | 14 ++--- rq-core/Cargo.toml | 1 + rq-core/src/grammar.pest | 36 +++++++++---- rq-core/src/parser.rs | 75 ++++++++++++++++++++------ rq-core/src/parser/variables.rs | 94 +++++++++++++++++++++++++++++++++ rq-core/src/request.rs | 10 ++-- 6 files changed, 193 insertions(+), 37 deletions(-) create mode 100644 rq-core/src/parser/variables.rs diff --git a/rq-cli/src/app.rs b/rq-cli/src/app.rs index 9f15748..eaf91ab 100644 --- a/rq-cli/src/app.rs +++ b/rq-cli/src/app.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use ratatui::{ prelude::{Constraint, Direction, Layout}, style::{Color, Modifier, Style}, @@ -35,7 +37,7 @@ impl MenuItem for HttpRequest { let mut first_line_spans = vec![ Span::styled(self.method.to_string(), Style::default().fg(Color::Green)), Span::raw(" "), - Span::raw(self.url.as_str()), + Span::raw(self.url.to_string()), ]; let version_span = Span::raw(format!(" {:?}", self.version)); @@ -67,13 +69,13 @@ impl MenuItem for HttpRequest { } let headers: Vec = self - .headers() + .headers .iter() .map(|(k, v)| { Line::from(vec![ Span::styled(k.to_string(), Style::default().fg(Color::Blue)), Span::raw(": "), - Span::raw(v.to_str().unwrap().to_string()), + Span::raw(v.to_string()), ]) }) .collect(); @@ -107,9 +109,9 @@ impl MenuItem for HttpRequest { lines.pop(); lines.pop(); - for line in self.body.lines() { + for line in self.body.to_string().lines() { lines.push(Line::styled( - line, + line.to_owned(), Style::default().fg(Color::Rgb(246, 133, 116)), )); } @@ -135,7 +137,7 @@ pub struct App { fn handle_requests(mut req_rx: Receiver<(HttpRequest, usize)>, res_tx: Sender<(Response, usize)>) { tokio::spawn(async move { while let Some((req, i)) = req_rx.recv().await { - let data = match rq_core::request::execute(&req).await { + let data = match rq_core::request::execute(&req, &HashMap::new()).await { Ok(data) => data, Err(e) => { MessageDialog::push_message(Message::Error(e.to_string())); diff --git a/rq-core/Cargo.toml b/rq-core/Cargo.toml index 6382073..732ad31 100644 --- a/rq-core/Cargo.toml +++ b/rq-core/Cargo.toml @@ -14,3 +14,4 @@ once_cell = "1.18.0" reqwest = { version = "0.11", features = ["json"] } encoding_rs = "0.8.33" mime = "0.3.17" +thiserror = "1.0.50" diff --git a/rq-core/src/grammar.pest b/rq-core/src/grammar.pest index ea16d4c..6df2476 100644 --- a/rq-core/src/grammar.pest +++ b/rq-core/src/grammar.pest @@ -1,4 +1,4 @@ -file = { SOI ~ (request ~ (delimiter ~ request)*)? ~ EOI} +file = { SOI ~ (request ~ (DELIM ~ request)*)? ~ EOI} request = { request_line ~ @@ -7,21 +7,39 @@ request = { body? } -request_line = _{ (method ~ " "+)? ~ uri ~ query? ~ (" "+ ~ version)? ~ NEWLINE } -uri = { (!(whitespace | "?") ~ ANY)+ } +request_line = _{ (method ~ " "+)? ~ url ~ query? ~ (" "+ ~ version)? ~ NEWLINE } + +url = { (variable | url_fragment)+ } +url_fragment = { (!(whitespace | VAR_BEGIN | "?") ~ ANY)+ } + method = { ("GET" | "DELETE" | "POST" | "PUT") } version = { "HTTP/" ~ ("0.9" | "1.0" | "1.1" | "2.0" | "3.0") } whitespace = _{ " " | "\t" | NEWLINE } query = { (NEWLINE ~ (" " | "\t")*)? ~ "?" ~ query_item ~ ((NEWLINE ~ (" " | "\t")*)? ~ "&" ~ query_item)* } query_item = { query_name ~ "=" ~ query_value } -query_name = { (!(NEWLINE | "=") ~ ANY)+ } -query_value = { (PUSH("'" | "\"") ~ (!PEEK ~ ANY)* ~ POP) | (!(NEWLINE | "&" | " ") ~ ANY)+ } + +query_name = { (variable | query_name_fragment)+ } +query_name_fragment = { (!(whitespace | VAR_BEGIN | "=") ~ ANY)+ } + +query_value = { (PUSH("'" | "\"") ~ (!PEEK ~ (variable | ANY))* ~ POP) | (!(NEWLINE | "&" | " ") ~ (variable | query_value_fragment))+ } +query_value_fragment = { (!(whitespace | VAR_BEGIN | "&") ~ ANY)+ } headers = { header+ } header = { header_name ~ ":" ~ whitespace ~ header_value ~ NEWLINE } -header_name = { (!(NEWLINE | ":") ~ ANY)+ } -header_value = { (!NEWLINE ~ ANY)+ } -body = { (!delimiter ~ ANY)+ } -delimiter = { "#"{3} ~ NEWLINE+ } +header_name = { (!(NEWLINE | ":") ~ (variable | header_name_fragment))+ } +header_name_fragment = { (!(whitespace | VAR_BEGIN | ":") ~ ANY)+ } + +header_value = { (!NEWLINE ~ (variable | header_value_fragment))+ } +header_value_fragment = { (!(NEWLINE | VAR_BEGIN) ~ ANY)+ } + +body = { (!DELIM ~ (variable | body_fragment))+ } +body_fragment = { (!DELIM ~ ANY)+ } + +DELIM = { "#"{3} ~ NEWLINE+ } + +VAR_BEGIN = { "{{" } +VAR_END = { "}}" } +variable = { VAR_BEGIN ~ variable_name ~ VAR_END } +variable_name = { (!(whitespace | VAR_END) ~ ANY)+ } diff --git a/rq-core/src/parser.rs b/rq-core/src/parser.rs index edd8922..4a40c2d 100644 --- a/rq-core/src/parser.rs +++ b/rq-core/src/parser.rs @@ -6,14 +6,43 @@ use reqwest::header::HeaderMap; use reqwest::{Method, Version}; use std::collections::HashMap; use std::fmt::Display; +use std::ops::Deref; use std::result::Result; +use self::variables::{FillError, Fragment, TemplateString, Variable}; + +mod variables; + #[derive(Parser)] #[grammar = "grammar.pest"] struct HttpParser; #[derive(Clone, Debug, Default)] -struct HttpHeaders(HashMap); +pub struct HttpHeaders(HashMap); + +impl HttpHeaders { + pub fn fill(&self, params: &HashMap) -> Result { + let filled = self + .0 + .iter() + .map(|(k, v)| { + let v = v.fill(params)?; + + Ok((k.to_owned(), v)) + }) + .collect::, FillError>>()?; + + Ok((&filled).try_into().unwrap()) + } +} + +impl Deref for HttpHeaders { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} impl Display for HttpHeaders { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -34,7 +63,7 @@ impl<'i> From> for HttpHeaders { .map(|pair| { let mut kv = pair.into_inner(); let key = kv.next().unwrap().as_str().to_string(); - let value = kv.next().unwrap().as_str().to_string(); + let value = parse_value(kv.next().unwrap()); (key, value) }) @@ -58,17 +87,27 @@ fn http_version_from_str(input: &str) -> Version { #[derive(Debug, Clone, Default)] pub struct HttpRequest { pub method: Method, - pub url: String, + pub url: TemplateString, pub query: HashMap, pub version: Version, - headers: HttpHeaders, - pub body: String, + pub headers: HttpHeaders, + pub body: TemplateString, } -impl HttpRequest { - pub fn headers(&self) -> HeaderMap { - (&self.headers.0).try_into().unwrap() - } +fn parse_value(input: Pair<'_, Rule>) -> TemplateString { + let inner = input.into_inner(); + + let fragments = inner + .map(|pair| match pair.as_rule() { + Rule::variable => { + let var_name = pair.into_inner().nth(1).unwrap().as_str(); + Fragment::Var(Variable::new(var_name)) + } + _ => Fragment::RawText(pair.as_str().to_owned()), + }) + .collect::>(); + + TemplateString::new(fragments) } impl<'i> From> for HttpRequest { @@ -80,7 +119,7 @@ impl<'i> From> for HttpRequest { .map(|pair| pair.as_str().try_into().unwrap()) .unwrap_or_default(); - let url = pairs.next().unwrap().as_str().to_string(); + let url = parse_value(pairs.next().unwrap()); let query = pairs .next_if(|pair| pair.as_rule() == Rule::query) @@ -114,10 +153,7 @@ impl<'i> From> for HttpRequest { .map(|pair| pair.into_inner().into()) .unwrap_or_default(); - let body = pairs - .next() - .map(|pair| pair.as_str().to_string()) - .unwrap_or_default(); + let body = pairs.next().map(parse_value).unwrap_or_default(); Self { method, @@ -205,7 +241,7 @@ GET foo.bar HTTP/1.1 let file = assert_parses(input); assert_eq!(file.requests.len(), 1); assert_eq!(file.requests[0].method, Method::GET); - assert_eq!(file.requests[0].url, "foo.bar"); + assert_eq!(file.requests[0].url.to_string(), "foo.bar"); assert_eq!(file.requests[0].version, Version::HTTP_11); } @@ -242,7 +278,12 @@ authorization: Bearer xxxx assert_eq!(file.requests.len(), 1); assert_eq!(file.requests[0].headers.0.len(), 1); assert_eq!( - file.requests[0].headers.0.get("authorization").unwrap(), + file.requests[0] + .headers + .0 + .get("authorization") + .unwrap() + .to_string(), "Bearer xxxx" ); } @@ -254,7 +295,7 @@ POST test.dev HTTP/1.0 { "test": "body" }"#; let file = assert_parses(input); - assert_eq!(file.requests[0].body, "{ \"test\": \"body\" }"); + assert_eq!(file.requests[0].body.to_string(), "{ \"test\": \"body\" }"); } #[test] diff --git a/rq-core/src/parser/variables.rs b/rq-core/src/parser/variables.rs new file mode 100644 index 0000000..84d1361 --- /dev/null +++ b/rq-core/src/parser/variables.rs @@ -0,0 +1,94 @@ +use std::{collections::HashMap, fmt::Display}; + +use thiserror::Error; + +#[derive(Debug, Clone)] +pub struct Variable { + name: String, +} + +impl Variable { + pub fn new(name: &str) -> Self { + Variable { + name: name.to_owned(), + } + } +} + +impl Display for Variable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // i.e. if self.name = 'foo', this outputs '{{foo}}' + write!(f, "{{{{{}}}}}", self.name) + } +} + +#[derive(Debug, Clone)] +pub enum Fragment { + Var(Variable), + RawText(String), +} + +#[derive(Debug, Clone, Default)] +pub struct TemplateString { + fragments: Vec, +} + +impl TemplateString { + pub fn new(fragments: Vec) -> Self { + Self { fragments } + } + + pub fn fill(&self, parameters: &HashMap) -> Result { + self.fragments + .iter() + .map(|fragment| { + let s = match fragment { + Fragment::Var(v) => parameters + .get(&v.name) + .map(|s| s.as_str()) + .ok_or(v.clone())?, + Fragment::RawText(s) => s.as_str(), + }; + + Ok(s) + }) + .collect() + } + + pub fn is_empty(&self) -> bool { + self.fragments.is_empty() + || self.fragments.iter().all(|fragment| match fragment { + Fragment::Var(_) => false, + Fragment::RawText(s) => s.is_empty(), + }) + } +} + +#[derive(Debug, Error)] +#[error("missing field '{}'", .missing_variable.name)] +pub struct FillError { + missing_variable: Variable, +} + +impl From for FillError { + fn from(value: Variable) -> Self { + FillError { + missing_variable: value, + } + } +} + +impl Display for TemplateString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = self + .fragments + .iter() + .map(|fragment| match fragment { + Fragment::Var(v) => v.to_string(), + Fragment::RawText(s) => s.to_owned(), + }) + .collect::(); + + write!(f, "{s}") + } +} diff --git a/rq-core/src/request.rs b/rq-core/src/request.rs index 183dbb6..5d8b4d3 100644 --- a/rq-core/src/request.rs +++ b/rq-core/src/request.rs @@ -5,7 +5,7 @@ pub use reqwest::StatusCode; use reqwest::{header::HeaderMap, Client}; use crate::parser::HttpRequest; -use std::time::Duration; +use std::{collections::HashMap, time::Duration}; use self::mime::Payload; @@ -46,12 +46,12 @@ impl Response { type RequestResult = Result>; -pub async fn execute(req: &HttpRequest) -> RequestResult { +pub async fn execute(req: &HttpRequest, params: &HashMap) -> RequestResult { let request = CLIENT - .request(req.method.clone(), &req.url) + .request(req.method.clone(), req.url.fill(params)?) .query(&req.query) - .headers(req.headers()) - .body(req.body.clone()); + .headers(req.headers.fill(params)?) + .body(req.body.fill(params)?); let response = request.send().await?; From 8841663a281fed6bc3cf005bec081b9b917bbf8c Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Wed, 27 Dec 2023 19:57:15 +0100 Subject: [PATCH 02/22] Parse file variables --- demo.http | 6 ++++- rq-core/src/grammar.pest | 7 ++++- rq-core/src/parser.rs | 46 +++++++++++++++++++++++++++------ rq-core/src/parser/variables.rs | 17 ++++++++++++ 4 files changed, 66 insertions(+), 10 deletions(-) diff --git a/demo.http b/demo.http index d5aef82..3ffbfd5 100644 --- a/demo.http +++ b/demo.http @@ -3,7 +3,11 @@ Foo: Bar ### -POST https://httpbin.org/post +@endpoint = foo + +### + +POST https://{{endpoint}}/post Bar: Baz { diff --git a/rq-core/src/grammar.pest b/rq-core/src/grammar.pest index 6df2476..a191bec 100644 --- a/rq-core/src/grammar.pest +++ b/rq-core/src/grammar.pest @@ -1,4 +1,9 @@ -file = { SOI ~ (request ~ (DELIM ~ request)*)? ~ EOI} +file = { SOI ~ ((request | var_block) ~ (DELIM ~ (request | var_block))*)? ~ EOI} + +var_block = { (var_def ~ NEWLINE*)+ } +var_def = { "@" ~ var_def_name ~ " "? ~ "=" ~ " "? ~ var_def_value } +var_def_name = { (!whitespace ~ ANY)+ } +var_def_value = { (PUSH("'" | "\"") ~ (!PEEK ~ (variable | ANY))* ~ POP) | (!whitespace ~ (variable | ANY))+ } request = { request_line ~ diff --git a/rq-core/src/parser.rs b/rq-core/src/parser.rs index 4a40c2d..3b9cc54 100644 --- a/rq-core/src/parser.rs +++ b/rq-core/src/parser.rs @@ -179,19 +179,29 @@ impl Display for HttpRequest { #[derive(Debug)] pub struct HttpFile { pub requests: Vec, + pub variables: HashMap, } impl<'i> From> for HttpFile { fn from(pair: Pair) -> Self { - let requests = pair - .into_inner() - .filter_map(|pair| match pair.as_rule() { - Rule::request => Some(pair.into()), - _ => None, - }) - .collect(); + let mut requests = Vec::new(); + let mut variables = HashMap::new(); + + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::request => requests.push(pair.into()), + Rule::var_block => variables.extend(variables::parse(pair)), + + Rule::EOI | Rule::DELIM => (), + + _ => unreachable!(), + } + } - Self { requests } + Self { + requests, + variables, + } } } @@ -356,4 +366,24 @@ authorization: token assert_eq!(file.requests[0].query.get("foo").unwrap(), "bar"); assert_eq!(file.requests[0].query.get("baz").unwrap(), "42"); } + + #[test] + fn test_file_variable() { + let input = r#" +@name = foo +@bar = baz + +### + +POST test.dev +?foo=bar + &baz=42 HTTP/1.0 +authorization: token + +"#; + let file = assert_parses(input); + assert_eq!(file.variables.len(), 2); + assert_eq!(file.variables.get("name"), Some(&"foo".into())); + assert_eq!(file.variables.get("bar"), Some(&"baz".into())); + } } diff --git a/rq-core/src/parser/variables.rs b/rq-core/src/parser/variables.rs index 84d1361..ee55a65 100644 --- a/rq-core/src/parser/variables.rs +++ b/rq-core/src/parser/variables.rs @@ -1,7 +1,10 @@ use std::{collections::HashMap, fmt::Display}; +use pest::iterators::Pair; use thiserror::Error; +use super::Rule; + #[derive(Debug, Clone)] pub struct Variable { name: String, @@ -92,3 +95,17 @@ impl Display for TemplateString { write!(f, "{s}") } } + +pub fn parse(var_def_block: Pair) -> HashMap { + var_def_block + .into_inner() + .map(|var_def| { + let mut pairs = var_def.into_inner(); + + let name = pairs.next().unwrap().as_str().to_string(); + let value = pairs.next().unwrap().as_str().to_string(); + + (name, value) + }) + .collect() +} From bdb2f3ef062a3b99430ba1617a24b66dd137338a Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Sat, 30 Dec 2023 11:46:27 +0100 Subject: [PATCH 03/22] Unquote variables value --- rq-core/src/parser.rs | 17 +++++++---------- rq-core/src/parser/values.rs | 9 +++++++++ rq-core/src/parser/variables.rs | 4 ++-- 3 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 rq-core/src/parser/values.rs diff --git a/rq-core/src/parser.rs b/rq-core/src/parser.rs index 3b9cc54..9267fb1 100644 --- a/rq-core/src/parser.rs +++ b/rq-core/src/parser.rs @@ -11,6 +11,7 @@ use std::result::Result; use self::variables::{FillError, Fragment, TemplateString, Variable}; +mod values; mod variables; #[derive(Parser)] @@ -131,13 +132,7 @@ impl<'i> From> for HttpRequest { let key = pairs.next().unwrap().as_str().to_string(); let value = pairs.next().unwrap().as_str().to_string(); - for c in ['\'', '"'] { - if value.starts_with(c) && value.ends_with(c) { - return (key, value.trim_matches(c).to_string()); - } - } - - (key, value) + (key, values::unquote(value)) }) .collect::>() }) @@ -372,6 +367,7 @@ authorization: token let input = r#" @name = foo @bar = baz +@foo = " 123" ### @@ -382,8 +378,9 @@ authorization: token "#; let file = assert_parses(input); - assert_eq!(file.variables.len(), 2); - assert_eq!(file.variables.get("name"), Some(&"foo".into())); - assert_eq!(file.variables.get("bar"), Some(&"baz".into())); + assert_eq!(file.variables.len(), 3); + assert_eq!(file.variables.get("name").map(String::as_str), Some("foo")); + assert_eq!(file.variables.get("bar").map(String::as_str), Some("baz")); + assert_eq!(file.variables.get("foo").map(String::as_str), Some(" 123")); } } diff --git a/rq-core/src/parser/values.rs b/rq-core/src/parser/values.rs new file mode 100644 index 0000000..f4baa30 --- /dev/null +++ b/rq-core/src/parser/values.rs @@ -0,0 +1,9 @@ +pub fn unquote(input: String) -> String { + for c in ['\'', '"'] { + if input.starts_with(c) && input.ends_with(c) { + return input.trim_matches(c).to_string(); + } + } + + input +} diff --git a/rq-core/src/parser/variables.rs b/rq-core/src/parser/variables.rs index ee55a65..e4cd410 100644 --- a/rq-core/src/parser/variables.rs +++ b/rq-core/src/parser/variables.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, fmt::Display}; use pest::iterators::Pair; use thiserror::Error; -use super::Rule; +use super::{values, Rule}; #[derive(Debug, Clone)] pub struct Variable { @@ -105,7 +105,7 @@ pub fn parse(var_def_block: Pair) -> HashMap { let name = pairs.next().unwrap().as_str().to_string(); let value = pairs.next().unwrap().as_str().to_string(); - (name, value) + (name, values::unquote(value)) }) .collect() } From 956d47ed39179d190c3c3095110942304b725c88 Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Wed, 10 Jan 2024 22:09:35 +0100 Subject: [PATCH 04/22] Add test and fix var in url --- rq-core/src/grammar.pest | 4 ++-- rq-core/src/parser.rs | 12 +++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/rq-core/src/grammar.pest b/rq-core/src/grammar.pest index 19a6d85..8264a5b 100644 --- a/rq-core/src/grammar.pest +++ b/rq-core/src/grammar.pest @@ -55,8 +55,8 @@ header_value_fragment = @{ (!VAR_BEGIN ~ (char | " "))+ } body = ${ (var | body_fragment)+ } body_fragment = @{ (!(VAR_BEGIN | DELIM)~ ANY)+ } -var = { VAR_BEGIN ~ var_name ~ VAR_END } -var_name = { (!VAR_END ~ char)+ } +var = ${ VAR_BEGIN ~ var_name ~ VAR_END } +var_name = @{ (!VAR_END ~ char)+ } var_def_block = { (var_def ~ NEWLINE*)+ } var_def = ${ "@" ~ var_def_name ~ " "? ~ "=" ~ " "? ~ var_def_value } diff --git a/rq-core/src/parser.rs b/rq-core/src/parser.rs index 3c8d312..a042ed0 100644 --- a/rq-core/src/parser.rs +++ b/rq-core/src/parser.rs @@ -101,7 +101,7 @@ fn parse_value(input: Pair<'_, Rule>) -> TemplateString { let fragments = inner .map(|pair| match pair.as_rule() { Rule::var => { - let var_name = pair.into_inner().nth(1).unwrap().as_str(); + let var_name = pair.into_inner().next().unwrap().as_str(); Fragment::Var(Variable::new(var_name)) } _ => Fragment::RawText(pair.as_str().to_owned()), @@ -276,6 +276,16 @@ GET foo.bar assert_eq!(file.requests[0].version, Version::default()); } + #[test] + fn test_var_in_url() { + let input = r#" +GET foo{{url}}bar HTTP/1.1 + +"#; + let file = assert_parses(input); + assert_eq!(file.requests[0].url.to_string(), "foo{{url}}bar"); + } + #[test] fn test_headers() { let input = r#" From f1b51730df0386e6c9e1334bb9ae9aef236dfefc Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Wed, 10 Jan 2024 22:22:07 +0100 Subject: [PATCH 05/22] Add test and fix var in headers --- rq-core/src/parser.rs | 35 ++++++++++++++++++++++++++++----- rq-core/src/parser/variables.rs | 14 +++++++++---- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/rq-core/src/parser.rs b/rq-core/src/parser.rs index a042ed0..73135c3 100644 --- a/rq-core/src/parser.rs +++ b/rq-core/src/parser.rs @@ -19,7 +19,7 @@ mod variables; struct HttpParser; #[derive(Clone, Debug, Default)] -pub struct HttpHeaders(HashMap); +pub struct HttpHeaders(HashMap); impl HttpHeaders { pub fn fill(&self, params: &HashMap) -> Result { @@ -27,9 +27,10 @@ impl HttpHeaders { .0 .iter() .map(|(k, v)| { + let k = k.fill(params)?; let v = v.fill(params)?; - Ok((k.to_owned(), v)) + Ok((k, v)) }) .collect::, FillError>>()?; @@ -38,7 +39,7 @@ impl HttpHeaders { } impl Deref for HttpHeaders { - type Target = HashMap; + type Target = HashMap; fn deref(&self) -> &Self::Target { &self.0 @@ -63,7 +64,7 @@ impl<'i> From> for HttpHeaders { let headers = pairs .map(|pair| { let mut kv = pair.into_inner(); - let key = kv.next().unwrap().as_str().to_string(); + let key = parse_value(kv.next().unwrap()); let value = parse_value(kv.next().unwrap()); (key, value) @@ -224,6 +225,8 @@ pub fn parse(input: &str) -> Result>> { mod tests { use core::panic; + use crate::parser::variables::{Fragment, TemplateString, Variable}; + use super::{parse, HttpFile}; use reqwest::{Method, Version}; @@ -300,13 +303,35 @@ authorization: Bearer xxxx file.requests[0] .headers .0 - .get("authorization") + .get(&TemplateString::raw("authorization")) .unwrap() .to_string(), "Bearer xxxx" ); } + #[test] + fn test_var_in_headers() { + let input = r#" +POST test.dev HTTP/1.0 +aa{{name}}bb: {{value}}{{barbar}} + +"#; + let file = assert_parses(input); + assert_eq!( + file.requests[0] + .headers + .0 + .get(&TemplateString::new(vec![ + Fragment::RawText("aa".into()), + Fragment::Var(Variable::new("name")), + Fragment::RawText("bb".into()) + ])) + .map(TemplateString::to_string), + Some("{{value}}{{barbar}}".into()) + ); + } + #[test] fn test_body() { let input = r#" diff --git a/rq-core/src/parser/variables.rs b/rq-core/src/parser/variables.rs index e4cd410..4d217e4 100644 --- a/rq-core/src/parser/variables.rs +++ b/rq-core/src/parser/variables.rs @@ -1,11 +1,11 @@ -use std::{collections::HashMap, fmt::Display}; +use std::{collections::HashMap, fmt::Display, hash::Hash}; use pest::iterators::Pair; use thiserror::Error; use super::{values, Rule}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct Variable { name: String, } @@ -25,13 +25,13 @@ impl Display for Variable { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum Fragment { Var(Variable), RawText(String), } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Eq, PartialEq, Hash)] pub struct TemplateString { fragments: Vec, } @@ -41,6 +41,12 @@ impl TemplateString { Self { fragments } } + pub fn raw(s: &str) -> Self { + Self { + fragments: vec![Fragment::RawText(s.into())], + } + } + pub fn fill(&self, parameters: &HashMap) -> Result { self.fragments .iter() From 9cc57e8acfc6b4d80185116314753875431c95dc Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Thu, 11 Jan 2024 09:53:51 +0100 Subject: [PATCH 06/22] Better testing TemplateString parsing --- rq-core/src/parser.rs | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/rq-core/src/parser.rs b/rq-core/src/parser.rs index 73135c3..d62421d 100644 --- a/rq-core/src/parser.rs +++ b/rq-core/src/parser.rs @@ -286,7 +286,14 @@ GET foo{{url}}bar HTTP/1.1 "#; let file = assert_parses(input); - assert_eq!(file.requests[0].url.to_string(), "foo{{url}}bar"); + assert_eq!( + file.requests[0].url, + TemplateString::new(vec![ + Fragment::RawText("foo".into()), + Fragment::Var(Variable::new("url")), + Fragment::RawText("bar".into()) + ]) + ); } #[test] @@ -319,16 +326,15 @@ aa{{name}}bb: {{value}}{{barbar}} "#; let file = assert_parses(input); assert_eq!( - file.requests[0] - .headers - .0 - .get(&TemplateString::new(vec![ - Fragment::RawText("aa".into()), - Fragment::Var(Variable::new("name")), - Fragment::RawText("bb".into()) - ])) - .map(TemplateString::to_string), - Some("{{value}}{{barbar}}".into()) + file.requests[0].headers.0.get(&TemplateString::new(vec![ + Fragment::RawText("aa".into()), + Fragment::Var(Variable::new("name")), + Fragment::RawText("bb".into()) + ])), + Some(&TemplateString::new(vec![ + Fragment::Var(Variable::new("value")), + Fragment::Var(Variable::new("barbar")) + ])) ); } From aab228e2054c42fe6c65d15737d0bdd8940b43ce Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Thu, 11 Jan 2024 09:56:27 +0100 Subject: [PATCH 07/22] Add test for var in body --- rq-core/src/parser.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/rq-core/src/parser.rs b/rq-core/src/parser.rs index d62421d..f78af61 100644 --- a/rq-core/src/parser.rs +++ b/rq-core/src/parser.rs @@ -348,6 +348,23 @@ POST test.dev HTTP/1.0 assert_eq!(file.requests[0].body.to_string(), "{ \"test\": \"body\" }"); } + #[test] + fn test_var_in_body() { + let input = r#" +POST test.dev HTTP/1.0 + +aaa{{var}}bbb"#; + let file = assert_parses(input); + assert_eq!( + file.requests[0].body, + TemplateString::new(vec![ + Fragment::RawText("aaa".into()), + Fragment::Var(Variable::new("var")), + Fragment::RawText("bbb".into()) + ]) + ) + } + #[test] fn test_multiple_requests() { let input = r#" From ba8288b92c27c1a827b1f8d0089dd534e5c2eca1 Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Thu, 11 Jan 2024 10:05:21 +0100 Subject: [PATCH 08/22] Add convenience methods to build Fragments --- rq-core/src/parser.rs | 30 +++++++++++++++--------------- rq-core/src/parser/variables.rs | 10 ++++++++++ 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/rq-core/src/parser.rs b/rq-core/src/parser.rs index f78af61..a76cadb 100644 --- a/rq-core/src/parser.rs +++ b/rq-core/src/parser.rs @@ -9,7 +9,7 @@ use std::fmt::Display; use std::ops::Deref; use std::result::Result; -use self::variables::{FillError, Fragment, TemplateString, Variable}; +use self::variables::{FillError, Fragment, TemplateString}; mod values; mod variables; @@ -103,9 +103,9 @@ fn parse_value(input: Pair<'_, Rule>) -> TemplateString { .map(|pair| match pair.as_rule() { Rule::var => { let var_name = pair.into_inner().next().unwrap().as_str(); - Fragment::Var(Variable::new(var_name)) + Fragment::var(var_name) } - _ => Fragment::RawText(pair.as_str().to_owned()), + _ => Fragment::raw(pair.as_str()), }) .collect::>(); @@ -225,7 +225,7 @@ pub fn parse(input: &str) -> Result>> { mod tests { use core::panic; - use crate::parser::variables::{Fragment, TemplateString, Variable}; + use crate::parser::variables::{Fragment, TemplateString}; use super::{parse, HttpFile}; use reqwest::{Method, Version}; @@ -289,9 +289,9 @@ GET foo{{url}}bar HTTP/1.1 assert_eq!( file.requests[0].url, TemplateString::new(vec![ - Fragment::RawText("foo".into()), - Fragment::Var(Variable::new("url")), - Fragment::RawText("bar".into()) + Fragment::raw("foo"), + Fragment::var("url"), + Fragment::raw("bar") ]) ); } @@ -327,13 +327,13 @@ aa{{name}}bb: {{value}}{{barbar}} let file = assert_parses(input); assert_eq!( file.requests[0].headers.0.get(&TemplateString::new(vec![ - Fragment::RawText("aa".into()), - Fragment::Var(Variable::new("name")), - Fragment::RawText("bb".into()) + Fragment::raw("aa"), + Fragment::var("name"), + Fragment::raw("bb") ])), Some(&TemplateString::new(vec![ - Fragment::Var(Variable::new("value")), - Fragment::Var(Variable::new("barbar")) + Fragment::var("value"), + Fragment::var("barbar") ])) ); } @@ -358,9 +358,9 @@ aaa{{var}}bbb"#; assert_eq!( file.requests[0].body, TemplateString::new(vec![ - Fragment::RawText("aaa".into()), - Fragment::Var(Variable::new("var")), - Fragment::RawText("bbb".into()) + Fragment::raw("aaa"), + Fragment::var("var"), + Fragment::raw("bbb") ]) ) } diff --git a/rq-core/src/parser/variables.rs b/rq-core/src/parser/variables.rs index 4d217e4..9293fa6 100644 --- a/rq-core/src/parser/variables.rs +++ b/rq-core/src/parser/variables.rs @@ -31,6 +31,16 @@ pub enum Fragment { RawText(String), } +impl Fragment { + pub fn raw(value: &str) -> Self { + Fragment::RawText(value.into()) + } + + pub fn var(name: &str) -> Self { + Fragment::Var(Variable::new(name)) + } +} + #[derive(Debug, Clone, Default, Eq, PartialEq, Hash)] pub struct TemplateString { fragments: Vec, From e34a1435515b6ad0b97984c2f5c22a9575976071 Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Thu, 11 Jan 2024 10:29:00 +0100 Subject: [PATCH 09/22] Add test and fix var in var def blocks --- rq-core/src/grammar.pest | 7 ++-- rq-core/src/parser.rs | 71 ++++++++++++++++++++------------- rq-core/src/parser/values.rs | 4 +- rq-core/src/parser/variables.rs | 24 +++++++++-- 4 files changed, 70 insertions(+), 36 deletions(-) diff --git a/rq-core/src/grammar.pest b/rq-core/src/grammar.pest index 8264a5b..80f9d0a 100644 --- a/rq-core/src/grammar.pest +++ b/rq-core/src/grammar.pest @@ -61,11 +61,12 @@ var_name = @{ (!VAR_END ~ char)+ } var_def_block = { (var_def ~ NEWLINE*)+ } var_def = ${ "@" ~ var_def_name ~ " "? ~ "=" ~ " "? ~ var_def_value } var_def_name = @{ (!"=" ~ char)+ } -var_def_value = @{ +var_def_value = ${ (var | var_def_value_fragment)+ } +var_def_value_fragment = @{ ( PUSH("'" | "\"") ~ - (!PEEK ~ (var | ANY))* ~ + (!PEEK ~ ANY)+ ~ POP ) | - (var | char)+ + (!VAR_BEGIN ~ char)+ } diff --git a/rq-core/src/parser.rs b/rq-core/src/parser.rs index a76cadb..fe014c8 100644 --- a/rq-core/src/parser.rs +++ b/rq-core/src/parser.rs @@ -9,7 +9,7 @@ use std::fmt::Display; use std::ops::Deref; use std::result::Result; -use self::variables::{FillError, Fragment, TemplateString}; +use self::variables::{FillError, TemplateString}; mod values; mod variables; @@ -64,8 +64,8 @@ impl<'i> From> for HttpHeaders { let headers = pairs .map(|pair| { let mut kv = pair.into_inner(); - let key = parse_value(kv.next().unwrap()); - let value = parse_value(kv.next().unwrap()); + let key = kv.next().unwrap().into(); + let value = kv.next().unwrap().into(); (key, value) }) @@ -96,22 +96,6 @@ pub struct HttpRequest { pub body: TemplateString, } -fn parse_value(input: Pair<'_, Rule>) -> TemplateString { - let inner = input.into_inner(); - - let fragments = inner - .map(|pair| match pair.as_rule() { - Rule::var => { - let var_name = pair.into_inner().next().unwrap().as_str(); - Fragment::var(var_name) - } - _ => Fragment::raw(pair.as_str()), - }) - .collect::>(); - - TemplateString::new(fragments) -} - impl<'i> From> for HttpRequest { fn from(request: Pair<'i, Rule>) -> Self { let mut pairs = request.into_inner().peekable(); @@ -121,7 +105,7 @@ impl<'i> From> for HttpRequest { .map(|pair| pair.as_str().try_into().unwrap()) .unwrap_or_default(); - let url = parse_value(pairs.next().unwrap()); + let url = pairs.next().unwrap().into(); let query = pairs .next_if(|pair| pair.as_rule() == Rule::query) @@ -131,9 +115,9 @@ impl<'i> From> for HttpRequest { let mut pairs = pair.into_inner(); let key = pairs.next().unwrap().as_str().to_string(); - let value = pairs.next().unwrap().as_str().to_string(); + let value = pairs.next().unwrap().as_str(); - (key, values::unquote(value)) + (key, values::unquote(value).to_string()) }) .collect::>() }) @@ -149,7 +133,7 @@ impl<'i> From> for HttpRequest { .map(|pair| pair.into_inner().into()) .unwrap_or_default(); - let body = pairs.next().map(parse_value).unwrap_or_default(); + let body = pairs.next().map(Pair::into).unwrap_or_default(); Self { method, @@ -175,7 +159,7 @@ impl Display for HttpRequest { #[derive(Debug)] pub struct HttpFile { pub requests: Vec, - pub variables: HashMap, + pub variables: HashMap, } impl<'i> From> for HttpFile { @@ -186,7 +170,7 @@ impl<'i> From> for HttpFile { for pair in pair.into_inner() { match pair.as_rule() { Rule::request => requests.push(pair.into()), - Rule::var_def_block => variables.extend(variables::parse(pair)), + Rule::var_def_block => variables.extend(variables::parse_def_block(pair)), Rule::EOI | Rule::DELIM => (), @@ -441,8 +425,39 @@ authorization: token "#; let file = assert_parses(input); assert_eq!(file.variables.len(), 3); - assert_eq!(file.variables.get("name").map(String::as_str), Some("foo")); - assert_eq!(file.variables.get("bar").map(String::as_str), Some("baz")); - assert_eq!(file.variables.get("foo").map(String::as_str), Some(" 123")); + assert_eq!( + file.variables.get("name"), + Some(&TemplateString::raw("foo")) + ); + assert_eq!(file.variables.get("bar"), Some(&TemplateString::raw("baz"))); + assert_eq!( + file.variables.get("foo"), + Some(&TemplateString::raw(" 123")) + ); + } + + #[test] + fn test_var_in_file_var() { + let input = r#" +@name = foo +@bar = aaa{{var}} +@foo = " 123" + +### + +POST test.dev + ?foo=bar + &baz=42 HTTP/1.0 +authorization: token + +"#; + let file = assert_parses(input); + assert_eq!( + file.variables.get("bar"), + Some(&TemplateString::new(vec![ + Fragment::raw("aaa"), + Fragment::var("var") + ])) + ); } } diff --git a/rq-core/src/parser/values.rs b/rq-core/src/parser/values.rs index f4baa30..40b9946 100644 --- a/rq-core/src/parser/values.rs +++ b/rq-core/src/parser/values.rs @@ -1,7 +1,7 @@ -pub fn unquote(input: String) -> String { +pub fn unquote(input: &str) -> &str { for c in ['\'', '"'] { if input.starts_with(c) && input.ends_with(c) { - return input.trim_matches(c).to_string(); + return input.trim_matches(c); } } diff --git a/rq-core/src/parser/variables.rs b/rq-core/src/parser/variables.rs index 9293fa6..550bc57 100644 --- a/rq-core/src/parser/variables.rs +++ b/rq-core/src/parser/variables.rs @@ -83,6 +83,24 @@ impl TemplateString { } } +impl From> for TemplateString { + fn from(value: Pair<'_, Rule>) -> Self { + let inner = value.into_inner(); + + let fragments = inner + .map(|pair| match pair.as_rule() { + Rule::var => { + let var_name = pair.into_inner().next().unwrap().as_str(); + Fragment::var(var_name) + } + _ => Fragment::raw(values::unquote(pair.as_str())), + }) + .collect::>(); + + Self::new(fragments) + } +} + #[derive(Debug, Error)] #[error("missing field '{}'", .missing_variable.name)] pub struct FillError { @@ -112,16 +130,16 @@ impl Display for TemplateString { } } -pub fn parse(var_def_block: Pair) -> HashMap { +pub fn parse_def_block(var_def_block: Pair) -> HashMap { var_def_block .into_inner() .map(|var_def| { let mut pairs = var_def.into_inner(); let name = pairs.next().unwrap().as_str().to_string(); - let value = pairs.next().unwrap().as_str().to_string(); + let value = pairs.next().unwrap().into(); - (name, values::unquote(value)) + (name, value) }) .collect() } From 8720c5ee682c2ffcf567175ff45366db34ff813e Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Thu, 11 Jan 2024 10:42:48 +0100 Subject: [PATCH 10/22] Vars in headers supported only for values --- rq-core/src/grammar.pest | 3 +-- rq-core/src/parser.rs | 19 +++++++------------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/rq-core/src/grammar.pest b/rq-core/src/grammar.pest index 80f9d0a..9e5f6a3 100644 --- a/rq-core/src/grammar.pest +++ b/rq-core/src/grammar.pest @@ -47,8 +47,7 @@ version = { "HTTP/" ~ ("0.9" | "1.0" | "1.1" | "2.0" | "3.0") } headers = { (header ~ NEWLINE)+ } header = { header_name ~ ":" ~ header_value } -header_name = ${ (var | header_name_fragment)+ } -header_name_fragment = @{ (!(VAR_BEGIN | ":") ~ char)+ } +header_name = @{ (!":" ~ char)+ } header_value = ${ (var | header_value_fragment)+ } header_value_fragment = @{ (!VAR_BEGIN ~ (char | " "))+ } diff --git a/rq-core/src/parser.rs b/rq-core/src/parser.rs index fe014c8..c9015eb 100644 --- a/rq-core/src/parser.rs +++ b/rq-core/src/parser.rs @@ -19,15 +19,14 @@ mod variables; struct HttpParser; #[derive(Clone, Debug, Default)] -pub struct HttpHeaders(HashMap); +pub struct HttpHeaders(HashMap); impl HttpHeaders { pub fn fill(&self, params: &HashMap) -> Result { let filled = self .0 - .iter() + .into_iter() .map(|(k, v)| { - let k = k.fill(params)?; let v = v.fill(params)?; Ok((k, v)) @@ -39,7 +38,7 @@ impl HttpHeaders { } impl Deref for HttpHeaders { - type Target = HashMap; + type Target = HashMap; fn deref(&self) -> &Self::Target { &self.0 @@ -64,7 +63,7 @@ impl<'i> From> for HttpHeaders { let headers = pairs .map(|pair| { let mut kv = pair.into_inner(); - let key = kv.next().unwrap().into(); + let key = kv.next().unwrap().as_str().to_string(); let value = kv.next().unwrap().into(); (key, value) @@ -294,7 +293,7 @@ authorization: Bearer xxxx file.requests[0] .headers .0 - .get(&TemplateString::raw("authorization")) + .get("authorization") .unwrap() .to_string(), "Bearer xxxx" @@ -305,16 +304,12 @@ authorization: Bearer xxxx fn test_var_in_headers() { let input = r#" POST test.dev HTTP/1.0 -aa{{name}}bb: {{value}}{{barbar}} +aabb: {{value}}{{barbar}} "#; let file = assert_parses(input); assert_eq!( - file.requests[0].headers.0.get(&TemplateString::new(vec![ - Fragment::raw("aa"), - Fragment::var("name"), - Fragment::raw("bb") - ])), + file.requests[0].headers.0.get("aabb"), Some(&TemplateString::new(vec![ Fragment::var("value"), Fragment::var("barbar") From 8d43582b90fb10c58234d50381585a40c6e665dc Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Thu, 11 Jan 2024 11:00:20 +0100 Subject: [PATCH 11/22] Refactor using HashTemplateMap and add var in query --- rq-cli/src/app.rs | 2 +- rq-core/src/grammar.pest | 5 +- rq-core/src/parser.rs | 163 +++++++++++--------------------- rq-core/src/parser/variables.rs | 49 +++++++++- rq-core/src/request.rs | 4 +- 5 files changed, 107 insertions(+), 116 deletions(-) diff --git a/rq-cli/src/app.rs b/rq-cli/src/app.rs index 1dee025..223de10 100644 --- a/rq-cli/src/app.rs +++ b/rq-cli/src/app.rs @@ -54,7 +54,7 @@ impl MenuItem for HttpRequest { ), Span::raw(k), Span::raw("="), - Span::raw(v), + Span::raw(v.to_string()), ]) }) .collect::>(); diff --git a/rq-core/src/grammar.pest b/rq-core/src/grammar.pest index 9e5f6a3..3338318 100644 --- a/rq-core/src/grammar.pest +++ b/rq-core/src/grammar.pest @@ -34,13 +34,14 @@ query = @{ } query_item = ${ query_name ~ "=" ~ query_value } query_name = @{ (!"=" ~ char)+ } -query_value = @{ +query_value = $ { (var | query_value_fragment)+ } +query_value_fragment = @{ ( PUSH("\"" | "'") ~ (!PEEK ~ ANY)+ ~ POP ) | - (!"&" ~ char)+ + (!("&" | VAR_BEGIN) ~ char)+ } version = { "HTTP/" ~ ("0.9" | "1.0" | "1.1" | "2.0" | "3.0") } diff --git a/rq-core/src/parser.rs b/rq-core/src/parser.rs index c9015eb..cd1a245 100644 --- a/rq-core/src/parser.rs +++ b/rq-core/src/parser.rs @@ -1,15 +1,12 @@ use pest::error::Error; -use pest::iterators::{Pair, Pairs}; +use pest::iterators::Pair; use pest::Parser; -use reqwest::header::HeaderMap; use reqwest::{Method, Version}; use std::collections::HashMap; -use std::fmt::Display; -use std::ops::Deref; use std::result::Result; -use self::variables::{FillError, TemplateString}; +use self::variables::{HashTemplateMap, TemplateString}; mod values; mod variables; @@ -18,62 +15,6 @@ mod variables; #[grammar = "grammar.pest"] struct HttpParser; -#[derive(Clone, Debug, Default)] -pub struct HttpHeaders(HashMap); - -impl HttpHeaders { - pub fn fill(&self, params: &HashMap) -> Result { - let filled = self - .0 - .into_iter() - .map(|(k, v)| { - let v = v.fill(params)?; - - Ok((k, v)) - }) - .collect::, FillError>>()?; - - Ok((&filled).try_into().unwrap()) - } -} - -impl Deref for HttpHeaders { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Display for HttpHeaders { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = self - .0 - .iter() - .map(|(key, value)| format!("{key}: {value}")) - .collect::>() - .join(", "); - - write!(f, "[{s}]") - } -} - -impl<'i> From> for HttpHeaders { - fn from(pairs: Pairs<'i, Rule>) -> Self { - let headers = pairs - .map(|pair| { - let mut kv = pair.into_inner(); - let key = kv.next().unwrap().as_str().to_string(); - let value = kv.next().unwrap().into(); - - (key, value) - }) - .collect(); - - HttpHeaders(headers) - } -} - fn http_version_from_str(input: &str) -> Version { match input { "HTTP/0.9" => Version::HTTP_09, @@ -89,9 +30,9 @@ fn http_version_from_str(input: &str) -> Version { pub struct HttpRequest { pub method: Method, pub url: TemplateString, - pub query: HashMap, + pub query: HashTemplateMap, pub version: Version, - pub headers: HttpHeaders, + pub headers: HashTemplateMap, pub body: TemplateString, } @@ -106,20 +47,9 @@ impl<'i> From> for HttpRequest { let url = pairs.next().unwrap().into(); - let query = pairs + let query: HashTemplateMap = pairs .next_if(|pair| pair.as_rule() == Rule::query) - .map(|pair| { - pair.into_inner() - .map(|pair| { - let mut pairs = pair.into_inner(); - - let key = pairs.next().unwrap().as_str().to_string(); - let value = pairs.next().unwrap().as_str(); - - (key, values::unquote(value).to_string()) - }) - .collect::>() - }) + .map(|pair| pair.into()) .unwrap_or_default(); let version = pairs @@ -127,9 +57,9 @@ impl<'i> From> for HttpRequest { .map(|pair| http_version_from_str(pair.as_str())) .unwrap_or_default(); - let headers: HttpHeaders = pairs + let headers: HashTemplateMap = pairs .next_if(|pair| pair.as_rule() == Rule::headers) - .map(|pair| pair.into_inner().into()) + .map(|pair| pair.into()) .unwrap_or_default(); let body = pairs.next().map(Pair::into).unwrap_or_default(); @@ -145,16 +75,6 @@ impl<'i> From> for HttpRequest { } } -impl Display for HttpRequest { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{} {} {:?} {}", - self.method, self.url, self.version, self.headers - ) - } -} - #[derive(Debug)] pub struct HttpFile { pub requests: Vec, @@ -184,19 +104,6 @@ impl<'i> From> for HttpFile { } } -impl Display for HttpFile { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.requests.is_empty() { - writeln!(f, "No requests found")?; - return Ok(()); - } - for (i, req) in self.requests.iter().enumerate() { - write!(f, "#{i}\n{req}\n")?; - } - Ok(()) - } -} - pub fn parse(input: &str) -> Result>> { let pair = HttpParser::parse(Rule::file, input.trim_start())? .next() @@ -288,11 +195,10 @@ authorization: Bearer xxxx "#; let file = assert_parses(input); assert_eq!(file.requests.len(), 1); - assert_eq!(file.requests[0].headers.0.len(), 1); + assert_eq!(file.requests[0].headers.len(), 1); assert_eq!( file.requests[0] .headers - .0 .get("authorization") .unwrap() .to_string(), @@ -309,7 +215,7 @@ aabb: {{value}}{{barbar}} "#; let file = assert_parses(input); assert_eq!( - file.requests[0].headers.0.get("aabb"), + file.requests[0].headers.get("aabb"), Some(&TemplateString::new(vec![ Fragment::var("value"), Fragment::var("barbar") @@ -369,8 +275,14 @@ authorization: token let file = assert_parses(input); assert_eq!(file.requests.len(), 1); assert_eq!(file.requests[0].query.len(), 3); - assert_eq!(file.requests[0].query.get("foo").unwrap(), "bar"); - assert_eq!(file.requests[0].query.get("baz").unwrap(), "2"); + assert_eq!( + file.requests[0].query.get("foo"), + Some(&TemplateString::new(vec![Fragment::raw("bar")])) + ); + assert_eq!( + file.requests[0].query.get("baz"), + Some(&TemplateString::new(vec![Fragment::raw("2")])) + ); } #[test] @@ -383,8 +295,14 @@ authorization: token let file = assert_parses(input); assert_eq!(file.requests.len(), 1); assert_eq!(file.requests[0].query.len(), 2); - assert_eq!(file.requests[0].query.get("foo").unwrap(), " bar"); - assert_eq!(file.requests[0].query.get("baz").unwrap(), " &ciao"); + assert_eq!( + file.requests[0].query.get("foo"), + Some(&TemplateString::raw(" bar")) + ); + assert_eq!( + file.requests[0].query.get("baz"), + Some(&TemplateString::raw(" &ciao")) + ); } #[test] @@ -399,8 +317,33 @@ authorization: token let file = assert_parses(input); assert_eq!(file.requests.len(), 1); assert_eq!(file.requests[0].query.len(), 2); - assert_eq!(file.requests[0].query.get("foo").unwrap(), "bar"); - assert_eq!(file.requests[0].query.get("baz").unwrap(), "42"); + assert_eq!( + file.requests[0].query.get("foo"), + Some(&TemplateString::raw("bar")) + ); + assert_eq!( + file.requests[0].query.get("baz"), + Some(&TemplateString::raw("42")) + ); + } + + #[test] + fn test_var_in_query() { + let input = r#" +POST test.dev + ?foo=aaa{{var}} + &baz="bbb"{{var2}} HTTP/1.0 +authorization: token + +"#; + let file = assert_parses(input); + assert_eq!( + file.requests[0].query.get("foo"), + Some(&TemplateString::new(vec![ + Fragment::raw("aaa"), + Fragment::var("var") + ])) + ); } #[test] diff --git a/rq-core/src/parser/variables.rs b/rq-core/src/parser/variables.rs index 550bc57..9cd3e2b 100644 --- a/rq-core/src/parser/variables.rs +++ b/rq-core/src/parser/variables.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, fmt::Display, hash::Hash}; +use std::{collections::HashMap, fmt::Display, hash::Hash, ops::Deref}; use pest::iterators::Pair; use thiserror::Error; @@ -143,3 +143,50 @@ pub fn parse_def_block(var_def_block: Pair) -> HashMap); + +impl HashTemplateMap { + pub fn fill( + &self, + params: &HashMap, + ) -> Result, FillError> { + let filled = self + .0 + .iter() + .map(|(k, v)| { + let v = v.fill(params)?; + + Ok((k.to_owned(), v)) + }) + .collect::, FillError>>()?; + + Ok(filled) + } +} + +impl Deref for HashTemplateMap { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From> for HashTemplateMap { + fn from(value: Pair<'_, Rule>) -> Self { + let headers = value + .into_inner() + .map(|pair| { + let mut kv = pair.into_inner(); + let key = kv.next().unwrap().as_str().to_string(); + let value = kv.next().unwrap().into(); + + (key, value) + }) + .collect(); + + Self(headers) + } +} diff --git a/rq-core/src/request.rs b/rq-core/src/request.rs index 5d8b4d3..b89258d 100644 --- a/rq-core/src/request.rs +++ b/rq-core/src/request.rs @@ -49,8 +49,8 @@ type RequestResult = Result>; pub async fn execute(req: &HttpRequest, params: &HashMap) -> RequestResult { let request = CLIENT .request(req.method.clone(), req.url.fill(params)?) - .query(&req.query) - .headers(req.headers.fill(params)?) + .query(&req.query.fill(params)?) + .headers((&req.headers.fill(params)?).try_into().unwrap()) .body(req.body.fill(params)?); let response = request.send().await?; From 341b2cf440fbe252519322def49f8724227ccd0b Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Thu, 11 Jan 2024 22:37:45 +0100 Subject: [PATCH 12/22] Fill TemplateRequest and send it --- rq-cli/src/app.rs | 34 ++++++++++++------------ rq-core/src/parser.rs | 46 ++++++++++++++++++++++++++------- rq-core/src/parser/variables.rs | 10 +++---- rq-core/src/request.rs | 12 ++++----- 4 files changed, 65 insertions(+), 37 deletions(-) diff --git a/rq-cli/src/app.rs b/rq-cli/src/app.rs index 223de10..d848a6f 100644 --- a/rq-cli/src/app.rs +++ b/rq-cli/src/app.rs @@ -7,7 +7,7 @@ use ratatui::{ widgets::{Block, Borders}, }; use rq_core::{ - parser::{HttpFile, HttpRequest}, + parser::{variables::TemplateString, HttpFile, HttpRequest, TemplateRequest}, request::Response, }; use tokio::sync::mpsc::{channel, Receiver, Sender}; @@ -30,7 +30,7 @@ enum FocusState { ResponseBuffer, } -impl MenuItem for HttpRequest { +impl MenuItem for TemplateRequest { fn render(&self) -> Vec> { let mut lines = Vec::new(); @@ -126,10 +126,12 @@ pub struct App { res_rx: Receiver<(Response, usize)>, req_tx: Sender<(HttpRequest, usize)>, - request_menu: Menu, + request_menu: Menu, + variables: HashMap, + file_path: String, + responses: Vec, should_exit: bool, - file_path: String, focus: FocusState, message_popup: Option>, } @@ -137,12 +139,9 @@ pub struct App { fn handle_requests(mut req_rx: Receiver<(HttpRequest, usize)>, res_tx: Sender<(Response, usize)>) { tokio::spawn(async move { while let Some((req, i)) = req_rx.recv().await { - match rq_core::request::execute(&req, &HashMap::new()).await { + match rq_core::request::execute(req).await { Ok(data) => res_tx.send((data, i)).await.unwrap(), - Err(e) => { - MessageDialog::push_message(Message::Error(e.to_string())); - continue; - } + Err(e) => MessageDialog::push_message(Message::Error(e.to_string())), }; } }); @@ -162,10 +161,13 @@ impl App { .collect(); App { - file_path, res_rx, req_tx, + request_menu: Menu::new(http_file.requests), + variables: http_file.variables, + file_path, + responses, should_exit: false, focus: FocusState::default(), @@ -219,12 +221,10 @@ impl App { let index = self.request_menu.idx(); self.responses[index].set_loading(); - self.req_tx - .send(( - self.request_menu.selected().clone(), - self.request_menu.idx(), - )) - .await?; + match self.request_menu.selected().fill(&self.variables) { + Ok(request) => self.req_tx.send((request, self.request_menu.idx())).await?, + Err(e) => MessageDialog::push_message(Message::Error(e.to_string())), + }; } }, _ => (), @@ -257,7 +257,7 @@ impl App { FocusState::RequestsList => ( Style::default().fg(Color::Blue), Style::default(), - Menu::::KEYMAPS.iter(), + Menu::::KEYMAPS.iter(), ), FocusState::ResponseBuffer => ( Style::default(), diff --git a/rq-core/src/parser.rs b/rq-core/src/parser.rs index cd1a245..8fe23fe 100644 --- a/rq-core/src/parser.rs +++ b/rq-core/src/parser.rs @@ -2,19 +2,47 @@ use pest::error::Error; use pest::iterators::Pair; use pest::Parser; -use reqwest::{Method, Version}; +use reqwest::{header::HeaderMap, Method, Version}; use std::collections::HashMap; use std::result::Result; -use self::variables::{HashTemplateMap, TemplateString}; +use self::variables::{FillError, HashTemplateMap, TemplateString}; mod values; -mod variables; +pub mod variables; #[derive(Parser)] #[grammar = "grammar.pest"] struct HttpParser; +#[derive(Debug)] +pub struct TemplateRequest { + pub method: Method, + pub url: TemplateString, + pub query: HashTemplateMap, + pub version: Version, + pub headers: HashTemplateMap, + pub body: TemplateString, +} + +impl TemplateRequest { + pub fn fill( + &self, + parameters: &HashMap, + ) -> Result { + let req = HttpRequest { + method: self.method.clone(), + url: self.url.fill(parameters)?, + query: self.query.fill(parameters)?, + version: self.version, + headers: (&self.headers.fill(parameters)?).try_into().unwrap(), + body: self.body.fill(parameters)?, + }; + + Ok(req) + } +} + fn http_version_from_str(input: &str) -> Version { match input { "HTTP/0.9" => Version::HTTP_09, @@ -29,14 +57,14 @@ fn http_version_from_str(input: &str) -> Version { #[derive(Debug, Clone, Default)] pub struct HttpRequest { pub method: Method, - pub url: TemplateString, - pub query: HashTemplateMap, + pub url: String, + pub query: HashMap, pub version: Version, - pub headers: HashTemplateMap, - pub body: TemplateString, + pub headers: HeaderMap, + pub body: String, } -impl<'i> From> for HttpRequest { +impl<'i> From> for TemplateRequest { fn from(request: Pair<'i, Rule>) -> Self { let mut pairs = request.into_inner().peekable(); @@ -77,7 +105,7 @@ impl<'i> From> for HttpRequest { #[derive(Debug)] pub struct HttpFile { - pub requests: Vec, + pub requests: Vec, pub variables: HashMap, } diff --git a/rq-core/src/parser/variables.rs b/rq-core/src/parser/variables.rs index 9cd3e2b..0190127 100644 --- a/rq-core/src/parser/variables.rs +++ b/rq-core/src/parser/variables.rs @@ -57,16 +57,16 @@ impl TemplateString { } } - pub fn fill(&self, parameters: &HashMap) -> Result { + pub fn fill(&self, parameters: &HashMap) -> Result { self.fragments .iter() .map(|fragment| { let s = match fragment { Fragment::Var(v) => parameters .get(&v.name) - .map(|s| s.as_str()) - .ok_or(v.clone())?, - Fragment::RawText(s) => s.as_str(), + .map(|s| s.fill(parameters)) + .ok_or(v.clone())??, + Fragment::RawText(s) => s.to_owned(), }; Ok(s) @@ -150,7 +150,7 @@ pub struct HashTemplateMap(HashMap); impl HashTemplateMap { pub fn fill( &self, - params: &HashMap, + params: &HashMap, ) -> Result, FillError> { let filled = self .0 diff --git a/rq-core/src/request.rs b/rq-core/src/request.rs index b89258d..6e78b7a 100644 --- a/rq-core/src/request.rs +++ b/rq-core/src/request.rs @@ -5,7 +5,7 @@ pub use reqwest::StatusCode; use reqwest::{header::HeaderMap, Client}; use crate::parser::HttpRequest; -use std::{collections::HashMap, time::Duration}; +use std::time::Duration; use self::mime::Payload; @@ -46,12 +46,12 @@ impl Response { type RequestResult = Result>; -pub async fn execute(req: &HttpRequest, params: &HashMap) -> RequestResult { +pub async fn execute(req: HttpRequest) -> RequestResult { let request = CLIENT - .request(req.method.clone(), req.url.fill(params)?) - .query(&req.query.fill(params)?) - .headers((&req.headers.fill(params)?).try_into().unwrap()) - .body(req.body.fill(params)?); + .request(req.method.clone(), req.url) + .query(&req.query) + .headers(req.headers) + .body(req.body); let response = request.send().await?; From 0fde1f05ce843dc6c21ff8fb31fe76c21c87d37f Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Fri, 12 Jan 2024 11:20:50 +0100 Subject: [PATCH 13/22] Add test and fix bug Newlines weren't supported before var defs --- demo.http | 2 +- rq-core/src/grammar.pest | 2 +- rq-core/src/parser.rs | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/demo.http b/demo.http index 3ffbfd5..0c4c565 100644 --- a/demo.http +++ b/demo.http @@ -3,7 +3,7 @@ Foo: Bar ### -@endpoint = foo +@endpoint = httpbin.org ### diff --git a/rq-core/src/grammar.pest b/rq-core/src/grammar.pest index 3338318..d651610 100644 --- a/rq-core/src/grammar.pest +++ b/rq-core/src/grammar.pest @@ -58,7 +58,7 @@ body_fragment = @{ (!(VAR_BEGIN | DELIM)~ ANY)+ } var = ${ VAR_BEGIN ~ var_name ~ VAR_END } var_name = @{ (!VAR_END ~ char)+ } -var_def_block = { (var_def ~ NEWLINE*)+ } +var_def_block = { (NEWLINE* ~ var_def ~ NEWLINE*)+ } var_def = ${ "@" ~ var_def_name ~ " "? ~ "=" ~ " "? ~ var_def_value } var_def_name = @{ (!"=" ~ char)+ } var_def_value = ${ (var | var_def_value_fragment)+ } diff --git a/rq-core/src/parser.rs b/rq-core/src/parser.rs index 8fe23fe..7cd3c59 100644 --- a/rq-core/src/parser.rs +++ b/rq-core/src/parser.rs @@ -133,9 +133,7 @@ impl<'i> From> for HttpFile { } pub fn parse(input: &str) -> Result>> { - let pair = HttpParser::parse(Rule::file, input.trim_start())? - .next() - .unwrap(); + let pair = HttpParser::parse(Rule::file, input)?.next().unwrap(); Ok(HttpFile::from(pair)) } @@ -416,6 +414,10 @@ POST test.dev &baz=42 HTTP/1.0 authorization: token +### + +@test = test + "#; let file = assert_parses(input); assert_eq!( From 58e8ed131740f404ac95532386cc73b5a76599cf Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Fri, 12 Jan 2024 11:31:53 +0100 Subject: [PATCH 14/22] Move impl MenuItem for TemplateRequest to own file --- rq-cli/src/app.rs | 97 +--------------------- rq-cli/src/components/mod.rs | 1 + rq-cli/src/components/template_request.rs | 99 +++++++++++++++++++++++ 3 files changed, 102 insertions(+), 95 deletions(-) create mode 100644 rq-cli/src/components/template_request.rs diff --git a/rq-cli/src/app.rs b/rq-cli/src/app.rs index d848a6f..ccb853d 100644 --- a/rq-cli/src/app.rs +++ b/rq-cli/src/app.rs @@ -2,8 +2,7 @@ use std::collections::HashMap; use ratatui::{ prelude::{Constraint, Direction, Layout}, - style::{Color, Modifier, Style}, - text::{Line, Span}, + style::{Color, Style}, widgets::{Block, Borders}, }; use rq_core::{ @@ -16,7 +15,7 @@ use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use crate::components::{ legend::Legend, - menu::{Menu, MenuItem}, + menu::Menu, message_dialog::{Message, MessageDialog}, popup::Popup, response_panel::ResponsePanel, @@ -30,98 +29,6 @@ enum FocusState { ResponseBuffer, } -impl MenuItem for TemplateRequest { - fn render(&self) -> Vec> { - let mut lines = Vec::new(); - - let mut first_line_spans = vec![ - Span::styled(self.method.to_string(), Style::default().fg(Color::Green)), - Span::raw(" "), - Span::raw(self.url.to_string()), - ]; - let version_span = Span::raw(format!(" {:?}", self.version)); - - let mut query = self - .query - .iter() - .enumerate() - .map(|(i, (k, v))| { - Line::from(vec![ - Span::raw(" ".repeat(self.method.to_string().len() + 1)), - Span::styled( - if i == 0 { "?" } else { "&" }, - Style::default().fg(Color::Blue), - ), - Span::raw(k), - Span::raw("="), - Span::raw(v.to_string()), - ]) - }) - .collect::>(); - - if query.is_empty() { - first_line_spans.push(version_span); - lines.push(Line::from(first_line_spans)); - } else { - lines.push(Line::from(first_line_spans)); - query.last_mut().unwrap().spans.push(version_span); - lines.extend(query); - } - - let headers: Vec = self - .headers - .iter() - .map(|(k, v)| { - Line::from(vec![ - Span::styled(k.to_string(), Style::default().fg(Color::Blue)), - Span::raw(": "), - Span::raw(v.to_string()), - ]) - }) - .collect(); - lines.extend(headers); - - if !self.body.is_empty() { - lines.push(Line::styled( - "Focus to show body", - Style::default() - .fg(Color::Rgb(246, 133, 116)) - .add_modifier(Modifier::ITALIC), - )); - } - - lines.push(Line::from("")); - lines - } - - fn render_highlighted(&self) -> Vec> { - let mut lines = self.render(); - - // Underline first line - lines[0].patch_style( - Style::default() - .add_modifier(Modifier::UNDERLINED) - .add_modifier(Modifier::BOLD), - ); - - // Replace body with expanded version - if !self.body.is_empty() { - lines.pop(); - lines.pop(); - - for line in self.body.to_string().lines() { - lines.push(Line::styled( - line.to_owned(), - Style::default().fg(Color::Rgb(246, 133, 116)), - )); - } - lines.push(Line::from("")); - } - - lines - } -} - pub struct App { res_rx: Receiver<(Response, usize)>, req_tx: Sender<(HttpRequest, usize)>, diff --git a/rq-cli/src/components/mod.rs b/rq-cli/src/components/mod.rs index 0e7ab1a..8cf5731 100644 --- a/rq-cli/src/components/mod.rs +++ b/rq-cli/src/components/mod.rs @@ -9,6 +9,7 @@ pub mod menu; pub mod message_dialog; pub mod popup; pub mod response_panel; +pub mod template_request; pub enum HandleSuccess { Consumed, diff --git a/rq-cli/src/components/template_request.rs b/rq-cli/src/components/template_request.rs new file mode 100644 index 0000000..18413cf --- /dev/null +++ b/rq-cli/src/components/template_request.rs @@ -0,0 +1,99 @@ +use ratatui::{ + style::{Color, Modifier, Style}, + text::{Line, Span}, +}; +use rq_core::parser::TemplateRequest; + +use super::menu::MenuItem; + +impl MenuItem for TemplateRequest { + fn render(&self) -> Vec> { + let mut lines = Vec::new(); + + let mut first_line_spans = vec![ + Span::styled(self.method.to_string(), Style::default().fg(Color::Green)), + Span::raw(" "), + Span::raw(self.url.to_string()), + ]; + let version_span = Span::raw(format!(" {:?}", self.version)); + + let mut query = self + .query + .iter() + .enumerate() + .map(|(i, (k, v))| { + Line::from(vec![ + Span::raw(" ".repeat(self.method.to_string().len() + 1)), + Span::styled( + if i == 0 { "?" } else { "&" }, + Style::default().fg(Color::Blue), + ), + Span::raw(k), + Span::raw("="), + Span::raw(v.to_string()), + ]) + }) + .collect::>(); + + if query.is_empty() { + first_line_spans.push(version_span); + lines.push(Line::from(first_line_spans)); + } else { + lines.push(Line::from(first_line_spans)); + query.last_mut().unwrap().spans.push(version_span); + lines.extend(query); + } + + let headers: Vec = self + .headers + .iter() + .map(|(k, v)| { + Line::from(vec![ + Span::styled(k.to_string(), Style::default().fg(Color::Blue)), + Span::raw(": "), + Span::raw(v.to_string()), + ]) + }) + .collect(); + lines.extend(headers); + + if !self.body.is_empty() { + lines.push(Line::styled( + "Focus to show body", + Style::default() + .fg(Color::Rgb(246, 133, 116)) + .add_modifier(Modifier::ITALIC), + )); + } + + lines.push(Line::from("")); + lines + } + + fn render_highlighted(&self) -> Vec> { + let mut lines = self.render(); + + // Underline first line + lines[0].patch_style( + Style::default() + .add_modifier(Modifier::UNDERLINED) + .add_modifier(Modifier::BOLD), + ); + + // Replace body with expanded version + if !self.body.is_empty() { + lines.pop(); + lines.pop(); + + for line in self.body.to_string().lines() { + lines.push(Line::styled( + line.to_owned(), + Style::default().fg(Color::Rgb(246, 133, 116)), + )); + } + lines.push(Line::from("")); + } + + lines + } +} From c3ce2ea7aa51ae74b269bed2891c46f450fc9989 Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Fri, 12 Jan 2024 11:47:43 +0100 Subject: [PATCH 15/22] Implement VarsPanel (empty) --- rq-cli/src/app.rs | 32 ++++++++++++++++++++++------- rq-cli/src/components/mod.rs | 1 + rq-cli/src/components/vars_panel.rs | 30 +++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 rq-cli/src/components/vars_panel.rs diff --git a/rq-cli/src/app.rs b/rq-cli/src/app.rs index ccb853d..7ac0eb5 100644 --- a/rq-cli/src/app.rs +++ b/rq-cli/src/app.rs @@ -1,12 +1,10 @@ -use std::collections::HashMap; - use ratatui::{ prelude::{Constraint, Direction, Layout}, style::{Color, Style}, widgets::{Block, Borders}, }; use rq_core::{ - parser::{variables::TemplateString, HttpFile, HttpRequest, TemplateRequest}, + parser::{HttpFile, HttpRequest, TemplateRequest}, request::Response, }; use tokio::sync::mpsc::{channel, Receiver, Sender}; @@ -19,6 +17,7 @@ use crate::components::{ message_dialog::{Message, MessageDialog}, popup::Popup, response_panel::ResponsePanel, + vars_panel::VarsPanel, BlockComponent, HandleSuccess, }; @@ -34,11 +33,12 @@ pub struct App { req_tx: Sender<(HttpRequest, usize)>, request_menu: Menu, - variables: HashMap, + vars_panel: VarsPanel, file_path: String, responses: Vec, should_exit: bool, + vars_visible: bool, focus: FocusState, message_popup: Option>, } @@ -72,11 +72,12 @@ impl App { req_tx, request_menu: Menu::new(http_file.requests), - variables: http_file.variables, + vars_panel: VarsPanel::new(http_file.variables), file_path, responses, should_exit: false, + vars_visible: true, focus: FocusState::default(), message_popup: None, } @@ -128,7 +129,7 @@ impl App { let index = self.request_menu.idx(); self.responses[index].set_loading(); - match self.request_menu.selected().fill(&self.variables) { + match self.request_menu.selected().fill(self.vars_panel.vars()) { Ok(request) => self.req_tx.send((request, self.request_menu.idx())).await?, Err(e) => MessageDialog::push_message(Message::Error(e.to_string())), }; @@ -151,7 +152,7 @@ impl App { }; // Create two chunks with equal screen space - let [list_chunk, response_chunk] = { + let [mut list_chunk, response_chunk] = { let x = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) @@ -182,6 +183,23 @@ impl App { .borders(Borders::ALL) .border_style(response_border_style); + if self.vars_visible { + let [new_list_chunk, var_chunk] = { + let x = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(70), Constraint::Percentage(30)]) + .split(list_chunk); + + [x[0], x[1]] + }; + + list_chunk = new_list_chunk; + + let var_block = Block::default().borders(Borders::ALL); + + self.vars_panel.render(f, var_chunk, var_block); + } + self.request_menu.render(f, list_chunk, list_block); let response_panel = &self.responses[self.request_menu.idx()]; response_panel.render(f, response_chunk, response_block); diff --git a/rq-cli/src/components/mod.rs b/rq-cli/src/components/mod.rs index 8cf5731..55b5bd8 100644 --- a/rq-cli/src/components/mod.rs +++ b/rq-cli/src/components/mod.rs @@ -10,6 +10,7 @@ pub mod message_dialog; pub mod popup; pub mod response_panel; pub mod template_request; +pub mod vars_panel; pub enum HandleSuccess { Consumed, diff --git a/rq-cli/src/components/vars_panel.rs b/rq-cli/src/components/vars_panel.rs new file mode 100644 index 0000000..4baa338 --- /dev/null +++ b/rq-cli/src/components/vars_panel.rs @@ -0,0 +1,30 @@ +use std::collections::HashMap; + +use rq_core::parser::variables::TemplateString; + +use super::BlockComponent; + +pub struct VarsPanel { + vars: HashMap, +} + +impl VarsPanel { + pub fn new(vars: HashMap) -> Self { + Self { vars } + } + + pub fn vars(&self) -> &HashMap { + &self.vars + } +} + +impl BlockComponent for VarsPanel { + fn render( + &self, + frame: &mut crate::terminal::Frame, + area: ratatui::prelude::Rect, + block: ratatui::widgets::Block, + ) { + frame.render_widget(block, area); + } +} From eb57df342c3c2fc9b0b9beb1dd34054b4e3a7fee Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Sun, 14 Jan 2024 12:51:07 +0100 Subject: [PATCH 16/22] Render variables inside panels --- demo.http | 3 ++- rq-cli/src/app.rs | 2 +- rq-cli/src/components/mod.rs | 2 +- rq-cli/src/components/variables.rs | 2 ++ rq-cli/src/components/variables/entry.rs | 18 ++++++++++++++++++ .../{vars_panel.rs => variables/panel.rs} | 10 +++++++--- 6 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 rq-cli/src/components/variables.rs create mode 100644 rq-cli/src/components/variables/entry.rs rename rq-cli/src/components/{vars_panel.rs => variables/panel.rs} (64%) diff --git a/demo.http b/demo.http index 0c4c565..91740db 100644 --- a/demo.http +++ b/demo.http @@ -4,10 +4,11 @@ Foo: Bar ### @endpoint = httpbin.org +@method = post ### -POST https://{{endpoint}}/post +POST https://{{endpoint}}/{{method}} Bar: Baz { diff --git a/rq-cli/src/app.rs b/rq-cli/src/app.rs index afade21..dd1338f 100644 --- a/rq-cli/src/app.rs +++ b/rq-cli/src/app.rs @@ -20,7 +20,7 @@ use crate::{ message_dialog::{Message, MessageDialog}, popup::Popup, response_panel::ResponsePanel, - vars_panel::VarsPanel, + variables::panel::VarsPanel, BlockComponent, HandleSuccess, }, event::Event, diff --git a/rq-cli/src/components/mod.rs b/rq-cli/src/components/mod.rs index 5783b7b..0f8bfac 100644 --- a/rq-cli/src/components/mod.rs +++ b/rq-cli/src/components/mod.rs @@ -12,7 +12,7 @@ pub mod message_dialog; pub mod popup; pub mod response_panel; pub mod template_request; -pub mod vars_panel; +pub mod variables; pub enum HandleSuccess { Consumed, diff --git a/rq-cli/src/components/variables.rs b/rq-cli/src/components/variables.rs new file mode 100644 index 0000000..0db5e70 --- /dev/null +++ b/rq-cli/src/components/variables.rs @@ -0,0 +1,2 @@ +pub mod entry; +pub mod panel; diff --git a/rq-cli/src/components/variables/entry.rs b/rq-cli/src/components/variables/entry.rs new file mode 100644 index 0000000..f491ed1 --- /dev/null +++ b/rq-cli/src/components/variables/entry.rs @@ -0,0 +1,18 @@ +use ratatui::{ + style::{Color, Style}, + text::{Line, Span}, +}; +use rq_core::parser::variables::TemplateString; + +use crate::components::menu::MenuItem; + +impl MenuItem for (String, TemplateString) { + fn render(&self) -> Vec> { + vec![Line::from(vec![ + Span::raw("@"), + Span::styled(self.0.as_str(), Style::default().fg(Color::Blue)), + Span::raw(" = "), + Span::raw(self.1.to_string()), + ])] + } +} diff --git a/rq-cli/src/components/vars_panel.rs b/rq-cli/src/components/variables/panel.rs similarity index 64% rename from rq-cli/src/components/vars_panel.rs rename to rq-cli/src/components/variables/panel.rs index 4baa338..dab89d2 100644 --- a/rq-cli/src/components/vars_panel.rs +++ b/rq-cli/src/components/variables/panel.rs @@ -2,15 +2,19 @@ use std::collections::HashMap; use rq_core::parser::variables::TemplateString; -use super::BlockComponent; +use crate::components::{menu::Menu, BlockComponent}; pub struct VarsPanel { vars: HashMap, + menu: Menu<(String, TemplateString)>, } impl VarsPanel { pub fn new(vars: HashMap) -> Self { - Self { vars } + Self { + menu: Menu::new(vars.iter().map(|(k, v)| (k.clone(), v.clone())).collect()), + vars, + } } pub fn vars(&self) -> &HashMap { @@ -25,6 +29,6 @@ impl BlockComponent for VarsPanel { area: ratatui::prelude::Rect, block: ratatui::widgets::Block, ) { - frame.render_widget(block, area); + self.menu.render(frame, area, block.title(" Variables ")); } } From e4247c6026cfd9f952175ae276f85801e71e19c6 Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Mon, 15 Jan 2024 11:03:22 +0100 Subject: [PATCH 17/22] Quote TemplateString if surrounded by spaces --- rq-core/src/parser/variables.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rq-core/src/parser/variables.rs b/rq-core/src/parser/variables.rs index 0190127..8c629c6 100644 --- a/rq-core/src/parser/variables.rs +++ b/rq-core/src/parser/variables.rs @@ -126,6 +126,10 @@ impl Display for TemplateString { }) .collect::(); + if s.starts_with(' ') | s.ends_with(' ') { + return write!(f, "\"{s}\""); + } + write!(f, "{s}") } } From 8fa4ea9fc5a4bf4e7bd5462c8acac78ddcb053d8 Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Mon, 15 Jan 2024 10:52:36 +0100 Subject: [PATCH 18/22] Make VarPanel focusable --- rq-cli/src/app.rs | 41 ++++++++++++++++-------- rq-cli/src/components/variables/panel.rs | 22 ++++++++++++- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/rq-cli/src/app.rs b/rq-cli/src/app.rs index dd1338f..d963b79 100644 --- a/rq-cli/src/app.rs +++ b/rq-cli/src/app.rs @@ -31,6 +31,7 @@ pub enum FocusState { #[default] RequestsList, ResponsePanel, + VarsPanel, } pub struct App { @@ -61,7 +62,8 @@ fn handle_requests(mut req_rx: Receiver<(HttpRequest, usize)>, res_tx: Sender<(R } impl App { - const KEYMAPS: &'static [(&'static str, &'static str); 1] = &[("q", "exit")]; + const KEYMAPS: &'static [(&'static str, &'static str); 2] = + &[("q", "exit"), ("v", "variables")]; pub fn new(file_path: String, http_file: HttpFile) -> Self { let (req_tx, req_rx) = channel::<(HttpRequest, usize)>(1); @@ -116,6 +118,7 @@ impl App { let event_result = match self.focus { FocusState::RequestsList => self.request_menu.on_event(event), FocusState::ResponsePanel => self.responses[self.request_menu.idx()].on_event(event), + FocusState::VarsPanel => self.vars_panel.on_event(event), }; match event_result { @@ -138,6 +141,7 @@ impl App { self.should_exit = true; } } + KeyCode::Char('v') => Event::emit(Event::Focus(FocusState::VarsPanel)), _ => (), }; @@ -164,21 +168,30 @@ impl App { [x[0], x[1]] }; - let (list_border_style, response_border_style, legend) = match self.focus { - FocusState::RequestsList => ( - Style::default().fg(Color::Blue), - Style::default(), + let mut list_border_style = Style::default(); + let mut response_border_style = Style::default(); + let mut vars_panel_border_style = Style::default(); + + let legend = match self.focus { + FocusState::RequestsList => { + list_border_style = list_border_style.fg(Color::Blue); + Legend::new( Self::KEYMAPS .iter() .chain(Menu::::keymaps()), - ), - ), - FocusState::ResponsePanel => ( - Style::default(), - Style::default().fg(Color::Blue), - Legend::new(Self::KEYMAPS.iter().chain(ResponsePanel::keymaps())), - ), + ) + } + FocusState::ResponsePanel => { + response_border_style = response_border_style.fg(Color::Blue); + + Legend::new(Self::KEYMAPS.iter().chain(ResponsePanel::keymaps())) + } + FocusState::VarsPanel => { + vars_panel_border_style = vars_panel_border_style.fg(Color::Blue); + + Legend::new(Self::KEYMAPS.iter().chain(VarsPanel::keymaps())) + } }; let list_block = Block::default() @@ -202,7 +215,9 @@ impl App { list_chunk = new_list_chunk; - let var_block = Block::default().borders(Borders::ALL); + let var_block = Block::default() + .borders(Borders::ALL) + .border_style(vars_panel_border_style); self.vars_panel.render(f, var_chunk, var_block); } diff --git a/rq-cli/src/components/variables/panel.rs b/rq-cli/src/components/variables/panel.rs index dab89d2..1af5e5e 100644 --- a/rq-cli/src/components/variables/panel.rs +++ b/rq-cli/src/components/variables/panel.rs @@ -1,8 +1,12 @@ use std::collections::HashMap; +use crossterm::event::KeyCode; use rq_core::parser::variables::TemplateString; -use crate::components::{menu::Menu, BlockComponent}; +use crate::{ + components::{menu::Menu, BlockComponent, HandleSuccess}, + event::Event, +}; pub struct VarsPanel { vars: HashMap, @@ -31,4 +35,20 @@ impl BlockComponent for VarsPanel { ) { self.menu.render(frame, area, block.title(" Variables ")); } + + fn on_event( + &mut self, + key_event: crossterm::event::KeyEvent, + ) -> crate::components::HandleResult { + match self.menu.on_event(key_event)? { + HandleSuccess::Consumed => return Ok(HandleSuccess::Consumed), + HandleSuccess::Ignored => (), + } + + if matches!(key_event.code, KeyCode::Esc) { + Event::emit(Event::Focus(crate::app::FocusState::RequestsList)); + } + + Ok(HandleSuccess::Ignored) + } } From cd966353aad4ec1d84035bbd19073d8af781c1ac Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Mon, 15 Jan 2024 11:14:50 +0100 Subject: [PATCH 19/22] Merge results --- rq-core/src/parser/variables.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rq-core/src/parser/variables.rs b/rq-core/src/parser/variables.rs index 8c629c6..1d118f5 100644 --- a/rq-core/src/parser/variables.rs +++ b/rq-core/src/parser/variables.rs @@ -64,8 +64,8 @@ impl TemplateString { let s = match fragment { Fragment::Var(v) => parameters .get(&v.name) - .map(|s| s.fill(parameters)) - .ok_or(v.clone())??, + .ok_or(FillError::from(v.clone())) + .and_then(|s| s.fill(parameters))?, Fragment::RawText(s) => s.to_owned(), }; From 70f0c22137d20f76cc390110b0021e7c30e4e010 Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Tue, 16 Jan 2024 15:29:13 +0100 Subject: [PATCH 20/22] Make vars editable --- rq-cli/src/app.rs | 17 ++++++++++++++ rq-cli/src/components/menu.rs | 13 +++++++++++ rq-cli/src/components/variables/panel.rs | 29 ++++++++++++++++++++---- rq-cli/src/event.rs | 2 ++ rq-core/src/parser/variables.rs | 18 ++++++++++++--- 5 files changed, 71 insertions(+), 8 deletions(-) diff --git a/rq-cli/src/app.rs b/rq-cli/src/app.rs index d963b79..88e2cf3 100644 --- a/rq-cli/src/app.rs +++ b/rq-cli/src/app.rs @@ -280,6 +280,16 @@ impl App { .popup(), ); } + crate::event::InputType::VarValue(name) => { + self.input_popup = Some( + InputComponent::from(content.as_str()) + .with_confirm_callback(move |value| { + Event::emit(Event::InputConfirm); + Event::emit(Event::UpdateVar((name.clone(), value))); + }) + .popup(), + ); + } }; Ok(()) } @@ -304,6 +314,13 @@ impl App { Err(e) => Err(anyhow!(e)), } } + Event::UpdateVar((name, value)) => match value.parse() { + Ok(value) => { + self.vars_panel.update(name, value); + Ok(()) + } + Err(e) => Err(anyhow!(e)), + }, }; if let Err(e) = result { MessageDialog::push_message(Message::Error(e.to_string())); diff --git a/rq-cli/src/components/menu.rs b/rq-cli/src/components/menu.rs index c1b277f..f8a2938 100644 --- a/rq-cli/src/components/menu.rs +++ b/rq-cli/src/components/menu.rs @@ -53,6 +53,19 @@ impl Menu { &self.items[idx] } + pub fn update

(&mut self, predicate: P, value: T) + where + P: Fn(&T) -> bool, + { + if let Some(idx) = self.items.iter().position(predicate) { + self.items[idx] = value; + } + } + + pub fn add(&mut self, value: T) { + self.items.push(value); + } + pub fn with_confirm_callback(self, confirm_callback: F) -> Self where F: Fn(&T) + 'static, diff --git a/rq-cli/src/components/variables/panel.rs b/rq-cli/src/components/variables/panel.rs index 1af5e5e..5b9749e 100644 --- a/rq-cli/src/components/variables/panel.rs +++ b/rq-cli/src/components/variables/panel.rs @@ -5,7 +5,7 @@ use rq_core::parser::variables::TemplateString; use crate::{ components::{menu::Menu, BlockComponent, HandleSuccess}, - event::Event, + event::{Event, InputType}, }; pub struct VarsPanel { @@ -15,15 +15,30 @@ pub struct VarsPanel { impl VarsPanel { pub fn new(vars: HashMap) -> Self { - Self { - menu: Menu::new(vars.iter().map(|(k, v)| (k.clone(), v.clone())).collect()), - vars, - } + let menu = Menu::new(vars.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) + .with_confirm_callback(|(name, value)| { + Event::emit(Event::NewInput(( + value.to_string(), + InputType::VarValue(name.clone()), + ))); + }); + + Self { vars, menu } } pub fn vars(&self) -> &HashMap { &self.vars } + + pub fn update(&mut self, name: String, value: TemplateString) { + match self.vars.insert(name.clone(), value.clone()) { + Some(_) => { + let cloned = name.clone(); + self.menu.update(move |(n, _)| n == &cloned, (name, value)); + } + None => self.menu.add((name, value)), + }; + } } impl BlockComponent for VarsPanel { @@ -51,4 +66,8 @@ impl BlockComponent for VarsPanel { Ok(HandleSuccess::Ignored) } + + fn keymaps() -> impl Iterator { + std::iter::once(&("Esc", "back to list")).chain(Menu::<(String, TemplateString)>::keymaps()) + } } diff --git a/rq-cli/src/event.rs b/rq-cli/src/event.rs index 295f74d..39a24ab 100644 --- a/rq-cli/src/event.rs +++ b/rq-cli/src/event.rs @@ -13,12 +13,14 @@ pub enum Event { InputConfirm, InputCancel, SendRequest(usize), + UpdateVar((String, String)), Key(crossterm::event::KeyEvent), Other(crossterm::event::Event), } pub enum InputType { FileName(SaveOption), + VarValue(String), } impl Event { diff --git a/rq-core/src/parser/variables.rs b/rq-core/src/parser/variables.rs index 1d118f5..60ab6ee 100644 --- a/rq-core/src/parser/variables.rs +++ b/rq-core/src/parser/variables.rs @@ -1,9 +1,9 @@ -use std::{collections::HashMap, fmt::Display, hash::Hash, ops::Deref}; +use std::{collections::HashMap, fmt::Display, hash::Hash, ops::Deref, str::FromStr}; -use pest::iterators::Pair; +use pest::{iterators::Pair, Parser}; use thiserror::Error; -use super::{values, Rule}; +use super::{values, HttpParser, Rule}; #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct Variable { @@ -101,6 +101,18 @@ impl From> for TemplateString { } } +impl FromStr for TemplateString { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(HttpParser::parse(Rule::var_def_value, s) + .map_err(|e| e.to_string())? + .next() + .unwrap() + .into()) + } +} + #[derive(Debug, Error)] #[error("missing field '{}'", .missing_variable.name)] pub struct FillError { From 6ff49bba9074e2bd7ef6a3bb5fa091e06a925106 Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Tue, 16 Jan 2024 16:11:53 +0100 Subject: [PATCH 21/22] Refactor with InputBuilder --- rq-cli/src/app.rs | 26 +---------- rq-cli/src/components/input.rs | 10 ++-- rq-cli/src/components/input/builder.rs | 59 ++++++++++++++++++++++++ rq-cli/src/components/response_panel.rs | 24 +++++----- rq-cli/src/components/variables/panel.rs | 16 ++++--- rq-cli/src/event.rs | 18 +++++--- 6 files changed, 100 insertions(+), 53 deletions(-) create mode 100644 rq-cli/src/components/input/builder.rs diff --git a/rq-cli/src/app.rs b/rq-cli/src/app.rs index 88e2cf3..08977bf 100644 --- a/rq-cli/src/app.rs +++ b/rq-cli/src/app.rs @@ -267,30 +267,8 @@ impl App { self.responses[self.request_menu.idx()].save_body(&file_name) } }, - Event::NewInput((content, typ)) => { - match typ { - crate::event::InputType::FileName(save_option) => { - self.input_popup = Some( - InputComponent::from(content.as_str()) - .with_cursor(0) - .with_confirm_callback(move |value| { - Event::emit(Event::InputConfirm); - Event::emit(Event::Save((value, save_option))); - }) - .popup(), - ); - } - crate::event::InputType::VarValue(name) => { - self.input_popup = Some( - InputComponent::from(content.as_str()) - .with_confirm_callback(move |value| { - Event::emit(Event::InputConfirm); - Event::emit(Event::UpdateVar((name.clone(), value))); - }) - .popup(), - ); - } - }; + Event::NewInput(builder) => { + self.input_popup = Some(builder.build().popup()); Ok(()) } Event::InputCancel => { diff --git a/rq-cli/src/components/input.rs b/rq-cli/src/components/input.rs index e950aa0..38e7751 100644 --- a/rq-cli/src/components/input.rs +++ b/rq-cli/src/components/input.rs @@ -6,6 +6,8 @@ use crate::event::Event; use super::BlockComponent; +pub mod builder; + type ConfirmCallback = Box; type CancelCallback = Box; @@ -26,21 +28,21 @@ impl Default for InputComponent { } impl InputComponent { - pub fn from(value: &str) -> Self { + fn from(value: &str) -> Self { Self { input: Input::from(value), ..Self::default() } } - pub fn with_cursor(self, cursor: usize) -> Self { + fn with_cursor(self, cursor: usize) -> Self { Self { input: self.input.with_cursor(cursor), ..self } } - pub fn with_confirm_callback(self, confirm_callback: F) -> Self + fn with_confirm_callback(self, confirm_callback: F) -> Self where F: Fn(String) + 'static, { @@ -50,7 +52,7 @@ impl InputComponent { } } - pub fn with_cancel_callback(self, cancel_callback: F) -> Self + fn with_cancel_callback(self, cancel_callback: F) -> Self where F: Fn() + 'static, { diff --git a/rq-cli/src/components/input/builder.rs b/rq-cli/src/components/input/builder.rs new file mode 100644 index 0000000..7d9d911 --- /dev/null +++ b/rq-cli/src/components/input/builder.rs @@ -0,0 +1,59 @@ +use crate::{components::response_panel::SaveOption, event::Event}; + +use super::InputComponent; + +pub struct InputBuilder { + content: String, + cursor: Option, + typ: InputType, +} + +pub enum InputType { + FileName(SaveOption), + VarValue(String), +} + +impl InputBuilder { + pub fn new(typ: InputType) -> Self { + Self { + content: String::new(), + cursor: None, + typ, + } + } + + pub fn with_content(self, content: String) -> Self { + Self { content, ..self } + } + + pub fn with_cursor(self, cursor: usize) -> Self { + Self { + cursor: Some(cursor), + ..self + } + } + + fn build_component(&self) -> InputComponent { + let input = InputComponent::from(&self.content); + + match self.cursor { + Some(i) => input.with_cursor(i), + None => input, + } + } + + pub fn build(self) -> InputComponent { + let input = self.build_component(); + + match self.typ { + InputType::FileName(save_option) => input.with_confirm_callback(move |value| { + Event::emit(Event::InputConfirm); + Event::emit(Event::Save((value, save_option))); + }), + InputType::VarValue(name) => input.with_confirm_callback(move |value| { + Event::emit(Event::InputConfirm); + Event::emit(Event::UpdateVar((name.clone(), value))); + }), + } + } +} diff --git a/rq-cli/src/components/response_panel.rs b/rq-cli/src/components/response_panel.rs index 5993079..712a8f1 100644 --- a/rq-cli/src/components/response_panel.rs +++ b/rq-cli/src/components/response_panel.rs @@ -9,12 +9,10 @@ use ratatui::{ use rq_core::request::{mime::Payload, Response, StatusCode}; use std::{fmt::Write, iter}; -use crate::{ - app::FocusState, - event::{Event, InputType}, -}; +use crate::{app::FocusState, event::Event}; use super::{ + input::builder::{InputBuilder, InputType}, message_dialog::{Message, MessageDialog}, BlockComponent, HandleResult, HandleSuccess, }; @@ -176,17 +174,19 @@ impl BlockComponent for ResponsePanel { KeyCode::Down | KeyCode::Char('j') => self.scroll_down(), KeyCode::Up | KeyCode::Char('k') => self.scroll_up(), KeyCode::Char('s') => { - Event::emit(Event::NewInput(( - self.extension().unwrap_or_default(), - InputType::FileName(SaveOption::Body), - ))); + Event::emit(Event::NewInput( + InputBuilder::new(InputType::FileName(SaveOption::Body)) + .with_content(self.extension().unwrap_or_default()) + .with_cursor(0), + )); } KeyCode::Char('S') => { - Event::emit(Event::NewInput(( - self.extension().unwrap_or_default(), - InputType::FileName(SaveOption::All), - ))); + Event::emit(Event::NewInput( + InputBuilder::new(InputType::FileName(SaveOption::All)) + .with_content(self.extension().unwrap_or_default()) + .with_cursor(0), + )); } KeyCode::Char('t') => { self.show_raw = !self.show_raw; diff --git a/rq-cli/src/components/variables/panel.rs b/rq-cli/src/components/variables/panel.rs index 5b9749e..28729a3 100644 --- a/rq-cli/src/components/variables/panel.rs +++ b/rq-cli/src/components/variables/panel.rs @@ -4,8 +4,12 @@ use crossterm::event::KeyCode; use rq_core::parser::variables::TemplateString; use crate::{ - components::{menu::Menu, BlockComponent, HandleSuccess}, - event::{Event, InputType}, + components::{ + input::builder::{InputBuilder, InputType}, + menu::Menu, + BlockComponent, HandleSuccess, + }, + event::Event, }; pub struct VarsPanel { @@ -17,10 +21,10 @@ impl VarsPanel { pub fn new(vars: HashMap) -> Self { let menu = Menu::new(vars.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) .with_confirm_callback(|(name, value)| { - Event::emit(Event::NewInput(( - value.to_string(), - InputType::VarValue(name.clone()), - ))); + Event::emit(Event::NewInput( + InputBuilder::new(InputType::VarValue(name.clone())) + .with_content(value.to_string()), + )) }); Self { vars, menu } diff --git a/rq-cli/src/event.rs b/rq-cli/src/event.rs index 39a24ab..cb6c7fb 100644 --- a/rq-cli/src/event.rs +++ b/rq-cli/src/event.rs @@ -2,27 +2,31 @@ use std::{collections::VecDeque, sync::Mutex}; use once_cell::sync::Lazy; -use crate::{app::FocusState, components::response_panel::SaveOption}; +use crate::{ + app::FocusState, + components::{input::builder::InputBuilder, response_panel::SaveOption}, +}; static EVENT_QUEUE: Lazy>> = Lazy::new(|| Mutex::new(VecDeque::new())); pub enum Event { Focus(FocusState), Save((String, SaveOption)), - NewInput((String, InputType)), + NewInput(InputBuilder), + InputConfirm, InputCancel, + + // Request index in menu SendRequest(usize), + + // Name, value UpdateVar((String, String)), + Key(crossterm::event::KeyEvent), Other(crossterm::event::Event), } -pub enum InputType { - FileName(SaveOption), - VarValue(String), -} - impl Event { pub fn emit(event: Event) { EVENT_QUEUE.lock().unwrap().push_front(event); From 92bd67bedc9e3f0360d1b81a0b3995b2eb8f147b Mon Sep 17 00:00:00 2001 From: TheRealLorenz Date: Tue, 16 Jan 2024 23:35:38 +0100 Subject: [PATCH 22/22] Add tests --- rq-core/src/parser/variables.rs | 67 ++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/rq-core/src/parser/variables.rs b/rq-core/src/parser/variables.rs index 60ab6ee..66bb47d 100644 --- a/rq-core/src/parser/variables.rs +++ b/rq-core/src/parser/variables.rs @@ -113,7 +113,7 @@ impl FromStr for TemplateString { } } -#[derive(Debug, Error)] +#[derive(Debug, Error, PartialEq)] #[error("missing field '{}'", .missing_variable.name)] pub struct FillError { missing_variable: Variable, @@ -206,3 +206,68 @@ impl From> for HashTemplateMap { Self(headers) } } + +#[cfg(test)] +mod tests { + mod variable {} + + mod template_string { + use std::collections::HashMap; + + use crate::parser::variables::{FillError, Fragment, TemplateString, Variable}; + + #[test] + fn test_display() { + let ts = TemplateString::new(vec![Fragment::var("foo")]); + let ts2 = TemplateString::raw("barbar"); + let ts_quoted = TemplateString::raw(" baz "); + + assert_eq!(ts.to_string(), "{{foo}}"); + assert_eq!(ts2.to_string(), "barbar"); + assert_eq!(ts_quoted.to_string(), "\" baz \""); + } + + #[test] + fn test_parse_str() { + let s = "' foo'{{bar}}baz"; + let expected = TemplateString::new(vec![ + Fragment::raw(" foo"), + Fragment::var("bar"), + Fragment::raw("baz"), + ]); + + assert_eq!(s.parse::().unwrap(), expected); + } + + #[test] + fn test_fill() { + let ts = TemplateString::new(vec![ + Fragment::raw(" foo"), + Fragment::var("bar"), + Fragment::raw("baz"), + ]); + let ts2 = TemplateString::raw("foobarbaz"); + let ts3 = TemplateString::new(vec![Fragment::var("baz")]); + let values = + HashMap::from([("bar".into(), "FOOBAR".parse::().unwrap())]); + + assert_eq!(ts.fill(&values).unwrap(), " fooFOOBARbaz"); + assert_eq!(ts2.fill(&values).unwrap(), "foobarbaz"); + assert_eq!( + ts3.fill(&values), + Err(FillError::from(Variable::new("baz"))) + ) + } + + #[test] + fn test_is_empty() { + let ts = TemplateString::new(vec![]); + let ts2 = TemplateString::raw(""); + let ts3 = TemplateString::new(vec![Fragment::raw(""), Fragment::raw("")]); + + assert!(ts.is_empty()); + assert!(ts2.is_empty()); + assert!(ts3.is_empty()); + } + } +}