Skip to content

Commit db0fca6

Browse files
committed
Add --from-gron support (pt2 )
1 parent f2a8adb commit db0fca6

5 files changed

Lines changed: 192 additions & 33 deletions

File tree

src/input_handler.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ fn handle_buffer<R: Read>(
6565
&& !input_params.from_json5
6666
&& !input_params.from_toml
6767
&& !input_params.from_yaml
68+
&& !input_params.from_gron
6869
{
6970
// Determine thread pool size
7071
anyhow::ensure!(
@@ -190,6 +191,7 @@ fn handle_json(
190191
input_params.from_json5,
191192
input_params.from_toml,
192193
input_params.from_yaml,
194+
input_params.from_gron,
193195
)
194196
.context("Failed to parse JSON input")?;
195197

src/json2cel.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ pub fn json_to_cel_variables(
1515
from_json5: bool,
1616
from_toml: bool,
1717
from_yaml: bool,
18+
from_gron: bool,
1819
) -> Result<BTreeMap<String, CelValue>, serde_json::Error> {
19-
let json_value: JsonValue = if !slurp && !from_json5 && !from_toml && !from_yaml {
20+
let json_value: JsonValue = if !slurp && !from_json5 && !from_toml && !from_yaml && !from_gron {
2021
serde_json::from_str(json_str)?
2122
} else if from_json5 {
2223
json5::from_str(json_str).map_err(serde_json::Error::custom)?
@@ -57,6 +58,18 @@ pub fn json_to_cel_variables(
5758
"Binary was compiled without YAML support",
5859
));
5960
}
61+
} else if from_gron {
62+
#[cfg(feature = "greppable")]
63+
{
64+
crate::gron_to_json(json_str).map_err(serde_json::Error::custom)?
65+
}
66+
67+
#[cfg(not(feature = "greppable"))]
68+
{
69+
return Err(serde_json::Error::custom(
70+
"Binary was compiled without greppable support",
71+
));
72+
}
6073
} else if slurp {
6174
slurp_json_lines(Some(json_str))?
6275
} else {

src/json2cel_test.rs

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,38 @@ const NO_SLURP: bool = false;
55
const NO_JSON5: bool = false;
66
const NO_TOML: bool = false;
77
const NO_YAML: bool = false;
8+
const NO_GRON: bool = false;
89

910
#[test]
1011
fn test_null() {
11-
let vars =
12-
json_to_cel_variables("null", ROOT_VAR, NO_SLURP, NO_JSON5, NO_TOML, NO_YAML).unwrap();
12+
let vars = json_to_cel_variables(
13+
"null", ROOT_VAR, NO_SLURP, NO_JSON5, NO_TOML, NO_YAML, NO_GRON,
14+
)
15+
.unwrap();
1316
assert!(matches!(vars.get("this").unwrap(), CelValue::Null));
1417
}
1518

1619
#[test]
1720
fn test_number() {
18-
let vars = json_to_cel_variables("42", ROOT_VAR, NO_SLURP, NO_JSON5, NO_TOML, NO_YAML).unwrap();
21+
let vars = json_to_cel_variables(
22+
"42", ROOT_VAR, NO_SLURP, NO_JSON5, NO_TOML, NO_YAML, NO_GRON,
23+
)
24+
.unwrap();
1925
assert!(matches!(vars.get("this").unwrap(), CelValue::Int(42)));
2026
}
2127

2228
#[test]
2329
fn test_string() {
24-
let vars = json_to_cel_variables(r#""hello""#, ROOT_VAR, NO_SLURP, NO_JSON5, NO_TOML, NO_YAML)
25-
.unwrap();
30+
let vars = json_to_cel_variables(
31+
r#""hello""#,
32+
ROOT_VAR,
33+
NO_SLURP,
34+
NO_JSON5,
35+
NO_TOML,
36+
NO_YAML,
37+
NO_GRON,
38+
)
39+
.unwrap();
2640
if let CelValue::String(s) = vars.get("this").unwrap() {
2741
assert_eq!(s.as_str(), "hello");
2842
} else {
@@ -32,15 +46,25 @@ fn test_string() {
3246

3347
#[test]
3448
fn test_bool() {
35-
let vars =
36-
json_to_cel_variables("true", ROOT_VAR, NO_SLURP, NO_JSON5, NO_TOML, NO_YAML).unwrap();
49+
let vars = json_to_cel_variables(
50+
"true", ROOT_VAR, NO_SLURP, NO_JSON5, NO_TOML, NO_YAML, NO_GRON,
51+
)
52+
.unwrap();
3753
assert!(matches!(vars.get("this").unwrap(), CelValue::Bool(true)));
3854
}
3955

4056
#[test]
4157
fn test_array() {
42-
let vars =
43-
json_to_cel_variables("[1, 2, 3]", ROOT_VAR, NO_SLURP, NO_JSON5, NO_TOML, NO_YAML).unwrap();
58+
let vars = json_to_cel_variables(
59+
"[1, 2, 3]",
60+
ROOT_VAR,
61+
NO_SLURP,
62+
NO_JSON5,
63+
NO_TOML,
64+
NO_YAML,
65+
NO_GRON,
66+
)
67+
.unwrap();
4468
if let CelValue::List(list) = vars.get("this").unwrap() {
4569
assert_eq!(list.len(), 3);
4670
} else {
@@ -57,6 +81,7 @@ fn test_object() {
5781
NO_JSON5,
5882
NO_TOML,
5983
NO_YAML,
84+
NO_GRON,
6085
)
6186
.unwrap();
6287

@@ -76,6 +101,7 @@ fn test_nested_object() {
76101
NO_JSON5,
77102
NO_TOML,
78103
NO_YAML,
104+
NO_GRON,
79105
)
80106
.unwrap();
81107

@@ -107,8 +133,16 @@ fn test_json5_with_comment() {
107133
"x": 42
108134
}
109135
"#;
110-
let vars =
111-
json_to_cel_variables(json5_input, ROOT_VAR, NO_SLURP, true, NO_TOML, NO_YAML).unwrap();
136+
let vars = json_to_cel_variables(
137+
json5_input,
138+
ROOT_VAR,
139+
NO_SLURP,
140+
true,
141+
NO_TOML,
142+
NO_YAML,
143+
NO_GRON,
144+
)
145+
.unwrap();
112146

113147
if let CelValue::Map(map) = vars.get("this").unwrap() {
114148
let x_key = Key::String(Arc::new("x".to_string()));

src/ungron.rs

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
use anyhow::{anyhow, Context, Result};
1+
use anyhow::{Context, Result, anyhow};
22
use resast::prelude::*;
33
use ressa::Parser;
4-
use serde_json::{json, Value as JsonValue};
4+
use serde_json::{Value as JsonValue, json};
55

66
pub fn gron_to_json(input: &str) -> Result<JsonValue> {
77
let mut root = JsonValue::Null;
88

9-
for (line_num, line) in input.lines().enumerate().filter(|(_, l)| !l.trim().is_empty()) {
9+
for (line_num, line) in input
10+
.lines()
11+
.enumerate()
12+
.filter(|(_, l)| !l.trim().is_empty())
13+
{
1014
let line = line.trim();
1115
parse_and_apply_line(line, &mut root)
1216
.with_context(|| format!("Error on line {}: {}", line_num + 1, line))?;
@@ -16,26 +20,29 @@ pub fn gron_to_json(input: &str) -> Result<JsonValue> {
1620

1721
fn parse_and_apply_line(line: &str, root: &mut JsonValue) -> Result<()> {
1822
let mut parser = Parser::new(line).map_err(|e| anyhow!("Parser init failed: {:?}", e))?;
19-
23+
2024
let part = parser
2125
.next()
2226
.ok_or_else(|| anyhow!("Empty line"))?
2327
.map_err(|e| anyhow!("Parse error: {:?}", e))?;
24-
28+
2529
if let ProgramPart::Stmt(Stmt::Expr(Expr::Assign(assign))) = part {
2630
let path = extract_path(&assign.left)?;
2731
let value = extract_value(&assign.right)?;
2832
set_value_at_path(root, &path, value)?;
2933
Ok(())
3034
} else {
31-
// gron occasionally emits 'const' or 'var' depending on flags,
35+
// gron occasionally emits 'const' or 'var' depending on flags,
3236
// but standard gron is pure assignment.
3337
Err(anyhow!("Expected assignment (e.g., json.a = 1)"))
3438
}
3539
}
3640

3741
#[derive(Debug)]
38-
enum PathSegment { Property(String), Index(usize) }
42+
enum PathSegment {
43+
Property(String),
44+
Index(usize),
45+
}
3946

4047
fn extract_path(left: &AssignLeft) -> Result<Vec<PathSegment>> {
4148
let mut segments = Vec::new();
@@ -50,8 +57,8 @@ fn extract_path(left: &AssignLeft) -> Result<Vec<PathSegment>> {
5057
while let Expr::Member(mem) = current_expr {
5158
let seg = match &*mem.property {
5259
Expr::Ident(i) => PathSegment::Property(i.name.to_string()),
53-
Expr::Lit(Lit::String(StringLit::Double(s))) |
54-
Expr::Lit(Lit::String(StringLit::Single(s))) => PathSegment::Property(s.to_string()),
60+
Expr::Lit(Lit::String(StringLit::Double(s)))
61+
| Expr::Lit(Lit::String(StringLit::Single(s))) => PathSegment::Property(s.to_string()),
5562
Expr::Lit(Lit::Number(n)) => PathSegment::Index(n.parse()?),
5663
_ => return Err(anyhow!("Unsupported path segment type")),
5764
};
@@ -66,7 +73,7 @@ fn extract_path(left: &AssignLeft) -> Result<Vec<PathSegment>> {
6673
}
6774
}
6875

69-
segments.reverse();
76+
segments.reverse();
7077
Ok(segments)
7178
}
7279

@@ -82,9 +89,10 @@ fn extract_value(expr: &Expr) -> Result<JsonValue> {
8289
} else {
8390
Ok(json!(n.parse::<f64>()?))
8491
}
85-
},
86-
Lit::String(StringLit::Double(s)) |
87-
Lit::String(StringLit::Single(s)) => Ok(json!(s.to_string())),
92+
}
93+
Lit::String(StringLit::Double(s)) | Lit::String(StringLit::Single(s)) => {
94+
Ok(json!(s.to_string()))
95+
}
8896
_ => Err(anyhow!("Unsupported literal")),
8997
},
9098
Expr::Unary(u) if u.operator == UnaryOp::Minus => {
@@ -96,9 +104,9 @@ fn extract_value(expr: &Expr) -> Result<JsonValue> {
96104
} else {
97105
Err(anyhow!("Cannot negate non-numeric value"))
98106
}
99-
},
100-
Expr::Array(_) => Ok(json!([])),
101-
Expr::Obj(_) => Ok(json!({})),
107+
}
108+
Expr::Array(_) => Ok(json!([])),
109+
Expr::Obj(_) => Ok(json!({})),
102110
_ => Err(anyhow!("Value type not supported")),
103111
}
104112
}
@@ -109,11 +117,19 @@ fn set_value_at_path(root: &mut JsonValue, path: &[PathSegment], value: JsonValu
109117
for seg in path {
110118
match seg {
111119
PathSegment::Property(p) => {
112-
if !cur.is_object() { *cur = json!({}); }
113-
cur = cur.as_object_mut().unwrap().entry(p.clone()).or_insert(JsonValue::Null);
120+
if !cur.is_object() {
121+
*cur = json!({});
122+
}
123+
cur = cur
124+
.as_object_mut()
125+
.unwrap()
126+
.entry(p.clone())
127+
.or_insert(JsonValue::Null);
114128
}
115129
PathSegment::Index(i) => {
116-
if !cur.is_array() { *cur = json!([]); }
130+
if !cur.is_array() {
131+
*cur = json!([]);
132+
}
117133
let arr = cur.as_array_mut().unwrap();
118134
if *i >= arr.len() {
119135
arr.resize(*i + 1, JsonValue::Null);
@@ -124,8 +140,9 @@ fn set_value_at_path(root: &mut JsonValue, path: &[PathSegment], value: JsonValu
124140
}
125141

126142
// Logic Fix: Don't overwrite an existing object/array with an empty one.
127-
if (value.is_object() && value.as_object().unwrap().is_empty() && cur.is_object()) ||
128-
(value.is_array() && value.as_array().unwrap().is_empty() && cur.is_array()) {
143+
if (value.is_object() && value.as_object().unwrap().is_empty() && cur.is_object())
144+
|| (value.is_array() && value.as_array().unwrap().is_empty() && cur.is_array())
145+
{
129146
return Ok(());
130147
}
131148

tests/golden.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,99 @@ json.y = 20;
764764
"#
765765
);
766766

767+
// Ungron (from-gron) tests
768+
#[cfg(feature = "greppable")]
769+
test!(
770+
from_gron_simple,
771+
&["--from-gron", "-S", "this"],
772+
r#"json = {};
773+
json.age = 30;
774+
json.name = "Alice";
775+
"#,
776+
r#"{"age":30,"name":"Alice"}"#
777+
);
778+
779+
#[cfg(feature = "greppable")]
780+
test!(
781+
from_gron_nested,
782+
&["--from-gron", "-S", "this"],
783+
r#"json = {};
784+
json.id = 1;
785+
json.person = {};
786+
json.person.city = "NYC";
787+
json.person.name = "Bob";
788+
"#,
789+
r#"{"id":1,"person":{"city":"NYC","name":"Bob"}}"#
790+
);
791+
792+
#[cfg(feature = "greppable")]
793+
test!(
794+
from_gron_arrays,
795+
&["--from-gron", "-S", "this"],
796+
r#"json = {};
797+
json.items = [];
798+
json.items[0] = "first";
799+
json.items[1] = "second";
800+
json.items[2] = "third";
801+
"#,
802+
r#"{"items":["first","second","third"]}"#
803+
);
804+
805+
#[cfg(feature = "greppable")]
806+
test!(
807+
from_gron_sparse_array,
808+
&["--from-gron", "-S", "this"],
809+
r#"json.likes = [];
810+
json.likes[0] = "code";
811+
json.likes[2] = "meat";
812+
"#,
813+
r#"{"likes":["code",null,"meat"]}"#
814+
);
815+
816+
#[cfg(feature = "greppable")]
817+
test!(
818+
from_gron_mixed_types,
819+
&["--from-gron", "-S", "this"],
820+
r#"json = {};
821+
json.integer = 42;
822+
json.float = 3.14;
823+
json.negative = -5;
824+
json.isTrue = true;
825+
json.isFalse = false;
826+
json.nothing = null;
827+
"#,
828+
r#"{"float":3.14,"integer":42,"isFalse":false,"isTrue":true,"negative":-5,"nothing":null}"#
829+
);
830+
831+
// Tricky edge cases
832+
#[cfg(feature = "greppable")]
833+
test!(
834+
from_gron_special_keys,
835+
&["--from-gron", "-S", "this"],
836+
r#"json = {};
837+
json["key-with-dashes"] = "value1";
838+
json["key.with.dots"] = "value2";
839+
json["key with spaces"] = "value3";
840+
json["123numeric"] = "value4";
841+
"#,
842+
r#"{"123numeric":"value4","key with spaces":"value3","key-with-dashes":"value1","key.with.dots":"value2"}"#
843+
);
844+
845+
#[cfg(feature = "greppable")]
846+
test!(
847+
from_gron_deeply_nested,
848+
&["--from-gron", "-S", "this"],
849+
r#"json = {};
850+
json.a = {};
851+
json.a.b = {};
852+
json.a.b.c = {};
853+
json.a.b.c.d = [];
854+
json.a.b.c.d[0] = {};
855+
json.a.b.c.d[0].e = "deep";
856+
"#,
857+
r#"{"a":{"b":{"c":{"d":[{"e":"deep"}]}}}}"#
858+
);
859+
767860
#[test]
768861
fn test_boolean_false_exit_code() -> io::Result<()> {
769862
let mut child = process::Command::new(env!("CARGO_BIN_EXE_celq"))

0 commit comments

Comments
 (0)