mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-28 06:14:03 +02:00
Auto-fix JSON comments and trailing commas for HTTP request bodies
Add a per-request "Automatically Fix JSON" toggle (default: on) that strips comments and trailing commas before sending HTTP requests. When disabled, comments are sent as-is and the editor linter flags them as errors. - New JsonBodyEditor component with settings dropdown and dismissible banner - Configurable JSON linter (allowComments/allowTrailingCommas) via lintExtension prop - strip_trailing_commas pass in Rust strip_json_comments pipeline - Fix JSON formatter pulling standalone comments onto previous comma line - Add get_bool_map helper, maybe_strip_json_comments, and related tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
ed6f9f8bd0
commit
c340a7d5ae
@@ -21,3 +21,10 @@ pub fn get_str_map<'a>(v: &'a BTreeMap<String, Value>, key: &str) -> &'a str {
|
||||
Some(v) => v.as_str().unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_bool_map(v: &BTreeMap<String, Value>, key: &str, fallback: bool) -> bool {
|
||||
match v.get(key) {
|
||||
None => fallback,
|
||||
Some(v) => v.as_bool().unwrap_or(fallback),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ use std::collections::BTreeMap;
|
||||
use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
use tokio::io::AsyncRead;
|
||||
use yaak_common::serde::{get_bool, get_str, get_str_map};
|
||||
use yaak_common::serde::{get_bool, get_bool_map, get_str, get_str_map};
|
||||
use yaak_models::models::HttpRequest;
|
||||
use yaak_templates::strip_json_comments::strip_json_comments;
|
||||
use yaak_templates::strip_json_comments::{maybe_strip_json_comments, strip_json_comments};
|
||||
|
||||
pub(crate) const MULTIPART_BOUNDARY: &str = "------YaakFormBoundary";
|
||||
|
||||
@@ -195,7 +195,7 @@ async fn build_body(
|
||||
(build_form_body(&body), Some("application/x-www-form-urlencoded".to_string()))
|
||||
}
|
||||
"multipart/form-data" => build_multipart_body(&body, &headers).await?,
|
||||
_ if body.contains_key("text") => (build_text_body(&body), None),
|
||||
_ if body.contains_key("text") => (build_text_body(&body, body_type), None),
|
||||
t => {
|
||||
warn!("Unsupported body type: {}", t);
|
||||
(None, None)
|
||||
@@ -270,13 +270,20 @@ async fn build_binary_body(
|
||||
}))
|
||||
}
|
||||
|
||||
fn build_text_body(body: &BTreeMap<String, serde_json::Value>) -> Option<SendableBodyWithMeta> {
|
||||
fn build_text_body(body: &BTreeMap<String, serde_json::Value>, body_type: &str) -> Option<SendableBodyWithMeta> {
|
||||
let text = get_str_map(body, "text");
|
||||
if text.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(SendableBodyWithMeta::Bytes(Bytes::from(text.to_string())))
|
||||
return None;
|
||||
}
|
||||
|
||||
let send_comments = get_bool_map(body, "sendJsonComments", false);
|
||||
let text = if !send_comments && body_type == "application/json" {
|
||||
maybe_strip_json_comments(text)
|
||||
} else {
|
||||
text.to_string()
|
||||
};
|
||||
|
||||
Some(SendableBodyWithMeta::Bytes(Bytes::from(text)))
|
||||
}
|
||||
|
||||
fn build_graphql_body(
|
||||
@@ -702,7 +709,7 @@ mod tests {
|
||||
let mut body = BTreeMap::new();
|
||||
body.insert("text".to_string(), json!("Hello, World!"));
|
||||
|
||||
let result = build_text_body(&body);
|
||||
let result = build_text_body(&body, "application/json");
|
||||
match result {
|
||||
Some(SendableBodyWithMeta::Bytes(bytes)) => {
|
||||
assert_eq!(bytes, Bytes::from("Hello, World!"))
|
||||
@@ -716,7 +723,7 @@ mod tests {
|
||||
let mut body = BTreeMap::new();
|
||||
body.insert("text".to_string(), json!(""));
|
||||
|
||||
let result = build_text_body(&body);
|
||||
let result = build_text_body(&body, "application/json");
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
@@ -724,10 +731,57 @@ mod tests {
|
||||
async fn test_text_body_missing() {
|
||||
let body = BTreeMap::new();
|
||||
|
||||
let result = build_text_body(&body);
|
||||
let result = build_text_body(&body, "application/json");
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_text_body_strips_json_comments_by_default() {
|
||||
let mut body = BTreeMap::new();
|
||||
body.insert("text".to_string(), json!("{\n // comment\n \"foo\": \"bar\"\n}"));
|
||||
|
||||
let result = build_text_body(&body, "application/json");
|
||||
match result {
|
||||
Some(SendableBodyWithMeta::Bytes(bytes)) => {
|
||||
let text = String::from_utf8_lossy(&bytes);
|
||||
assert!(!text.contains("// comment"));
|
||||
assert!(text.contains("\"foo\": \"bar\""));
|
||||
}
|
||||
_ => panic!("Expected Some(SendableBody::Bytes)"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_text_body_send_json_comments_when_opted_in() {
|
||||
let mut body = BTreeMap::new();
|
||||
body.insert("text".to_string(), json!("{\n // comment\n \"foo\": \"bar\"\n}"));
|
||||
body.insert("sendJsonComments".to_string(), json!(true));
|
||||
|
||||
let result = build_text_body(&body, "application/json");
|
||||
match result {
|
||||
Some(SendableBodyWithMeta::Bytes(bytes)) => {
|
||||
let text = String::from_utf8_lossy(&bytes);
|
||||
assert!(text.contains("// comment"));
|
||||
}
|
||||
_ => panic!("Expected Some(SendableBody::Bytes)"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_text_body_no_strip_for_non_json() {
|
||||
let mut body = BTreeMap::new();
|
||||
body.insert("text".to_string(), json!("// not json\nsome text"));
|
||||
|
||||
let result = build_text_body(&body, "text/plain");
|
||||
match result {
|
||||
Some(SendableBodyWithMeta::Bytes(bytes)) => {
|
||||
let text = String::from_utf8_lossy(&bytes);
|
||||
assert!(text.contains("// not json"));
|
||||
}
|
||||
_ => panic!("Expected Some(SendableBody::Bytes)"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_form_urlencoded_body() -> Result<()> {
|
||||
let mut body = BTreeMap::new();
|
||||
|
||||
@@ -11,6 +11,7 @@ pub fn format_json(text: &str, tab: &str) -> String {
|
||||
let mut new_json = "".to_string();
|
||||
let mut depth = 0;
|
||||
let mut state = FormatState::None;
|
||||
let mut saw_newline_in_whitespace = false;
|
||||
|
||||
loop {
|
||||
let rest_of_chars = chars.clone();
|
||||
@@ -74,8 +75,8 @@ pub fn format_json(text: &str, tab: &str) -> String {
|
||||
}
|
||||
// Check if the comma handler already added \n + indent
|
||||
let trimmed = new_json.trim_end_matches(|c: char| c == ' ' || c == '\t');
|
||||
if trimmed.ends_with(",\n") {
|
||||
// After comma: undo the newline+indent, make comment trailing
|
||||
if trimmed.ends_with(",\n") && !saw_newline_in_whitespace {
|
||||
// Trailing comment on the same line as comma (e.g. "foo",// comment)
|
||||
new_json.truncate(trimmed.len() - 1);
|
||||
new_json.push(' ');
|
||||
} else if !trimmed.ends_with('\n') && !new_json.is_empty() {
|
||||
@@ -85,6 +86,7 @@ pub fn format_json(text: &str, tab: &str) -> String {
|
||||
new_json.push_str(&comment);
|
||||
new_json.push('\n');
|
||||
new_json.push_str(tab.to_string().repeat(depth).as_str());
|
||||
saw_newline_in_whitespace = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -180,8 +182,12 @@ pub fn format_json(text: &str, tab: &str) -> String {
|
||||
|| current_char == '\t'
|
||||
|| current_char == '\r'
|
||||
{
|
||||
if current_char == '\n' {
|
||||
saw_newline_in_whitespace = true;
|
||||
}
|
||||
// Don't add these
|
||||
} else {
|
||||
saw_newline_in_whitespace = false;
|
||||
new_json.push(current_char);
|
||||
}
|
||||
}
|
||||
@@ -500,6 +506,26 @@ mod tests {
|
||||
"foo": "// not a comment",
|
||||
"bar": "/* also not */"
|
||||
}
|
||||
"#
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_comment_on_line_after_comma() {
|
||||
assert_eq!(
|
||||
format_json(
|
||||
r#"{
|
||||
"a": "aaa",
|
||||
// "b": "bbb"
|
||||
}"#,
|
||||
" "
|
||||
),
|
||||
r#"
|
||||
{
|
||||
"a": "aaa",
|
||||
// "b": "bbb"
|
||||
}
|
||||
"#
|
||||
.trim()
|
||||
);
|
||||
|
||||
@@ -113,11 +113,67 @@ pub fn strip_json_comments(text: &str) -> String {
|
||||
}
|
||||
|
||||
// Remove lines that are now empty (were comment-only lines)
|
||||
result
|
||||
let result = result
|
||||
.lines()
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n")
|
||||
.join("\n");
|
||||
|
||||
// Remove trailing commas before } or ]
|
||||
strip_trailing_commas(&result)
|
||||
}
|
||||
|
||||
/// Removes trailing commas before closing braces/brackets, respecting strings.
|
||||
fn strip_trailing_commas(text: &str) -> String {
|
||||
let mut result = String::with_capacity(text.len());
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let mut i = 0;
|
||||
let mut in_string = false;
|
||||
|
||||
while i < chars.len() {
|
||||
let ch = chars[i];
|
||||
|
||||
if in_string {
|
||||
result.push(ch);
|
||||
match ch {
|
||||
'"' => in_string = false,
|
||||
'\\' => {
|
||||
i += 1;
|
||||
if i < chars.len() {
|
||||
result.push(chars[i]);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ch == '"' {
|
||||
in_string = true;
|
||||
result.push(ch);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ch == ',' {
|
||||
// Look ahead past whitespace/newlines for } or ]
|
||||
let mut j = i + 1;
|
||||
while j < chars.len() && chars[j].is_whitespace() {
|
||||
j += 1;
|
||||
}
|
||||
if j < chars.len() && (chars[j] == '}' || chars[j] == ']') {
|
||||
// Skip the comma
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result.push(ch);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -232,4 +288,31 @@ mod tests {
|
||||
}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trailing_comma_after_comment_removed() {
|
||||
assert_eq!(
|
||||
strip_json_comments(r#"{
|
||||
"a": "aaa",
|
||||
// "b": "bbb"
|
||||
}"#),
|
||||
r#"{
|
||||
"a": "aaa"
|
||||
}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trailing_comma_in_array() {
|
||||
assert_eq!(
|
||||
strip_json_comments(r#"[1, 2, /* 3 */]"#),
|
||||
r#"[1, 2]"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_comma_inside_string_preserved() {
|
||||
let input = r#"{"a": "hello,}"#;
|
||||
assert_eq!(strip_json_comments(input), input);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user