Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 3 additions & 16 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
RUST_BACKTRACE: full

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: dsherret/rust-toolchain-file@v1
- uses: Swatinem/rust-cache@v2
with:
Expand All @@ -35,32 +35,19 @@ jobs:
if: matrix.config.kind == 'test_release'
run: cargo test --release --all-features

# CARGO PUBLISH
- name: Cargo login
if: matrix.config.kind == 'test_release' && startsWith(github.ref, 'refs/tags/')
run: cargo login ${{ secrets.CRATES_TOKEN }}

- name: Cargo publish
if: matrix.config.kind == 'test_release' && startsWith(github.ref, 'refs/tags/')
run: cargo publish

benchmark:
name: Benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install latest nightly
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
- name: Cache cargo
uses: actions/cache@v4
uses: Swatinem/rust-cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
# Run benchmark and stores the output to a file
- name: Run benchmark
Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: publish

on:
push:
tags:
- "*"

jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Clone repository
uses: actions/checkout@v6
- uses: rust-lang/crates-io-auth-action@v1
id: auth
- run: cargo publish
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:

steps:
- name: Clone repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
token: ${{ secrets.GH_DPRINTBOT_PAT }}

Expand Down
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[toolchain]
channel = "1.89.0"
channel = "1.92.0"
components = ["clippy", "rustfmt"]
4 changes: 4 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub enum ParseErrorKind {
ExpectedPlusMinusOrDigitInNumberLiteral,
ExpectedStringObjectProperty,
HexadecimalNumbersNotAllowed,
ExpectedComma,
MultipleRootJsonValues,
SingleQuotedStringsNotAllowed,
String(ParseStringErrorKind),
Expand Down Expand Up @@ -59,6 +60,9 @@ impl std::fmt::Display for ParseErrorKind {
HexadecimalNumbersNotAllowed => {
write!(f, "Hexadecimal numbers are not allowed")
}
ExpectedComma => {
write!(f, "Expected comma")
}
MultipleRootJsonValues => {
write!(f, "Text cannot contain more than one JSON value")
}
Expand Down
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@
//!
//! ## Parse Strictly as JSON
//!
//! Provide `ParseOptions` and set all the options to false:
//! By default this library is extremely loose in what it allows parsing. To be strict,
//! provide `ParseOptions` and set all the options to false:
//!
//! ```
//! use jsonc_parser::parse_to_value;
Expand All @@ -106,6 +107,7 @@
//! allow_comments: false,
//! allow_loose_object_property_names: false,
//! allow_trailing_commas: false,
//! allow_missing_commas: false,
//! allow_single_quoted_strings: false,
//! allow_hexadecimal_numbers: false,
//! allow_unary_plus_numbers: false,
Expand Down
74 changes: 68 additions & 6 deletions src/parse_to_ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ pub struct ParseOptions {
pub allow_loose_object_property_names: bool,
/// Allow trailing commas on object literal and array literal values (defaults to `true`).
pub allow_trailing_commas: bool,
/// Allow missing commas between object properties or array elements (defaults to `true`).
pub allow_missing_commas: bool,
/// Allow single-quoted strings (defaults to `true`).
pub allow_single_quoted_strings: bool,
/// Allow hexadecimal numbers like 0xFF (defaults to `true`).
Expand All @@ -69,6 +71,7 @@ impl Default for ParseOptions {
allow_comments: true,
allow_loose_object_property_names: true,
allow_trailing_commas: true,
allow_missing_commas: true,
allow_single_quoted_strings: true,
allow_hexadecimal_numbers: true,
allow_unary_plus_numbers: true,
Expand Down Expand Up @@ -102,6 +105,7 @@ struct Context<'a> {
collect_comments_as_tokens: bool,
allow_comments: bool,
allow_trailing_commas: bool,
allow_missing_commas: bool,
allow_loose_object_property_names: bool,
}

Expand Down Expand Up @@ -261,6 +265,7 @@ pub fn parse_to_ast<'a>(
collect_comments_as_tokens: collect_options.comments == CommentCollectionStrategy::AsTokens,
allow_comments: parse_options.allow_comments,
allow_trailing_commas: parse_options.allow_trailing_commas,
allow_missing_commas: parse_options.allow_missing_commas,
allow_loose_object_property_names: parse_options.allow_loose_object_property_names,
};
context.scan()?;
Expand Down Expand Up @@ -321,13 +326,24 @@ fn parse_object<'a>(context: &mut Context<'a>) -> Result<Object<'a>, ParseError>
}

