mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-20 12:45:05 +00:00
Add JSONC Support for Project, Meta, and Model JSON files (#1144)
Replaces `serde_json` parsing with `jsonc-parser` throughout the codebase, enabling support for **comments** and **trailing commas** in all JSON files including `.project.json`, `.model.json`, and `.meta.json` files. MSRV bumps from `1.83.0` to `1.88.0` in order to use the jsonc_parser dependency.
This commit is contained in:
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@1.83.0
|
||||
uses: dtolnay/rust-toolchain@1.88.0
|
||||
|
||||
- name: Restore Rust Cache
|
||||
uses: actions/cache/restore@v4
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
* Added `sourcemap.json` into the defualt `.gitignore` files ([#1145])
|
||||
* Fixed a bug where the last sync timestamp was not updating correctly in the plugin ([#1132])
|
||||
* Improved the reliability of sync replacements by adding better error handling and recovery ([#1135])
|
||||
* Added support for JSON comments and trailing commas in project, meta, and model json files ([#1144])
|
||||
|
||||
[#1145]: https://github.com/rojo-rbx/rojo/pull/1145
|
||||
[#1132]: https://github.com/rojo-rbx/rojo/pull/1132
|
||||
[#1135]: https://github.com/rojo-rbx/rojo/pull/1135
|
||||
[#1144]: https://github.com/rojo-rbx/rojo/pull/1144
|
||||
|
||||
## 7.6.0 - October 10th, 2025
|
||||
* Added flag to `rojo init` to skip initializing a git repository ([#1122])
|
||||
|
||||
92
Cargo.lock
generated
92
Cargo.lock
generated
@@ -1,6 +1,6 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
@@ -243,7 +243,7 @@ checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro-error",
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
@@ -665,9 +665,9 @@ version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1033,6 +1033,15 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonc-parser"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ec4ac49f13c7b00f435f8a5bb55d725705e2cf620df35a5859321595102eb7e"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kernel32-sys"
|
||||
version = "0.2.2"
|
||||
@@ -1393,9 +1402,9 @@ checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_meta",
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1478,7 +1487,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 1.0.109",
|
||||
"version_check",
|
||||
@@ -1490,7 +1499,7 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"version_check",
|
||||
]
|
||||
@@ -1518,9 +1527,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.78"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -1542,7 +1551,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
|
||||
dependencies = [
|
||||
"quote 1.0.35",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1560,7 +1569,7 @@ version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1903,6 +1912,7 @@ dependencies = [
|
||||
"hyper",
|
||||
"insta",
|
||||
"jod-thread",
|
||||
"jsonc-parser",
|
||||
"log",
|
||||
"maplit",
|
||||
"memofs",
|
||||
@@ -2054,10 +2064,11 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.197"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
@@ -2072,25 +2083,36 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.197"
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.78",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.114"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
|
||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2195,18 +2217,18 @@ version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.52"
|
||||
version = "2.0.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
|
||||
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -2289,9 +2311,9 @@ version = "1.0.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2401,9 +2423,9 @@ version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2638,9 +2660,9 @@ dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.108",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -2672,9 +2694,9 @@ version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.108",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -2970,9 +2992,9 @@ version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.78",
|
||||
"proc-macro2 1.0.103",
|
||||
"quote 1.0.35",
|
||||
"syn 2.0.52",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "rojo"
|
||||
version = "7.6.0"
|
||||
rust-version = "1.83"
|
||||
rust-version = "1.88"
|
||||
authors = [
|
||||
"Lucien Greathouse <me@lpghatguy.com>",
|
||||
"Micah Reid <git@dekkonot.com>",
|
||||
@@ -85,7 +85,8 @@ reqwest = { version = "0.11.24", default-features = false, features = [
|
||||
ritz = "0.1.0"
|
||||
roblox_install = "1.0.0"
|
||||
serde = { version = "1.0.197", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.114"
|
||||
serde_json = "1.0.145"
|
||||
jsonc-parser = { version = "0.27.0", features = ["serde"] }
|
||||
toml = "0.5.11"
|
||||
termcolor = "1.4.1"
|
||||
thiserror = "1.0.57"
|
||||
|
||||
@@ -40,7 +40,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
|
||||
|
||||
Pull requests are welcome!
|
||||
|
||||
Rojo supports Rust 1.83 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
||||
Rojo supports Rust 1.88 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
||||
|
||||
## License
|
||||
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
||||
|
||||
@@ -43,8 +43,8 @@ impl Serialize for Glob {
|
||||
|
||||
impl<'de> Deserialize<'de> for Glob {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let glob = <&str as Deserialize>::deserialize(deserializer)?;
|
||||
let glob = String::deserialize(deserializer)?;
|
||||
|
||||
Glob::new(glob).map_err(D::Error::custom)
|
||||
Glob::new(&glob).map_err(D::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
313
src/json.rs
Normal file
313
src/json.rs
Normal file
@@ -0,0 +1,313 @@
|
||||
//! Utilities for parsing JSON with comments (JSONC) and deserializing to Rust types.
|
||||
//!
|
||||
//! This module provides convenient wrappers around `jsonc_parser` and `serde_json`
|
||||
//! to reduce boilerplate and improve ergonomics when working with JSONC files.
|
||||
|
||||
use anyhow::Context as _;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
/// Parse JSONC text into a `serde_json::Value`.
|
||||
///
|
||||
/// This handles the common pattern of calling `jsonc_parser::parse_to_serde_value`
|
||||
/// and unwrapping the `Option` with a clear error message.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - The text is not valid JSONC
|
||||
/// - The text contains no JSON value
|
||||
pub fn parse_value(text: &str) -> anyhow::Result<serde_json::Value> {
|
||||
jsonc_parser::parse_to_serde_value(text, &Default::default())
|
||||
.context("Failed to parse JSONC")?
|
||||
.ok_or_else(|| anyhow::anyhow!("File contains no JSON value"))
|
||||
}
|
||||
|
||||
/// Parse JSONC text into a `serde_json::Value` with a custom context message.
|
||||
///
|
||||
/// This is useful when you want to provide a specific error message that includes
|
||||
/// additional information like the file path.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - The text is not valid JSONC
|
||||
/// - The text contains no JSON value
|
||||
pub fn parse_value_with_context(
|
||||
text: &str,
|
||||
context: impl Fn() -> String,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
jsonc_parser::parse_to_serde_value(text, &Default::default())
|
||||
.with_context(|| format!("{}: JSONC parse error", context()))?
|
||||
.ok_or_else(|| anyhow::anyhow!("{}: File contains no JSON value", context()))
|
||||
}
|
||||
|
||||
/// Parse JSONC text and deserialize it into a specific type.
|
||||
///
|
||||
/// This combines parsing JSONC and deserializing into a single operation,
|
||||
/// eliminating the need to manually chain `parse_to_serde_value` and `from_value`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - The text is not valid JSONC
|
||||
/// - The text contains no JSON value
|
||||
/// - The value cannot be deserialized into type `T`
|
||||
pub fn from_str<T: DeserializeOwned>(text: &str) -> anyhow::Result<T> {
|
||||
let value = parse_value(text)?;
|
||||
serde_json::from_value(value).context("Failed to deserialize JSON")
|
||||
}
|
||||
|
||||
/// Parse JSONC text and deserialize it into a specific type with a custom context message.
|
||||
///
|
||||
/// This is useful when you want to provide a specific error message that includes
|
||||
/// additional information like the file path.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - The text is not valid JSONC
|
||||
/// - The text contains no JSON value
|
||||
/// - The value cannot be deserialized into type `T`
|
||||
pub fn from_str_with_context<T: DeserializeOwned>(
|
||||
text: &str,
|
||||
context: impl Fn() -> String,
|
||||
) -> anyhow::Result<T> {
|
||||
let value = parse_value_with_context(text, &context)?;
|
||||
serde_json::from_value(value).with_context(|| format!("{}: Invalid JSON structure", context()))
|
||||
}
|
||||
|
||||
/// Parse JSONC bytes into a `serde_json::Value` with a custom context message.
|
||||
///
|
||||
/// This handles UTF-8 conversion and JSONC parsing in one step.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - The bytes are not valid UTF-8
|
||||
/// - The text is not valid JSONC
|
||||
/// - The text contains no JSON value
|
||||
pub fn parse_value_from_slice_with_context(
|
||||
slice: &[u8],
|
||||
context: impl Fn() -> String,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
let text = std::str::from_utf8(slice)
|
||||
.with_context(|| format!("{}: File is not valid UTF-8", context()))?;
|
||||
parse_value_with_context(text, context)
|
||||
}
|
||||
|
||||
/// Parse JSONC bytes and deserialize it into a specific type.
|
||||
///
|
||||
/// This handles UTF-8 conversion, JSONC parsing, and deserialization in one step.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - The bytes are not valid UTF-8
|
||||
/// - The text is not valid JSONC
|
||||
/// - The text contains no JSON value
|
||||
/// - The value cannot be deserialized into type `T`
|
||||
pub fn from_slice<T: DeserializeOwned>(slice: &[u8]) -> anyhow::Result<T> {
|
||||
let text = std::str::from_utf8(slice).context("File is not valid UTF-8")?;
|
||||
from_str(text)
|
||||
}
|
||||
|
||||
/// Parse JSONC bytes and deserialize it into a specific type with a custom context message.
|
||||
///
|
||||
/// This handles UTF-8 conversion, JSONC parsing, and deserialization in one step.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - The bytes are not valid UTF-8
|
||||
/// - The text is not valid JSONC
|
||||
/// - The text contains no JSON value
|
||||
/// - The value cannot be deserialized into type `T`
|
||||
pub fn from_slice_with_context<T: DeserializeOwned>(
|
||||
slice: &[u8],
|
||||
context: impl Fn() -> String,
|
||||
) -> anyhow::Result<T> {
|
||||
let text = std::str::from_utf8(slice)
|
||||
.with_context(|| format!("{}: File is not valid UTF-8", context()))?;
|
||||
from_str_with_context(text, context)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[test]
|
||||
fn test_parse_value() {
|
||||
let value = parse_value(r#"{"foo": "bar"}"#).unwrap();
|
||||
assert_eq!(value["foo"], "bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value_with_comments() {
|
||||
let value = parse_value(
|
||||
r#"{
|
||||
// This is a comment
|
||||
"foo": "bar" // Inline comment
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(value["foo"], "bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value_with_trailing_comma() {
|
||||
let value = parse_value(
|
||||
r#"{
|
||||
"foo": "bar",
|
||||
"baz": 123,
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(value["foo"], "bar");
|
||||
assert_eq!(value["baz"], 123);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value_empty() {
|
||||
let err = parse_value("").unwrap_err();
|
||||
assert!(err.to_string().contains("no JSON value"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value_invalid() {
|
||||
let err = parse_value("{invalid}").unwrap_err();
|
||||
assert!(err.to_string().contains("parse"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value_with_context() {
|
||||
let err = parse_value_with_context("{invalid}", || "test.json".to_string()).unwrap_err();
|
||||
assert!(err.to_string().contains("test.json"));
|
||||
assert!(err.to_string().contains("parse"));
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
struct TestStruct {
|
||||
foo: String,
|
||||
bar: i32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
let result: TestStruct = from_str(r#"{"foo": "hello", "bar": 42}"#).unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
TestStruct {
|
||||
foo: "hello".to_string(),
|
||||
bar: 42
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str_with_comments() {
|
||||
let result: TestStruct = from_str(
|
||||
r#"{
|
||||
// Comment
|
||||
"foo": "hello",
|
||||
"bar": 42, // Trailing comma is fine
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
TestStruct {
|
||||
foo: "hello".to_string(),
|
||||
bar: 42
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str_invalid_type() {
|
||||
let err = from_str::<TestStruct>(r#"{"foo": "hello"}"#).unwrap_err();
|
||||
assert!(err.to_string().contains("deserialize"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str_with_context() {
|
||||
let err = from_str_with_context::<TestStruct>(r#"{"foo": "hello"}"#, || {
|
||||
"config.json".to_string()
|
||||
})
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("config.json"));
|
||||
assert!(err.to_string().contains("Invalid JSON structure"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value_from_slice_with_context() {
|
||||
let err = parse_value_from_slice_with_context(b"{invalid}", || "test.json".to_string())
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("test.json"));
|
||||
assert!(err.to_string().contains("parse"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value_from_slice_with_context_invalid_utf8() {
|
||||
let err = parse_value_from_slice_with_context(&[0xFF, 0xFF], || "test.json".to_string())
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("test.json"));
|
||||
assert!(err.to_string().contains("UTF-8"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_slice() {
|
||||
let result: TestStruct = from_slice(br#"{"foo": "hello", "bar": 42}"#).unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
TestStruct {
|
||||
foo: "hello".to_string(),
|
||||
bar: 42
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_slice_with_comments() {
|
||||
let result: TestStruct = from_slice(
|
||||
br#"{
|
||||
// Comment
|
||||
"foo": "hello",
|
||||
"bar": 42, // Trailing comma is fine
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
TestStruct {
|
||||
foo: "hello".to_string(),
|
||||
bar: 42
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_slice_invalid_utf8() {
|
||||
let err = from_slice::<TestStruct>(&[0xFF, 0xFF]).unwrap_err();
|
||||
assert!(err.to_string().contains("UTF-8"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_slice_with_context() {
|
||||
let err = from_slice_with_context::<TestStruct>(br#"{"foo": "hello"}"#, || {
|
||||
"config.json".to_string()
|
||||
})
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("config.json"));
|
||||
assert!(err.to_string().contains("Invalid JSON structure"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_slice_with_context_invalid_utf8() {
|
||||
let err =
|
||||
from_slice_with_context::<TestStruct>(&[0xFF, 0xFF], || "config.json".to_string())
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("config.json"));
|
||||
assert!(err.to_string().contains("UTF-8"));
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ mod tree_view;
|
||||
mod auth_cookie;
|
||||
mod change_processor;
|
||||
mod glob;
|
||||
mod json;
|
||||
mod lua_ast;
|
||||
mod message_queue;
|
||||
mod multimap;
|
||||
|
||||
@@ -11,7 +11,7 @@ use rbx_dom_weak::{Ustr, UstrMap};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{glob::Glob, resolution::UnresolvedValue, snapshot::SyncRule};
|
||||
use crate::{glob::Glob, json, resolution::UnresolvedValue, snapshot::SyncRule};
|
||||
|
||||
static PROJECT_FILENAME: &str = "default.project.json";
|
||||
|
||||
@@ -214,8 +214,11 @@ impl Project {
|
||||
project_file_location: PathBuf,
|
||||
fallback_name: Option<&str>,
|
||||
) -> Result<Self, Error> {
|
||||
let mut project: Self = serde_json::from_slice(contents).map_err(|source| Error::Json {
|
||||
source,
|
||||
let mut project: Self = json::from_slice(contents).map_err(|e| Error::Json {
|
||||
source: serde_json::Error::io(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
e.to_string(),
|
||||
)),
|
||||
path: project_file_location.clone(),
|
||||
})?;
|
||||
project.file_location = project_file_location;
|
||||
@@ -399,13 +402,13 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn path_node_required() {
|
||||
let path_node: PathNode = serde_json::from_str(r#""src""#).unwrap();
|
||||
let path_node: PathNode = json::from_str(r#""src""#).unwrap();
|
||||
assert_eq!(path_node, PathNode::Required(PathBuf::from("src")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_node_optional() {
|
||||
let path_node: PathNode = serde_json::from_str(r#"{ "optional": "src" }"#).unwrap();
|
||||
let path_node: PathNode = json::from_str(r#"{ "optional": "src" }"#).unwrap();
|
||||
assert_eq!(
|
||||
path_node,
|
||||
PathNode::Optional(OptionalPathNode::new(PathBuf::from("src")))
|
||||
@@ -414,7 +417,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn project_node_required() {
|
||||
let project_node: ProjectNode = serde_json::from_str(
|
||||
let project_node: ProjectNode = json::from_str(
|
||||
r#"{
|
||||
"$path": "src"
|
||||
}"#,
|
||||
@@ -429,7 +432,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn project_node_optional() {
|
||||
let project_node: ProjectNode = serde_json::from_str(
|
||||
let project_node: ProjectNode = json::from_str(
|
||||
r#"{
|
||||
"$path": { "optional": "src" }
|
||||
}"#,
|
||||
@@ -446,7 +449,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn project_node_none() {
|
||||
let project_node: ProjectNode = serde_json::from_str(
|
||||
let project_node: ProjectNode = json::from_str(
|
||||
r#"{
|
||||
"$className": "Folder"
|
||||
}"#,
|
||||
@@ -458,7 +461,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn project_node_optional_serialize_absolute() {
|
||||
let project_node: ProjectNode = serde_json::from_str(
|
||||
let project_node: ProjectNode = json::from_str(
|
||||
r#"{
|
||||
"$path": { "optional": "..\\src" }
|
||||
}"#,
|
||||
@@ -471,7 +474,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn project_node_optional_serialize_absolute_no_change() {
|
||||
let project_node: ProjectNode = serde_json::from_str(
|
||||
let project_node: ProjectNode = json::from_str(
|
||||
r#"{
|
||||
"$path": { "optional": "../src" }
|
||||
}"#,
|
||||
@@ -484,7 +487,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn project_node_optional_serialize_optional() {
|
||||
let project_node: ProjectNode = serde_json::from_str(
|
||||
let project_node: ProjectNode = json::from_str(
|
||||
r#"{
|
||||
"$path": "..\\src"
|
||||
}"#,
|
||||
@@ -494,4 +497,57 @@ mod test {
|
||||
let serialized = serde_json::to_string(&project_node).unwrap();
|
||||
assert_eq!(serialized, r#"{"$path":"../src"}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_with_jsonc_features() {
|
||||
// Test that JSONC features (comments and trailing commas) are properly handled
|
||||
let project_json = r#"{
|
||||
// This is a single-line comment
|
||||
"name": "TestProject",
|
||||
/* This is a
|
||||
multi-line comment */
|
||||
"tree": {
|
||||
"$path": "src", // Comment after value
|
||||
},
|
||||
"servePort": 34567,
|
||||
"emitLegacyScripts": false,
|
||||
// Test glob parsing with comments
|
||||
"globIgnorePaths": [
|
||||
"**/*.spec.lua", // Ignore test files
|
||||
"**/*.test.lua",
|
||||
],
|
||||
"syncRules": [
|
||||
{
|
||||
"pattern": "*.data.json",
|
||||
"use": "json", // Trailing comma in object
|
||||
},
|
||||
{
|
||||
"pattern": "*.module.lua",
|
||||
"use": "moduleScript",
|
||||
}, // Trailing comma in array
|
||||
], // Another trailing comma
|
||||
}"#;
|
||||
|
||||
let project = Project::load_from_slice(
|
||||
project_json.as_bytes(),
|
||||
PathBuf::from("/test/default.project.json"),
|
||||
None,
|
||||
)
|
||||
.expect("Failed to parse project with JSONC features");
|
||||
|
||||
// Verify the parsed values
|
||||
assert_eq!(project.name, Some("TestProject".to_string()));
|
||||
assert_eq!(project.serve_port, Some(34567));
|
||||
assert_eq!(project.emit_legacy_scripts, Some(false));
|
||||
|
||||
// Verify glob_ignore_paths were parsed correctly
|
||||
assert_eq!(project.glob_ignore_paths.len(), 2);
|
||||
assert!(project.glob_ignore_paths[0].is_match("test/foo.spec.lua"));
|
||||
assert!(project.glob_ignore_paths[1].is_match("test/bar.test.lua"));
|
||||
|
||||
// Verify sync_rules were parsed correctly
|
||||
assert_eq!(project.sync_rules.len(), 2);
|
||||
assert!(project.sync_rules[0].include.is_match("data.data.json"));
|
||||
assert!(project.sync_rules[1].include.is_match("init.module.lua"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,14 +248,15 @@ fn nonexhaustive_list(values: &[&str]) -> String {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::json;
|
||||
|
||||
fn resolve(class: &str, prop: &str, json_value: &str) -> Variant {
|
||||
let unresolved: UnresolvedValue = serde_json::from_str(json_value).unwrap();
|
||||
let unresolved: UnresolvedValue = json::from_str(json_value).unwrap();
|
||||
unresolved.resolve(class, prop).unwrap()
|
||||
}
|
||||
|
||||
fn resolve_unambiguous(json_value: &str) -> Variant {
|
||||
let unresolved: UnresolvedValue = serde_json::from_str(json_value).unwrap();
|
||||
let unresolved: UnresolvedValue = json::from_str(json_value).unwrap();
|
||||
unresolved.resolve_unambiguous().unwrap()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Context;
|
||||
use memofs::{IoResultExt, Vfs};
|
||||
use rbx_dom_weak::ustr;
|
||||
|
||||
use crate::{
|
||||
json,
|
||||
lua_ast::{Expression, Statement},
|
||||
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
|
||||
};
|
||||
@@ -19,8 +19,9 @@ pub fn snapshot_json(
|
||||
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
||||
let contents = vfs.read(path)?;
|
||||
|
||||
let value: serde_json::Value = serde_json::from_slice(&contents)
|
||||
.with_context(|| format!("File contains malformed JSON: {}", path.display()))?;
|
||||
let value = json::parse_value_from_slice_with_context(&contents, || {
|
||||
format!("File contains malformed JSON: {}", path.display())
|
||||
})?;
|
||||
|
||||
let as_lua = json_to_lua(value).to_string();
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use rbx_dom_weak::{
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
json,
|
||||
resolution::UnresolvedValue,
|
||||
snapshot::{InstanceContext, InstanceSnapshot},
|
||||
RojoRef,
|
||||
@@ -28,8 +29,9 @@ pub fn snapshot_json_model(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut instance: JsonModel = serde_json::from_str(contents_str)
|
||||
.with_context(|| format!("File is not a valid JSON model: {}", path.display()))?;
|
||||
let mut instance: JsonModel = json::from_str_with_context(contents_str, || {
|
||||
format!("File is not a valid JSON model: {}", path.display())
|
||||
})?;
|
||||
|
||||
if let Some(top_level_name) = &instance.name {
|
||||
let new_name = format!("{}.model.json", top_level_name);
|
||||
|
||||
@@ -4,7 +4,7 @@ use anyhow::{format_err, Context};
|
||||
use rbx_dom_weak::{types::Attributes, Ustr, UstrMap};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{resolution::UnresolvedValue, snapshot::InstanceSnapshot, RojoRef};
|
||||
use crate::{json, resolution::UnresolvedValue, snapshot::InstanceSnapshot, RojoRef};
|
||||
|
||||
/// Represents metadata in a sibling file with the same basename.
|
||||
///
|
||||
@@ -34,7 +34,7 @@ pub struct AdjacentMetadata {
|
||||
|
||||
impl AdjacentMetadata {
|
||||
pub fn from_slice(slice: &[u8], path: PathBuf) -> anyhow::Result<Self> {
|
||||
let mut meta: Self = serde_json::from_slice(slice).with_context(|| {
|
||||
let mut meta: Self = json::from_slice_with_context(slice, || {
|
||||
format!(
|
||||
"File contained malformed .meta.json data: {}",
|
||||
path.display()
|
||||
@@ -131,7 +131,7 @@ pub struct DirectoryMetadata {
|
||||
|
||||
impl DirectoryMetadata {
|
||||
pub fn from_slice(slice: &[u8], path: PathBuf) -> anyhow::Result<Self> {
|
||||
let mut meta: Self = serde_json::from_slice(slice).with_context(|| {
|
||||
let mut meta: Self = json::from_slice_with_context(slice, || {
|
||||
format!(
|
||||
"File contained malformed init.meta.json data: {}",
|
||||
path.display()
|
||||
|
||||
@@ -17,6 +17,7 @@ use rbx_dom_weak::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
json,
|
||||
serve_session::ServeSession,
|
||||
snapshot::{InstanceWithMeta, PatchSet, PatchUpdate},
|
||||
web::{
|
||||
@@ -139,7 +140,7 @@ impl ApiService {
|
||||
|
||||
let body = body::to_bytes(request.into_body()).await.unwrap();
|
||||
|
||||
let request: WriteRequest = match serde_json::from_slice(&body) {
|
||||
let request: WriteRequest = match json::from_slice(&body) {
|
||||
Ok(request) => request,
|
||||
Err(err) => {
|
||||
return json(
|
||||
|
||||
@@ -157,14 +157,20 @@ impl TestServeSession {
|
||||
let url = format!("http://localhost:{}/api/rojo", self.port);
|
||||
let body = reqwest::blocking::get(url)?.text()?;
|
||||
|
||||
Ok(serde_json::from_str(&body).expect("Server returned malformed response"))
|
||||
let value = jsonc_parser::parse_to_serde_value(&body, &Default::default())
|
||||
.expect("Failed to parse JSON")
|
||||
.expect("No JSON value");
|
||||
Ok(serde_json::from_value(value).expect("Server returned malformed response"))
|
||||
}
|
||||
|
||||
pub fn get_api_read(&self, id: Ref) -> Result<ReadResponse<'_>, reqwest::Error> {
|
||||
let url = format!("http://localhost:{}/api/read/{}", self.port, id);
|
||||
let body = reqwest::blocking::get(url)?.text()?;
|
||||
|
||||
Ok(serde_json::from_str(&body).expect("Server returned malformed response"))
|
||||
let value = jsonc_parser::parse_to_serde_value(&body, &Default::default())
|
||||
.expect("Failed to parse JSON")
|
||||
.expect("No JSON value");
|
||||
Ok(serde_json::from_value(value).expect("Server returned malformed response"))
|
||||
}
|
||||
|
||||
pub fn get_api_subscribe(
|
||||
|
||||
Reference in New Issue
Block a user