Skip to content

Commit ec4480d

Browse files
committed
Add --from-xml
1 parent 4554394 commit ec4480d

8 files changed

Lines changed: 218 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Changelog
22
=========
33

4+
[v0.3.3](https://github.com/IvanIsCoding/celq/releases/tag/v0.3.1) - 2026-02-05
5+
6+
### Added
7+
8+
* Added XML support via the `--from-xml` flag.
9+
410
[v0.3.2](https://github.com/IvanIsCoding/celq/releases/tag/v0.3.2) - 2026-01-31
511

612
### Miscellaneous

Cargo.lock

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ name = "celq"
3333
path = "src/documentation.rs"
3434

3535
[features]
36-
default = ["mimalloc", "from-toml", "from-yaml", "greppable"]
36+
default = ["mimalloc", "from-toml", "from-yaml", "greppable", "from-xml"]
3737
from-toml = ["dep:toml"]
3838
from-yaml = ["dep:serde-saphyr"]
39+
from-xml = ["dep:xml2json-rs"]
3940
greppable = ["dep:ressa", "dep:resast"]
4041

4142
[dependencies]
@@ -51,6 +52,7 @@ toml = { version = "=0.9.8", default-features = false, features = ["parse", "ser
5152
serde-saphyr = { version = "=0.0.16", default-features = false, optional = true }
5253
ressa = { version = "=0.8.2", optional = true}
5354
resast = { version = "=0.5.0", optional = true }
55+
xml2json-rs = {version = "=1.0.1", optional = true }
5456

5557
[dev-dependencies]
5658
tempfile = "3"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ celq -n --arg='fruit:string=apple' 'fruit.contains("a")'
1919
# Outputs: true
2020
```
2121

22-
Popular configuration formats such as JSON5, YAML, and TOML are supported. The closely related format NDJSON is also supported.
22+
Popular configuration formats such as JSON5, YAML, TOML, and XML are supported. The closely related format NDJSON is also supported.
2323

2424
For detailed usage examples and recipes, see the [manual](https://docs.rs/celq/latest/celq/).
2525

docs/manual.md

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ See the [installation guide](`crate::installation_guide`) for installation instr
3939
```none
4040
A CEL command-line query tool for JSON data
4141
42-
Usage: celq [OPTIONS] <expr|--from-file <FILE>>
42+
Usage: celq [OPTIONS] [expr]
4343
4444
Arguments:
45-
[expr] CEL expression to evaluate
45+
[expr] CEL expression to evaluate [default: this]
4646
4747
Options:
4848
-a, --arg <name:type=value> Define argument variables, types, and values. Format: name:type=value. Supported types: int, uint, float, bool, string
@@ -53,6 +53,7 @@ Options:
5353
--from-json5 Parse input as JSON5 instead of JSON
5454
--from-toml Parse input as TOML instead of JSON
5555
--from-yaml Parse input as YAML instead of JSON
56+
--from-xml Parse input as XML instead of JSON
5657
--from-gron Parse input as gron (greppable output) instead of JSON
5758
-j, --jobs <N> Parallelism level for NDJSON inputs (number of threads, -1 for all available) [default: 1]
5859
-R, --root-var <ROOT_VAR> Variable name for the root JSON input [default: this]
@@ -144,8 +145,8 @@ This file contains the simplified response from the Yahoo Finance Unofficial JSO
144145
* [this keyword](#this-keyword)
145146
* [Writing Files](#writing-files)
146147
* [Output JSON](#output-json)
147-
* [Slicing lists](#slicing-lists)
148148
* [Reading CEL from a file](#reading-cel-from-a-file)
149+
* [Slicing lists](#slicing-lists)
149150
* [Dealing with NDJSON](#dealing-with-ndjson)
150151
* [Slurping](#slurping)
151152
* [Logical Calculator](#logical-calculator)
@@ -156,6 +157,7 @@ This file contains the simplified response from the Yahoo Finance Unofficial JSO
156157
* [TOML Support](#toml-support)
157158
* [YAML Support](#yaml-support)
158159
* [YAML with multiple documents](#yaml-with-multiple-documents)
160+
* [XML Support](#xml-support)
159161
* [Pretty Printing](#pretty-printing)
160162
* [Raw Output](#raw-output)
161163
* [Grep friendly output](#grep-friendly-output)
@@ -444,6 +446,59 @@ The document gets parsed as a list of documents. To access the `tags` field of t
444446
celq --from-yaml 'this[1].tags' < multi.yaml
445447
```
446448

449+
### XML Support
450+
451+
`celq` supports XML via the `--froml-xml` flag. Take `example.xml`:
452+
453+
<details>
454+
<summary>example.xml</summary>
455+
456+
```xml
457+
<?xml version="1.0"?>
458+
<items>
459+
<item id="1">
460+
<name>apple</name>
461+
<price>1.25</price>
462+
</item>
463+
<item id="2">
464+
<name>banana</name>
465+
<price>0.75</price>
466+
</item>
467+
</items>
468+
```
469+
</details>
470+
471+
Running `celq --from-xml -S < example.xml` converts it to the following:
472+
473+
<details>
474+
<summary>example_xml.json</summary>
475+
476+
```json
477+
{
478+
"items": {
479+
"item": [
480+
{
481+
"$": {
482+
"id": "1"
483+
},
484+
"name": "apple",
485+
"price": "1.25"
486+
},
487+
{
488+
"$": {
489+
"id": "2"
490+
},
491+
"name": "banana",
492+
"price": "0.75"
493+
}
494+
]
495+
}
496+
}
497+
```
498+
</details>
499+
500+
`celq`'s XML parser does not try to convert types and puts attributes in the `$` field.
501+
447502
### Pretty Printing
448503
449504
`celq` by default uses a compact output. This is a contrast to `jq` where the compact output is an opt-in with the `-c` flag.

src/cli.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub enum InputFormat {
2424
Json5,
2525
Toml,
2626
Yaml,
27+
Xml,
2728
Gron,
2829
}
2930

@@ -129,6 +130,10 @@ pub struct Cli {
129130
#[arg(long = "from-yaml")]
130131
pub from_yaml: bool,
131132

133+
/// Parse input as XML instead of JSON
134+
#[arg(long = "from-xml")]
135+
pub from_xml: bool,
136+
132137
/// Parse input as gron (greppable output) instead of JSON
133138
#[arg(long = "from-gron")]
134139
pub from_gron: bool,
@@ -184,6 +189,8 @@ impl Cli {
184189
InputFormat::Yaml
185190
} else if self.from_toml {
186191
InputFormat::Toml
192+
} else if self.from_xml {
193+
InputFormat::Xml
187194
} else if self.from_json5 {
188195
InputFormat::Json5
189196
} else if self.slurp {

src/json2cel.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,23 @@ pub fn json_to_cel_variables(
6262
));
6363
}
6464
}
65+
InputFormat::Xml => {
66+
#[cfg(feature = "from-xml")]
67+
{
68+
xml2json_rs::JsonConfig::new()
69+
.explicit_array(false)
70+
.finalize()
71+
.build_from_xml(json_str)
72+
.map_err(serde_json::Error::custom)?
73+
}
74+
75+
#[cfg(not(feature = "from-xml"))]
76+
{
77+
return Err(serde_json::Error::custom(
78+
"Binary was compiled without XML support",
79+
));
80+
}
81+
}
6582
InputFormat::Gron => {
6683
#[cfg(feature = "greppable")]
6784
{

tests/golden.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,3 +951,106 @@ fn test_slice_fails_without_extensions() -> io::Result<()> {
951951

952952
Ok(())
953953
}
954+
955+
// XML tests
956+
957+
// XML with nested elements
958+
#[cfg(feature = "from-xml")]
959+
test!(
960+
xml_nested_elements,
961+
&[
962+
"--from-xml",
963+
"this.database.host + ':' + string(this.database.port)"
964+
],
965+
r#"<database>
966+
<host>localhost</host>
967+
<port>5432</port>
968+
</database>"#,
969+
"\"localhost:5432\""
970+
);
971+
972+
#[cfg(feature = "from-xml")]
973+
test!(
974+
xml_array_of_elements,
975+
&["--from-xml", "this.servers.server.map(s, s.ip)"],
976+
r#"<servers>
977+
<server>
978+
<ip>192.168.1.1</ip>
979+
<name>alpha</name>
980+
</server>
981+
<server>
982+
<ip>192.168.1.2</ip>
983+
<name>beta</name>
984+
</server>
985+
</servers>"#,
986+
"[\"192.168.1.1\",\"192.168.1.2\"]"
987+
);
988+
989+
#[cfg(feature = "from-xml")]
990+
test!(
991+
xml_with_attributes,
992+
&[
993+
"--from-xml",
994+
"this.user.name + ' <' + this.user.email + '>'"
995+
],
996+
r#"<user>
997+
<name>Alice</name>
998+
<email>alice@example.com</email>
999+
</user>"#,
1000+
"\"Alice <alice@example.com>\""
1001+
);
1002+
1003+
#[cfg(feature = "from-xml")]
1004+
test!(
1005+
xml_simple_structure,
1006+
&["--from-xml", "int(this.point.x) + int(this.point.y)"],
1007+
r#"<point>
1008+
<x>10</x>
1009+
<y>20</y>
1010+
</point>"#,
1011+
"30"
1012+
);
1013+
1014+
#[cfg(feature = "from-xml")]
1015+
test!(
1016+
xml_text_content,
1017+
&["--from-xml", "this.message"],
1018+
r#"<message>Hello, World!</message>"#,
1019+
r#""Hello, World!""#
1020+
);
1021+
1022+
#[cfg(feature = "from-xml")]
1023+
test!(
1024+
xml_mixed_content,
1025+
&[
1026+
"--from-xml",
1027+
"this.person.name + ' is ' + string(this.person.age)"
1028+
],
1029+
r#"<person>
1030+
<name>Bob</name>
1031+
<age>25</age>
1032+
</person>"#,
1033+
"\"Bob is 25\""
1034+
);
1035+
1036+
#[cfg(feature = "from-xml")]
1037+
test!(
1038+
xml_to_pretty_sorted_json,
1039+
&["--from-xml", "--sort-keys", "--pretty-print", "this"],
1040+
r#"<root>
1041+
<person>
1042+
<name>Alice</name>
1043+
<age>30</age>
1044+
</person>
1045+
<id>1</id>
1046+
</root>"#,
1047+
r#"{
1048+
"root": {
1049+
"id": "1",
1050+
"person": {
1051+
"age": "30",
1052+
"name": "Alice"
1053+
}
1054+
}
1055+
}"#
1056+
);

0 commit comments

Comments
 (0)