// skip the comma
if let Some(Token::Comma) = context.scan()? {
let comma_range = context.create_range_from_last_token();
if let Some(Token::CloseBrace) = context.scan()?
&& !context.allow_trailing_commas
{
return Err(context.create_error_for_range(comma_range, ParseErrorKind::TrailingCommasNotAllowed));
let after_value_end = context.last_token_end;
match context.scan()? {
Some(Token::Comma) => {
let comma_range = context.create_range_from_last_token();
if let Some(Token::CloseBrace) = context.scan()?
&& !context.allow_trailing_commas
{
return Err(context.create_error_for_range(comma_range, ParseErrorKind::TrailingCommasNotAllowed));
}
}
Some(Token::String(_) | Token::Word(_) | Token::Number(_)) if !context.allow_missing_commas => {
let range = Range {
start: after_value_end,
end: after_value_end,
};
return Err(context.create_error_for_range(range, ParseErrorKind::ExpectedComma));
}
_ => {}
}
}

Expand Down Expand Up @@ -567,6 +583,7 @@ mod tests {
allow_comments: false,
allow_loose_object_property_names: false,
allow_trailing_commas: false,
allow_missing_commas: false,
allow_single_quoted_strings: false,
allow_hexadecimal_numbers: false,
allow_unary_plus_numbers: false,
Expand Down Expand Up @@ -673,4 +690,49 @@ mod tests {
let number_value = obj.properties[0].value.as_number_lit().unwrap();
assert_eq!(number_value.value, "+42");
}

#[test]
fn missing_comma_between_properties() {
let text = r#"{
"name": "alice"
"age": 25
}"#;
let result = parse_to_ast(text, &Default::default(), &Default::default()).unwrap();
assert_eq!(
result
.value
.unwrap()
.as_object()
.unwrap()
.get_number("age")
.unwrap()
.value,
"25"
);

// but is strict when strict
assert_has_strict_error(text, "Expected comma on line 2 column 18");
}

#[test]
fn missing_comma_with_comment_between_properties() {
// when comments are allowed but missing commas are not,
// should still detect the missing comma after the comment is skipped
let result = parse_to_ast(
r#"{
"name": "alice" // comment here
"age": 25
}"#,
&Default::default(),
&ParseOptions {
allow_comments: true,
allow_missing_commas: false,
..Default::default()
},
);
match result {
Ok(_) => panic!("Expected error, but did not find one."),
Err(err) => assert_eq!(err.to_string(), "Expected comma on line 2 column 18"),
}
}
}
7 changes: 5 additions & 2 deletions src/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use super::ParseOptions;
use super::errors::ParseError;
use super::parse_to_ast;

/// Parses a string containing JSONC to a `serde_json::Value.
/// Parses a string containing JSONC to a `serde_json::Value`.
///
/// Requires the "serde" cargo feature:
///
Expand All @@ -16,7 +16,10 @@ use super::parse_to_ast;
/// ```rs
/// use jsonc_parser::parse_to_serde_value;
///
/// let json_value = parse_to_serde_value(r#"{ "test": 5 } // test"#, &Default::default()).unwrap();
/// let json_value = parse_to_serde_value(
/// r#"{ "test": 5 } // test"#,
/// &Default::default(),
/// ).unwrap();
/// ```
pub fn parse_to_serde_value(text: &str, parse_options: &ParseOptions) -> Result<Option<serde_json::Value>, ParseError> {
let value = parse_to_ast(
Expand Down
Loading