Skip to content

Commit 3c9eab0

Browse files
authored
feat: CST and first class manipulation API (#41)
1 parent a3690d0 commit 3c9eab0

22 files changed

+3899
-213
lines changed

.github/workflows/ci.yml

+1-8
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,12 @@ jobs:
1919
RUST_BACKTRACE: full
2020

2121
steps:
22-
- uses: actions/checkout@v2
22+
- uses: actions/checkout@v4
2323
- uses: dsherret/rust-toolchain-file@v1
2424
- uses: Swatinem/rust-cache@v2
2525
with:
2626
save-if: ${{ github.ref == 'refs/heads/main' }}
2727

28-
- name: Build debug
29-
if: matrix.config.kind == 'test_debug'
30-
run: cargo build --verbose
31-
- name: Build release
32-
if: matrix.config.kind == 'test_release'
33-
run: cargo build --release --verbose
34-
3528
- name: Test debug
3629
if: matrix.config.kind == 'test_debug'
3730
run: |

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
.vscode
12
target
23
Cargo.lock

Cargo.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@ license = "MIT"
77
description = "JSONC parser."
88
repository = "https://github.com/dprint/jsonc-parser"
99

10+
[package.metadata.docs.rs]
11+
all-features = true
12+
1013
[dependencies]
11-
indexmap = { version = "2.0.2", optional = true }
14+
indexmap = { version = "2.2.6", optional = true }
1215
serde_json = { version = "1.0", optional = true }
1316

1417
[features]
18+
cst = []
1519
preserve_order = ["indexmap", "serde_json/preserve_order"]
1620
serde = ["serde_json"]
1721

README.md

+33-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
[![](https://img.shields.io/crates/v/jsonc-parser.svg)](https://crates.io/crates/jsonc-parser)
44

5-
JSONC parser implemented in Rust.
5+
JSONC parsing and manipulation for Rust.
66

7-
## Example
7+
## Parsing
88

99
To a simple `JsonValue`:
1010

@@ -28,6 +28,37 @@ let parse_result = parse_to_ast(r#"{ "test": 5 } // test"#, &CollectOptions {
2828
// ...inspect parse_result for value, tokens, and comments here...
2929
```
3030

31+
## Manipulation (CST)
32+
33+
When enabling the `cst` cargo feature, parsing to a CST provides a first class manipulation API:
34+
35+
```rs
36+
use jsonc_parser::cst::CstRootNode;
37+
use jsonc_parser::ParseOptions;
38+
use jsonc_parser::json;
39+
40+
let json_text = r#"{
41+
// comment
42+
"data": 123
43+
}"#;
44+
45+
let root = CstRootNode::parse(json_text, &ParseOptions::default()).unwrap();
46+
let root_obj = root.root_value().unwrap().as_object().unwrap();
47+
48+
root_obj.get("data").unwrap().set_value(json!({
49+
"nested": true
50+
}));
51+
root_obj.append("new_key", json!([456, 789, false]));
52+
53+
assert_eq!(root.to_string(), r#"{
54+
// comment
55+
"data": {
56+
"nested": true,
57+
},
58+
"new_key": [456, 789, false]
59+
}"#);
60+
```
61+
3162
## Serde
3263

3364
If you enable the `"serde"` feature as follows:

dprint.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"incremental": true,
32
"indentWidth": 2,
43
"exec": {
54
"commands": [{
@@ -13,7 +12,7 @@
1312
"./benches/json"
1413
],
1514
"plugins": [
16-
"https://plugins.dprint.dev/markdown-0.15.2.wasm",
17-
"https://plugins.dprint.dev/exec-0.4.3.json@42343548b8022c99b1d750be6b894fe6b6c7ee25f72ae9f9082226dd2e515072"
15+
"https://plugins.dprint.dev/markdown-0.17.8.wasm",
16+
"https://plugins.dprint.dev/exec-0.5.0.json@8d9972eee71fa1590e04873540421f3eda7674d0f1aae3d7c788615e7b7413d0"
1817
]
1918
}

rust-toolchain.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[toolchain]
2-
channel = "1.71.0"
2+
channel = "1.81.0"
33
components = ["clippy", "rustfmt"]

src/ast.rs

+25-25
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ impl<'a> Comment<'a> {
411411
}
412412

413413
impl<'a> Ranged for Comment<'a> {
414-
fn range(&self) -> &Range {
414+
fn range(&self) -> Range {
415415
match self {
416416
Comment::Line(line) => line.range(),
417417
Comment::Block(line) => line.range(),
@@ -445,7 +445,7 @@ impl<'a, 'b> From<&'b ObjectPropName<'a>> for Node<'a, 'b> {
445445
}
446446

447447
impl<'a> Ranged for ObjectPropName<'a> {
448-
fn range(&self) -> &Range {
448+
fn range(&self) -> Range {
449449
match self {
450450
ObjectPropName::String(lit) => lit.range(),
451451
ObjectPropName::Word(lit) => lit.range(),
@@ -456,29 +456,29 @@ impl<'a> Ranged for ObjectPropName<'a> {
456456
// Implement Traits
457457

458458
macro_rules! impl_ranged {
459-
($($node_name:ident),*) => {
460-
$(
461-
impl Ranged for $node_name {
462-
fn range(&self) -> &Range {
463-
&self.range
464-
}
465-
}
466-
)*
467-
};
459+
($($node_name:ident),*) => {
460+
$(
461+
impl Ranged for $node_name {
462+
fn range(&self) -> Range {
463+
self.range
464+
}
465+
}
466+
)*
467+
};
468468
}
469469

470470
impl_ranged![BooleanLit, NullKeyword];
471471

472472
macro_rules! impl_ranged_lifetime {
473-
($($node_name:ident),*) => {
474-
$(
475-
impl<'a> Ranged for $node_name<'a> {
476-
fn range(&self) -> &Range {
477-
&self.range
478-
}
479-
}
480-
)*
481-
};
473+
($($node_name:ident),*) => {
474+
$(
475+
impl<'a> Ranged for $node_name<'a> {
476+
fn range(&self) -> Range {
477+
self.range
478+
}
479+
}
480+
)*
481+
};
482482
}
483483

484484
impl_ranged_lifetime![
@@ -493,7 +493,7 @@ impl_ranged_lifetime![
493493
];
494494

495495
impl<'a> Ranged for Value<'a> {
496-
fn range(&self) -> &Range {
496+
fn range(&self) -> Range {
497497
match self {
498498
Value::Array(node) => node.range(),
499499
Value::BooleanLit(node) => node.range(),
@@ -506,7 +506,7 @@ impl<'a> Ranged for Value<'a> {
506506
}
507507

508508
impl<'a, 'b> Ranged for Node<'a, 'b> {
509-
fn range(&self) -> &Range {
509+
fn range(&self) -> Range {
510510
match self {
511511
Node::StringLit(node) => node.range(),
512512
Node::NumberLit(node) => node.range(),
@@ -586,11 +586,11 @@ mod test {
586586
assert_eq!(obj.properties.len(), 2);
587587
assert_eq!(obj.take_number("prop"), None);
588588
assert_eq!(obj.properties.len(), 2);
589-
assert_eq!(obj.take_string("prop").is_some(), true);
589+
assert!(obj.take_string("prop").is_some());
590590
assert_eq!(obj.properties.len(), 1);
591591
assert_eq!(obj.take("something"), None);
592592
assert_eq!(obj.properties.len(), 1);
593-
assert_eq!(obj.take("other").is_some(), true);
593+
assert!(obj.take("other").is_some());
594594
assert_eq!(obj.properties.len(), 0);
595595
}
596596

@@ -604,7 +604,7 @@ mod test {
604604

605605
assert_eq!(obj.properties.len(), 1);
606606
assert_eq!(obj.get_string("asdf"), None);
607-
assert_eq!(obj.get_string("prop").is_some(), true);
607+
assert!(obj.get_string("prop").is_some());
608608
assert_eq!(obj.get("asdf"), None);
609609
assert_eq!(obj.properties.len(), 1);
610610
}

src/common.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ impl Range {
1818
}
1919

2020
impl Ranged for Range {
21-
fn range(&self) -> &Range {
22-
self
21+
fn range(&self) -> Range {
22+
*self
2323
}
2424
}
2525

2626
/// Represents an object that has a range in the text.
2727
pub trait Ranged {
2828
/// Gets the range.
29-
fn range(&self) -> &Range;
29+
fn range(&self) -> Range;
3030

3131
/// Gets the byte index of the first character in the text.
3232
fn start(&self) -> usize {

src/cst/input.rs

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#[derive(Debug, Clone)]
2+
pub enum CstInputValue {
3+
Null,
4+
Bool(bool),
5+
Number(String),
6+
String(String),
7+
Array(Vec<CstInputValue>),
8+
Object(Vec<(String, CstInputValue)>),
9+
}
10+
11+
impl CstInputValue {
12+
pub(crate) fn force_multiline(&self) -> bool {
13+
match self {
14+
CstInputValue::Null | CstInputValue::Bool(_) | CstInputValue::Number(_) | CstInputValue::String(_) => false,
15+
CstInputValue::Array(v) => v.iter().any(|v| v.is_object_or_array_with_elements()),
16+
CstInputValue::Object(v) => !v.is_empty(),
17+
}
18+
}
19+
20+
fn is_object_or_array_with_elements(&self) -> bool {
21+
match self {
22+
CstInputValue::Null | CstInputValue::Bool(_) | CstInputValue::Number(_) | CstInputValue::String(_) => false,
23+
CstInputValue::Array(v) => !v.is_empty(),
24+
CstInputValue::Object(v) => !v.is_empty(),
25+
}
26+
}
27+
}
28+
29+
#[macro_export]
30+
macro_rules! json {
31+
(null) => {
32+
$crate::cst::CstInputValue::Null
33+
};
34+
35+
(true) => {
36+
$crate::cst::CstInputValue::Bool(true)
37+
};
38+
39+
(false) => {
40+
$crate::cst::CstInputValue::Bool(false)
41+
};
42+
43+
($num:literal) => {
44+
$crate::cst::CstInputValue::Number($num.to_string())
45+
};
46+
47+
($str:literal) => {
48+
$crate::cst::CstInputValue::String($str.to_string())
49+
};
50+
51+
([ $($elems:tt),* $(,)? ]) => {
52+
$crate::cst::CstInputValue::Array(vec![
53+
$(json!($elems)),*
54+
])
55+
};
56+
57+
({ $($key:tt : $value:tt),* $(,)? }) => {
58+
$crate::cst::CstInputValue::Object(vec![
59+
$(($key.to_string(), json!($value))),*
60+
])
61+
};
62+
}

0 commit comments

Comments
 (0)