use nox_json::{parse, Json}; #[test] fn parses_a_roblox_client_version_response() { let doc = parse(r#"{"version":"version-4a1d0c9f7b2e4d88","clientVersionUpload":"version-2e3f1a","bootstrapperVersion":"2.688.1092","nextClientVersionUpload":null,"IsRolloutComplete":true}"#).unwrap(); assert_eq!(doc.str_at("version"), Some("version-4a1d0c9f7b2e4d88")); assert_eq!(doc.str_at("clientVersionUpload"), Some("version-2e3f1a")); assert_eq!(doc.str_at("bootstrapperVersion"), Some("2.688.1092")); assert!(doc.get("nextClientVersionUpload").unwrap().is_null()); assert_eq!(doc.path("IsRolloutComplete").unwrap().as_bool(), Some(true)); assert_eq!(doc.str_at("nope"), None); } #[test] fn handles_escapes_and_unicode() { let doc = parse(r#"{"a":"line\nbreak\ttab \"q\" \\ \/ \u00e9 \u5b57 \ud83d\ude00","b":"plain é字"}"#).unwrap(); assert_eq!(doc.str_at("a"), Some("line\nbreak\ttab \"q\" \\ / é 字 😀")); assert_eq!(doc.str_at("b"), Some("plain é字")); } #[test] fn numbers_match_expectations() { let doc = parse(r#"[0,-1,3.5,1e3,-2.5E-2,12345678901,0.0]"#).unwrap(); let a = doc.as_array().unwrap(); assert_eq!(a[0].as_f64(), Some(0.0)); assert_eq!(a[1].as_f64(), Some(-1.0)); assert_eq!(a[2].as_f64(), Some(3.5)); assert_eq!(a[3].as_f64(), Some(1000.0)); assert_eq!(a[4].as_f64(), Some(-0.025)); assert_eq!(a[5].as_u64(), Some(12345678901)); assert_eq!(a[1].as_u64(), None, "negative must not become u64"); } #[test] fn nested_structures() { let doc = parse(r#"{"a":{"b":{"c":[1,{"d":"deep"}]}}}"#).unwrap(); assert_eq!(doc.path("a.b.c").unwrap().at(0).unwrap().as_f64(), Some(1.0)); assert_eq!(doc.path("a.b.c").unwrap().at(1).unwrap().str_at("d"), Some("deep")); assert_eq!(doc.path("a.b.zzz"), None); } #[test] fn rejects_malformed_input() { for bad in [ "", "{", "}", "[", "[1,", "[1,]", "{\"a\"}", "{\"a\":}", "{a:1}", "\"unterminated", "tru", "nul", "01x", "--1", "{\"a\":1}x", "[1] [2]", "\"\\q\"", "\"\\u12\"", "\"\\ud800\"", "\"\\ud800\\u0041\"", ] { assert!(parse(bad).is_err(), "{bad:?} should not parse"); } } #[test] fn depth_is_bounded() { let deep = format!("{}1{}", "[".repeat(500), "]".repeat(500)); assert!(parse(&deep).is_err(), "runaway nesting must be rejected"); let ok = format!("{}1{}", "[".repeat(20), "]".repeat(20)); assert!(parse(&ok).is_ok()); } #[test] fn survives_random_junk_without_panicking() { let mut seed = 7u64; let alphabet = b"{}[]\":,0123456789.eE-+ truefalsn\\/\x00\xff"; for _ in 0..4000 { let mut s = String::new(); seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1); let n = (seed >> 33) as usize % 40; for _ in 0..n { seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1); s.push(alphabet[(seed >> 33) as usize % alphabet.len()] as char); } let _ = parse(&s); } } #[test] fn duplicate_keys_keep_the_first() { let doc = parse(r#"{"k":"first","k":"second"}"#).unwrap(); assert_eq!(doc.str_at("k"), Some("first")); match doc { Json::Obj(kv) => assert_eq!(kv.len(), 2, "both pairs are retained"), _ => panic!(), } } #[test] fn writes_json_that_parses_back_identically() { let src = r#"{"a":1,"b":[true,false,null,"x\ny"],"c":{"d":-2.5,"e":"é字\"q\""},"f":[],"g":{}}"#; let doc = parse(src).unwrap(); let text = doc.to_string(); let back = parse(&text).unwrap_or_else(|e| panic!("re-parse failed: {e}\n{text}")); assert_eq!(doc, back, "round trip changed the document"); assert!(text.contains("\\n"), "control characters must stay escaped"); } #[test] fn set_updates_or_appends() { let mut o = Json::obj(); o.set("a", Json::Num(1.0)); o.set("b", Json::Str("x".into())); o.set("a", Json::Num(2.0)); assert_eq!(o.path("a").unwrap().as_f64(), Some(2.0)); assert_eq!(o.str_at("b"), Some("x")); match &o { Json::Obj(kv) => assert_eq!(kv.len(), 2), _ => panic!(), } }